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; }