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