From b555eee3c7a8bef0e804b161c1361863cff04cad Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Wed, 31 May 2023 12:13:45 +0200 Subject: [PATCH 01/18] Use Coin from amino --- types/txMsg.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/types/txMsg.ts b/types/txMsg.ts index 0d256304..e1c8dbaa 100644 --- a/types/txMsg.ts +++ b/types/txMsg.ts @@ -1,3 +1,5 @@ +import { Coin } from "@cosmjs/amino"; + export type MsgType = | "send" | "delegate" @@ -19,7 +21,7 @@ export interface TxMsgSend { readonly value: { readonly fromAddress: string; readonly toAddress: string; - readonly amount: [{ readonly amount: string; readonly denom: string }]; + readonly amount: readonly Readonly[]; }; } @@ -28,7 +30,7 @@ export interface TxMsgDelegate { readonly value: { readonly delegatorAddress: string; readonly validatorAddress: string; - readonly amount: { readonly amount: string; readonly denom: string }; + readonly amount: Readonly; }; } @@ -37,7 +39,7 @@ export interface TxMsgUndelegate { readonly value: { readonly delegatorAddress: string; readonly validatorAddress: string; - readonly amount: { readonly amount: string; readonly denom: string }; + readonly amount: Readonly; }; } @@ -47,7 +49,7 @@ export interface TxMsgRedelegate { readonly delegatorAddress: string; readonly validatorSrcAddress: string; readonly validatorDstAddress: string; - readonly amount: { readonly amount: string; readonly denom: string }; + readonly amount: Readonly; }; } From c17111cf6a81e14e38ba439555ae52b400309ef4 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Wed, 31 May 2023 12:14:05 +0200 Subject: [PATCH 02/18] Add MsgCreateVestingAccount type --- types/txMsg.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/types/txMsg.ts b/types/txMsg.ts index e1c8dbaa..fe133ae8 100644 --- a/types/txMsg.ts +++ b/types/txMsg.ts @@ -6,7 +6,8 @@ export type MsgType = | "undelegate" | "redelegate" | "claimRewards" - | "setWithdrawAddress"; + | "setWithdrawAddress" + | "createVestingAccount"; export type TxMsg = | TxMsgSend @@ -14,7 +15,8 @@ export type TxMsg = | TxMsgUndelegate | TxMsgRedelegate | TxMsgClaimRewards - | TxMsgSetWithdrawAddress; + | TxMsgSetWithdrawAddress + | TxMsgCreateVestingAccount; export interface TxMsgSend { readonly typeUrl: "/cosmos.bank.v1beta1.MsgSend"; @@ -68,3 +70,14 @@ export interface TxMsgSetWithdrawAddress { readonly withdrawAddress: string; }; } + +export interface TxMsgCreateVestingAccount { + readonly typeUrl: "/cosmos.vesting.v1beta1.MsgCreateVestingAccount"; + readonly value: { + readonly fromAddress: string; + readonly toAddress: string; + readonly amount: readonly Readonly[]; + readonly endTime: number; + readonly delayed: boolean; + }; +} From 2b7495f557c8d83079492fbbe62b3caa12c4a082 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Wed, 31 May 2023 12:14:38 +0200 Subject: [PATCH 03/18] Add MsgCreateVestingAccount helpers --- lib/txMsgHelpers.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/txMsgHelpers.ts b/lib/txMsgHelpers.ts index a6d2eb47..8c96f43c 100644 --- a/lib/txMsgHelpers.ts +++ b/lib/txMsgHelpers.ts @@ -3,6 +3,7 @@ import { MsgType, TxMsg, TxMsgClaimRewards, + TxMsgCreateVestingAccount, TxMsgDelegate, TxMsgRedelegate, TxMsgSend, @@ -68,6 +69,20 @@ const isTxMsgSetWithdrawAddress = (msg: TxMsg | EncodeObject): msg is TxMsgSetWi !!msg.value.delegatorAddress && !!msg.value.withdrawAddress; +const isTxMsgCreateVestingAccount = (msg: TxMsg | EncodeObject): msg is TxMsgCreateVestingAccount => + msg.typeUrl === "/cosmos.vesting.v1beta1.MsgCreateVestingAccount" && + "value" in msg && + "fromAddress" in msg.value && + "toAddress" in msg.value && + "amount" in msg.value && + "endTime" in msg.value && + "delayed" in msg.value && + !!msg.value.fromAddress && + !!msg.value.toAddress && + !!msg.value.amount.length && + typeof msg.value.endTime === "number" && + typeof msg.value.delayed === "boolean"; + const gasOfMsg = (msgType: MsgType): number => { switch (msgType) { case "send": @@ -82,6 +97,8 @@ const gasOfMsg = (msgType: MsgType): number => { return 100_000; case "setWithdrawAddress": return 100_000; + case "createVestingAccount": + return 100_000; default: throw new Error("Unknown msg type"); } @@ -100,6 +117,7 @@ export { isTxMsgRedelegate, isTxMsgClaimRewards, isTxMsgSetWithdrawAddress, + isTxMsgCreateVestingAccount, gasOfMsg, gasOfTx, }; From 351c6f3c2b768208d63325ea1517bb882fdf3e56 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Wed, 31 May 2023 12:15:10 +0200 Subject: [PATCH 04/18] Add date helper --- lib/dateHelpers.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 lib/dateHelpers.ts diff --git a/lib/dateHelpers.ts b/lib/dateHelpers.ts new file mode 100644 index 00000000..5782485f --- /dev/null +++ b/lib/dateHelpers.ts @@ -0,0 +1,16 @@ +const timestampFromDatetimeLocal = (datetimeLocal: string): number => { + const [date, time] = datetimeLocal.split("T"); + const [year, month, day] = date.split("-"); + const [hours, minutes] = time.split(":"); + + const dateObj = new Date( + Number(year), + Number(month) - 1, + Number(day), + Number(hours), + Number(minutes), + ); + return Math.floor(dateObj.getTime() / 1000); +}; + +export { timestampFromDatetimeLocal }; From bb5787d04a3fb96a85c9c13fb8df2c769a46df49 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Wed, 31 May 2023 12:15:32 +0200 Subject: [PATCH 05/18] Support checkbox and datetime-local in Input --- components/inputs/Input.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/components/inputs/Input.tsx b/components/inputs/Input.tsx index 2a6632f0..ee0d7017 100644 --- a/components/inputs/Input.tsx +++ b/components/inputs/Input.tsx @@ -1,11 +1,11 @@ -import React from "react"; - interface Props { label?: string; type?: string; name?: string; onChange?: (e: React.ChangeEvent) => void; value: number | string | undefined; + min?: string | number | undefined; + checked?: boolean; onBlur?: (e: React.ChangeEvent) => void; disabled?: boolean; error?: string; @@ -21,6 +21,8 @@ const Input = (props: Props) => ( name={props.name || "text-input"} onChange={props.onChange} value={props.value} + checked={props.checked} + min={props.type === "datetime-local" ? props.min : undefined} placeholder={props.placeholder || ""} autoComplete="off" onBlur={props.onBlur} @@ -29,15 +31,16 @@ const Input = (props: Props) => ( {props.error &&
{props.error}
} + + ); +}; + +export default TxMsgCreateVestingAccountDetails; diff --git a/components/dataViews/TransactionInfo/index.tsx b/components/dataViews/TransactionInfo/index.tsx index f517302c..1da85d57 100644 --- a/components/dataViews/TransactionInfo/index.tsx +++ b/components/dataViews/TransactionInfo/index.tsx @@ -2,6 +2,7 @@ import { useAppContext } from "../../../context/AppContext"; import { printableCoins } from "../../../lib/displayHelpers"; import { isTxMsgClaimRewards, + isTxMsgCreateVestingAccount, isTxMsgDelegate, isTxMsgRedelegate, isTxMsgSend, @@ -11,6 +12,7 @@ import { import { DbTransaction } from "../../../types"; import StackableContainer from "../../layout/StackableContainer"; import TxMsgClaimRewardsDetails from "./TxMsgClaimRewardsDetails"; +import TxMsgCreateVestingAccountDetails from "./TxMsgCreateVestingAccountDetails"; import TxMsgDelegateDetails from "./TxMsgDelegateDetails"; import TxMsgRedelegateDetails from "./TxMsgRedelegateDetails"; import TxMsgSendDetails from "./TxMsgSendDetails"; @@ -59,6 +61,9 @@ const TransactionInfo = ({ tx }: Props) => { {isTxMsgSetWithdrawAddress(msg) ? ( ) : null} + {isTxMsgCreateVestingAccount(msg) ? ( + + ) : null} ))} From cc53c2e77baf06f5c5e57814efa61a5522e79645 Mon Sep 17 00:00:00 2001 From: abefernan <44572727+abefernan@users.noreply.github.com> Date: Wed, 31 May 2023 12:16:14 +0200 Subject: [PATCH 07/18] Add MsgCreateVestingAccountForm --- .../MsgForm/MsgCreateVestingAccountForm.tsx | 189 ++++++++++++++++++ .../forms/CreateTxForm/MsgForm/index.tsx | 3 + components/forms/CreateTxForm/index.tsx | 4 + 3 files changed, 196 insertions(+) create mode 100644 components/forms/CreateTxForm/MsgForm/MsgCreateVestingAccountForm.tsx diff --git a/components/forms/CreateTxForm/MsgForm/MsgCreateVestingAccountForm.tsx b/components/forms/CreateTxForm/MsgForm/MsgCreateVestingAccountForm.tsx new file mode 100644 index 00000000..6626c656 --- /dev/null +++ b/components/forms/CreateTxForm/MsgForm/MsgCreateVestingAccountForm.tsx @@ -0,0 +1,189 @@ +import { Decimal } from "@cosmjs/math"; +import { assert } from "@cosmjs/utils"; +import { useEffect, useState } from "react"; +import { MsgGetter } from ".."; +import { useAppContext } from "../../../../context/AppContext"; +import { timestampFromDatetimeLocal } from "../../../../lib/dateHelpers"; +import { checkAddress, exampleAddress } from "../../../../lib/displayHelpers"; +import { isTxMsgCreateVestingAccount } from "../../../../lib/txMsgHelpers"; +import { TxMsg, TxMsgCreateVestingAccount } from "../../../../types/txMsg"; +import Input from "../../../inputs/Input"; +import StackableContainer from "../../../layout/StackableContainer"; + +/* + One month from now + With stripped seconds and milliseconds + Matching the crazy datetime-local input format +*/ +const getMinEndTime = (): string => { + const minDate = new Date(); + + const minMonth = minDate.getMonth() + 1 + 1; // It's 0-indexed and we want next month + const minMonthStr = minMonth < 10 ? `0${minMonth}` : String(minMonth); + + const minDay = minDate.getDate(); + const minDayStr = minDay < 10 ? `0${minDay}` : String(minDay); + + const minHours = minDate.getHours(); + const minHoursStr = minHours < 10 ? `0${minHours}` : String(minHours); + + const minMinutes = minDate.getMinutes(); + const minMinutesStr = minMinutes < 10 ? `0${minMinutes}` : String(minMinutes); + + return `${minDate.getFullYear()}-${minMonthStr}-${minDayStr}T${minHoursStr}:${minMinutesStr}`; +}; + +interface MsgCreateVestingAccountFormProps { + readonly fromAddress: string; + readonly setMsgGetter: (msgGetter: MsgGetter) => void; + readonly deleteMsg: () => void; +} + +const MsgCreateVestingAccountForm = ({ + fromAddress, + setMsgGetter, + deleteMsg, +}: MsgCreateVestingAccountFormProps) => { + const { state } = useAppContext(); + assert(state.chain.addressPrefix, "addressPrefix missing"); + + const [toAddress, setToAddress] = useState(""); + const [amount, setAmount] = useState("0"); + const minEndTime = getMinEndTime(); + const [endTime, setEndTime] = useState(minEndTime); + const [delayed, setDelayed] = useState(true); + + const [toAddressError, setToAddressError] = useState(""); + const [amountError, setAmountError] = useState(""); + const [endTimeError, setEndTimeError] = useState(""); + + useEffect(() => { + try { + assert(state.chain.denom, "denom missing"); + + setToAddressError(""); + setAmountError(""); + setEndTimeError(""); + + const isMsgValid = (msg: TxMsg): msg is TxMsgCreateVestingAccount => { + assert(state.chain.addressPrefix, "addressPrefix missing"); + + const addressErrorMsg = checkAddress(toAddress, state.chain.addressPrefix); + if (addressErrorMsg) { + setToAddressError( + `Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`, + ); + return false; + } + + if (!amount || Number(amount) <= 0) { + setAmountError("Amount must be greater than 0"); + return false; + } + + if (!endTime) { + setEndTimeError("End time is required"); + return false; + } + + return isTxMsgCreateVestingAccount(msg); + }; + + const amountInAtomics = amount + ? Decimal.fromUserInput(amount, Number(state.chain.displayDenomExponent)).atomics + : "0"; + + const msg: TxMsgCreateVestingAccount = { + typeUrl: "/cosmos.vesting.v1beta1.MsgCreateVestingAccount", + value: { + fromAddress, + toAddress, + amount: [{ amount: amountInAtomics, denom: state.chain.denom }], + endTime: timestampFromDatetimeLocal(endTime), + delayed, + }, + }; + + setMsgGetter({ isMsgValid, msg }); + } catch {} + }, [ + amount, + delayed, + endTime, + fromAddress, + setMsgGetter, + state.chain.addressPrefix, + state.chain.chainId, + state.chain.denom, + state.chain.displayDenomExponent, + toAddress, + ]); + + return ( + + +

MsgCreateVestingAccount

+
+ setToAddress(target.value)} + error={toAddressError} + placeholder={`E.g. ${exampleAddress(0, state.chain.addressPrefix)}`} + /> +
+
+ setAmount(target.value)} + error={amountError} + /> +
+
+ setEndTime(target.value)} + error={endTimeError} + /> +
+
+ setDelayed(target.checked)} + /> +
+ +
+ ); +}; + +export default MsgCreateVestingAccountForm; diff --git a/components/forms/CreateTxForm/MsgForm/index.tsx b/components/forms/CreateTxForm/MsgForm/index.tsx index 62df7649..37602104 100644 --- a/components/forms/CreateTxForm/MsgForm/index.tsx +++ b/components/forms/CreateTxForm/MsgForm/index.tsx @@ -1,6 +1,7 @@ import { MsgGetter } from ".."; import { MsgType } from "../../../../types/txMsg"; import MsgClaimRewardsForm from "./MsgClaimRewardsForm"; +import MsgCreateVestingAccountForm from "./MsgCreateVestingAccountForm"; import MsgDelegateForm from "./MsgDelegateForm"; import MsgRedelegateForm from "./MsgRedelegateForm"; import MsgSendForm from "./MsgSendForm"; @@ -28,6 +29,8 @@ const MsgForm = ({ msgType, senderAddress, ...restProps }: MsgFormProps) => { return ; case "setWithdrawAddress": return ; + case "createVestingAccount": + return ; default: return null; } diff --git a/components/forms/CreateTxForm/index.tsx b/components/forms/CreateTxForm/index.tsx index 27331ddd..9d53e80c 100644 --- a/components/forms/CreateTxForm/index.tsx +++ b/components/forms/CreateTxForm/index.tsx @@ -148,6 +148,10 @@ const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormPro label="Add MsgSetWithdrawAddress" onClick={() => addMsgType("setWithdrawAddress")} /> +