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