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")} /> +