Merge pull request #158 from cosmos/feat/add-cw-msgs

Add cosmwasm msgs
This commit is contained in:
Abel Fernández
2023-07-27 10:42:56 +02:00
committed by GitHub
21 changed files with 1800 additions and 31 deletions
@@ -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 (
<>
<li>
<h3>MsgExecuteContract</h3>
</li>
<li>
<label>Contract:</label>
<div title={msgValue.contract}>
<HashView hash={msgValue.contract} />
</div>
</li>
<li>
<label>Funds:</label>
<div>{printableCoins(msgValue.funds, chain)}</div>
</li>
{parseError ? (
<li className="parse-error">
<p>{parseError}</p>
</li>
) : (
<li>
<JsonEditor readOnly content={{ json }} />
</li>
)}
<style jsx>{`
li:not(:has(h3)) {
background: rgba(255, 255, 255, 0.03);
padding: 6px 10px;
border-radius: 8px;
display: flex;
align-items: center;
}
li + li:nth-child(2) {
margin-top: 25px;
}
li + li {
margin-top: 10px;
}
li div {
padding: 3px 6px;
}
label {
font-size: 12px;
background: rgba(255, 255, 255, 0.1);
padding: 3px 6px;
border-radius: 5px;
display: block;
}
.parse-error p {
max-width: 550px;
color: red;
font-size: 16px;
line-height: 1.4;
}
`}</style>
</>
);
};
export default TxMsgExecuteContractDetails;
@@ -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 (
<>
<li>
<h3>MsgInstantiateContract2</h3>
</li>
<li>
<label>Code ID:</label>
<div>{msgValue.codeId.toString()}</div>
</li>
<li>
<label>Label:</label>
<div>{msgValue.label || "None"}</div>
</li>
<li>
<label>Admin:</label>
{msgValue.admin ? (
<div title={msgValue.admin}>
<HashView hash={msgValue.admin} />
</div>
) : (
<div>None</div>
)}
</li>
<li>
<label>Salt:</label>
<div>{toHex(msgValue.salt)}</div>
</li>
<li>
<label>Funds:</label>
<div>{printableCoins(msgValue.funds, chain)}</div>
</li>
{parseError ? (
<li className="parse-error">
<p>{parseError}</p>
</li>
) : (
<li>
<JsonEditor readOnly content={{ json }} />
</li>
)}
<style jsx>{`
li:not(:has(h3)) {
background: rgba(255, 255, 255, 0.03);
padding: 6px 10px;
border-radius: 8px;
display: flex;
align-items: center;
}
li + li:nth-child(2) {
margin-top: 25px;
}
li + li {
margin-top: 10px;
}
li div {
padding: 3px 6px;
}
label {
font-size: 12px;
background: rgba(255, 255, 255, 0.1);
padding: 3px 6px;
border-radius: 5px;
display: block;
}
.parse-error p {
max-width: 550px;
color: red;
font-size: 16px;
line-height: 1.4;
}
`}</style>
</>
);
};
export default TxMsgInstantiateContract2Details;
@@ -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 (
<>
<li>
<h3>MsgInstantiateContract</h3>
</li>
<li>
<label>Code ID:</label>
<div>{msgValue.codeId.toString()}</div>
</li>
<li>
<label>Label:</label>
<div>{msgValue.label || "None"}</div>
</li>
<li>
<label>Admin:</label>
{msgValue.admin ? (
<div title={msgValue.admin}>
<HashView hash={msgValue.admin} />
</div>
) : (
<div>None</div>
)}
</li>
<li>
<label>Funds:</label>
<div>{printableCoins(msgValue.funds, chain)}</div>
</li>
{parseError ? (
<li className="parse-error">
<p>{parseError}</p>
</li>
) : (
<li>
<JsonEditor readOnly content={{ json }} />
</li>
)}
<style jsx>{`
li:not(:has(h3)) {
background: rgba(255, 255, 255, 0.03);
padding: 6px 10px;
border-radius: 8px;
display: flex;
align-items: center;
}
li + li:nth-child(2) {
margin-top: 25px;
}
li + li {
margin-top: 10px;
}
li div {
padding: 3px 6px;
}
label {
font-size: 12px;
background: rgba(255, 255, 255, 0.1);
padding: 3px 6px;
border-radius: 5px;
display: block;
}
.parse-error p {
max-width: 550px;
color: red;
font-size: 16px;
line-height: 1.4;
}
`}</style>
</>
);
};
export default TxMsgInstantiateContractDetails;
@@ -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 (
<>
<li>
<h3>MsgMigrateContract</h3>
</li>
<li>
<label>Contract:</label>
<div title={msgValue.contract}>
<HashView hash={msgValue.contract} />
</div>
</li>
<li>
<label>Code ID:</label>
<div>{msgValue.codeId.toString()}</div>
</li>
{parseError ? (
<li className="parse-error">
<p>{parseError}</p>
</li>
) : (
<li>
<JsonEditor readOnly content={{ json }} />
</li>
)}
<style jsx>{`
li:not(:has(h3)) {
background: rgba(255, 255, 255, 0.03);
padding: 6px 10px;
border-radius: 8px;
display: flex;
align-items: center;
}
li + li:nth-child(2) {
margin-top: 25px;
}
li + li {
margin-top: 10px;
}
li div {
padding: 3px 6px;
}
label {
font-size: 12px;
background: rgba(255, 255, 255, 0.1);
padding: 3px 6px;
border-radius: 5px;
display: block;
}
.parse-error p {
max-width: 550px;
color: red;
font-size: 16px;
line-height: 1.4;
}
`}</style>
</>
);
};
export default TxMsgMigrateContractDetails;
@@ -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 <TxMsgCreateVestingAccountDetails msgValue={msgValue} />;
case MsgTypeUrls.Transfer:
return <TxMsgTransferDetails msgValue={msgValue} />;
case MsgTypeUrls.Execute:
return <TxMsgExecuteContractDetails msgValue={msgValue} />;
case MsgTypeUrls.Instantiate:
return <TxMsgInstantiateContractDetails msgValue={msgValue} />;
case MsgTypeUrls.Instantiate2:
return <TxMsgInstantiateContract2Details msgValue={msgValue} />;
case MsgTypeUrls.Migrate:
return <TxMsgMigrateContractDetails msgValue={msgValue} />;
default:
return null;
}
@@ -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 (
<StackableContainer lessPadding lessMargin>
<button className="remove" onClick={() => deleteMsg()}>
</button>
<h2>MsgExecuteContract</h2>
<div className="form-item">
<Input
label="Contract Address"
name="contract-address"
value={contractAddress}
onChange={({ target }) => setContractAddress(target.value)}
error={contractAddressError}
placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`}
/>
</div>
<div className="form-item">
<JsonEditor
label="Msg JSON"
content={{ text: msgContent }}
onChange={(newMsgContent, _, { contentErrors }) => {
setMsgContent("text" in newMsgContent ? newMsgContent.text ?? "{}" : "{}");
jsonError.current = !!contentErrors;
}}
/>
</div>
<div className="form-item form-select">
<label>Choose a denom:</label>
<Select
label="Select denom"
name="denom-select"
options={denomOptions}
value={selectedDenom}
onChange={(option: (typeof denomOptions)[number]) => {
setSelectedDenom(option);
if (option.value !== customDenomOption.value) {
setCustomDenom("");
}
}}
/>
</div>
<div className="form-item">
<Input
label="Custom denom"
name="custom-denom"
value={customDenom}
onChange={({ target }) => setCustomDenom(target.value)}
placeholder={
selectedDenom.value === customDenomOption.value
? "Enter custom denom"
: "Select Custom denom above"
}
disabled={selectedDenom.value !== customDenomOption.value}
error={customDenomError}
/>
</div>
<div className="form-item">
<Input
type="number"
label="Amount"
name="amount"
value={amount}
onChange={({ target }) => setAmount(target.value)}
error={amountError}
/>
</div>
<style jsx>{`
.form-item {
margin-top: 1.5em;
}
.form-item label {
font-style: italic;
font-size: 12px;
}
.form-select {
display: flex;
flex-direction: column;
gap: 0.8em;
}
button.remove {
background: rgba(255, 255, 255, 0.2);
width: 30px;
height: 30px;
border-radius: 50%;
border: none;
color: white;
position: absolute;
right: 10px;
top: 10px;
}
`}</style>
</StackableContainer>
);
};
export default MsgExecuteContractForm;
@@ -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 (
<StackableContainer lessPadding lessMargin>
<button className="remove" onClick={() => deleteMsg()}>
</button>
<h2>MsgInstantiateContract2</h2>
<div className="form-item">
<Input
label="Code ID"
name="code-id"
value={codeId}
onChange={({ target }) => setCodeId(target.value)}
error={codeIdError}
/>
</div>
<div className="form-item">
<Input
label="Label"
name="label"
value={label}
onChange={({ target }) => setLabel(target.value)}
error={labelError}
/>
</div>
<div className="form-item">
<Input
label="Admin Address"
name="admin-address"
value={adminAddress}
onChange={({ target }) => setAdminAddress(target.value)}
error={adminAddressError}
placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`}
/>
</div>
<div className="form-item">
<Input
label="Salt (hex encoded)"
name="salt"
placeholder="E.g. 1bac68"
value={salt}
onChange={({ target }) => setSalt(target.value)}
error={saltError}
/>
</div>
<div className="form-item">
<JsonEditor
label="Msg JSON"
content={{ text: msgContent }}
onChange={(newMsgContent, _, { contentErrors }) => {
setMsgContent("text" in newMsgContent ? newMsgContent.text ?? "{}" : "{}");
jsonError.current = !!contentErrors;
}}
/>
</div>
<div className="form-item form-select">
<label>Choose a denom:</label>
<Select
label="Select denom"
name="denom-select"
options={denomOptions}
value={selectedDenom}
onChange={(option: (typeof denomOptions)[number]) => {
setSelectedDenom(option);
if (option.value !== customDenomOption.value) {
setCustomDenom("");
}
}}
/>
</div>
<div className="form-item">
<Input
label="Custom denom"
name="custom-denom"
value={customDenom}
onChange={({ target }) => setCustomDenom(target.value)}
placeholder={
selectedDenom.value === customDenomOption.value
? "Enter custom denom"
: "Select Custom denom above"
}
disabled={selectedDenom.value !== customDenomOption.value}
error={customDenomError}
/>
</div>
<div className="form-item">
<Input
type="number"
label="Amount"
name="amount"
value={amount}
onChange={({ target }) => setAmount(target.value)}
error={amountError}
/>
</div>
<style jsx>{`
.form-item {
margin-top: 1.5em;
}
.form-item label {
font-style: italic;
font-size: 12px;
}
.form-select {
display: flex;
flex-direction: column;
gap: 0.8em;
}
button.remove {
background: rgba(255, 255, 255, 0.2);
width: 30px;
height: 30px;
border-radius: 50%;
border: none;
color: white;
position: absolute;
right: 10px;
top: 10px;
}
`}</style>
</StackableContainer>
);
};
export default MsgInstantiateContract2Form;
@@ -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 (
<StackableContainer lessPadding lessMargin>
<button className="remove" onClick={() => deleteMsg()}>
</button>
<h2>MsgInstantiateContract</h2>
<div className="form-item">
<Input
label="Code ID"
name="code-id"
value={codeId}
onChange={({ target }) => setCodeId(target.value)}
error={codeIdError}
/>
</div>
<div className="form-item">
<Input
label="Label"
name="label"
value={label}
onChange={({ target }) => setLabel(target.value)}
error={labelError}
/>
</div>
<div className="form-item">
<Input
label="Admin Address"
name="admin-address"
value={adminAddress}
onChange={({ target }) => setAdminAddress(target.value)}
error={adminAddressError}
placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`}
/>
</div>
<div className="form-item">
<JsonEditor
label="Msg JSON"
content={{ text: msgContent }}
onChange={(newMsgContent, _, { contentErrors }) => {
setMsgContent("text" in newMsgContent ? newMsgContent.text ?? "{}" : "{}");
jsonError.current = !!contentErrors;
}}
/>
</div>
<div className="form-item form-select">
<label>Choose a denom:</label>
<Select
label="Select denom"
name="denom-select"
options={denomOptions}
value={selectedDenom}
onChange={(option: (typeof denomOptions)[number]) => {
setSelectedDenom(option);
if (option.value !== customDenomOption.value) {
setCustomDenom("");
}
}}
/>
</div>
<div className="form-item">
<Input
label="Custom denom"
name="custom-denom"
value={customDenom}
onChange={({ target }) => setCustomDenom(target.value)}
placeholder={
selectedDenom.value === customDenomOption.value
? "Enter custom denom"
: "Select Custom denom above"
}
disabled={selectedDenom.value !== customDenomOption.value}
error={customDenomError}
/>
</div>
<div className="form-item">
<Input
type="number"
label="Amount"
name="amount"
value={amount}
onChange={({ target }) => setAmount(target.value)}
error={amountError}
/>
</div>
<style jsx>{`
.form-item {
margin-top: 1.5em;
}
.form-item label {
font-style: italic;
font-size: 12px;
}
.form-select {
display: flex;
flex-direction: column;
gap: 0.8em;
}
button.remove {
background: rgba(255, 255, 255, 0.2);
width: 30px;
height: 30px;
border-radius: 50%;
border: none;
color: white;
position: absolute;
right: 10px;
top: 10px;
}
`}</style>
</StackableContainer>
);
};
export default MsgInstantiateContractForm;
@@ -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 (
<StackableContainer lessPadding lessMargin>
<button className="remove" onClick={() => deleteMsg()}>
</button>
<h2>MsgMigrateContract</h2>
<div className="form-item">
<Input
label="Contract Address"
name="contract-address"
value={contractAddress}
onChange={({ target }) => setContractAddress(target.value)}
error={contractAddressError}
placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`}
/>
</div>
<div className="form-item">
<Input
label="Code ID"
name="code-id"
value={codeId}
onChange={({ target }) => setCodeId(target.value)}
error={codeIdError}
/>
</div>
<div className="form-item">
<JsonEditor
label="Msg JSON"
content={{ text: msgContent }}
onChange={(newMsgContent, _, { contentErrors }) => {
setMsgContent("text" in newMsgContent ? newMsgContent.text ?? "{}" : "{}");
jsonError.current = !!contentErrors;
}}
/>
</div>
<style jsx>{`
.form-item {
margin-top: 1.5em;
}
.form-item label {
font-style: italic;
font-size: 12px;
}
.form-select {
display: flex;
flex-direction: column;
gap: 0.8em;
}
button.remove {
background: rgba(255, 255, 255, 0.2);
width: 30px;
height: 30px;
border-radius: 50%;
border: none;
color: white;
position: absolute;
right: 10px;
top: 10px;
}
`}</style>
</StackableContainer>
);
};
export default MsgMigrateContractForm;
@@ -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 };
@@ -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 <MsgCreateVestingAccountForm fromAddress={senderAddress} {...restProps} />;
case MsgTypeUrls.Transfer:
return <MsgTransferForm fromAddress={senderAddress} {...restProps} />;
case MsgTypeUrls.Execute:
return <MsgExecuteContractForm fromAddress={senderAddress} {...restProps} />;
case MsgTypeUrls.Instantiate:
return <MsgInstantiateContractForm fromAddress={senderAddress} {...restProps} />;
case MsgTypeUrls.Instantiate2:
return <MsgInstantiateContract2Form fromAddress={senderAddress} {...restProps} />;
case MsgTypeUrls.Migrate:
return <MsgMigrateContractForm fromAddress={senderAddress} {...restProps} />;
default:
return null;
}
+10
View File
@@ -173,6 +173,16 @@ const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormPro
onClick={() => addMsgType(MsgTypeUrls.CreateVestingAccount)}
/>
<Button label="Add MsgTransfer" onClick={() => addMsgType(MsgTypeUrls.Transfer)} />
<Button label="Add MsgExecuteContract" onClick={() => addMsgType(MsgTypeUrls.Execute)} />
<Button
label="Add MsgInstantiateContract"
onClick={() => addMsgType(MsgTypeUrls.Instantiate)}
/>
<Button
label="Add MsgInstantiateContract2"
onClick={() => addMsgType(MsgTypeUrls.Instantiate2)}
/>
<Button label="Add MsgMigrateContract" onClick={() => addMsgType(MsgTypeUrls.Migrate)} />
</StackableContainer>
{showCreateTxError ? (
<StackableContainer lessMargin lessPadding>
+15 -2
View File
@@ -1,7 +1,14 @@
import { MultisigThresholdPubkey, makeCosmoshubPath } from "@cosmjs/amino";
import { createWasmAminoConverters, wasmTypes } from "@cosmjs/cosmwasm-stargate";
import { toBase64 } from "@cosmjs/encoding";
import { LedgerSigner } from "@cosmjs/ledger-amino";
import { SigningStargateClient } from "@cosmjs/stargate";
import { Registry } from "@cosmjs/proto-signing";
import {
AminoTypes,
SigningStargateClient,
createDefaultAminoConverters,
defaultRegistryTypes,
} from "@cosmjs/stargate";
import { assert } from "@cosmjs/utils";
import TransportWebUSB from "@ledgerhq/hw-transport-webusb";
import { useCallback, useLayoutEffect, useState } from "react";
@@ -142,7 +149,13 @@ const TransactionSigning = (props: TransactionSigningProps) => {
const signerAddress = walletAccount?.bech32Address;
assert(signerAddress, "Missing signer address");
const signingClient = await SigningStargateClient.offline(offlineSigner);
const signingClient = await SigningStargateClient.offline(offlineSigner, {
registry: new Registry([...defaultRegistryTypes, ...wasmTypes]),
aminoTypes: new AminoTypes({
...createDefaultAminoConverters(),
...createWasmAminoConverters(),
}),
});
const signerData = {
accountNumber: props.tx.accountNumber,
+65
View File
@@ -0,0 +1,65 @@
import { CSSProperties, useEffect, useRef } from "react";
import { JSONEditorPropsOptional, Mode, JSONEditor as VanillaJsonEditor } from "vanilla-jsoneditor";
const editorStyle: { [key: string]: string } & CSSProperties = {
"--jse-a-color": "white",
"--jse-delimiter-color": "white",
"--jse-key-color": "white",
"--jse-text-color": "white",
"--jse-value-color": "white",
"--jse-value-color-boolean": "white",
"--jse-value-color-null": "white",
"--jse-value-color-number": "white",
"--jse-value-color-string": "white",
"--jse-value-color-url": "white",
"--jse-background-color": "transparent",
"--jse-panel-background": "transparent",
"--jse-selection-background-color": "rgba(255, 255, 255, 0.5)",
"--jse-main-border": "2px solid rgba(255, 255, 255, 0.5)",
"--jse-panel-border": "2px solid rgba(255, 255, 255, 0.5)",
};
interface JsonEditorProps extends JSONEditorPropsOptional {
readonly label?: string;
}
export default function JsonEditor({ label, ...editorProps }: JsonEditorProps) {
const refContainer = useRef<HTMLDivElement>(null);
const refEditor = useRef<VanillaJsonEditor | null>(null);
useEffect(() => {
if (!refContainer.current) return;
refEditor.current = new VanillaJsonEditor({ target: refContainer.current, props: {} });
return () => {
if (refEditor.current) {
refEditor.current.destroy();
refEditor.current = null;
}
};
}, []);
useEffect(() => {
if (refEditor.current) {
refEditor.current.updateProps({ mode: Mode.text, mainMenuBar: false, ...editorProps });
}
}, [editorProps]);
return (
<div className="container" style={editorStyle} ref={refContainer}>
{label ? <label>{label}</label> : null}
<style jsx>{`
.container {
display: flex;
flex-direction: column;
gap: 0.8em;
}
label {
font-style: italic;
font-size: 12px;
}
`}</style>
</div>
);
}
+146
View File
@@ -0,0 +1,146 @@
import { RegistryAsset } from "../types/chainRegistry";
import { macroCoinToMicroCoin } from "./coinHelpers";
const assets: readonly RegistryAsset[] = [
{
denom_units: [
{
denom: "ujunox",
exponent: 0,
},
{
denom: "junox",
exponent: 6,
},
],
symbol: "JUNOX",
display: "junox",
name: "Juno Testnet",
base: "ujunox",
},
{
denom_units: [
{
denom: "ncheq",
exponent: 0,
},
{
denom: "cheq",
exponent: 9,
},
],
symbol: "CHEQ",
display: "cheq",
name: "cheqd",
base: "ncheq",
},
{
denom_units: [
{
denom: "erc20/0x2Cbea61fdfDFA520Ee99700F104D5b75ADf50B0c",
exponent: 0,
},
{
denom: "arusd",
exponent: 18,
},
],
symbol: "arUSD",
display: "arusd",
name: "Arable USD",
base: "erc20/0x2Cbea61fdfDFA520Ee99700F104D5b75ADf50B0c",
},
{
denom_units: [
{
denom: "wei",
exponent: 0,
},
{
denom: "gwei",
exponent: 9,
},
{
denom: "eth",
exponent: 18,
aliases: ["ether"],
},
],
symbol: "ETH",
display: "eth",
name: "Ether",
base: "wei",
},
];
describe("macroCoinToMicroCoin", () => {
it("works with symbol", () => {
expect(macroCoinToMicroCoin({ denom: "JUNOX", amount: "12" }, assets)).toEqual({
denom: "ujunox",
amount: "12000000",
});
expect(macroCoinToMicroCoin({ denom: "CHEQ", amount: "34" }, assets)).toEqual({
denom: "ncheq",
amount: "34000000000",
});
expect(macroCoinToMicroCoin({ denom: "arUSD", amount: "56" }, assets)).toEqual({
denom: "erc20/0x2Cbea61fdfDFA520Ee99700F104D5b75ADf50B0c",
amount: "56000000000000000000",
});
expect(macroCoinToMicroCoin({ denom: "ETH", amount: "78" }, assets)).toEqual({
denom: "wei",
amount: "78000000000000000000",
});
});
it("works with base unit", () => {
expect(macroCoinToMicroCoin({ denom: "ujunox", amount: "12" }, assets)).toEqual({
denom: "ujunox",
amount: "12",
});
expect(macroCoinToMicroCoin({ denom: "ncheq", amount: "34" }, assets)).toEqual({
denom: "ncheq",
amount: "34",
});
expect(
macroCoinToMicroCoin(
{ denom: "erc20/0x2Cbea61fdfDFA520Ee99700F104D5b75ADf50B0c", amount: "56" },
assets,
),
).toEqual({ denom: "erc20/0x2Cbea61fdfDFA520Ee99700F104D5b75ADf50B0c", amount: "56" });
expect(macroCoinToMicroCoin({ denom: "wei", amount: "78" }, assets)).toEqual({
denom: "wei",
amount: "78",
});
});
it("works with biggest unit", () => {
expect(macroCoinToMicroCoin({ denom: "junox", amount: "12" }, assets)).toEqual({
denom: "ujunox",
amount: "12000000",
});
expect(macroCoinToMicroCoin({ denom: "cheq", amount: "34" }, assets)).toEqual({
denom: "ncheq",
amount: "34000000000",
});
expect(macroCoinToMicroCoin({ denom: "arusd", amount: "56" }, assets)).toEqual({
denom: "erc20/0x2Cbea61fdfDFA520Ee99700F104D5b75ADf50B0c",
amount: "56000000000000000000",
});
expect(macroCoinToMicroCoin({ denom: "eth", amount: "78" }, assets)).toEqual({
denom: "wei",
amount: "78000000000000000000",
});
});
it("works with intermediate unit", () => {
expect(macroCoinToMicroCoin({ denom: "gwei", amount: "78" }, assets)).toEqual({
denom: "wei",
amount: "78000000000",
});
expect(macroCoinToMicroCoin({ denom: "GWEI", amount: "78" }, assets)).toEqual({
denom: "wei",
amount: "78000000000",
});
});
});
+36
View File
@@ -0,0 +1,36 @@
import { Decimal } from "@cosmjs/math";
import { Coin } from "@cosmjs/stargate";
import { assert } from "@cosmjs/utils";
import { RegistryAsset } from "../types/chainRegistry";
const macroCoinToMicroCoin = (macroCoin: Coin, assets: readonly RegistryAsset[]): Coin => {
const lowerCaseDenom = macroCoin.denom.toLowerCase();
const asset = assets.find(
(currentAsset) =>
lowerCaseDenom === currentAsset.symbol.toLowerCase() ||
lowerCaseDenom === currentAsset.display.toLowerCase() ||
lowerCaseDenom === currentAsset.name.toLowerCase() ||
lowerCaseDenom === currentAsset.base.toLowerCase() ||
currentAsset.denom_units.find(
(unit) => unit.denom === lowerCaseDenom || unit.aliases?.includes(lowerCaseDenom),
),
);
assert(asset, `An asset with the given symbol ${macroCoin.denom} was not found`);
const macroUnit = asset.denom_units.find(
(currentUnit) => lowerCaseDenom === currentUnit.denom.toLowerCase(),
);
assert(macroUnit, `A unit with the given symbol ${lowerCaseDenom} was not found`);
const baseUnit = asset.denom_units.find((currentUnit) => currentUnit.exponent === 0);
assert(baseUnit, `A base unit with exponent = 0 was not found`);
const denom = baseUnit.denom;
const amount = Decimal.fromUserInput(macroCoin.amount, macroUnit.exponent).atomics;
return { denom, amount };
};
export { macroCoinToMicroCoin };
+1 -1
View File
@@ -1,5 +1,5 @@
import { Coin } from "@cosmjs/stargate";
import { ChainInfo } from "../types";
import { ChainInfo } from "../context/ChainsContext/types";
import { printableCoin, printableCoins, thinSpace } from "./displayHelpers";
const testChainInfo: ChainInfo = {
+8
View File
@@ -20,6 +20,14 @@ const gasOfMsg = (msgType: MsgTypeUrl): number => {
return 100_000;
case MsgTypeUrls.Transfer:
return 180_000;
case MsgTypeUrls.Execute:
return 150_000;
case MsgTypeUrls.Instantiate:
return 150_000;
case MsgTypeUrls.Instantiate2:
return 150_000;
case MsgTypeUrls.Migrate:
return 150_000;
default:
throw new Error("Unknown msg type");
}
+136 -1
View File
@@ -7,6 +7,7 @@
"name": "create-next-example-app",
"dependencies": {
"@cosmjs/amino": "^0.31.0",
"@cosmjs/cosmwasm-stargate": "^0.31.0",
"@cosmjs/crypto": "^0.31.0",
"@cosmjs/encoding": "^0.31.0",
"@cosmjs/ledger-amino": "^0.31.0",
@@ -22,7 +23,8 @@
"next": "^13.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-select": "^5.7.2"
"react-select": "^5.7.2",
"vanilla-jsoneditor": "^0.17.8"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
@@ -608,6 +610,24 @@
"@cosmjs/utils": "^0.31.0"
}
},
"node_modules/@cosmjs/cosmwasm-stargate": {
"version": "0.31.0",
"resolved": "https://registry.npmjs.org/@cosmjs/cosmwasm-stargate/-/cosmwasm-stargate-0.31.0.tgz",
"integrity": "sha512-l6aX++3LhaAGZO46qIgrrNF40lYhOrdPfl35Z32ks6Wf3mwgbQEZwaxnoGzwUePY7/yaIiEFJ1JO6MlVPZVuag==",
"dependencies": {
"@cosmjs/amino": "^0.31.0",
"@cosmjs/crypto": "^0.31.0",
"@cosmjs/encoding": "^0.31.0",
"@cosmjs/math": "^0.31.0",
"@cosmjs/proto-signing": "^0.31.0",
"@cosmjs/stargate": "^0.31.0",
"@cosmjs/tendermint-rpc": "^0.31.0",
"@cosmjs/utils": "^0.31.0",
"cosmjs-types": "^0.8.0",
"long": "^4.0.0",
"pako": "^2.0.2"
}
},
"node_modules/@cosmjs/crypto": {
"version": "0.31.0",
"resolved": "https://registry.npmjs.org/@cosmjs/crypto/-/crypto-0.31.0.tgz",
@@ -1529,6 +1549,66 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.3.0.tgz",
"integrity": "sha512-DmIQCNq6JtccLPPBzf0dgh2vzMWt5wjxbP71pCi5EWpWYE3MsP6FcRXi4MlAmFNDQOfcFXR2r7kBeG1LpZUh1w==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.3.0.tgz",
"integrity": "sha512-oQoqFa88OGgwnYlnAGHVct618FRI/749se0N3S8t9Bzdv5CRbscnO0RcX901+YnNK4Q6yeiizfgO3b7kogtsZg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.3.0.tgz",
"integrity": "sha512-Wzz2p/WqAJUqTVoLo6H18WMeAXo3i+9DkPDae4oQG8LMloJ3if4NEZTnOnTUlro6cq+S/W4pTGa97nWTrOjbGw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.3.0.tgz",
"integrity": "sha512-xPVrIQOQo9WXJYgmoTlMnAD/HlR/1e1ZIWGbwIzEirXBVBqMARUulBEIKdC19zuvoJ477qZJgBDCKtKEykCpyQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "13.3.0",
"cpu": [
@@ -1557,6 +1637,51 @@
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.3.0.tgz",
"integrity": "sha512-OeHiA6YEvndxT46g+rzFK/MQTfftKxJmzslERMu9LDdC6Kez0bdrgEYed5eXFK2Z1viKZJCGRlhd06rBusyztA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-ia32-msvc": {
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.3.0.tgz",
"integrity": "sha512-4aB7K9mcVK1lYEzpOpqWrXHEZympU3oK65fnNcY1Qc4HLJFLJj8AViuqQd4jjjPNuV4sl8jAwTz3gN5VNGWB7w==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "13.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.3.0.tgz",
"integrity": "sha512-Reer6rkLLcoOvB0dd66+Y7WrWVFH7sEEkF/4bJCIfsSKnTStTYaHtwIJAwbqnt9I392Tqvku0KkoqZOryWV9LQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@noble/hashes": {
"version": "1.0.0",
"license": "MIT"
@@ -6659,6 +6784,11 @@
"node": ">=6"
}
},
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
},
"node_modules/parent-module": {
"version": "1.0.1",
"license": "MIT",
@@ -7857,6 +7987,11 @@
"node": ">=10.12.0"
}
},
"node_modules/vanilla-jsoneditor": {
"version": "0.17.8",
"resolved": "https://registry.npmjs.org/vanilla-jsoneditor/-/vanilla-jsoneditor-0.17.8.tgz",
"integrity": "sha512-DP9GP/IBQjYOnC820CYoFuXs3vgrL+zdGGp1X83qFirSkRWPeP+6zB/14a0LYhvomQNNezes5Gwel89MKc4Qbg=="
},
"node_modules/w3c-xmlserializer": {
"version": "4.0.0",
"dev": true,
+3 -1
View File
@@ -11,6 +11,7 @@
},
"dependencies": {
"@cosmjs/amino": "^0.31.0",
"@cosmjs/cosmwasm-stargate": "^0.31.0",
"@cosmjs/crypto": "^0.31.0",
"@cosmjs/encoding": "^0.31.0",
"@cosmjs/ledger-amino": "^0.31.0",
@@ -26,7 +27,8 @@
"next": "^13.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-select": "^5.7.2"
"react-select": "^5.7.2",
"vanilla-jsoneditor": "^0.17.8"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
+14
View File
@@ -9,6 +9,12 @@ import {
MsgUndelegate,
} from "cosmjs-types/cosmos/staking/v1beta1/tx";
import { MsgCreateVestingAccount } from "cosmjs-types/cosmos/vesting/v1beta1/tx";
import {
MsgExecuteContract,
MsgInstantiateContract,
MsgInstantiateContract2,
MsgMigrateContract,
} from "cosmjs-types/cosmwasm/wasm/v1/tx";
import { MsgTransfer } from "cosmjs-types/ibc/applications/transfer/v1/tx";
export const MsgTypeUrls = {
@@ -20,6 +26,10 @@ export const MsgTypeUrls = {
Undelegate: "/cosmos.staking.v1beta1.MsgUndelegate",
CreateVestingAccount: "/cosmos.vesting.v1beta1.MsgCreateVestingAccount",
Transfer: "/ibc.applications.transfer.v1.MsgTransfer",
Execute: "/cosmwasm.wasm.v1.MsgExecuteContract",
Instantiate: "/cosmwasm.wasm.v1.MsgInstantiateContract",
Instantiate2: "/cosmwasm.wasm.v1.MsgInstantiateContract2",
Migrate: "/cosmwasm.wasm.v1.MsgMigrateContract",
} as const;
export type MsgTypeUrl = (typeof MsgTypeUrls)[keyof typeof MsgTypeUrls];
@@ -33,4 +43,8 @@ export const MsgCodecs = {
[MsgTypeUrls.Undelegate]: MsgUndelegate,
[MsgTypeUrls.CreateVestingAccount]: MsgCreateVestingAccount,
[MsgTypeUrls.Transfer]: MsgTransfer,
[MsgTypeUrls.Execute]: MsgExecuteContract,
[MsgTypeUrls.Instantiate]: MsgInstantiateContract,
[MsgTypeUrls.Instantiate2]: MsgInstantiateContract2,
[MsgTypeUrls.Migrate]: MsgMigrateContract,
};