Skip to content

Commit

Permalink
Merge pull request #128 from cosmos/feat/support-ibc-msgtransfer
Browse files Browse the repository at this point in the history
Support IBC MsgTransfer
  • Loading branch information
abefernan authored Jun 13, 2023
2 parents d16bdc4 + ec71bf3 commit e12b360
Show file tree
Hide file tree
Showing 9 changed files with 369 additions and 32 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ yarn-error.log*

# IDE folder
.idea

# Visual Studio Code
.vscode
76 changes: 76 additions & 0 deletions components/dataViews/TransactionInfo/TxMsgTransferDetails.tsx
Original file line number Diff line number Diff line change
@@ -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;
11 changes: 5 additions & 6 deletions components/dataViews/TransactionInfo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
isTxMsgRedelegate,
isTxMsgSend,
isTxMsgSetWithdrawAddress,
isTxMsgTransfer,
isTxMsgUndelegate,
} from "../../../lib/txMsgHelpers";
import { DbTransaction } from "../../../types";
Expand All @@ -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 {
Expand Down Expand Up @@ -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>
Expand Down
234 changes: 234 additions & 0 deletions components/forms/CreateTxForm/MsgForm/MsgTransferForm.tsx
Original file line number Diff line number Diff line change
@@ -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;
3 changes: 3 additions & 0 deletions components/forms/CreateTxForm/MsgForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
Expand Down
11 changes: 3 additions & 8 deletions components/forms/CreateTxForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading

1 comment on commit e12b360

@vercel
Copy link

@vercel vercel bot commented on e12b360 Jun 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.