diff --git a/components/dataViews/TransactionInfo/TxMsgExecuteContractDetails.tsx b/components/dataViews/TransactionInfo/TxMsgExecuteContractDetails.tsx new file mode 100644 index 00000000..ea943012 --- /dev/null +++ b/components/dataViews/TransactionInfo/TxMsgExecuteContractDetails.tsx @@ -0,0 +1,92 @@ +import { fromUtf8 } from "@cosmjs/encoding"; +import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; +import dynamic from "next/dynamic"; +import { useState } from "react"; +import { JSONValue } from "vanilla-jsoneditor"; +import { useChains } from "../../../context/ChainsContext"; +import { printableCoins } from "../../../lib/displayHelpers"; +import HashView from "../HashView"; + +const JsonEditor = dynamic(() => import("../../inputs/JsonEditor"), { ssr: false }); + +interface TxMsgExecuteContractDetailsProps { + readonly msgValue: MsgExecuteContract; +} + +const TxMsgExecuteContractDetails = ({ msgValue }: TxMsgExecuteContractDetailsProps) => { + const { chain } = useChains(); + const [parseError, setParseError] = useState(""); + + const json: JSONValue = (() => { + if (parseError) { + return {}; + } + + try { + return JSON.parse(fromUtf8(msgValue.msg)); + } catch (e) { + setParseError(e instanceof Error ? e.message : "Failed to decode UTF-8 msg"); + return {}; + } + })(); + + return ( + <> +
  • +

    MsgExecuteContract

    +
  • +
  • + +
    + +
    +
  • +
  • + +
    {printableCoins(msgValue.funds, chain)}
    +
  • + {parseError ? ( +
  • +

    {parseError}

    +
  • + ) : ( +
  • + +
  • + )} + + + ); +}; + +export default TxMsgExecuteContractDetails; diff --git a/components/dataViews/TransactionInfo/TxMsgInstantiateContract2Details.tsx b/components/dataViews/TransactionInfo/TxMsgInstantiateContract2Details.tsx new file mode 100644 index 00000000..c390a8d9 --- /dev/null +++ b/components/dataViews/TransactionInfo/TxMsgInstantiateContract2Details.tsx @@ -0,0 +1,108 @@ +import { fromUtf8, toHex } from "@cosmjs/encoding"; +import { MsgInstantiateContract2 } from "cosmjs-types/cosmwasm/wasm/v1/tx"; +import dynamic from "next/dynamic"; +import { useState } from "react"; +import { JSONValue } from "vanilla-jsoneditor"; +import { useChains } from "../../../context/ChainsContext"; +import { printableCoins } from "../../../lib/displayHelpers"; +import HashView from "../HashView"; + +const JsonEditor = dynamic(() => import("../../inputs/JsonEditor"), { ssr: false }); + +interface TxMsgInstantiateContract2DetailsProps { + readonly msgValue: MsgInstantiateContract2; +} + +const TxMsgInstantiateContract2Details = ({ msgValue }: TxMsgInstantiateContract2DetailsProps) => { + const { chain } = useChains(); + const [parseError, setParseError] = useState(""); + + const json: JSONValue = (() => { + if (parseError) { + return {}; + } + + try { + return JSON.parse(fromUtf8(msgValue.msg)); + } catch (e) { + setParseError(e instanceof Error ? e.message : "Failed to decode UTF-8 msg"); + return {}; + } + })(); + + return ( + <> +
  • +

    MsgInstantiateContract2

    +
  • +
  • + +
    {msgValue.codeId.toString()}
    +
  • +
  • + +
    {msgValue.label || "None"}
    +
  • +
  • + + {msgValue.admin ? ( +
    + +
    + ) : ( +
    None
    + )} +
  • +
  • + +
    {toHex(msgValue.salt)}
    +
  • +
  • + +
    {printableCoins(msgValue.funds, chain)}
    +
  • + {parseError ? ( +
  • +

    {parseError}

    +
  • + ) : ( +
  • + +
  • + )} + + + ); +}; + +export default TxMsgInstantiateContract2Details; diff --git a/components/dataViews/TransactionInfo/TxMsgInstantiateContractDetails.tsx b/components/dataViews/TransactionInfo/TxMsgInstantiateContractDetails.tsx new file mode 100644 index 00000000..fe6513a7 --- /dev/null +++ b/components/dataViews/TransactionInfo/TxMsgInstantiateContractDetails.tsx @@ -0,0 +1,104 @@ +import { fromUtf8 } from "@cosmjs/encoding"; +import { MsgInstantiateContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; +import dynamic from "next/dynamic"; +import { useState } from "react"; +import { JSONValue } from "vanilla-jsoneditor"; +import { useChains } from "../../../context/ChainsContext"; +import { printableCoins } from "../../../lib/displayHelpers"; +import HashView from "../HashView"; + +const JsonEditor = dynamic(() => import("../../inputs/JsonEditor"), { ssr: false }); + +interface TxMsgInstantiateContractDetailsProps { + readonly msgValue: MsgInstantiateContract; +} + +const TxMsgInstantiateContractDetails = ({ msgValue }: TxMsgInstantiateContractDetailsProps) => { + const { chain } = useChains(); + const [parseError, setParseError] = useState(""); + + const json: JSONValue = (() => { + if (parseError) { + return {}; + } + + try { + return JSON.parse(fromUtf8(msgValue.msg)); + } catch (e) { + setParseError(e instanceof Error ? e.message : "Failed to decode UTF-8 msg"); + return {}; + } + })(); + + return ( + <> +
  • +

    MsgInstantiateContract

    +
  • +
  • + +
    {msgValue.codeId.toString()}
    +
  • +
  • + +
    {msgValue.label || "None"}
    +
  • +
  • + + {msgValue.admin ? ( +
    + +
    + ) : ( +
    None
    + )} +
  • +
  • + +
    {printableCoins(msgValue.funds, chain)}
    +
  • + {parseError ? ( +
  • +

    {parseError}

    +
  • + ) : ( +
  • + +
  • + )} + + + ); +}; + +export default TxMsgInstantiateContractDetails; diff --git a/components/dataViews/TransactionInfo/TxMsgMigrateContractDetails.tsx b/components/dataViews/TransactionInfo/TxMsgMigrateContractDetails.tsx new file mode 100644 index 00000000..086222e7 --- /dev/null +++ b/components/dataViews/TransactionInfo/TxMsgMigrateContractDetails.tsx @@ -0,0 +1,89 @@ +import { fromUtf8 } from "@cosmjs/encoding"; +import { MsgMigrateContract } from "cosmjs-types/cosmwasm/wasm/v1/tx"; +import dynamic from "next/dynamic"; +import { useState } from "react"; +import { JSONValue } from "vanilla-jsoneditor"; +import HashView from "../HashView"; + +const JsonEditor = dynamic(() => import("../../inputs/JsonEditor"), { ssr: false }); + +interface TxMsgMigrateContractDetailsProps { + readonly msgValue: MsgMigrateContract; +} + +const TxMsgMigrateContractDetails = ({ msgValue }: TxMsgMigrateContractDetailsProps) => { + const [parseError, setParseError] = useState(""); + + const json: JSONValue = (() => { + if (parseError) { + return {}; + } + + try { + return JSON.parse(fromUtf8(msgValue.msg)); + } catch (e) { + setParseError(e instanceof Error ? e.message : "Failed to decode UTF-8 msg"); + return {}; + } + })(); + + return ( + <> +
  • +

    MsgMigrateContract

    +
  • +
  • + +
    + +
    +
  • +
  • + +
    {msgValue.codeId.toString()}
    +
  • + {parseError ? ( +
  • +

    {parseError}

    +
  • + ) : ( +
  • + +
  • + )} + + + ); +}; + +export default TxMsgMigrateContractDetails; diff --git a/components/dataViews/TransactionInfo/index.tsx b/components/dataViews/TransactionInfo/index.tsx index 5e3a46a9..08196610 100644 --- a/components/dataViews/TransactionInfo/index.tsx +++ b/components/dataViews/TransactionInfo/index.tsx @@ -7,6 +7,10 @@ import StackableContainer from "../../layout/StackableContainer"; import TxMsgClaimRewardsDetails from "./TxMsgClaimRewardsDetails"; import TxMsgCreateVestingAccountDetails from "./TxMsgCreateVestingAccountDetails"; import TxMsgDelegateDetails from "./TxMsgDelegateDetails"; +import TxMsgExecuteContractDetails from "./TxMsgExecuteContractDetails"; +import TxMsgInstantiateContract2Details from "./TxMsgInstantiateContract2Details"; +import TxMsgInstantiateContractDetails from "./TxMsgInstantiateContractDetails"; +import TxMsgMigrateContractDetails from "./TxMsgMigrateContractDetails"; import TxMsgRedelegateDetails from "./TxMsgRedelegateDetails"; import TxMsgSendDetails from "./TxMsgSendDetails"; import TxMsgSetWithdrawAddressDetails from "./TxMsgSetWithdrawAddressDetails"; @@ -31,6 +35,14 @@ const TxMsgDetails = ({ typeUrl, value: msgValue }: EncodeObject) => { return ; case MsgTypeUrls.Transfer: return ; + case MsgTypeUrls.Execute: + return ; + case MsgTypeUrls.Instantiate: + return ; + case MsgTypeUrls.Instantiate2: + return ; + case MsgTypeUrls.Migrate: + return ; default: return null; } diff --git a/components/forms/CreateTxForm/MsgForm/MsgExecuteContractForm.tsx b/components/forms/CreateTxForm/MsgForm/MsgExecuteContractForm.tsx new file mode 100644 index 00000000..a335b984 --- /dev/null +++ b/components/forms/CreateTxForm/MsgForm/MsgExecuteContractForm.tsx @@ -0,0 +1,225 @@ +import { MsgExecuteContractEncodeObject } from "@cosmjs/cosmwasm-stargate"; +import { toUtf8 } from "@cosmjs/encoding"; +import dynamic from "next/dynamic"; +import { useEffect, useRef, useState } from "react"; +import { MsgGetter } from ".."; +import { useChains } from "../../../../context/ChainsContext"; +import { ChainInfo } from "../../../../context/ChainsContext/types"; +import { macroCoinToMicroCoin } from "../../../../lib/coinHelpers"; +import { checkAddress, exampleAddress } from "../../../../lib/displayHelpers"; +import { MsgCodecs, MsgTypeUrls } from "../../../../types/txMsg"; +import Input from "../../../inputs/Input"; +import Select from "../../../inputs/Select"; +import StackableContainer from "../../../layout/StackableContainer"; + +const JsonEditor = dynamic(() => import("../../../inputs/JsonEditor"), { ssr: false }); + +const customDenomOption = { label: "Custom (enter denom below)", value: "custom" } as const; + +const getDenomOptions = (assets: ChainInfo["assets"]) => { + if (!assets?.length) { + return [customDenomOption]; + } + + return [...assets.map((asset) => ({ label: asset.symbol, value: asset })), customDenomOption]; +}; + +interface MsgExecuteContractFormProps { + readonly fromAddress: string; + readonly setMsgGetter: (msgGetter: MsgGetter) => void; + readonly deleteMsg: () => void; +} + +const MsgExecuteContractForm = ({ + fromAddress, + setMsgGetter, + deleteMsg, +}: MsgExecuteContractFormProps) => { + const { chain } = useChains(); + + const denomOptions = getDenomOptions(chain.assets); + + const [contractAddress, setContractAddress] = useState(""); + const [msgContent, setMsgContent] = useState("{}"); + const [selectedDenom, setSelectedDenom] = useState(denomOptions[0]); + const [customDenom, setCustomDenom] = useState(""); + const [amount, setAmount] = useState("0"); + + const jsonError = useRef(false); + const [contractAddressError, setContractAddressError] = useState(""); + const [customDenomError, setCustomDenomError] = useState(""); + const [amountError, setAmountError] = useState(""); + + useEffect(() => { + setContractAddressError(""); + setCustomDenomError(""); + setAmountError(""); + + const isMsgValid = (): boolean => { + if (jsonError.current) { + return false; + } + + const addressErrorMsg = checkAddress(contractAddress, chain.addressPrefix); + if (addressErrorMsg) { + setContractAddressError(`Invalid address for network ${chain.chainId}: ${addressErrorMsg}`); + return false; + } + + if (selectedDenom.value === customDenomOption.value && !customDenom) { + setCustomDenomError("Custom denom must be set because of selection above"); + return false; + } + + if (amount && Number(amount) < 0) { + setAmountError("Amount must be empty or a positive number"); + return false; + } + + if (selectedDenom.value === customDenomOption.value && !Number.isInteger(Number(amount))) { + setAmountError("Amount cannot be decimal for custom denom"); + return false; + } + + return true; + }; + + const denom = + selectedDenom.value === customDenomOption.value ? customDenom : selectedDenom.value.symbol; + + const microCoin = (() => { + try { + return macroCoinToMicroCoin({ denom, amount }, chain.assets); + } catch { + return { denom, amount: "0" }; + } + })(); + + const msgContentUtf8Array = (() => { + try { + // The JsonEditor does not escape \n or remove whitespaces, so we need to parse + stringify + return toUtf8(JSON.stringify(JSON.parse(msgContent))); + } catch { + return undefined; + } + })(); + + const msgValue = MsgCodecs[MsgTypeUrls.Execute].fromPartial({ + sender: fromAddress, + contract: contractAddress, + msg: msgContentUtf8Array, + funds: [microCoin], + }); + + const msg: MsgExecuteContractEncodeObject = { typeUrl: MsgTypeUrls.Execute, value: msgValue }; + + setMsgGetter({ isMsgValid, msg }); + }, [ + amount, + chain.addressPrefix, + chain.assets, + chain.chainId, + contractAddress, + customDenom, + fromAddress, + msgContent, + selectedDenom.value, + setMsgGetter, + ]); + + return ( + + +

    MsgExecuteContract

    +
    + setContractAddress(target.value)} + error={contractAddressError} + placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`} + /> +
    +
    + { + setMsgContent("text" in newMsgContent ? newMsgContent.text ?? "{}" : "{}"); + jsonError.current = !!contentErrors; + }} + /> +
    +
    + + setCustomDenom(target.value)} + placeholder={ + selectedDenom.value === customDenomOption.value + ? "Enter custom denom" + : "Select Custom denom above" + } + disabled={selectedDenom.value !== customDenomOption.value} + error={customDenomError} + /> +
    +
    + setAmount(target.value)} + error={amountError} + /> +
    + +
    + ); +}; + +export default MsgExecuteContractForm; diff --git a/components/forms/CreateTxForm/MsgForm/MsgInstantiateContract2Form.tsx b/components/forms/CreateTxForm/MsgForm/MsgInstantiateContract2Form.tsx new file mode 100644 index 00000000..64b8a161 --- /dev/null +++ b/components/forms/CreateTxForm/MsgForm/MsgInstantiateContract2Form.tsx @@ -0,0 +1,301 @@ +import { MsgInstantiateContract2EncodeObject } from "@cosmjs/cosmwasm-stargate"; +import { fromHex, toUtf8 } from "@cosmjs/encoding"; +import dynamic from "next/dynamic"; +import { useEffect, useRef, useState } from "react"; +import { MsgGetter } from ".."; +import { useChains } from "../../../../context/ChainsContext"; +import { ChainInfo } from "../../../../context/ChainsContext/types"; +import { macroCoinToMicroCoin } from "../../../../lib/coinHelpers"; +import { checkAddress, exampleAddress } from "../../../../lib/displayHelpers"; +import { MsgCodecs, MsgTypeUrls } from "../../../../types/txMsg"; +import Input from "../../../inputs/Input"; +import Select from "../../../inputs/Select"; +import StackableContainer from "../../../layout/StackableContainer"; + +const JsonEditor = dynamic(() => import("../../../inputs/JsonEditor"), { ssr: false }); + +const customDenomOption = { label: "Custom (enter denom below)", value: "custom" } as const; + +const getDenomOptions = (assets: ChainInfo["assets"]) => { + if (!assets?.length) { + return [customDenomOption]; + } + + return [...assets.map((asset) => ({ label: asset.symbol, value: asset })), customDenomOption]; +}; + +interface MsgInstantiateContract2FormProps { + readonly fromAddress: string; + readonly setMsgGetter: (msgGetter: MsgGetter) => void; + readonly deleteMsg: () => void; +} + +const MsgInstantiateContract2Form = ({ + fromAddress, + setMsgGetter, + deleteMsg, +}: MsgInstantiateContract2FormProps) => { + const { chain } = useChains(); + + const denomOptions = getDenomOptions(chain.assets); + + const [codeId, setCodeId] = useState(""); + const [label, setLabel] = useState(""); + const [adminAddress, setAdminAddress] = useState(""); + const [salt, setSalt] = useState(""); + const [msgContent, setMsgContent] = useState("{}"); + const [selectedDenom, setSelectedDenom] = useState(denomOptions[0]); + const [customDenom, setCustomDenom] = useState(""); + const [amount, setAmount] = useState("0"); + + const jsonError = useRef(false); + const [codeIdError, setCodeIdError] = useState(""); + const [labelError, setLabelError] = useState(""); + const [adminAddressError, setAdminAddressError] = useState(""); + const [saltError, setSaltError] = useState(""); + const [customDenomError, setCustomDenomError] = useState(""); + const [amountError, setAmountError] = useState(""); + + useEffect(() => { + setCodeIdError(""); + setLabelError(""); + setAdminAddressError(""); + setSaltError(""); + setCustomDenomError(""); + setAmountError(""); + + const isMsgValid = (): boolean => { + if (jsonError.current) { + return false; + } + + if (!codeId || !Number.isSafeInteger(Number(codeId)) || Number(codeId) <= 0) { + setCodeIdError("Code ID must be a positive integer"); + return false; + } + + if (!label) { + setLabelError("Label is required"); + return false; + } + + const addressErrorMsg = checkAddress(adminAddress, chain.addressPrefix); + if (adminAddress && addressErrorMsg) { + setAdminAddressError(`Invalid address for network ${chain.chainId}: ${addressErrorMsg}`); + return false; + } + + try { + if (!salt) { + throw new Error("Salt is required"); + } + + fromHex(salt); + } catch (e) { + setSaltError(e instanceof Error ? e.message : "Salt needs to be an hexadecimal string"); + return false; + } + + if (selectedDenom.value === customDenomOption.value && !customDenom) { + setCustomDenomError("Custom denom must be set because of selection above"); + return false; + } + + if (!amount || Number(amount) <= 0) { + setAmountError("Amount must be greater than 0"); + return false; + } + + if (selectedDenom.value === customDenomOption.value && !Number.isInteger(Number(amount))) { + setAmountError("Amount cannot be decimal for custom denom"); + return false; + } + + return true; + }; + + const denom = + selectedDenom.value === customDenomOption.value ? customDenom : selectedDenom.value.symbol; + + const microCoin = (() => { + try { + return macroCoinToMicroCoin({ denom, amount }, chain.assets); + } catch { + return { denom, amount: "0" }; + } + })(); + + const hexSalt = (() => { + try { + return fromHex(salt); + } catch { + return undefined; + } + })(); + + const msgContentUtf8Array = (() => { + try { + // The JsonEditor does not escape \n or remove whitespaces, so we need to parse + stringify + return toUtf8(JSON.stringify(JSON.parse(msgContent))); + } catch { + return undefined; + } + })(); + + const msgValue = MsgCodecs[MsgTypeUrls.Instantiate2].fromPartial({ + sender: fromAddress, + codeId: codeId || 0, + label, + admin: adminAddress, + fixMsg: false, + salt: hexSalt, + msg: msgContentUtf8Array, + funds: [microCoin], + }); + + const msg: MsgInstantiateContract2EncodeObject = { + typeUrl: MsgTypeUrls.Instantiate2, + value: msgValue, + }; + + setMsgGetter({ isMsgValid, msg }); + }, [ + adminAddress, + amount, + chain.addressPrefix, + chain.assets, + chain.chainId, + codeId, + customDenom, + fromAddress, + label, + msgContent, + salt, + selectedDenom.value, + setMsgGetter, + ]); + + return ( + + +

    MsgInstantiateContract2

    +
    + setCodeId(target.value)} + error={codeIdError} + /> +
    +
    + setLabel(target.value)} + error={labelError} + /> +
    +
    + setAdminAddress(target.value)} + error={adminAddressError} + placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`} + /> +
    +
    + setSalt(target.value)} + error={saltError} + /> +
    +
    + { + setMsgContent("text" in newMsgContent ? newMsgContent.text ?? "{}" : "{}"); + jsonError.current = !!contentErrors; + }} + /> +
    +
    + + setCustomDenom(target.value)} + placeholder={ + selectedDenom.value === customDenomOption.value + ? "Enter custom denom" + : "Select Custom denom above" + } + disabled={selectedDenom.value !== customDenomOption.value} + error={customDenomError} + /> +
    +
    + setAmount(target.value)} + error={amountError} + /> +
    + +
    + ); +}; + +export default MsgInstantiateContract2Form; diff --git a/components/forms/CreateTxForm/MsgForm/MsgInstantiateContractForm.tsx b/components/forms/CreateTxForm/MsgForm/MsgInstantiateContractForm.tsx new file mode 100644 index 00000000..77d1f186 --- /dev/null +++ b/components/forms/CreateTxForm/MsgForm/MsgInstantiateContractForm.tsx @@ -0,0 +1,266 @@ +import { MsgInstantiateContractEncodeObject } from "@cosmjs/cosmwasm-stargate"; +import { toUtf8 } from "@cosmjs/encoding"; +import dynamic from "next/dynamic"; +import { useEffect, useRef, useState } from "react"; +import { MsgGetter } from ".."; +import { useChains } from "../../../../context/ChainsContext"; +import { ChainInfo } from "../../../../context/ChainsContext/types"; +import { macroCoinToMicroCoin } from "../../../../lib/coinHelpers"; +import { checkAddress, exampleAddress } from "../../../../lib/displayHelpers"; +import { MsgCodecs, MsgTypeUrls } from "../../../../types/txMsg"; +import Input from "../../../inputs/Input"; +import Select from "../../../inputs/Select"; +import StackableContainer from "../../../layout/StackableContainer"; + +const JsonEditor = dynamic(() => import("../../../inputs/JsonEditor"), { ssr: false }); + +const customDenomOption = { label: "Custom (enter denom below)", value: "custom" } as const; + +const getDenomOptions = (assets: ChainInfo["assets"]) => { + if (!assets?.length) { + return [customDenomOption]; + } + + return [...assets.map((asset) => ({ label: asset.symbol, value: asset })), customDenomOption]; +}; + +interface MsgInstantiateContractFormProps { + readonly fromAddress: string; + readonly setMsgGetter: (msgGetter: MsgGetter) => void; + readonly deleteMsg: () => void; +} + +const MsgInstantiateContractForm = ({ + fromAddress, + setMsgGetter, + deleteMsg, +}: MsgInstantiateContractFormProps) => { + const { chain } = useChains(); + + const denomOptions = getDenomOptions(chain.assets); + + const [codeId, setCodeId] = useState(""); + const [label, setLabel] = useState(""); + const [adminAddress, setAdminAddress] = useState(""); + const [msgContent, setMsgContent] = useState("{}"); + const [selectedDenom, setSelectedDenom] = useState(denomOptions[0]); + const [customDenom, setCustomDenom] = useState(""); + const [amount, setAmount] = useState("0"); + + const jsonError = useRef(false); + const [codeIdError, setCodeIdError] = useState(""); + const [labelError, setLabelError] = useState(""); + const [adminAddressError, setAdminAddressError] = useState(""); + const [customDenomError, setCustomDenomError] = useState(""); + const [amountError, setAmountError] = useState(""); + + useEffect(() => { + setCodeIdError(""); + setLabelError(""); + setAdminAddressError(""); + setCustomDenomError(""); + setAmountError(""); + + const isMsgValid = (): boolean => { + if (jsonError.current) { + return false; + } + + if (!codeId || !Number.isSafeInteger(Number(codeId)) || Number(codeId) <= 0) { + setCodeIdError("Code ID must be a positive integer"); + return false; + } + + if (!label) { + setLabelError("Label is required"); + return false; + } + + const addressErrorMsg = checkAddress(adminAddress, chain.addressPrefix); + if (adminAddress && addressErrorMsg) { + setAdminAddressError(`Invalid address for network ${chain.chainId}: ${addressErrorMsg}`); + return false; + } + + if (selectedDenom.value === customDenomOption.value && !customDenom) { + setCustomDenomError("Custom denom must be set because of selection above"); + return false; + } + + if (!amount || Number(amount) <= 0) { + setAmountError("Amount must be greater than 0"); + return false; + } + + if (selectedDenom.value === customDenomOption.value && !Number.isInteger(Number(amount))) { + setAmountError("Amount cannot be decimal for custom denom"); + return false; + } + + return true; + }; + + const denom = + selectedDenom.value === customDenomOption.value ? customDenom : selectedDenom.value.symbol; + + const microCoin = (() => { + try { + return macroCoinToMicroCoin({ denom, amount }, chain.assets); + } catch { + return { denom, amount: "0" }; + } + })(); + + const msgContentUtf8Array = (() => { + try { + // The JsonEditor does not escape \n or remove whitespaces, so we need to parse + stringify + return toUtf8(JSON.stringify(JSON.parse(msgContent))); + } catch { + return undefined; + } + })(); + + const msgValue = MsgCodecs[MsgTypeUrls.Instantiate].fromPartial({ + sender: fromAddress, + codeId: codeId || 1, + label, + admin: adminAddress, + msg: msgContentUtf8Array, + funds: [microCoin], + }); + + const msg: MsgInstantiateContractEncodeObject = { + typeUrl: MsgTypeUrls.Instantiate, + value: msgValue, + }; + + setMsgGetter({ isMsgValid, msg }); + }, [ + adminAddress, + amount, + chain.addressPrefix, + chain.assets, + chain.chainId, + codeId, + customDenom, + fromAddress, + label, + msgContent, + selectedDenom.value, + setMsgGetter, + ]); + + return ( + + +

    MsgInstantiateContract

    +
    + setCodeId(target.value)} + error={codeIdError} + /> +
    +
    + setLabel(target.value)} + error={labelError} + /> +
    +
    + setAdminAddress(target.value)} + error={adminAddressError} + placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`} + /> +
    +
    + { + setMsgContent("text" in newMsgContent ? newMsgContent.text ?? "{}" : "{}"); + jsonError.current = !!contentErrors; + }} + /> +
    +
    + + setCustomDenom(target.value)} + placeholder={ + selectedDenom.value === customDenomOption.value + ? "Enter custom denom" + : "Select Custom denom above" + } + disabled={selectedDenom.value !== customDenomOption.value} + error={customDenomError} + /> +
    +
    + setAmount(target.value)} + error={amountError} + /> +
    + +
    + ); +}; + +export default MsgInstantiateContractForm; diff --git a/components/forms/CreateTxForm/MsgForm/MsgMigrateContractForm.tsx b/components/forms/CreateTxForm/MsgForm/MsgMigrateContractForm.tsx new file mode 100644 index 00000000..c1f4dae2 --- /dev/null +++ b/components/forms/CreateTxForm/MsgForm/MsgMigrateContractForm.tsx @@ -0,0 +1,151 @@ +import { MsgMigrateContractEncodeObject } from "@cosmjs/cosmwasm-stargate"; +import { toUtf8 } from "@cosmjs/encoding"; +import dynamic from "next/dynamic"; +import { useEffect, useRef, useState } from "react"; +import { MsgGetter } from ".."; +import { useChains } from "../../../../context/ChainsContext"; +import { checkAddress, exampleAddress } from "../../../../lib/displayHelpers"; +import { MsgCodecs, MsgTypeUrls } from "../../../../types/txMsg"; +import Input from "../../../inputs/Input"; +import StackableContainer from "../../../layout/StackableContainer"; + +const JsonEditor = dynamic(() => import("../../../inputs/JsonEditor"), { ssr: false }); + +interface MsgMigrateContractFormProps { + readonly fromAddress: string; + readonly setMsgGetter: (msgGetter: MsgGetter) => void; + readonly deleteMsg: () => void; +} + +const MsgMigrateContractForm = ({ + fromAddress, + setMsgGetter, + deleteMsg, +}: MsgMigrateContractFormProps) => { + const { chain } = useChains(); + + const [contractAddress, setContractAddress] = useState(""); + const [codeId, setCodeId] = useState(""); + const [msgContent, setMsgContent] = useState("{}"); + + const jsonError = useRef(false); + const [contractAddressError, setContractAddressError] = useState(""); + const [codeIdError, setCodeIdError] = useState(""); + + useEffect(() => { + setCodeIdError(""); + setContractAddressError(""); + + const isMsgValid = (): boolean => { + if (jsonError.current) { + return false; + } + + const addressErrorMsg = checkAddress(contractAddress, chain.addressPrefix); + if (addressErrorMsg) { + setContractAddressError(`Invalid address for network ${chain.chainId}: ${addressErrorMsg}`); + return false; + } + + if (!codeId || !Number.isSafeInteger(Number(codeId)) || Number(codeId) <= 0) { + setCodeIdError("Code ID must be a positive integer"); + return false; + } + + return true; + }; + + const msgContentUtf8Array = (() => { + try { + // The JsonEditor does not escape \n or remove whitespaces, so we need to parse + stringify + return toUtf8(JSON.stringify(JSON.parse(msgContent))); + } catch { + return undefined; + } + })(); + + const msgValue = MsgCodecs[MsgTypeUrls.Migrate].fromPartial({ + sender: fromAddress, + contract: contractAddress, + codeId: codeId || 1, + msg: msgContentUtf8Array, + }); + + const msg: MsgMigrateContractEncodeObject = { typeUrl: MsgTypeUrls.Migrate, value: msgValue }; + + setMsgGetter({ isMsgValid, msg }); + }, [ + chain.addressPrefix, + chain.chainId, + codeId, + contractAddress, + fromAddress, + msgContent, + setMsgGetter, + ]); + + return ( + + +

    MsgMigrateContract

    +
    + setContractAddress(target.value)} + error={contractAddressError} + placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`} + /> +
    +
    + setCodeId(target.value)} + error={codeIdError} + /> +
    +
    + { + setMsgContent("text" in newMsgContent ? newMsgContent.text ?? "{}" : "{}"); + jsonError.current = !!contentErrors; + }} + /> +
    + +
    + ); +}; + +export default MsgMigrateContractForm; diff --git a/components/forms/CreateTxForm/MsgForm/MsgSendForm.tsx b/components/forms/CreateTxForm/MsgForm/MsgSendForm.tsx index ba211f52..e97355a7 100644 --- a/components/forms/CreateTxForm/MsgForm/MsgSendForm.tsx +++ b/components/forms/CreateTxForm/MsgForm/MsgSendForm.tsx @@ -1,9 +1,8 @@ -import { Decimal } from "@cosmjs/math"; import { MsgSendEncodeObject } from "@cosmjs/stargate"; -import { assert } from "@cosmjs/utils"; import { useEffect, useState } from "react"; import { MsgGetter } from ".."; import { useChains } from "../../../../context/ChainsContext"; +import { macroCoinToMicroCoin } from "../../../../lib/coinHelpers"; import { checkAddress, exampleAddress } from "../../../../lib/displayHelpers"; import { RegistryAsset } from "../../../../types/chainRegistry"; import { MsgCodecs, MsgTypeUrls } from "../../../../types/txMsg"; @@ -71,40 +70,21 @@ const MsgSendForm = ({ fromAddress, setMsgGetter, deleteMsg }: MsgSendFormProps) return true; }; - const symbol = + const denom = selectedDenom.value === customDenomOption.value ? customDenom : selectedDenom.value.symbol; - const [denom, amountInAtomics] = (() => { + const microCoin = (() => { try { - if (selectedDenom.value === customDenomOption.value) { - return [symbol, Decimal.fromUserInput(amount, 0).atomics]; - } - - const foundAsset = chain.assets.find((asset) => asset.symbol === symbol); - assert(foundAsset, `An asset with the given symbol ${symbol} was not found`); - if (!foundAsset) return [undefined, undefined]; - - const units = foundAsset.denom_units ?? []; - const macroUnit = units.find( - (unit) => unit.denom.toLowerCase() === foundAsset.symbol.toLowerCase(), - ); - assert(macroUnit, `An unit with the given denom ${symbol} was not found`); - if (!macroUnit) return [undefined, undefined]; - - const smallestUnit = units.reduce((prevUnit, currentUnit) => - currentUnit.exponent < prevUnit.exponent ? currentUnit : prevUnit, - ); - - return [smallestUnit.denom, Decimal.fromUserInput(amount, macroUnit.exponent).atomics]; + return macroCoinToMicroCoin({ denom, amount }, chain.assets); } catch { - return "0"; + return { denom, amount: "0" }; } })(); const msgValue = MsgCodecs[MsgTypeUrls.Send].fromPartial({ fromAddress, toAddress, - amount: [{ denom, amount: amountInAtomics }], + amount: [microCoin], }); const msg: MsgSendEncodeObject = { typeUrl: MsgTypeUrls.Send, value: msgValue }; diff --git a/components/forms/CreateTxForm/MsgForm/index.tsx b/components/forms/CreateTxForm/MsgForm/index.tsx index bcd9027a..21f048fb 100644 --- a/components/forms/CreateTxForm/MsgForm/index.tsx +++ b/components/forms/CreateTxForm/MsgForm/index.tsx @@ -3,6 +3,10 @@ import { MsgTypeUrl, MsgTypeUrls } from "../../../../types/txMsg"; import MsgClaimRewardsForm from "./MsgClaimRewardsForm"; import MsgCreateVestingAccountForm from "./MsgCreateVestingAccountForm"; import MsgDelegateForm from "./MsgDelegateForm"; +import MsgExecuteContractForm from "./MsgExecuteContractForm"; +import MsgInstantiateContract2Form from "./MsgInstantiateContract2Form"; +import MsgInstantiateContractForm from "./MsgInstantiateContractForm"; +import MsgMigrateContractForm from "./MsgMigrateContractForm"; import MsgRedelegateForm from "./MsgRedelegateForm"; import MsgSendForm from "./MsgSendForm"; import MsgSetWithdrawAddressForm from "./MsgSetWithdrawAddressForm"; @@ -34,6 +38,14 @@ const MsgForm = ({ msgType, senderAddress, ...restProps }: MsgFormProps) => { return ; case MsgTypeUrls.Transfer: return ; + case MsgTypeUrls.Execute: + return ; + case MsgTypeUrls.Instantiate: + return ; + case MsgTypeUrls.Instantiate2: + return ; + case MsgTypeUrls.Migrate: + return ; default: return null; } diff --git a/components/forms/CreateTxForm/index.tsx b/components/forms/CreateTxForm/index.tsx index 65afcdef..dd4b315f 100644 --- a/components/forms/CreateTxForm/index.tsx +++ b/components/forms/CreateTxForm/index.tsx @@ -173,6 +173,16 @@ const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormPro onClick={() => addMsgType(MsgTypeUrls.CreateVestingAccount)} />