Merge pull request #128 from cosmos/feat/support-ibc-msgtransfer

Support IBC MsgTransfer
This commit is contained in:
Abel Fernández
2023-06-13 16:19:02 +02:00
committed by GitHub
9 changed files with 369 additions and 32 deletions
+3
View File
@@ -25,3 +25,6 @@ yarn-error.log*
# IDE folder
.idea
# Visual Studio Code
.vscode
@@ -0,0 +1,76 @@
import { thinSpace } from "../../../lib/displayHelpers";
import { TxMsgTransfer } from "../../../types/txMsg";
import HashView from "../HashView";
interface TxMsgTransferDetailsProps {
readonly msg: TxMsgTransfer;
}
const TxMsgTransferDetails = ({ msg }: TxMsgTransferDetailsProps) => {
const timeoutDateObj = new Date(msg.value.timeoutTimestamp.divide(1_000_000).toNumber());
const timeoutDate = timeoutDateObj.toLocaleDateString();
const timeoutTime = timeoutDateObj.toLocaleTimeString();
return (
<>
<li>
<h3>MsgTransfer</h3>
</li>
<li>
<label>Source Port:</label>
<div>{msg.value.sourcePort}</div>
</li>
<li>
<label>Source Channel:</label>
<div>{msg.value.sourceChannel}</div>
</li>
<li>
<label>Token:</label>
<div>{msg.value.token.amount + thinSpace + msg.value.token.denom}</div>
</li>
<li>
<label>To:</label>
<div title={msg.value.receiver}>
<HashView hash={msg.value.receiver} />
</div>
</li>
<li>
<label>Timeout:</label>
<div>
{timeoutDate} {timeoutTime}
</div>
</li>
<li>
<label>Memo:</label>
<div>{msg.value.memo}</div>
</li>
<style jsx>{`
li:not(:has(h3)) {
background: rgba(255, 255, 255, 0.03);
padding: 6px 10px;
border-radius: 8px;
display: flex;
align-items: center;
}
li + li:nth-child(2) {
margin-top: 25px;
}
li + li {
margin-top: 10px;
}
li div {
padding: 3px 6px;
}
label {
font-size: 12px;
background: rgba(255, 255, 255, 0.1);
padding: 3px 6px;
border-radius: 5px;
display: block;
}
`}</style>
</>
);
};
export default TxMsgTransferDetails;
@@ -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) ? <TxMsgUndelegateDetails msg={msg} /> : null}
{isTxMsgRedelegate(msg) ? <TxMsgRedelegateDetails msg={msg} /> : null}
{isTxMsgClaimRewards(msg) ? <TxMsgClaimRewardsDetails msg={msg} /> : null}
{isTxMsgSetWithdrawAddress(msg) ? (
<TxMsgSetWithdrawAddressDetails msg={msg} />
) : null}
{isTxMsgCreateVestingAccount(msg) ? (
<TxMsgCreateVestingAccountDetails msg={msg} />
) : null}
{isTxMsgSetWithdrawAddress(msg) ? (<TxMsgSetWithdrawAddressDetails msg={msg} />) : null}
{isTxMsgCreateVestingAccount(msg) ? (<TxMsgCreateVestingAccountDetails msg={msg} />) : null}
{isTxMsgTransfer(msg) ? <TxMsgTransferDetails msg={msg} /> : null}
</StackableContainer>
))}
</StackableContainer>
@@ -0,0 +1,234 @@
import { assert } from "@cosmjs/utils";
import Long from "long";
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 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 },
];
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 [timeout, setTimeout] = 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 [timeoutError, setTimeoutError] = useState("");
useEffect(() => {
setSourcePortError("");
setSourceChannelError("");
setDenomError("");
setAmountError("");
setToAddressError("");
setTimeoutError("");
const isMsgValid = (msg: TxMsg): msg is TxMsgTransfer => {
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, ""); // Allow address from any chain
if (addressErrorMsg) {
setToAddressError(`Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`);
return false;
}
const foundTimeout = humanTimestampOptions.find(({ label }) => label === timeout);
const isTimeoutNumber = !isNaN(Number(timeout));
const isTimeoutInFuture = Number(timeout) > Date.now();
if (!foundTimeout || !isTimeoutNumber || !isTimeoutInFuture) {
setTimeoutError("Timeout must be a valid timestamp in the future");
}
return isTxMsgTransfer(msg);
};
const timeoutTimestamp = (() => {
const foundTimeout = humanTimestampOptions.find(({ label }) => label === timeout)?.value;
if (foundTimeout) {
return Long.fromNumber(Date.now() + foundTimeout).multiply(1_000_000); // In nanoseconds
}
try {
return Long.fromString(timeout);
} catch {
return Long.fromNumber(0);
}
})();
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,
denom,
fromAddress,
memo,
setMsgGetter,
sourceChannel,
sourcePort,
state.chain.chainId,
timeout,
toAddress,
]);
return (
<StackableContainer lessPadding lessMargin>
<button className="remove" onClick={() => deleteMsg()}>
</button>
<h2>MsgTransfer</h2>
<div className="form-item">
<Input
label="Recipient Address"
name="recipient-address"
value={toAddress}
onChange={({ target }) => setToAddress(target.value)}
error={toAddressError}
placeholder={`E.g. ${exampleAddress(0, state.chain.addressPrefix)}`}
/>
</div>
<div className="form-item">
<Input
label="Denom"
name="denom"
value={denom}
onChange={({ target }) => setDenom(target.value)}
error={denomError}
/>
</div>
<div className="form-item">
<Input
type="number"
label="Amount"
name="amount"
value={amount}
onChange={({ target }) => setAmount(target.value)}
error={amountError}
/>
</div>
<div className="form-item">
<Input
label="Source Port"
name="source-port"
value={sourcePort}
onChange={({ target }) => setSourcePort(target.value)}
error={sourcePortError}
/>
</div>
<div className="form-item">
<Input
label="Source Channel"
name="source-channel"
value={sourceChannel}
onChange={({ target }) => setSourceChannel(target.value)}
error={sourceChannelError}
/>
</div>
<div className="form-item">
<Input
type="text"
list="timestamp-options"
label="Timeout"
name="timeout"
placeholder="Enter timestamp in nanoseconds or select from list"
value={timeout}
onChange={({ target }) => setTimeout(target.value)}
onFocus={() => setTimeout("")}
error={timeoutError}
/>
<datalist id="timestamp-options">
{humanTimestampOptions.map(({ label }) => (
<option key={label}>{label}</option>
))}
</datalist>
</div>
<div className="form-item">
<Input
label="Memo"
name="memo"
value={memo}
onChange={({ target }) => setMemo(target.value)}
/>
</div>
<style jsx>{`
.form-item {
margin-top: 1.5em;
}
button.remove {
background: rgba(255, 255, 255, 0.2);
width: 30px;
height: 30px;
border-radius: 50%;
border: none;
color: white;
position: absolute;
right: 10px;
top: 10px;
}
`}</style>
</StackableContainer>
);
};
export default MsgTransferForm;
@@ -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 <MsgSetWithdrawAddressForm delegatorAddress={senderAddress} {...restProps} />;
case "createVestingAccount":
return <MsgCreateVestingAccountForm fromAddress={senderAddress} {...restProps} />;
case "msgTransfer":
return <MsgTransferForm fromAddress={senderAddress} {...restProps} />;
default:
return null;
}
+3 -8
View File
@@ -144,14 +144,9 @@ const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormPro
<Button label="Add MsgUndelegate" onClick={() => addMsgType("undelegate")} />
<Button label="Add MsgBeginRedelegate" onClick={() => addMsgType("redelegate")} />
<Button label="Add MsgWithdrawDelegatorReward" onClick={() => addMsgType("claimRewards")} />
<Button
label="Add MsgSetWithdrawAddress"
onClick={() => addMsgType("setWithdrawAddress")}
/>
<Button
label="Add MsgCreateVestingAccount"
onClick={() => addMsgType("createVestingAccount")}
/>
<Button label="Add MsgSetWithdrawAddress" onClick={() => addMsgType("setWithdrawAddress")} />
<Button label="Add MsgCreateVestingAccount" onClick={() => addMsgType("createVestingAccount")} />
<Button label="Add MsgTransfer" onClick={() => addMsgType("msgTransfer")} />
</StackableContainer>
<Button
label="Create Transaction"
+7 -12
View File
@@ -1,25 +1,20 @@
interface Props {
import { DetailedHTMLProps, InputHTMLAttributes } from "react";
interface InputProps
extends DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> {
label?: string;
type?: string;
name?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
value: number | string | undefined;
min?: string | number | undefined;
checked?: boolean;
onBlur?: (e: React.ChangeEvent<HTMLInputElement>) => void;
disabled?: boolean;
error?: string;
placeholder?: string;
width?: string;
}
const Input = (props: Props) => (
const Input = (props: InputProps) => (
<div className="text-input">
<label>{props.label || ""}</label>
<input
type={props.type || "text"}
list={props.list}
name={props.name || "text-input"}
onChange={props.onChange}
onFocus={props.onFocus}
value={props.value}
checked={props.checked}
min={props.type === "datetime-local" ? props.min : undefined}
+25 -3
View File
@@ -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");
}
@@ -112,16 +127,22 @@ const gasOfTx = (msgTypes: readonly MsgType[]): number => {
return totalTxGas;
};
const importMsgCreateVestingAccount = (msg: EncodeObject): EncodeObject => {
const importMsgType = (msg: EncodeObject): EncodeObject => {
if (isTxMsgCreateVestingAccount(msg)) {
return { ...msg, value: { ...msg.value, endTime: Long.fromValue(msg.value.endTime) } };
}
if (isTxMsgTransfer(msg)) {
return {
...msg,
value: { ...msg.value, timeoutTimestamp: Long.fromValue(msg.value.timeoutTimestamp) },
};
}
return msg;
};
const importMsgTypes = (msgs: readonly EncodeObject[]): EncodeObject[] =>
msgs.map(importMsgCreateVestingAccount);
const importMsgTypes = (msgs: readonly EncodeObject[]): EncodeObject[] => msgs.map(importMsgType);
const dbTxFromJson = (txJson: string): DbTransaction | null => {
try {
@@ -148,6 +169,7 @@ export {
isTxMsgClaimRewards,
isTxMsgSetWithdrawAddress,
isTxMsgCreateVestingAccount,
isTxMsgTransfer,
gasOfMsg,
gasOfTx,
dbTxFromJson,
+13 -3
View File
@@ -1,4 +1,5 @@
import { Coin } from "@cosmjs/amino";
import { Coin, MsgTransferEncodeObject } from "@cosmjs/stargate";
import { MsgTransfer } from "cosmjs-types/ibc/applications/transfer/v1/tx";
export type MsgType =
| "send"
@@ -7,7 +8,8 @@ export type MsgType =
| "redelegate"
| "claimRewards"
| "setWithdrawAddress"
| "createVestingAccount";
| "createVestingAccount"
| "msgTransfer";
export type TxMsg =
| TxMsgSend
@@ -16,7 +18,8 @@ export type TxMsg =
| TxMsgRedelegate
| TxMsgClaimRewards
| TxMsgSetWithdrawAddress
| TxMsgCreateVestingAccount;
| TxMsgCreateVestingAccount
| TxMsgTransfer;
export interface TxMsgSend {
readonly typeUrl: "/cosmos.bank.v1beta1.MsgSend";
@@ -81,3 +84,10 @@ export interface TxMsgCreateVestingAccount {
readonly delayed: boolean;
};
}
type OmmitedMsgTransfer = Omit<MsgTransfer, "timeoutHeight">;
type MsgTransferRequiredToken = OmmitedMsgTransfer & Required<Pick<OmmitedMsgTransfer, "token">>;
export interface TxMsgTransfer extends MsgTransferEncodeObject {
readonly value: Readonly<MsgTransferRequiredToken>;
}