diff --git a/components/dataViews/TransactionInfo/TxMsgCreateVestingAccountDetails.tsx b/components/dataViews/TransactionInfo/TxMsgCreateVestingAccountDetails.tsx
new file mode 100644
index 00000000..421e39bb
--- /dev/null
+++ b/components/dataViews/TransactionInfo/TxMsgCreateVestingAccountDetails.tsx
@@ -0,0 +1,80 @@
+import { useAppContext } from "../../../context/AppContext";
+import { printableCoins } from "../../../lib/displayHelpers";
+import { TxMsgCreateVestingAccount } from "../../../types/txMsg";
+import HashView from "../HashView";
+
+interface TxMsgCreateVestingAccountDetailsProps {
+ readonly msg: TxMsgCreateVestingAccount;
+}
+
+const TxMsgCreateVestingAccountDetails = ({ msg }: TxMsgCreateVestingAccountDetailsProps) => {
+ const { state } = useAppContext();
+ const endTimeDateObj = new Date(msg.value.endTime.multiply(1000).toNumber());
+ const endTimeDate = endTimeDateObj.toLocaleDateString();
+ const endTimeHours = endTimeDateObj.toLocaleTimeString().slice(0, -3);
+
+ return (
+ <>
+
+ MsgCreateVestingAccount
+
+
+
+ {printableCoins(msg.value.amount, state.chain)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {endTimeDate} {endTimeHours}
+
+
+
+
+
+
+
{msg.value.delayed ? "Yes" : "No"}
+
+
+
+ >
+ );
+};
+
+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}
))}
diff --git a/components/forms/CreateTxForm/MsgForm/MsgCreateVestingAccountForm.tsx b/components/forms/CreateTxForm/MsgForm/MsgCreateVestingAccountForm.tsx
new file mode 100644
index 00000000..424515de
--- /dev/null
+++ b/components/forms/CreateTxForm/MsgForm/MsgCreateVestingAccountForm.tsx
@@ -0,0 +1,190 @@
+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 minTimestamp = Date.now() + 1000 * 60 * 60 * 24 * 30;
+ const minDate = new Date(minTimestamp);
+
+ const minMonth = minDate.getMonth() + 1; // It's 0-indexed
+ 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")}
/>
+