diff --git a/components/dataViews/TransactionInfo/TxMsgSetWithdrawAddressDetails.tsx b/components/dataViews/TransactionInfo/TxMsgSetWithdrawAddressDetails.tsx new file mode 100644 index 0000000..56d646e --- /dev/null +++ b/components/dataViews/TransactionInfo/TxMsgSetWithdrawAddressDetails.tsx @@ -0,0 +1,47 @@ +import { TxMsgSetWithdrawAddress } from "../../../types/txMsg"; +import HashView from "../HashView"; + +interface TxMsgSetWithdrawAddressDetailsProps { + readonly msg: TxMsgSetWithdrawAddress; +} + +const TxMsgSetWithdrawAddressDetails = ({ msg }: TxMsgSetWithdrawAddressDetailsProps) => ( + <> +
  • +

    MsgSetWithdrawAddress

    +
  • +
  • + +
    + +
    +
  • + + +); + +export default TxMsgSetWithdrawAddressDetails; diff --git a/components/dataViews/TransactionInfo/index.tsx b/components/dataViews/TransactionInfo/index.tsx index 1537739..722746d 100644 --- a/components/dataViews/TransactionInfo/index.tsx +++ b/components/dataViews/TransactionInfo/index.tsx @@ -5,6 +5,7 @@ import { isTxMsgDelegate, isTxMsgRedelegate, isTxMsgSend, + isTxMsgSetWithdrawAddress, isTxMsgUndelegate, } from "../../../lib/txMsgHelpers"; import { DbTransaction } from "../../../types"; @@ -13,6 +14,7 @@ import TxMsgClaimRewardsDetails from "./TxMsgClaimRewardsDetails"; import TxMsgDelegateDetails from "./TxMsgDelegateDetails"; import TxMsgRedelegateDetails from "./TxMsgRedelegateDetails"; import TxMsgSendDetails from "./TxMsgSendDetails"; +import TxMsgSetWithdrawAddressDetails from "./TxMsgSetWithdrawAddressDetails"; import TxMsgUndelegateDetails from "./TxMsgUndelegateDetails"; interface Props { @@ -23,43 +25,47 @@ const TransactionInfo = ({ tx }: Props) => { const { state } = useAppContext(); return ( - + <> - + ); }; diff --git a/components/forms/CreateTxForm/MsgForm/MsgClaimRewardsForm.tsx b/components/forms/CreateTxForm/MsgForm/MsgClaimRewardsForm.tsx index 24247c9..1251886 100644 --- a/components/forms/CreateTxForm/MsgForm/MsgClaimRewardsForm.tsx +++ b/components/forms/CreateTxForm/MsgForm/MsgClaimRewardsForm.tsx @@ -1,47 +1,69 @@ import { assert } from "@cosmjs/utils"; -import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; +import { MsgGetter } from ".."; import { useAppContext } from "../../../../context/AppContext"; import { checkAddress, exampleAddress } from "../../../../lib/displayHelpers"; +import { isTxMsgClaimRewards } from "../../../../lib/txMsgHelpers"; import { TxMsg, TxMsgClaimRewards } from "../../../../types/txMsg"; import Input from "../../../inputs/Input"; +import StackableContainer from "../../../layout/StackableContainer"; interface MsgClaimRewardsFormProps { readonly delegatorAddress: string; - readonly setCheckAndGetMsg: Dispatch TxMsg | null) | undefined>>; + readonly setMsgGetter: (msgGetter: MsgGetter) => void; + readonly deleteMsg: () => void; } -const MsgClaimRewardsForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgClaimRewardsFormProps) => { +const MsgClaimRewardsForm = ({ + delegatorAddress, + setMsgGetter, + deleteMsg, +}: MsgClaimRewardsFormProps) => { const { state } = useAppContext(); assert(state.chain.addressPrefix, "addressPrefix missing"); const [validatorAddress, setValidatorAddress] = useState(""); const [validatorAddressError, setValidatorAddressError] = useState(""); - const checkAndGetMsg = useCallback(() => { - assert(state.chain.addressPrefix, "addressPrefix missing"); - - const addressErrorMsg = checkAddress(validatorAddress, state.chain.addressPrefix); - if (addressErrorMsg) { - setValidatorAddressError( - `Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`, - ); - return null; - } - - const msg: TxMsgClaimRewards = { - typeUrl: "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", - value: { delegatorAddress, validatorAddress }, - }; - - return msg; - }, [delegatorAddress, state.chain.addressPrefix, state.chain.chainId, validatorAddress]); - useEffect(() => { - setCheckAndGetMsg(() => checkAndGetMsg); - }, [checkAndGetMsg, setCheckAndGetMsg]); + try { + setValidatorAddressError(""); + + const isMsgValid = (msg: TxMsg): msg is TxMsgClaimRewards => { + assert(state.chain.addressPrefix, "addressPrefix missing"); + + const addressErrorMsg = checkAddress(validatorAddress, state.chain.addressPrefix); + if (addressErrorMsg) { + setValidatorAddressError( + `Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`, + ); + return false; + } + + return isTxMsgClaimRewards(msg); + }; + + const msg: TxMsgClaimRewards = { + typeUrl: "/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", + value: { delegatorAddress, validatorAddress }, + }; + + setMsgGetter({ isMsgValid, msg }); + } catch {} + }, [ + delegatorAddress, + setMsgGetter, + state.chain.addressPrefix, + state.chain.chainId, + validatorAddress, + ]); return ( - <> + + +

    MsgWithdrawDelegatorReward

    - +
    ); }; diff --git a/components/forms/CreateTxForm/MsgForm/MsgDelegateForm.tsx b/components/forms/CreateTxForm/MsgForm/MsgDelegateForm.tsx index dc1ef5c..6819458 100644 --- a/components/forms/CreateTxForm/MsgForm/MsgDelegateForm.tsx +++ b/components/forms/CreateTxForm/MsgForm/MsgDelegateForm.tsx @@ -1,17 +1,21 @@ import { Decimal } from "@cosmjs/math"; import { assert } from "@cosmjs/utils"; -import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; +import { MsgGetter } from ".."; import { useAppContext } from "../../../../context/AppContext"; import { checkAddress, exampleAddress } from "../../../../lib/displayHelpers"; +import { isTxMsgDelegate } from "../../../../lib/txMsgHelpers"; import { TxMsg, TxMsgDelegate } from "../../../../types/txMsg"; import Input from "../../../inputs/Input"; +import StackableContainer from "../../../layout/StackableContainer"; interface MsgDelegateFormProps { readonly delegatorAddress: string; - readonly setCheckAndGetMsg: Dispatch TxMsg | null) | undefined>>; + readonly setMsgGetter: (msgGetter: MsgGetter) => void; + readonly deleteMsg: () => void; } -const MsgDelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgDelegateFormProps) => { +const MsgDelegateForm = ({ delegatorAddress, setMsgGetter, deleteMsg }: MsgDelegateFormProps) => { const { state } = useAppContext(); assert(state.chain.addressPrefix, "addressPrefix missing"); @@ -21,41 +25,52 @@ const MsgDelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgDelegateFor const [validatorAddressError, setValidatorAddressError] = useState(""); const [amountError, setAmountError] = useState(""); - const checkAndGetMsg = useCallback(() => { - assert(state.chain.addressPrefix, "addressPrefix missing"); - assert(state.chain.denom, "denom missing"); + useEffect(() => { + try { + assert(state.chain.denom, "denom missing"); - const addressErrorMsg = checkAddress(validatorAddress, state.chain.addressPrefix); - if (addressErrorMsg) { - setValidatorAddressError( - `Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`, - ); - return null; - } + setValidatorAddressError(""); + setAmountError(""); - if (!amount || Number(amount) <= 0) { - setAmountError("Amount must be greater than 0"); - return null; - } + const isMsgValid = (msg: TxMsg): msg is TxMsgDelegate => { + assert(state.chain.addressPrefix, "addressPrefix missing"); - const amountInAtomics = Decimal.fromUserInput( - amount, - Number(state.chain.displayDenomExponent), - ).atomics; + const addressErrorMsg = checkAddress(validatorAddress, state.chain.addressPrefix); + if (addressErrorMsg) { + setValidatorAddressError( + `Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`, + ); + return false; + } - const msg: TxMsgDelegate = { - typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", - value: { - delegatorAddress, - validatorAddress, - amount: { amount: amountInAtomics, denom: state.chain.denom }, - }, - }; + if (!amount || Number(amount) <= 0) { + setAmountError("Amount must be greater than 0"); + return false; + } - return msg; + return isTxMsgDelegate(msg); + }; + + const amountInAtomics = Decimal.fromUserInput( + amount || "0", + Number(state.chain.displayDenomExponent), + ).atomics; + + const msg: TxMsgDelegate = { + typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", + value: { + delegatorAddress, + validatorAddress, + amount: { amount: amountInAtomics, denom: state.chain.denom }, + }, + }; + + setMsgGetter({ isMsgValid, msg }); + } catch {} }, [ amount, delegatorAddress, + setMsgGetter, state.chain.addressPrefix, state.chain.chainId, state.chain.denom, @@ -63,12 +78,12 @@ const MsgDelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgDelegateFor validatorAddress, ]); - useEffect(() => { - setCheckAndGetMsg(() => checkAndGetMsg); - }, [checkAndGetMsg, setCheckAndGetMsg]); - return ( - <> + + +

    MsgDelegate

    - +
    ); }; diff --git a/components/forms/CreateTxForm/MsgForm/MsgRedelegateForm.tsx b/components/forms/CreateTxForm/MsgForm/MsgRedelegateForm.tsx index f9c7814..096b270 100644 --- a/components/forms/CreateTxForm/MsgForm/MsgRedelegateForm.tsx +++ b/components/forms/CreateTxForm/MsgForm/MsgRedelegateForm.tsx @@ -1,17 +1,25 @@ import { Decimal } from "@cosmjs/math"; import { assert } from "@cosmjs/utils"; -import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; +import { MsgGetter } from ".."; import { useAppContext } from "../../../../context/AppContext"; import { checkAddress, exampleAddress } from "../../../../lib/displayHelpers"; +import { isTxMsgRedelegate } from "../../../../lib/txMsgHelpers"; import { TxMsg, TxMsgRedelegate } from "../../../../types/txMsg"; import Input from "../../../inputs/Input"; +import StackableContainer from "../../../layout/StackableContainer"; interface MsgRedelegateFormProps { readonly delegatorAddress: string; - readonly setCheckAndGetMsg: Dispatch TxMsg | null) | undefined>>; + readonly setMsgGetter: (msgGetter: MsgGetter) => void; + readonly deleteMsg: () => void; } -const MsgRedelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgRedelegateFormProps) => { +const MsgRedelegateForm = ({ + delegatorAddress, + setMsgGetter, + deleteMsg, +}: MsgRedelegateFormProps) => { const { state } = useAppContext(); assert(state.chain.addressPrefix, "addressPrefix missing"); @@ -23,50 +31,62 @@ const MsgRedelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgRedelegat const [validatorDstAddressError, setValidatorDstAddressError] = useState(""); const [amountError, setAmountError] = useState(""); - const checkAndGetMsg = useCallback(() => { - assert(state.chain.addressPrefix, "addressPrefix missing"); - assert(state.chain.denom, "denom missing"); + useEffect(() => { + try { + assert(state.chain.denom, "denom missing"); - const srcAddressErrorMsg = checkAddress(validatorSrcAddress, state.chain.addressPrefix); - if (srcAddressErrorMsg) { - setValidatorSrcAddressError( - `Invalid address for network ${state.chain.chainId}: ${srcAddressErrorMsg}`, - ); - return null; - } + setValidatorSrcAddressError(""); + setValidatorDstAddressError(""); + setAmountError(""); - const dstAddressErrorMsg = checkAddress(validatorDstAddress, state.chain.addressPrefix); - if (dstAddressErrorMsg) { - setValidatorDstAddressError( - `Invalid address for network ${state.chain.chainId}: ${dstAddressErrorMsg}`, - ); - return null; - } + const isMsgValid = (msg: TxMsg): msg is TxMsgRedelegate => { + assert(state.chain.addressPrefix, "addressPrefix missing"); - if (!amount || Number(amount) <= 0) { - setAmountError("Amount must be greater than 0"); - return null; - } + const srcAddressErrorMsg = checkAddress(validatorSrcAddress, state.chain.addressPrefix); + if (srcAddressErrorMsg) { + setValidatorSrcAddressError( + `Invalid address for network ${state.chain.chainId}: ${srcAddressErrorMsg}`, + ); + return false; + } - const amountInAtomics = Decimal.fromUserInput( - amount, - Number(state.chain.displayDenomExponent), - ).atomics; + const dstAddressErrorMsg = checkAddress(validatorDstAddress, state.chain.addressPrefix); + if (dstAddressErrorMsg) { + setValidatorDstAddressError( + `Invalid address for network ${state.chain.chainId}: ${dstAddressErrorMsg}`, + ); + return false; + } - const msg: TxMsgRedelegate = { - typeUrl: "/cosmos.staking.v1beta1.MsgBeginRedelegate", - value: { - delegatorAddress, - validatorSrcAddress, - validatorDstAddress, - amount: { amount: amountInAtomics, denom: state.chain.denom }, - }, - }; + if (!amount || Number(amount) <= 0) { + setAmountError("Amount must be greater than 0"); + return false; + } - return msg; + return isTxMsgRedelegate(msg); + }; + + const amountInAtomics = Decimal.fromUserInput( + amount || "0", + Number(state.chain.displayDenomExponent), + ).atomics; + + const msg: TxMsgRedelegate = { + typeUrl: "/cosmos.staking.v1beta1.MsgBeginRedelegate", + value: { + delegatorAddress, + validatorSrcAddress, + validatorDstAddress, + amount: { amount: amountInAtomics, denom: state.chain.denom }, + }, + }; + + setMsgGetter({ isMsgValid, msg }); + } catch {} }, [ amount, delegatorAddress, + setMsgGetter, state.chain.addressPrefix, state.chain.chainId, state.chain.denom, @@ -75,12 +95,12 @@ const MsgRedelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgRedelegat validatorSrcAddress, ]); - useEffect(() => { - setCheckAndGetMsg(() => checkAndGetMsg); - }, [checkAndGetMsg, setCheckAndGetMsg]); - return ( - <> + + +

    MsgBeginRedelegate

    - +
    ); }; diff --git a/components/forms/CreateTxForm/MsgForm/MsgSendForm.tsx b/components/forms/CreateTxForm/MsgForm/MsgSendForm.tsx index e96c023..e667cdb 100644 --- a/components/forms/CreateTxForm/MsgForm/MsgSendForm.tsx +++ b/components/forms/CreateTxForm/MsgForm/MsgSendForm.tsx @@ -1,17 +1,21 @@ import { Decimal } from "@cosmjs/math"; import { assert } from "@cosmjs/utils"; -import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; +import { MsgGetter } from ".."; import { useAppContext } from "../../../../context/AppContext"; import { checkAddress, exampleAddress } from "../../../../lib/displayHelpers"; +import { isTxMsgSend } from "../../../../lib/txMsgHelpers"; import { TxMsg, TxMsgSend } from "../../../../types/txMsg"; import Input from "../../../inputs/Input"; +import StackableContainer from "../../../layout/StackableContainer"; interface MsgSendFormProps { readonly fromAddress: string; - readonly setCheckAndGetMsg: Dispatch TxMsg | null) | undefined>>; + readonly setMsgGetter: (msgGetter: MsgGetter) => void; + readonly deleteMsg: () => void; } -const MsgSendForm = ({ fromAddress, setCheckAndGetMsg }: MsgSendFormProps) => { +const MsgSendForm = ({ fromAddress, setMsgGetter, deleteMsg }: MsgSendFormProps) => { const { state } = useAppContext(); assert(state.chain.addressPrefix, "addressPrefix missing"); @@ -21,39 +25,51 @@ const MsgSendForm = ({ fromAddress, setCheckAndGetMsg }: MsgSendFormProps) => { const [toAddressError, setToAddressError] = useState(""); const [amountError, setAmountError] = useState(""); - const checkAndGetMsg = useCallback(() => { - assert(state.chain.addressPrefix, "addressPrefix missing"); - assert(state.chain.denom, "denom missing"); + useEffect(() => { + try { + assert(state.chain.denom, "denom missing"); - const addressErrorMsg = checkAddress(toAddress, state.chain.addressPrefix); - if (addressErrorMsg) { - setToAddressError(`Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`); - return null; - } + setToAddressError(""); + setAmountError(""); - if (!amount || Number(amount) <= 0) { - setAmountError("Amount must be greater than 0"); - return null; - } + const isMsgValid = (msg: TxMsg): msg is TxMsgSend => { + assert(state.chain.addressPrefix, "addressPrefix missing"); - const amountInAtomics = Decimal.fromUserInput( - amount, - Number(state.chain.displayDenomExponent), - ).atomics; + const addressErrorMsg = checkAddress(toAddress, state.chain.addressPrefix); + if (addressErrorMsg) { + setToAddressError( + `Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`, + ); + return false; + } - const msg: TxMsgSend = { - typeUrl: "/cosmos.bank.v1beta1.MsgSend", - value: { - fromAddress, - toAddress, - amount: [{ amount: amountInAtomics, denom: state.chain.denom }], - }, - }; + if (!amount || Number(amount) <= 0) { + setAmountError("Amount must be greater than 0"); + return false; + } - return msg; + return isTxMsgSend(msg); + }; + + const amountInAtomics = amount + ? Decimal.fromUserInput(amount, Number(state.chain.displayDenomExponent)).atomics + : "0"; + + const msg: TxMsgSend = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: { + fromAddress, + toAddress, + amount: [{ amount: amountInAtomics, denom: state.chain.denom }], + }, + }; + + setMsgGetter({ isMsgValid, msg }); + } catch {} }, [ amount, fromAddress, + setMsgGetter, state.chain.addressPrefix, state.chain.chainId, state.chain.denom, @@ -61,12 +77,12 @@ const MsgSendForm = ({ fromAddress, setCheckAndGetMsg }: MsgSendFormProps) => { toAddress, ]); - useEffect(() => { - setCheckAndGetMsg(() => checkAndGetMsg); - }, [checkAndGetMsg, setCheckAndGetMsg]); - return ( - <> + + +

    MsgSend

    { />
    - +
    ); }; diff --git a/components/forms/CreateTxForm/MsgForm/MsgSetWithdrawAddressForm.tsx b/components/forms/CreateTxForm/MsgForm/MsgSetWithdrawAddressForm.tsx new file mode 100644 index 0000000..530ee56 --- /dev/null +++ b/components/forms/CreateTxForm/MsgForm/MsgSetWithdrawAddressForm.tsx @@ -0,0 +1,97 @@ +import { assert } from "@cosmjs/utils"; +import { useEffect, useState } from "react"; +import { MsgGetter } from ".."; +import { useAppContext } from "../../../../context/AppContext"; +import { checkAddress, exampleAddress } from "../../../../lib/displayHelpers"; +import { isTxMsgSetWithdrawAddress } from "../../../../lib/txMsgHelpers"; +import { TxMsg, TxMsgSetWithdrawAddress } from "../../../../types/txMsg"; +import Input from "../../../inputs/Input"; +import StackableContainer from "../../../layout/StackableContainer"; + +interface MsgSetWithdrawAddressFormProps { + readonly delegatorAddress: string; + readonly setMsgGetter: (msgGetter: MsgGetter) => void; + readonly deleteMsg: () => void; +} + +const MsgSetWithdrawAddressForm = ({ + delegatorAddress, + setMsgGetter, + deleteMsg, +}: MsgSetWithdrawAddressFormProps) => { + const { state } = useAppContext(); + assert(state.chain.addressPrefix, "addressPrefix missing"); + + const [withdrawAddress, setWithdrawAddress] = useState(""); + const [withdrawAddressError, setWithdrawAddressError] = useState(""); + + useEffect(() => { + try { + setWithdrawAddressError(""); + + const isMsgValid = (msg: TxMsg): msg is TxMsgSetWithdrawAddress => { + assert(state.chain.addressPrefix, "addressPrefix missing"); + + const addressErrorMsg = checkAddress(withdrawAddress, state.chain.addressPrefix); + if (addressErrorMsg) { + setWithdrawAddressError( + `Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`, + ); + return false; + } + + return isTxMsgSetWithdrawAddress(msg); + }; + + const msg: TxMsgSetWithdrawAddress = { + typeUrl: "/cosmos.distribution.v1beta1.MsgSetWithdrawAddress", + value: { delegatorAddress, withdrawAddress }, + }; + + setMsgGetter({ isMsgValid, msg }); + } catch {} + }, [ + delegatorAddress, + setMsgGetter, + state.chain.addressPrefix, + state.chain.chainId, + withdrawAddress, + ]); + + return ( + + +

    MsgSetWithdrawAddress

    +
    + setWithdrawAddress(target.value)} + error={withdrawAddressError} + placeholder={`E.g. ${exampleAddress(0, state.chain.addressPrefix)}`} + /> +
    + +
    + ); +}; + +export default MsgSetWithdrawAddressForm; diff --git a/components/forms/CreateTxForm/MsgForm/MsgUndelegateForm.tsx b/components/forms/CreateTxForm/MsgForm/MsgUndelegateForm.tsx index e38a9d7..fccb754 100644 --- a/components/forms/CreateTxForm/MsgForm/MsgUndelegateForm.tsx +++ b/components/forms/CreateTxForm/MsgForm/MsgUndelegateForm.tsx @@ -1,17 +1,25 @@ import { Decimal } from "@cosmjs/math"; import { assert } from "@cosmjs/utils"; -import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; +import { MsgGetter } from ".."; import { useAppContext } from "../../../../context/AppContext"; import { checkAddress, exampleAddress } from "../../../../lib/displayHelpers"; +import { isTxMsgUndelegate } from "../../../../lib/txMsgHelpers"; import { TxMsg, TxMsgUndelegate } from "../../../../types/txMsg"; import Input from "../../../inputs/Input"; +import StackableContainer from "../../../layout/StackableContainer"; interface MsgUndelegateFormProps { readonly delegatorAddress: string; - readonly setCheckAndGetMsg: Dispatch TxMsg | null) | undefined>>; + readonly setMsgGetter: (msgGetter: MsgGetter) => void; + readonly deleteMsg: () => void; } -const MsgUndelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgUndelegateFormProps) => { +const MsgUndelegateForm = ({ + delegatorAddress, + setMsgGetter, + deleteMsg, +}: MsgUndelegateFormProps) => { const { state } = useAppContext(); assert(state.chain.addressPrefix, "addressPrefix missing"); @@ -21,41 +29,52 @@ const MsgUndelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgUndelegat const [validatorAddressError, setValidatorAddressError] = useState(""); const [amountError, setAmountError] = useState(""); - const checkAndGetMsg = useCallback(() => { - assert(state.chain.addressPrefix, "addressPrefix missing"); - assert(state.chain.denom, "denom missing"); + useEffect(() => { + try { + assert(state.chain.denom, "denom missing"); - const addressErrorMsg = checkAddress(validatorAddress, state.chain.addressPrefix); - if (addressErrorMsg) { - setValidatorAddressError( - `Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`, - ); - return null; - } + setValidatorAddressError(""); + setAmountError(""); - if (!amount || Number(amount) <= 0) { - setAmountError("Amount must be greater than 0"); - return null; - } + const isMsgValid = (msg: TxMsg): msg is TxMsgUndelegate => { + assert(state.chain.addressPrefix, "addressPrefix missing"); - const amountInAtomics = Decimal.fromUserInput( - amount, - Number(state.chain.displayDenomExponent), - ).atomics; + const addressErrorMsg = checkAddress(validatorAddress, state.chain.addressPrefix); + if (addressErrorMsg) { + setValidatorAddressError( + `Invalid address for network ${state.chain.chainId}: ${addressErrorMsg}`, + ); + return false; + } - const msg: TxMsgUndelegate = { - typeUrl: "/cosmos.staking.v1beta1.MsgUndelegate", - value: { - delegatorAddress, - validatorAddress, - amount: { amount: amountInAtomics, denom: state.chain.denom }, - }, - }; + if (!amount || Number(amount) <= 0) { + setAmountError("Amount must be greater than 0"); + return false; + } - return msg; + return isTxMsgUndelegate(msg); + }; + + const amountInAtomics = Decimal.fromUserInput( + amount || "0", + Number(state.chain.displayDenomExponent), + ).atomics; + + const msg: TxMsgUndelegate = { + typeUrl: "/cosmos.staking.v1beta1.MsgUndelegate", + value: { + delegatorAddress, + validatorAddress, + amount: { amount: amountInAtomics, denom: state.chain.denom }, + }, + }; + + setMsgGetter({ isMsgValid, msg }); + } catch {} }, [ amount, delegatorAddress, + setMsgGetter, state.chain.addressPrefix, state.chain.chainId, state.chain.denom, @@ -63,12 +82,12 @@ const MsgUndelegateForm = ({ delegatorAddress, setCheckAndGetMsg }: MsgUndelegat validatorAddress, ]); - useEffect(() => { - setCheckAndGetMsg(() => checkAndGetMsg); - }, [checkAndGetMsg, setCheckAndGetMsg]); - return ( - <> + + +

    MsgUndelegate

    - +
    ); }; diff --git a/components/forms/CreateTxForm/MsgForm/index.tsx b/components/forms/CreateTxForm/MsgForm/index.tsx index a9be224..62df764 100644 --- a/components/forms/CreateTxForm/MsgForm/index.tsx +++ b/components/forms/CreateTxForm/MsgForm/index.tsx @@ -1,20 +1,21 @@ -import { Dispatch, SetStateAction } from "react"; -import { TxMsg, TxType } from "../../../../types/txMsg"; +import { MsgGetter } from ".."; +import { MsgType } from "../../../../types/txMsg"; import MsgClaimRewardsForm from "./MsgClaimRewardsForm"; import MsgDelegateForm from "./MsgDelegateForm"; import MsgRedelegateForm from "./MsgRedelegateForm"; import MsgSendForm from "./MsgSendForm"; +import MsgSetWithdrawAddressForm from "./MsgSetWithdrawAddressForm"; import MsgUndelegateForm from "./MsgUndelegateForm"; -type MsgFormProps = { - readonly txType: TxType; -} & { +interface MsgFormProps { + readonly msgType: MsgType; readonly senderAddress: string; - readonly setCheckAndGetMsg: Dispatch TxMsg | null) | undefined>>; -}; + readonly setMsgGetter: (msgGetter: MsgGetter) => void; + readonly deleteMsg: () => void; +} -const MsgForm = ({ txType, senderAddress, ...restProps }: MsgFormProps) => { - switch (txType) { +const MsgForm = ({ msgType, senderAddress, ...restProps }: MsgFormProps) => { + switch (msgType) { case "send": return ; case "delegate": @@ -25,6 +26,8 @@ const MsgForm = ({ txType, senderAddress, ...restProps }: MsgFormProps) => { return ; case "claimRewards": return ; + case "setWithdrawAddress": + return ; default: return null; } diff --git a/components/forms/CreateTxForm/index.tsx b/components/forms/CreateTxForm/index.tsx index 1ccef67..27331dd 100644 --- a/components/forms/CreateTxForm/index.tsx +++ b/components/forms/CreateTxForm/index.tsx @@ -2,47 +2,59 @@ import { Account, calculateFee } from "@cosmjs/stargate"; import { assert } from "@cosmjs/utils"; import axios from "axios"; import { NextRouter, withRouter } from "next/router"; -import { useState } from "react"; +import { useRef, useState } from "react"; import { useAppContext } from "../../../context/AppContext"; -import { capitalizeFirstLetter } from "../../../lib/displayHelpers"; -import { TxMsg, TxType } from "../../../types/txMsg"; +import { gasOfTx } from "../../../lib/txMsgHelpers"; +import { MsgType, TxMsg } from "../../../types/txMsg"; import Button from "../../inputs/Button"; import Input from "../../inputs/Input"; import StackableContainer from "../../layout/StackableContainer"; import MsgForm from "./MsgForm"; -interface CreateTxFormProps { - readonly router: NextRouter; - readonly txType: TxType; - readonly senderAddress: string; - readonly accountOnChain: Account; - readonly closeForm: () => void; +export interface MsgGetter { + readonly isMsgValid: (msg: TxMsg) => msg is TxMsg; + readonly msg: TxMsg; } -const CreateTxForm = ({ - router, - txType, - senderAddress, - accountOnChain, - closeForm, -}: CreateTxFormProps) => { +const getMsgFormKey = (msgType: MsgType, msg: TxMsg) => JSON.stringify({ msgType, msg }); + +interface CreateTxFormProps { + readonly router: NextRouter; + readonly senderAddress: string; + readonly accountOnChain: Account; +} + +const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormProps) => { const { state } = useAppContext(); const [processing, setProcessing] = useState(false); - const [checkAndGetMsg, setCheckAndGetMsg] = useState<() => TxMsg | null>(); + const [msgTypes, setMsgTypes] = useState([]); + const msgGetters = useRef([]); const [memo, setMemo] = useState(""); - const [gasLimit, setGasLimit] = useState(200000); + const [gasLimit, setGasLimit] = useState(gasOfTx([])); const [gasLimitError, setGasLimitError] = useState(""); const gasPrice = state.chain.gasPrice; assert(gasPrice, "gasPrice missing"); + const addMsgType = (newMsgType: MsgType) => { + setMsgTypes((oldMsgTypes) => { + const newMsgTypes = [...oldMsgTypes, newMsgType]; + setGasLimit(gasOfTx(newMsgTypes)); + return newMsgTypes; + }); + }; + const createTx = async () => { try { assert(typeof accountOnChain.accountNumber === "number", "accountNumber missing"); - assert(!!checkAndGetMsg, "form filled incorrectly"); - const msg = checkAndGetMsg(); - if (!msg) return; + assert(msgGetters.current.length, "form filled incorrectly"); + + const msgs = msgGetters.current + .filter(({ isMsgValid, msg }) => isMsgValid(msg)) + .map(({ msg }) => msg); + + if (!msgs.length || msgs.length !== msgTypes.length) return; if (!Number.isSafeInteger(gasLimit) || gasLimit <= 0) { setGasLimitError("gas limit must be a positive integer"); @@ -55,7 +67,7 @@ const CreateTxForm = ({ accountNumber: accountOnChain.accountNumber, sequence: accountOnChain.sequence, chainId: state.chain.chainId, - msgs: [msg], + msgs, fee: calculateFee(gasLimit, gasPrice), memo, }; @@ -76,15 +88,30 @@ const CreateTxForm = ({ return ( - -

    Create New {capitalizeFirstLetter(txType)} Transaction

    - +

    Create New Transaction

    + {msgTypes.map((msgType, index) => ( + { + msgGetters.current = [ + ...msgGetters.current.slice(0, index), + msgGetter, + ...msgGetters.current.slice(index + 1), + ]; + }} + deleteMsg={() => { + msgGetters.current.splice(index, 1); + setMsgTypes((oldMsgTypes) => { + const newMsgTypes: MsgType[] = oldMsgTypes.slice(); + newMsgTypes.splice(index, 1); + setGasLimit(gasOfTx(newMsgTypes)); + return newMsgTypes; + }); + }} + /> + ))}
    setMemo(target.value)} />
    + +