From 7b492ec765b9e4b4636cddcf85774dab4481949b Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 5 Jun 2023 08:21:24 +0200 Subject: [PATCH 01/15] Add TxMsgTransfer type --- types/txMsg.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/types/txMsg.ts b/types/txMsg.ts index a58614f..cd8cdb7 100644 --- a/types/txMsg.ts +++ b/types/txMsg.ts @@ -7,7 +7,8 @@ export type MsgType = | "redelegate" | "claimRewards" | "setWithdrawAddress" - | "createVestingAccount"; + | "createVestingAccount" + | "msgTransfer"; export type TxMsg = | TxMsgSend @@ -16,7 +17,8 @@ export type TxMsg = | TxMsgRedelegate | TxMsgClaimRewards | TxMsgSetWithdrawAddress - | TxMsgCreateVestingAccount; + | TxMsgCreateVestingAccount + | TxMsgTransfer; export interface TxMsgSend { readonly typeUrl: "/cosmos.bank.v1beta1.MsgSend"; @@ -81,3 +83,16 @@ export interface TxMsgCreateVestingAccount { readonly delayed: boolean; }; } + +export interface TxMsgTransfer { + readonly typeUrl: "/ibc.applications.transfer.v1.MsgTransfer"; + readonly value: { + readonly sourcePort: string; + readonly sourceChannel: string; + readonly token: Coin; + readonly sender: string; + readonly receiver: string; + readonly timeoutTimestamp: number; + readonly memo: string; + }; +} From bcf7f45b44bd0b8d40be00afdc62740a794f92a2 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 5 Jun 2023 08:21:31 +0200 Subject: [PATCH 02/15] Add TxMsgTransfer helpers --- lib/txMsgHelpers.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/txMsgHelpers.ts b/lib/txMsgHelpers.ts index c9da8ec..e4e3bbc 100644 --- a/lib/txMsgHelpers.ts +++ b/lib/txMsgHelpers.ts @@ -10,6 +10,7 @@ import { TxMsgRedelegate, TxMsgSend, TxMsgSetWithdrawAddress, + TxMsgTransfer, TxMsgUndelegate, } from "../types/txMsg"; @@ -85,6 +86,18 @@ const isTxMsgCreateVestingAccount = (msg: TxMsg | EncodeObject): msg is TxMsgCre !!msg.value.endTime && typeof msg.value.delayed === "boolean"; +const isTxMsgTransfer = (msg: TxMsg | EncodeObject): msg is TxMsgTransfer => + msg.typeUrl === "/ibc.applications.transfer.v1.MsgTransfer" && + "value" in msg && + "sourcePort" in msg.value && + "sourceChannel" in msg.value && + "sender" in msg.value && + "receiver" in msg.value && + !!msg.value.sourcePort && + !!msg.value.sourceChannel && + !!msg.value.sender && + !!msg.value.receiver; + const gasOfMsg = (msgType: MsgType): number => { switch (msgType) { case "send": @@ -101,6 +114,8 @@ const gasOfMsg = (msgType: MsgType): number => { return 100_000; case "createVestingAccount": return 100_000; + case "msgTransfer": + return 100_000; default: throw new Error("Unknown msg type"); } @@ -148,6 +163,7 @@ export { isTxMsgClaimRewards, isTxMsgSetWithdrawAddress, isTxMsgCreateVestingAccount, + isTxMsgTransfer, gasOfMsg, gasOfTx, dbTxFromJson, From 61009ebec02ce94d85e8074d0e23c0716695d186 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 5 Jun 2023 08:21:49 +0200 Subject: [PATCH 03/15] Add TxMsgTransferDetails --- .../TransactionInfo/TxMsgTransferDetails.tsx | 79 +++++++++++++++++++ .../dataViews/TransactionInfo/index.tsx | 11 ++- 2 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 components/dataViews/TransactionInfo/TxMsgTransferDetails.tsx diff --git a/components/dataViews/TransactionInfo/TxMsgTransferDetails.tsx b/components/dataViews/TransactionInfo/TxMsgTransferDetails.tsx new file mode 100644 index 0000000..66f8e82 --- /dev/null +++ b/components/dataViews/TransactionInfo/TxMsgTransferDetails.tsx @@ -0,0 +1,79 @@ +import { useAppContext } from "../../../context/AppContext"; +import { printableCoin } from "../../../lib/displayHelpers"; +import { TxMsgTransfer } from "../../../types/txMsg"; +import HashView from "../HashView"; + +interface TxMsgTransferDetailsProps { + readonly msg: TxMsgTransfer; +} + +const TxMsgTransferDetails = ({ msg }: TxMsgTransferDetailsProps) => { + const { state } = useAppContext(); + + const timeoutDateObj = new Date(msg.value.timeoutTimestamp / 1000000); + const timeoutDate = timeoutDateObj.toLocaleDateString(); + const timeoutTime = timeoutDateObj.toLocaleTimeString(); + + return ( + <> +
  • +

    MsgTransfer

    +
  • +
  • + +
    {msg.value.sourcePort}
    +
  • +
  • + +
    {msg.value.sourceChannel}
    +
  • +
  • + +
    {printableCoin(msg.value.token, state.chain)}
    +
  • +
  • + +
    + +
    +
  • +
  • + +
    + {timeoutDate} {timeoutTime} +
    +
  • +
  • + +
    {msg.value.memo}
    +
  • + + + ); +}; + +export default TxMsgTransferDetails; diff --git a/components/dataViews/TransactionInfo/index.tsx b/components/dataViews/TransactionInfo/index.tsx index 1da85d5..616884f 100644 --- a/components/dataViews/TransactionInfo/index.tsx +++ b/components/dataViews/TransactionInfo/index.tsx @@ -7,6 +7,7 @@ import { isTxMsgRedelegate, isTxMsgSend, isTxMsgSetWithdrawAddress, + isTxMsgTransfer, isTxMsgUndelegate, } from "../../../lib/txMsgHelpers"; import { DbTransaction } from "../../../types"; @@ -17,6 +18,7 @@ import TxMsgDelegateDetails from "./TxMsgDelegateDetails"; import TxMsgRedelegateDetails from "./TxMsgRedelegateDetails"; import TxMsgSendDetails from "./TxMsgSendDetails"; import TxMsgSetWithdrawAddressDetails from "./TxMsgSetWithdrawAddressDetails"; +import TxMsgTransferDetails from "./TxMsgTransferDetails"; import TxMsgUndelegateDetails from "./TxMsgUndelegateDetails"; interface Props { @@ -58,12 +60,9 @@ const TransactionInfo = ({ tx }: Props) => { {isTxMsgUndelegate(msg) ? : null} {isTxMsgRedelegate(msg) ? : null} {isTxMsgClaimRewards(msg) ? : null} - {isTxMsgSetWithdrawAddress(msg) ? ( - - ) : null} - {isTxMsgCreateVestingAccount(msg) ? ( - - ) : null} + {isTxMsgSetWithdrawAddress(msg) ? () : null} + {isTxMsgCreateVestingAccount(msg) ? () : null} + {isTxMsgTransfer(msg) ? : null} ))} From dc56ace5456f443b33f4128f77df20c4f93a3f19 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 5 Jun 2023 08:22:43 +0200 Subject: [PATCH 04/15] Add MsgTransferForm --- .../CreateTxForm/MsgForm/MsgTransferForm.tsx | 248 ++++++++++++++++++ .../forms/CreateTxForm/MsgForm/index.tsx | 3 + 2 files changed, 251 insertions(+) create mode 100644 components/forms/CreateTxForm/MsgForm/MsgTransferForm.tsx diff --git a/components/forms/CreateTxForm/MsgForm/MsgTransferForm.tsx b/components/forms/CreateTxForm/MsgForm/MsgTransferForm.tsx new file mode 100644 index 0000000..605c513 --- /dev/null +++ b/components/forms/CreateTxForm/MsgForm/MsgTransferForm.tsx @@ -0,0 +1,248 @@ +import { assert } from "@cosmjs/utils"; +import { useEffect, useState } from "react"; +import { MsgGetter } from ".."; +import { useAppContext } from "../../../../context/AppContext"; +import { checkAddress, exampleAddress } from "../../../../lib/displayHelpers"; +import { isTxMsgTransfer } from "../../../../lib/txMsgHelpers"; +import { TxMsg, TxMsgTransfer } from "../../../../types/txMsg"; +import Input from "../../../inputs/Input"; +import Select from "../../../inputs/Select"; +import StackableContainer from "../../../layout/StackableContainer"; + +const humanTimestampOptions = [ + { label: "12 hours from now", value: 12 * 60 * 60 * 1000 }, + { label: "1 day from now", value: 24 * 60 * 60 * 1000 }, + { label: "2 days from now", value: 2 * 24 * 60 * 60 * 1000 }, + { label: "3 days from now", value: 3 * 24 * 60 * 60 * 1000 }, + { label: "7 days from now", value: 7 * 24 * 60 * 60 * 1000 }, + { label: "10 days from now", value: 10 * 24 * 60 * 60 * 1000 }, + { label: "2 weeks from now", value: 2 * 7 * 24 * 60 * 60 * 1000 }, + { label: "3 weeks from now", value: 3 * 7 * 24 * 60 * 60 * 1000 }, + { label: "1 month from now", value: 4 * 7 * 24 * 60 * 60 * 1000 }, + { label: "custom", value: 0 }, +]; + +type HumanTimestampOption = (typeof humanTimestampOptions)[number]; + +interface MsgTransferFormProps { + readonly fromAddress: string; + readonly setMsgGetter: (msgGetter: MsgGetter) => void; + readonly deleteMsg: () => void; +} + +const MsgTransferForm = ({ fromAddress, setMsgGetter, deleteMsg }: MsgTransferFormProps) => { + const { state } = useAppContext(); + assert(state.chain.addressPrefix, "addressPrefix missing"); + + const [sourcePort, setSourcePort] = useState("transfer"); + const [sourceChannel, setSourceChannel] = useState(""); + const [denom, setDenom] = useState(""); + const [amount, setAmount] = useState("0"); + const [toAddress, setToAddress] = useState(""); + const [selectedTimestamp, setSelectedTimestamp] = useState( + humanTimestampOptions[0], + ); + const [customTimestamp, setCustomTimestamp] = useState(""); + const [memo, setMemo] = useState(""); + + const [sourcePortError, setSourcePortError] = useState(""); + const [sourceChannelError, setSourceChannelError] = useState(""); + const [denomError, setDenomError] = useState(""); + const [amountError, setAmountError] = useState(""); + const [toAddressError, setToAddressError] = useState(""); + const [customTimestampError, setCustomTimestampError] = useState(""); + + useEffect(() => { + setSourcePortError(""); + setSourceChannelError(""); + setDenomError(""); + setAmountError(""); + setToAddressError(""); + setCustomTimestampError(""); + + const isMsgValid = (msg: TxMsg): msg is TxMsgTransfer => { + assert(state.chain.addressPrefix, "addressPrefix missing"); + + if (!sourcePort) { + setSourcePortError("Source port is required"); + return false; + } + + if (!sourceChannel) { + setSourceChannelError("Source channel is required"); + return false; + } + + if (!denom) { + setDenomError("Denom is required"); + return false; + } + + if (!amount || Number(amount) <= 0) { + setAmountError("Amount must be greater than 0"); + return false; + } + + const addressErrorMsg = checkAddress(toAddress, state.chain.addressPrefix); + if (addressErrorMsg) { + setToAddressError(`Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`); + return false; + } + + if (selectedTimestamp.label === "custom") { + if (!customTimestamp || isNaN(Number(customTimestamp))) { + setCustomTimestampError("A timestamp is required"); + return false; + } + + if (Number(customTimestamp) <= Date.now()) { + setCustomTimestampError("Timestamp needs to be in the future"); + return false; + } + } + + return isTxMsgTransfer(msg); + }; + + const timeoutTimestamp = + selectedTimestamp.label === "custom" + ? Number(customTimestamp) + : (Date.now() + selectedTimestamp.value) * 1000000; // In nanoseconds + + const msg: TxMsgTransfer = { + typeUrl: "/ibc.applications.transfer.v1.MsgTransfer", + value: { + sender: fromAddress, + receiver: toAddress, + token: { denom, amount }, + sourcePort, + sourceChannel, + timeoutTimestamp, + memo, + }, + }; + + setMsgGetter({ isMsgValid, msg }); + }, [ + amount, + customTimestamp, + denom, + fromAddress, + memo, + selectedTimestamp.label, + selectedTimestamp.value, + setMsgGetter, + sourceChannel, + sourcePort, + state.chain.addressPrefix, + state.chain.chainId, + toAddress, + ]); + + return ( + + +

    MsgTransfer

    +
    + setToAddress(target.value)} + error={toAddressError} + placeholder={`E.g. ${exampleAddress(0, state.chain.addressPrefix)}`} + /> +
    +
    + setDenom(target.value)} + error={denomError} + /> +
    +
    + setAmount(target.value)} + error={amountError} + /> +
    +
    + setSourcePort(target.value)} + error={sourcePortError} + /> +
    +
    + setSourceChannel(target.value)} + error={sourceChannelError} + /> +
    +
    + setCustomTimestamp(target.value)} + error={customTimestampError} + /> +
    +
    + setMemo(target.value)} + /> +
    + +
    + ); +}; + +export default MsgTransferForm; diff --git a/components/forms/CreateTxForm/MsgForm/index.tsx b/components/forms/CreateTxForm/MsgForm/index.tsx index 3760210..1489fa9 100644 --- a/components/forms/CreateTxForm/MsgForm/index.tsx +++ b/components/forms/CreateTxForm/MsgForm/index.tsx @@ -6,6 +6,7 @@ import MsgDelegateForm from "./MsgDelegateForm"; import MsgRedelegateForm from "./MsgRedelegateForm"; import MsgSendForm from "./MsgSendForm"; import MsgSetWithdrawAddressForm from "./MsgSetWithdrawAddressForm"; +import MsgTransferForm from "./MsgTransferForm"; import MsgUndelegateForm from "./MsgUndelegateForm"; interface MsgFormProps { @@ -31,6 +32,8 @@ const MsgForm = ({ msgType, senderAddress, ...restProps }: MsgFormProps) => { return ; case "createVestingAccount": return ; + case "msgTransfer": + return ; default: return null; } From e9a34cf432d33dd8a6c892d1c8189733c4eccb53 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Mon, 5 Jun 2023 08:23:15 +0200 Subject: [PATCH 05/15] Add MsgTransfer button --- components/forms/CreateTxForm/index.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/components/forms/CreateTxForm/index.tsx b/components/forms/CreateTxForm/index.tsx index 9d53e80..f29b0a1 100644 --- a/components/forms/CreateTxForm/index.tsx +++ b/components/forms/CreateTxForm/index.tsx @@ -144,14 +144,9 @@ const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormPro