Prefix current create form with Old
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
import SelectValidator from "@/components/SelectValidator";
|
||||
import { MsgBeginRedelegateEncodeObject } from "@cosmjs/stargate";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MsgGetter } from "..";
|
||||
import { useChains } from "../../../../context/ChainsContext";
|
||||
import { displayCoinToBaseCoin } from "../../../../lib/coinHelpers";
|
||||
import { checkAddress, exampleAddress, trimStringsObj } from "../../../../lib/displayHelpers";
|
||||
import { MsgCodecs, MsgTypeUrls } from "../../../../types/txMsg";
|
||||
import Input from "../../../inputs/Input";
|
||||
import StackableContainer from "../../../layout/StackableContainer";
|
||||
|
||||
interface MsgBeginRedelegateFormProps {
|
||||
readonly senderAddress: string;
|
||||
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
|
||||
readonly deleteMsg: () => void;
|
||||
}
|
||||
|
||||
const MsgBeginRedelegateForm = ({
|
||||
senderAddress,
|
||||
setMsgGetter,
|
||||
deleteMsg,
|
||||
}: MsgBeginRedelegateFormProps) => {
|
||||
const { chain } = useChains();
|
||||
|
||||
const [validatorSrcAddress, setValidatorSrcAddress] = useState("");
|
||||
const [validatorDstAddress, setValidatorDstAddress] = useState("");
|
||||
const [amount, setAmount] = useState("0");
|
||||
|
||||
const [validatorSrcAddressError, setValidatorSrcAddressError] = useState("");
|
||||
const [validatorDstAddressError, setValidatorDstAddressError] = useState("");
|
||||
const [amountError, setAmountError] = useState("");
|
||||
|
||||
const trimmedInputs = trimStringsObj({ validatorSrcAddress, validatorDstAddress, amount });
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
const { validatorSrcAddress, validatorDstAddress, amount } = trimmedInputs;
|
||||
|
||||
const isMsgValid = (): boolean => {
|
||||
setValidatorSrcAddressError("");
|
||||
setValidatorDstAddressError("");
|
||||
setAmountError("");
|
||||
|
||||
const srcAddressErrorMsg = checkAddress(validatorSrcAddress, chain.addressPrefix);
|
||||
if (srcAddressErrorMsg) {
|
||||
setValidatorSrcAddressError(
|
||||
`Invalid address for network ${chain.chainId}: ${srcAddressErrorMsg}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const dstAddressErrorMsg = checkAddress(validatorDstAddress, chain.addressPrefix);
|
||||
if (dstAddressErrorMsg) {
|
||||
setValidatorDstAddressError(
|
||||
`Invalid address for network ${chain.chainId}: ${dstAddressErrorMsg}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!amount || Number(amount) <= 0) {
|
||||
setAmountError("Amount must be greater than 0");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
displayCoinToBaseCoin({ denom: chain.displayDenom, amount }, chain.assets);
|
||||
} catch (e: unknown) {
|
||||
setAmountError(e instanceof Error ? e.message : "Could not set decimals");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const microCoin = (() => {
|
||||
try {
|
||||
return displayCoinToBaseCoin({ denom: chain.displayDenom, amount }, chain.assets);
|
||||
} catch {
|
||||
return { denom: chain.displayDenom, amount: "0" };
|
||||
}
|
||||
})();
|
||||
|
||||
const msgValue = MsgCodecs[MsgTypeUrls.BeginRedelegate].fromPartial({
|
||||
delegatorAddress: senderAddress,
|
||||
validatorSrcAddress,
|
||||
validatorDstAddress,
|
||||
amount: microCoin,
|
||||
});
|
||||
|
||||
const msg: MsgBeginRedelegateEncodeObject = {
|
||||
typeUrl: MsgTypeUrls.BeginRedelegate,
|
||||
value: msgValue,
|
||||
};
|
||||
|
||||
setMsgGetter({ isMsgValid, msg });
|
||||
}, [
|
||||
chain.addressPrefix,
|
||||
chain.assets,
|
||||
chain.chainId,
|
||||
chain.displayDenom,
|
||||
senderAddress,
|
||||
setMsgGetter,
|
||||
trimmedInputs,
|
||||
]);
|
||||
|
||||
return (
|
||||
<StackableContainer lessPadding lessMargin>
|
||||
<button className="remove" onClick={() => deleteMsg()}>
|
||||
✕
|
||||
</button>
|
||||
<h2>MsgBeginRedelegate</h2>
|
||||
<div className="form-item">
|
||||
<SelectValidator
|
||||
validatorAddress={validatorSrcAddress}
|
||||
setValidatorAddress={setValidatorSrcAddress}
|
||||
/>
|
||||
<Input
|
||||
label="Source Validator Address"
|
||||
name="src-validator-address"
|
||||
value={validatorSrcAddress}
|
||||
onChange={({ target }) => {
|
||||
setValidatorSrcAddress(target.value);
|
||||
setValidatorSrcAddressError("");
|
||||
}}
|
||||
error={validatorSrcAddressError}
|
||||
placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<SelectValidator
|
||||
validatorAddress={validatorDstAddress}
|
||||
setValidatorAddress={setValidatorDstAddress}
|
||||
/>
|
||||
<Input
|
||||
label="Destination Validator Address"
|
||||
name="dst-validator-address"
|
||||
value={validatorDstAddress}
|
||||
onChange={({ target }) => {
|
||||
setValidatorDstAddress(target.value);
|
||||
setValidatorDstAddressError("");
|
||||
}}
|
||||
error={validatorDstAddressError}
|
||||
placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
type="number"
|
||||
label={`Amount (${chain.displayDenom})`}
|
||||
name="amount"
|
||||
value={amount}
|
||||
onChange={({ target }) => {
|
||||
setAmount(target.value);
|
||||
setAmountError("");
|
||||
}}
|
||||
error={amountError}
|
||||
/>
|
||||
</div>
|
||||
<style jsx>{`
|
||||
.form-item {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
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 MsgBeginRedelegateForm;
|
||||
@@ -0,0 +1,186 @@
|
||||
import { EncodeObject } from "@cosmjs/proto-signing";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MsgGetter } from "..";
|
||||
import { useChains } from "../../../../context/ChainsContext";
|
||||
import { displayCoinToBaseCoin } from "../../../../lib/coinHelpers";
|
||||
import {
|
||||
datetimeLocalFromTimestamp,
|
||||
timestampFromDatetimeLocal,
|
||||
} from "../../../../lib/dateHelpers";
|
||||
import { checkAddress, exampleAddress, trimStringsObj } from "../../../../lib/displayHelpers";
|
||||
import { MsgCodecs, MsgTypeUrls } from "../../../../types/txMsg";
|
||||
import Input from "../../../inputs/Input";
|
||||
import StackableContainer from "../../../layout/StackableContainer";
|
||||
|
||||
interface MsgCreateVestingAccountFormProps {
|
||||
readonly senderAddress: string;
|
||||
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
|
||||
readonly deleteMsg: () => void;
|
||||
}
|
||||
|
||||
const MsgCreateVestingAccountForm = ({
|
||||
senderAddress,
|
||||
setMsgGetter,
|
||||
deleteMsg,
|
||||
}: MsgCreateVestingAccountFormProps) => {
|
||||
const { chain } = useChains();
|
||||
|
||||
const [toAddress, setToAddress] = useState("");
|
||||
const [amount, setAmount] = useState("0");
|
||||
const [endTime, setEndTime] = useState(
|
||||
datetimeLocalFromTimestamp(Date.now() + 30 * 24 * 60 * 60 * 1000), // Default is one month from now
|
||||
);
|
||||
const [delayed, setDelayed] = useState(true);
|
||||
|
||||
const [toAddressError, setToAddressError] = useState("");
|
||||
const [amountError, setAmountError] = useState("");
|
||||
const [endTimeError, setEndTimeError] = useState("");
|
||||
|
||||
const trimmedInputs = trimStringsObj({ toAddress, amount, endTime });
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
const { toAddress, amount, endTime } = trimmedInputs;
|
||||
|
||||
const isMsgValid = (): boolean => {
|
||||
setToAddressError("");
|
||||
setAmountError("");
|
||||
setEndTimeError("");
|
||||
|
||||
const addressErrorMsg = checkAddress(toAddress, chain.addressPrefix);
|
||||
if (addressErrorMsg) {
|
||||
setToAddressError(`Invalid address for network ${chain.chainId}: ${addressErrorMsg}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!amount || Number(amount) <= 0) {
|
||||
setAmountError("Amount must be greater than 0");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
displayCoinToBaseCoin({ denom: chain.displayDenom, amount }, chain.assets);
|
||||
} catch (e: unknown) {
|
||||
setAmountError(e instanceof Error ? e.message : "Could not set decimals");
|
||||
return false;
|
||||
}
|
||||
|
||||
const timeoutDate = new Date(Number(timestampFromDatetimeLocal(endTime, "ms")));
|
||||
if (timeoutDate <= new Date()) {
|
||||
setEndTimeError("End time must be a date in the future");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const microCoin = (() => {
|
||||
try {
|
||||
if (!amount || amount === "0") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return displayCoinToBaseCoin({ denom: chain.displayDenom, amount }, chain.assets);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const msgValue = MsgCodecs[MsgTypeUrls.CreateVestingAccount].fromPartial({
|
||||
fromAddress: senderAddress,
|
||||
toAddress,
|
||||
amount: microCoin ? [microCoin] : [],
|
||||
endTime: timestampFromDatetimeLocal(endTime, "s"),
|
||||
delayed,
|
||||
});
|
||||
|
||||
const msg: EncodeObject = { typeUrl: MsgTypeUrls.CreateVestingAccount, value: msgValue };
|
||||
|
||||
setMsgGetter({ isMsgValid, msg });
|
||||
}, [
|
||||
chain.addressPrefix,
|
||||
chain.assets,
|
||||
chain.chainId,
|
||||
chain.displayDenom,
|
||||
delayed,
|
||||
senderAddress,
|
||||
setMsgGetter,
|
||||
trimmedInputs,
|
||||
]);
|
||||
|
||||
return (
|
||||
<StackableContainer lessPadding lessMargin>
|
||||
<button className="remove" onClick={() => deleteMsg()}>
|
||||
✕
|
||||
</button>
|
||||
<h2>MsgCreateVestingAccount</h2>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
label="Recipient Address"
|
||||
name="recipient-address"
|
||||
value={toAddress}
|
||||
onChange={({ target }) => {
|
||||
setToAddress(target.value);
|
||||
setToAddressError("");
|
||||
}}
|
||||
error={toAddressError}
|
||||
placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
type="number"
|
||||
label={`Amount (${chain.displayDenom})`}
|
||||
name="amount"
|
||||
value={amount}
|
||||
onChange={({ target }) => {
|
||||
setAmount(target.value);
|
||||
setAmountError("");
|
||||
}}
|
||||
error={amountError}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
type="datetime-local"
|
||||
label="End time"
|
||||
name="end-time"
|
||||
value={endTime}
|
||||
onChange={({ target }) => {
|
||||
setEndTime(target.value);
|
||||
setEndTimeError("");
|
||||
}}
|
||||
error={endTimeError}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
type="checkbox"
|
||||
label="Delayed"
|
||||
name="delayed"
|
||||
checked={delayed}
|
||||
value={String(delayed)}
|
||||
onChange={({ target }) => setDelayed(target.checked)}
|
||||
/>
|
||||
</div>
|
||||
<style jsx>{`
|
||||
.form-item {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
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 MsgCreateVestingAccountForm;
|
||||
@@ -0,0 +1,143 @@
|
||||
import SelectValidator from "@/components/SelectValidator";
|
||||
import { MsgDelegateEncodeObject } from "@cosmjs/stargate";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MsgGetter } from "..";
|
||||
import { useChains } from "../../../../context/ChainsContext";
|
||||
import { displayCoinToBaseCoin } from "../../../../lib/coinHelpers";
|
||||
import { checkAddress, exampleAddress, trimStringsObj } from "../../../../lib/displayHelpers";
|
||||
import { MsgCodecs, MsgTypeUrls } from "../../../../types/txMsg";
|
||||
import Input from "../../../inputs/Input";
|
||||
import StackableContainer from "../../../layout/StackableContainer";
|
||||
|
||||
interface MsgDelegateFormProps {
|
||||
readonly senderAddress: string;
|
||||
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
|
||||
readonly deleteMsg: () => void;
|
||||
}
|
||||
|
||||
const MsgDelegateForm = ({ senderAddress, setMsgGetter, deleteMsg }: MsgDelegateFormProps) => {
|
||||
const { chain } = useChains();
|
||||
|
||||
const [validatorAddress, setValidatorAddress] = useState("");
|
||||
const [amount, setAmount] = useState("0");
|
||||
|
||||
const [validatorAddressError, setValidatorAddressError] = useState("");
|
||||
const [amountError, setAmountError] = useState("");
|
||||
|
||||
const trimmedInputs = trimStringsObj({ validatorAddress, amount });
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
const { validatorAddress, amount } = trimmedInputs;
|
||||
|
||||
const isMsgValid = (): boolean => {
|
||||
setValidatorAddressError("");
|
||||
setAmountError("");
|
||||
|
||||
const addressErrorMsg = checkAddress(validatorAddress, chain.addressPrefix);
|
||||
if (addressErrorMsg) {
|
||||
setValidatorAddressError(
|
||||
`Invalid address for network ${chain.chainId}: ${addressErrorMsg}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!amount || Number(amount) <= 0) {
|
||||
setAmountError("Amount must be greater than 0");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
displayCoinToBaseCoin({ denom: chain.displayDenom, amount }, chain.assets);
|
||||
} catch (e: unknown) {
|
||||
setAmountError(e instanceof Error ? e.message : "Could not set decimals");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const microCoin = (() => {
|
||||
try {
|
||||
return displayCoinToBaseCoin({ denom: chain.displayDenom, amount }, chain.assets);
|
||||
} catch {
|
||||
return { denom: chain.displayDenom, amount: "0" };
|
||||
}
|
||||
})();
|
||||
|
||||
const msgValue = MsgCodecs[MsgTypeUrls.Delegate].fromPartial({
|
||||
delegatorAddress: senderAddress,
|
||||
validatorAddress,
|
||||
amount: microCoin,
|
||||
});
|
||||
|
||||
const msg: MsgDelegateEncodeObject = { typeUrl: MsgTypeUrls.Delegate, value: msgValue };
|
||||
|
||||
setMsgGetter({ isMsgValid, msg });
|
||||
}, [
|
||||
chain.addressPrefix,
|
||||
chain.assets,
|
||||
chain.chainId,
|
||||
chain.displayDenom,
|
||||
senderAddress,
|
||||
setMsgGetter,
|
||||
trimmedInputs,
|
||||
]);
|
||||
|
||||
return (
|
||||
<StackableContainer lessPadding lessMargin>
|
||||
<button className="remove" onClick={() => deleteMsg()}>
|
||||
✕
|
||||
</button>
|
||||
<h2>MsgDelegate</h2>
|
||||
<div className="form-item">
|
||||
<SelectValidator
|
||||
validatorAddress={validatorAddress}
|
||||
setValidatorAddress={setValidatorAddress}
|
||||
/>
|
||||
<Input
|
||||
label="Validator Address"
|
||||
name="validator-address"
|
||||
value={validatorAddress}
|
||||
onChange={({ target }) => {
|
||||
setValidatorAddress(target.value);
|
||||
setValidatorAddressError("");
|
||||
}}
|
||||
error={validatorAddressError}
|
||||
placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
type="number"
|
||||
label={`Amount (${chain.displayDenom})`}
|
||||
name="amount"
|
||||
value={amount}
|
||||
onChange={({ target }) => {
|
||||
setAmount(target.value);
|
||||
setAmountError("");
|
||||
}}
|
||||
error={amountError}
|
||||
/>
|
||||
</div>
|
||||
<style jsx>{`
|
||||
.form-item {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
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 MsgDelegateForm;
|
||||
@@ -0,0 +1,260 @@
|
||||
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 { displayCoinToBaseCoin } from "../../../../lib/coinHelpers";
|
||||
import { checkAddress, exampleAddress, trimStringsObj } 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 senderAddress: string;
|
||||
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
|
||||
readonly deleteMsg: () => void;
|
||||
}
|
||||
|
||||
const MsgExecuteContractForm = ({
|
||||
senderAddress,
|
||||
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("");
|
||||
|
||||
const trimmedInputs = trimStringsObj({ contractAddress, customDenom, amount });
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
const { contractAddress, customDenom, amount } = trimmedInputs;
|
||||
const denom =
|
||||
selectedDenom.value === customDenomOption.value ? customDenom : selectedDenom.value.symbol;
|
||||
|
||||
const isMsgValid = (): boolean => {
|
||||
setContractAddressError("");
|
||||
setCustomDenomError("");
|
||||
setAmountError("");
|
||||
|
||||
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 &&
|
||||
amount &&
|
||||
amount !== "0"
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
||||
if (denom && amount) {
|
||||
try {
|
||||
displayCoinToBaseCoin({ denom, amount }, chain.assets);
|
||||
} catch (e: unknown) {
|
||||
setAmountError(e instanceof Error ? e.message : "Could not set decimals");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const microCoin = (() => {
|
||||
try {
|
||||
if (!denom || !amount || amount === "0") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return displayCoinToBaseCoin({ denom, amount }, chain.assets);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
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.ExecuteContract].fromPartial({
|
||||
sender: senderAddress,
|
||||
contract: contractAddress,
|
||||
msg: msgContentUtf8Array,
|
||||
funds: microCoin ? [microCoin] : [],
|
||||
});
|
||||
|
||||
const msg: MsgExecuteContractEncodeObject = {
|
||||
typeUrl: MsgTypeUrls.ExecuteContract,
|
||||
value: msgValue,
|
||||
};
|
||||
|
||||
setMsgGetter({ isMsgValid, msg });
|
||||
}, [
|
||||
chain.addressPrefix,
|
||||
chain.assets,
|
||||
chain.chainId,
|
||||
msgContent,
|
||||
selectedDenom.value,
|
||||
senderAddress,
|
||||
setMsgGetter,
|
||||
trimmedInputs,
|
||||
]);
|
||||
|
||||
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);
|
||||
setContractAddressError("");
|
||||
}}
|
||||
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("");
|
||||
}
|
||||
setCustomDenomError("");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{selectedDenom.value === customDenomOption.value ? (
|
||||
<div className="form-item">
|
||||
<Input
|
||||
label="Custom denom"
|
||||
name="custom-denom"
|
||||
value={customDenom}
|
||||
onChange={({ target }) => {
|
||||
setCustomDenom(target.value);
|
||||
setCustomDenomError("");
|
||||
}}
|
||||
placeholder={
|
||||
selectedDenom.value === customDenomOption.value
|
||||
? "Enter custom denom"
|
||||
: "Select Custom denom above"
|
||||
}
|
||||
disabled={selectedDenom.value !== customDenomOption.value}
|
||||
error={customDenomError}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="form-item">
|
||||
<Input
|
||||
type="number"
|
||||
label="Amount"
|
||||
name="amount"
|
||||
value={amount}
|
||||
onChange={({ target }) => {
|
||||
setAmount(target.value);
|
||||
setAmountError("");
|
||||
}}
|
||||
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,194 @@
|
||||
import { EncodeObject } from "@cosmjs/proto-signing";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MsgGetter } from "..";
|
||||
import { useChains } from "../../../../context/ChainsContext";
|
||||
import { displayCoinToBaseCoin } from "../../../../lib/coinHelpers";
|
||||
import { trimStringsObj } from "../../../../lib/displayHelpers";
|
||||
import { RegistryAsset } from "../../../../types/chainRegistry";
|
||||
import { MsgCodecs, MsgTypeUrls } from "../../../../types/txMsg";
|
||||
import Input from "../../../inputs/Input";
|
||||
import Select from "../../../inputs/Select";
|
||||
import StackableContainer from "../../../layout/StackableContainer";
|
||||
|
||||
const customDenomOption = { label: "Custom (enter denom below)", value: "custom" } as const;
|
||||
|
||||
const getDenomOptions = (assets: readonly RegistryAsset[]) => {
|
||||
if (!assets?.length) {
|
||||
return [customDenomOption];
|
||||
}
|
||||
|
||||
return [...assets.map((asset) => ({ label: asset.symbol, value: asset })), customDenomOption];
|
||||
};
|
||||
|
||||
interface MsgFundCommunityPoolFormProps {
|
||||
readonly senderAddress: string;
|
||||
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
|
||||
readonly deleteMsg: () => void;
|
||||
}
|
||||
|
||||
const MsgFundCommunityPoolForm = ({
|
||||
senderAddress,
|
||||
setMsgGetter,
|
||||
deleteMsg,
|
||||
}: MsgFundCommunityPoolFormProps) => {
|
||||
const { chain } = useChains();
|
||||
|
||||
const denomOptions = getDenomOptions(chain.assets);
|
||||
|
||||
const [selectedDenom, setSelectedDenom] = useState(denomOptions[0]);
|
||||
const [customDenom, setCustomDenom] = useState("");
|
||||
const [amount, setAmount] = useState("0");
|
||||
|
||||
const [customDenomError, setCustomDenomError] = useState("");
|
||||
const [amountError, setAmountError] = useState("");
|
||||
|
||||
const trimmedInputs = trimStringsObj({ customDenom, amount });
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
const { customDenom, amount } = trimmedInputs;
|
||||
|
||||
const isMsgValid = (): boolean => {
|
||||
setCustomDenomError("");
|
||||
setAmountError("");
|
||||
|
||||
if (
|
||||
selectedDenom.value === customDenomOption.value &&
|
||||
!customDenom &&
|
||||
amount &&
|
||||
amount !== "0"
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
displayCoinToBaseCoin({ denom, amount }, chain.assets);
|
||||
} catch (e: unknown) {
|
||||
setAmountError(e instanceof Error ? e.message : "Could not set decimals");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const denom =
|
||||
selectedDenom.value === customDenomOption.value ? customDenom : selectedDenom.value.symbol;
|
||||
|
||||
const microCoin = (() => {
|
||||
try {
|
||||
if (!denom || !amount || amount === "0") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return displayCoinToBaseCoin({ denom, amount }, chain.assets);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const msgValue = MsgCodecs[MsgTypeUrls.FundCommunityPool].fromPartial({
|
||||
depositor: senderAddress,
|
||||
amount: microCoin ? [microCoin] : [],
|
||||
});
|
||||
|
||||
const msg: EncodeObject = { typeUrl: MsgTypeUrls.FundCommunityPool, value: msgValue };
|
||||
|
||||
setMsgGetter({ isMsgValid, msg });
|
||||
}, [chain.assets, selectedDenom.value, senderAddress, setMsgGetter, trimmedInputs]);
|
||||
|
||||
return (
|
||||
<StackableContainer lessPadding lessMargin>
|
||||
<button className="remove" onClick={() => deleteMsg()}>
|
||||
✕
|
||||
</button>
|
||||
<h2>MsgFundCommunityPool</h2>
|
||||
<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("");
|
||||
}
|
||||
setCustomDenomError("");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{selectedDenom.value === customDenomOption.value ? (
|
||||
<div className="form-item">
|
||||
<Input
|
||||
label="Custom denom"
|
||||
name="custom-denom"
|
||||
value={customDenom}
|
||||
onChange={({ target }) => {
|
||||
setCustomDenom(target.value);
|
||||
setCustomDenomError("");
|
||||
}}
|
||||
placeholder={
|
||||
selectedDenom.value === customDenomOption.value
|
||||
? "Enter custom denom"
|
||||
: "Select Custom denom above"
|
||||
}
|
||||
disabled={selectedDenom.value !== customDenomOption.value}
|
||||
error={customDenomError}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="form-item">
|
||||
<Input
|
||||
type="number"
|
||||
label="Amount"
|
||||
name="amount"
|
||||
value={amount}
|
||||
onChange={({ target }) => {
|
||||
setAmount(target.value);
|
||||
setAmountError("");
|
||||
}}
|
||||
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 MsgFundCommunityPoolForm;
|
||||
@@ -0,0 +1,338 @@
|
||||
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 { displayCoinToBaseCoin } from "../../../../lib/coinHelpers";
|
||||
import { checkAddress, exampleAddress, trimStringsObj } 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 senderAddress: string;
|
||||
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
|
||||
readonly deleteMsg: () => void;
|
||||
}
|
||||
|
||||
const MsgInstantiateContract2Form = ({
|
||||
senderAddress,
|
||||
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("");
|
||||
|
||||
const trimmedInputs = trimStringsObj({ codeId, label, adminAddress, salt, customDenom, amount });
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
const { codeId, label, adminAddress, salt, customDenom, amount } = trimmedInputs;
|
||||
|
||||
const isMsgValid = (): boolean => {
|
||||
setCodeIdError("");
|
||||
setLabelError("");
|
||||
setAdminAddressError("");
|
||||
setSaltError("");
|
||||
setCustomDenomError("");
|
||||
setAmountError("");
|
||||
|
||||
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 &&
|
||||
amount &&
|
||||
amount !== "0"
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
displayCoinToBaseCoin({ denom, amount }, chain.assets);
|
||||
} catch (e: unknown) {
|
||||
setAmountError(e instanceof Error ? e.message : "Could not set decimals");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const denom =
|
||||
selectedDenom.value === customDenomOption.value ? customDenom : selectedDenom.value.symbol;
|
||||
|
||||
const microCoin = (() => {
|
||||
try {
|
||||
if (!denom || !amount || amount === "0") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return displayCoinToBaseCoin({ denom, amount }, chain.assets);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
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.InstantiateContract2].fromPartial({
|
||||
sender: senderAddress,
|
||||
codeId: BigInt(codeId),
|
||||
label,
|
||||
admin: adminAddress,
|
||||
fixMsg: false,
|
||||
salt: hexSalt,
|
||||
msg: msgContentUtf8Array,
|
||||
funds: microCoin ? [microCoin] : [],
|
||||
});
|
||||
|
||||
const msg: MsgInstantiateContract2EncodeObject = {
|
||||
typeUrl: MsgTypeUrls.InstantiateContract2,
|
||||
value: msgValue,
|
||||
};
|
||||
|
||||
setMsgGetter({ isMsgValid, msg });
|
||||
}, [
|
||||
chain.addressPrefix,
|
||||
chain.assets,
|
||||
chain.chainId,
|
||||
msgContent,
|
||||
selectedDenom.value,
|
||||
senderAddress,
|
||||
setMsgGetter,
|
||||
trimmedInputs,
|
||||
]);
|
||||
|
||||
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);
|
||||
setCodeIdError("");
|
||||
}}
|
||||
error={codeIdError}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
label="Label"
|
||||
name="label"
|
||||
value={label}
|
||||
onChange={({ target }) => {
|
||||
setLabel(target.value);
|
||||
setLabelError("");
|
||||
}}
|
||||
error={labelError}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
label="Admin Address"
|
||||
name="admin-address"
|
||||
value={adminAddress}
|
||||
onChange={({ target }) => {
|
||||
setAdminAddress(target.value);
|
||||
setAdminAddressError("");
|
||||
}}
|
||||
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);
|
||||
setSaltError("");
|
||||
}}
|
||||
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("");
|
||||
}
|
||||
setCustomDenomError("");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{selectedDenom.value === customDenomOption.value ? (
|
||||
<div className="form-item">
|
||||
<Input
|
||||
label="Custom denom"
|
||||
name="custom-denom"
|
||||
value={customDenom}
|
||||
onChange={({ target }) => {
|
||||
setCustomDenom(target.value);
|
||||
setCustomDenomError("");
|
||||
}}
|
||||
placeholder={
|
||||
selectedDenom.value === customDenomOption.value
|
||||
? "Enter custom denom"
|
||||
: "Select Custom denom above"
|
||||
}
|
||||
disabled={selectedDenom.value !== customDenomOption.value}
|
||||
error={customDenomError}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="form-item">
|
||||
<Input
|
||||
type="number"
|
||||
label="Amount"
|
||||
name="amount"
|
||||
value={amount}
|
||||
onChange={({ target }) => {
|
||||
setAmount(target.value);
|
||||
setAmountError("");
|
||||
}}
|
||||
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,301 @@
|
||||
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 { displayCoinToBaseCoin } from "../../../../lib/coinHelpers";
|
||||
import { checkAddress, exampleAddress, trimStringsObj } 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 senderAddress: string;
|
||||
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
|
||||
readonly deleteMsg: () => void;
|
||||
}
|
||||
|
||||
const MsgInstantiateContractForm = ({
|
||||
senderAddress,
|
||||
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("");
|
||||
|
||||
const trimmedInputs = trimStringsObj({ codeId, label, adminAddress, customDenom, amount });
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
const { codeId, label, adminAddress, customDenom, amount } = trimmedInputs;
|
||||
|
||||
const isMsgValid = (): boolean => {
|
||||
setCodeIdError("");
|
||||
setLabelError("");
|
||||
setAdminAddressError("");
|
||||
setCustomDenomError("");
|
||||
setAmountError("");
|
||||
|
||||
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 &&
|
||||
amount &&
|
||||
amount !== "0"
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
displayCoinToBaseCoin({ denom, amount }, chain.assets);
|
||||
} catch (e: unknown) {
|
||||
setAmountError(e instanceof Error ? e.message : "Could not set decimals");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const denom =
|
||||
selectedDenom.value === customDenomOption.value ? customDenom : selectedDenom.value.symbol;
|
||||
|
||||
const microCoin = (() => {
|
||||
try {
|
||||
if (!denom || !amount || amount === "0") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return displayCoinToBaseCoin({ denom, amount }, chain.assets);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
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.InstantiateContract].fromPartial({
|
||||
sender: senderAddress,
|
||||
codeId: BigInt(codeId),
|
||||
label,
|
||||
admin: adminAddress,
|
||||
msg: msgContentUtf8Array,
|
||||
funds: microCoin ? [microCoin] : [],
|
||||
});
|
||||
|
||||
const msg: MsgInstantiateContractEncodeObject = {
|
||||
typeUrl: MsgTypeUrls.InstantiateContract,
|
||||
value: msgValue,
|
||||
};
|
||||
|
||||
setMsgGetter({ isMsgValid, msg });
|
||||
}, [
|
||||
chain.addressPrefix,
|
||||
chain.assets,
|
||||
chain.chainId,
|
||||
msgContent,
|
||||
selectedDenom.value,
|
||||
senderAddress,
|
||||
setMsgGetter,
|
||||
trimmedInputs,
|
||||
]);
|
||||
|
||||
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);
|
||||
setCodeIdError("");
|
||||
}}
|
||||
error={codeIdError}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
label="Label"
|
||||
name="label"
|
||||
value={label}
|
||||
onChange={({ target }) => {
|
||||
setLabel(target.value);
|
||||
setLabelError("");
|
||||
}}
|
||||
error={labelError}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
label="Admin Address"
|
||||
name="admin-address"
|
||||
value={adminAddress}
|
||||
onChange={({ target }) => {
|
||||
setAdminAddress(target.value);
|
||||
setAdminAddressError("");
|
||||
}}
|
||||
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("");
|
||||
}
|
||||
setCustomDenomError("");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{selectedDenom.value === customDenomOption.value ? (
|
||||
<div className="form-item">
|
||||
<Input
|
||||
label="Custom denom"
|
||||
name="custom-denom"
|
||||
value={customDenom}
|
||||
onChange={({ target }) => {
|
||||
setCustomDenom(target.value);
|
||||
setCustomDenomError("");
|
||||
}}
|
||||
placeholder={
|
||||
selectedDenom.value === customDenomOption.value
|
||||
? "Enter custom denom"
|
||||
: "Select Custom denom above"
|
||||
}
|
||||
disabled={selectedDenom.value !== customDenomOption.value}
|
||||
error={customDenomError}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="form-item">
|
||||
<Input
|
||||
type="number"
|
||||
label="Amount"
|
||||
name="amount"
|
||||
value={amount}
|
||||
onChange={({ target }) => {
|
||||
setAmount(target.value);
|
||||
setAmountError("");
|
||||
}}
|
||||
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,157 @@
|
||||
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, trimStringsObj } 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 senderAddress: string;
|
||||
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
|
||||
readonly deleteMsg: () => void;
|
||||
}
|
||||
|
||||
const MsgMigrateContractForm = ({
|
||||
senderAddress,
|
||||
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("");
|
||||
|
||||
const trimmedInputs = trimStringsObj({ contractAddress, codeId });
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
const { contractAddress, codeId } = trimmedInputs;
|
||||
|
||||
const isMsgValid = (): boolean => {
|
||||
setContractAddressError("");
|
||||
setCodeIdError("");
|
||||
|
||||
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.MigrateContract].fromPartial({
|
||||
sender: senderAddress,
|
||||
contract: contractAddress,
|
||||
codeId: BigInt(codeId),
|
||||
msg: msgContentUtf8Array,
|
||||
});
|
||||
|
||||
const msg: MsgMigrateContractEncodeObject = {
|
||||
typeUrl: MsgTypeUrls.MigrateContract,
|
||||
value: msgValue,
|
||||
};
|
||||
|
||||
setMsgGetter({ isMsgValid, msg });
|
||||
}, [chain.addressPrefix, chain.chainId, msgContent, senderAddress, setMsgGetter, trimmedInputs]);
|
||||
|
||||
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);
|
||||
setContractAddressError("");
|
||||
}}
|
||||
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);
|
||||
setCodeIdError("");
|
||||
}}
|
||||
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;
|
||||
@@ -0,0 +1,221 @@
|
||||
import { MsgSendEncodeObject } from "@cosmjs/stargate";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MsgGetter } from "..";
|
||||
import { useChains } from "../../../../context/ChainsContext";
|
||||
import { displayCoinToBaseCoin } from "../../../../lib/coinHelpers";
|
||||
import { checkAddress, exampleAddress, trimStringsObj } from "../../../../lib/displayHelpers";
|
||||
import { RegistryAsset } from "../../../../types/chainRegistry";
|
||||
import { MsgCodecs, MsgTypeUrls } from "../../../../types/txMsg";
|
||||
import Input from "../../../inputs/Input";
|
||||
import Select from "../../../inputs/Select";
|
||||
import StackableContainer from "../../../layout/StackableContainer";
|
||||
|
||||
const customDenomOption = { label: "Custom (enter denom below)", value: "custom" } as const;
|
||||
|
||||
const getDenomOptions = (assets: readonly RegistryAsset[]) => {
|
||||
if (!assets?.length) {
|
||||
return [customDenomOption];
|
||||
}
|
||||
|
||||
return [...assets.map((asset) => ({ label: asset.symbol, value: asset })), customDenomOption];
|
||||
};
|
||||
|
||||
interface MsgSendFormProps {
|
||||
readonly senderAddress: string;
|
||||
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
|
||||
readonly deleteMsg: () => void;
|
||||
}
|
||||
|
||||
const MsgSendForm = ({ senderAddress, setMsgGetter, deleteMsg }: MsgSendFormProps) => {
|
||||
const { chain } = useChains();
|
||||
|
||||
const denomOptions = getDenomOptions(chain.assets);
|
||||
|
||||
const [toAddress, setToAddress] = useState("");
|
||||
const [selectedDenom, setSelectedDenom] = useState(denomOptions[0]);
|
||||
const [customDenom, setCustomDenom] = useState("");
|
||||
const [amount, setAmount] = useState("0");
|
||||
|
||||
const [toAddressError, setToAddressError] = useState("");
|
||||
const [customDenomError, setCustomDenomError] = useState("");
|
||||
const [amountError, setAmountError] = useState("");
|
||||
|
||||
const trimmedInputs = trimStringsObj({ toAddress, customDenom, amount });
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
const { toAddress, customDenom, amount } = trimmedInputs;
|
||||
|
||||
const isMsgValid = (): boolean => {
|
||||
setToAddressError("");
|
||||
setCustomDenomError("");
|
||||
setAmountError("");
|
||||
|
||||
const addressErrorMsg = checkAddress(toAddress, chain.addressPrefix);
|
||||
if (addressErrorMsg) {
|
||||
setToAddressError(`Invalid address for network ${chain.chainId}: ${addressErrorMsg}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
selectedDenom.value === customDenomOption.value &&
|
||||
!customDenom &&
|
||||
amount &&
|
||||
amount !== "0"
|
||||
) {
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
displayCoinToBaseCoin({ denom, amount }, chain.assets);
|
||||
} catch (e: unknown) {
|
||||
setAmountError(e instanceof Error ? e.message : "Could not set decimals");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const denom =
|
||||
selectedDenom.value === customDenomOption.value ? customDenom : selectedDenom.value.symbol;
|
||||
|
||||
const microCoin = (() => {
|
||||
try {
|
||||
if (!denom || !amount || amount === "0") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return displayCoinToBaseCoin({ denom, amount }, chain.assets);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const msgValue = MsgCodecs[MsgTypeUrls.Send].fromPartial({
|
||||
fromAddress: senderAddress,
|
||||
toAddress,
|
||||
amount: microCoin ? [microCoin] : [],
|
||||
});
|
||||
|
||||
const msg: MsgSendEncodeObject = { typeUrl: MsgTypeUrls.Send, value: msgValue };
|
||||
|
||||
setMsgGetter({ isMsgValid, msg });
|
||||
}, [
|
||||
chain.addressPrefix,
|
||||
chain.assets,
|
||||
chain.chainId,
|
||||
selectedDenom.value,
|
||||
senderAddress,
|
||||
setMsgGetter,
|
||||
trimmedInputs,
|
||||
]);
|
||||
|
||||
return (
|
||||
<StackableContainer lessPadding lessMargin>
|
||||
<button className="remove" onClick={() => deleteMsg()}>
|
||||
✕
|
||||
</button>
|
||||
<h2>MsgSend</h2>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
label="Recipient Address"
|
||||
name="recipient-address"
|
||||
value={toAddress}
|
||||
onChange={({ target }) => {
|
||||
setToAddress(target.value);
|
||||
setToAddressError("");
|
||||
}}
|
||||
error={toAddressError}
|
||||
placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`}
|
||||
/>
|
||||
</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("");
|
||||
}
|
||||
setCustomDenomError("");
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{selectedDenom.value === customDenomOption.value ? (
|
||||
<div className="form-item">
|
||||
<Input
|
||||
label="Custom denom"
|
||||
name="custom-denom"
|
||||
value={customDenom}
|
||||
onChange={({ target }) => {
|
||||
setCustomDenom(target.value);
|
||||
setCustomDenomError("");
|
||||
}}
|
||||
placeholder={
|
||||
selectedDenom.value === customDenomOption.value
|
||||
? "Enter custom denom"
|
||||
: "Select Custom denom above"
|
||||
}
|
||||
disabled={selectedDenom.value !== customDenomOption.value}
|
||||
error={customDenomError}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="form-item">
|
||||
<Input
|
||||
type="number"
|
||||
label="Amount"
|
||||
name="amount"
|
||||
value={amount}
|
||||
onChange={({ target }) => {
|
||||
setAmount(target.value);
|
||||
setAmountError("");
|
||||
}}
|
||||
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 MsgSendForm;
|
||||
@@ -0,0 +1,92 @@
|
||||
import { EncodeObject } from "@cosmjs/proto-signing";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MsgGetter } from "..";
|
||||
import { useChains } from "../../../../context/ChainsContext";
|
||||
import { checkAddress, exampleAddress, trimStringsObj } from "../../../../lib/displayHelpers";
|
||||
import { MsgCodecs, MsgTypeUrls } from "../../../../types/txMsg";
|
||||
import Input from "../../../inputs/Input";
|
||||
import StackableContainer from "../../../layout/StackableContainer";
|
||||
|
||||
interface MsgSetWithdrawAddressFormProps {
|
||||
readonly senderAddress: string;
|
||||
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
|
||||
readonly deleteMsg: () => void;
|
||||
}
|
||||
|
||||
const MsgSetWithdrawAddressForm = ({
|
||||
senderAddress,
|
||||
setMsgGetter,
|
||||
deleteMsg,
|
||||
}: MsgSetWithdrawAddressFormProps) => {
|
||||
const { chain } = useChains();
|
||||
|
||||
const [withdrawAddress, setWithdrawAddress] = useState("");
|
||||
const [withdrawAddressError, setWithdrawAddressError] = useState("");
|
||||
|
||||
const trimmedInputs = trimStringsObj({ withdrawAddress });
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
const { withdrawAddress } = trimmedInputs;
|
||||
|
||||
const isMsgValid = (): boolean => {
|
||||
setWithdrawAddressError("");
|
||||
|
||||
const addressErrorMsg = checkAddress(withdrawAddress, chain.addressPrefix);
|
||||
if (addressErrorMsg) {
|
||||
setWithdrawAddressError(`Invalid address for network ${chain.chainId}: ${addressErrorMsg}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const msgValue = MsgCodecs[MsgTypeUrls.SetWithdrawAddress].fromPartial({
|
||||
delegatorAddress: senderAddress,
|
||||
withdrawAddress,
|
||||
});
|
||||
const msg: EncodeObject = { typeUrl: MsgTypeUrls.SetWithdrawAddress, value: msgValue };
|
||||
|
||||
setMsgGetter({ isMsgValid, msg });
|
||||
}, [chain.addressPrefix, chain.chainId, senderAddress, setMsgGetter, trimmedInputs]);
|
||||
|
||||
return (
|
||||
<StackableContainer lessPadding lessMargin>
|
||||
<button className="remove" onClick={() => deleteMsg()}>
|
||||
✕
|
||||
</button>
|
||||
<h2>MsgSetWithdrawAddress</h2>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
label="Withdraw Address"
|
||||
name="withdraw-address"
|
||||
value={withdrawAddress}
|
||||
onChange={({ target }) => {
|
||||
setWithdrawAddress(target.value);
|
||||
setWithdrawAddressError("");
|
||||
}}
|
||||
error={withdrawAddressError}
|
||||
placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`}
|
||||
/>
|
||||
</div>
|
||||
<style jsx>{`
|
||||
.form-item {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
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 MsgSetWithdrawAddressForm;
|
||||
@@ -0,0 +1,260 @@
|
||||
import { MsgTransferEncodeObject } from "@cosmjs/stargate";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MsgGetter } from "..";
|
||||
import { useChains } from "../../../../context/ChainsContext";
|
||||
import {
|
||||
datetimeLocalFromTimestamp,
|
||||
timestampFromDatetimeLocal,
|
||||
} from "../../../../lib/dateHelpers";
|
||||
import { checkAddress, exampleAddress, trimStringsObj } from "../../../../lib/displayHelpers";
|
||||
import { MsgCodecs, MsgTypeUrls } from "../../../../types/txMsg";
|
||||
import Input from "../../../inputs/Input";
|
||||
import StackableContainer from "../../../layout/StackableContainer";
|
||||
|
||||
const humanTimestampOptions = [
|
||||
{ label: "12 hours from now", value: 12 * 60 * 60 * 1000 },
|
||||
{ label: "1 day from now", value: 24 * 60 * 60 * 1000 },
|
||||
{ label: "2 days from now", value: 2 * 24 * 60 * 60 * 1000 },
|
||||
{ label: "3 days from now", value: 3 * 24 * 60 * 60 * 1000 },
|
||||
{ label: "7 days from now", value: 7 * 24 * 60 * 60 * 1000 },
|
||||
{ label: "10 days from now", value: 10 * 24 * 60 * 60 * 1000 },
|
||||
{ label: "2 weeks from now", value: 2 * 7 * 24 * 60 * 60 * 1000 },
|
||||
{ label: "3 weeks from now", value: 3 * 7 * 24 * 60 * 60 * 1000 },
|
||||
{ label: "1 month from now", value: 30 * 24 * 60 * 60 * 1000 },
|
||||
];
|
||||
|
||||
interface MsgTransferFormProps {
|
||||
readonly senderAddress: string;
|
||||
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
|
||||
readonly deleteMsg: () => void;
|
||||
}
|
||||
|
||||
const MsgTransferForm = ({ senderAddress, setMsgGetter, deleteMsg }: MsgTransferFormProps) => {
|
||||
const { chain } = useChains();
|
||||
|
||||
const [toAddress, setToAddress] = useState("");
|
||||
const [denom, setDenom] = useState("");
|
||||
const [amount, setAmount] = useState("0");
|
||||
const [sourcePort, setSourcePort] = useState("transfer");
|
||||
const [sourceChannel, setSourceChannel] = useState("");
|
||||
const [timeout, setTimeout] = useState(
|
||||
datetimeLocalFromTimestamp(Date.now() + humanTimestampOptions[0].value),
|
||||
);
|
||||
const [memo, setMemo] = useState("");
|
||||
|
||||
const [toAddressError, setToAddressError] = useState("");
|
||||
const [denomError, setDenomError] = useState("");
|
||||
const [amountError, setAmountError] = useState("");
|
||||
const [sourcePortError, setSourcePortError] = useState("");
|
||||
const [sourceChannelError, setSourceChannelError] = useState("");
|
||||
const [timeoutError, setTimeoutError] = useState("");
|
||||
|
||||
const trimmedInputs = trimStringsObj({
|
||||
toAddress,
|
||||
denom,
|
||||
amount,
|
||||
sourcePort,
|
||||
sourceChannel,
|
||||
timeout,
|
||||
memo,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
const { toAddress, denom, amount, sourcePort, sourceChannel, timeout, memo } = trimmedInputs;
|
||||
|
||||
const isMsgValid = (): boolean => {
|
||||
setToAddressError("");
|
||||
setDenomError("");
|
||||
setAmountError("");
|
||||
setSourcePortError("");
|
||||
setSourceChannelError("");
|
||||
setTimeoutError("");
|
||||
|
||||
const addressErrorMsg = checkAddress(toAddress, null); // Allow address from any chain
|
||||
if (addressErrorMsg) {
|
||||
setToAddressError(`Invalid address for network ${chain.chainId}: ${addressErrorMsg}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!denom) {
|
||||
setDenomError("Denom is required");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!amount || Number(amount) <= 0) {
|
||||
setAmountError("Amount must be greater than 0");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sourcePort) {
|
||||
setSourcePortError("Source port is required");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sourceChannel) {
|
||||
setSourceChannelError("Source channel is required");
|
||||
return false;
|
||||
}
|
||||
|
||||
const timeoutDate = new Date(Number(timestampFromDatetimeLocal(timeout, "ms")));
|
||||
if (timeoutDate <= new Date()) {
|
||||
setTimeoutError("Timeout must be a date in the future");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const msgValue = MsgCodecs[MsgTypeUrls.Transfer].fromPartial({
|
||||
sender: senderAddress,
|
||||
receiver: toAddress,
|
||||
token: { denom, amount },
|
||||
sourcePort,
|
||||
sourceChannel,
|
||||
timeoutTimestamp: timestampFromDatetimeLocal(timeout, "ns"),
|
||||
memo,
|
||||
});
|
||||
|
||||
const msg: MsgTransferEncodeObject = { typeUrl: MsgTypeUrls.Transfer, value: msgValue };
|
||||
|
||||
setMsgGetter({ isMsgValid, msg });
|
||||
}, [chain.chainId, senderAddress, setMsgGetter, trimmedInputs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!denom || !denom.startsWith("ibc/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const foundDenom = chain.assets.find((asset) => asset.base === denom);
|
||||
if (!foundDenom) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trace = foundDenom.traces?.[0];
|
||||
if (!trace) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSourcePort(trace.chain?.path?.split("/")?.[0] || "transfer");
|
||||
setSourceChannel(trace.chain?.channel_id || "");
|
||||
}, [chain.assets, denom]);
|
||||
|
||||
return (
|
||||
<StackableContainer lessPadding lessMargin>
|
||||
<button className="remove" onClick={() => deleteMsg()}>
|
||||
✕
|
||||
</button>
|
||||
<h2>MsgTransfer</h2>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
label="Recipient Address"
|
||||
name="recipient-address"
|
||||
value={toAddress}
|
||||
onChange={({ target }) => {
|
||||
setToAddress(target.value);
|
||||
setToAddressError("");
|
||||
}}
|
||||
error={toAddressError}
|
||||
placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
label="Denom"
|
||||
name="denom"
|
||||
value={denom}
|
||||
onChange={({ target }) => {
|
||||
setDenom(target.value);
|
||||
setDenomError("");
|
||||
}}
|
||||
error={denomError}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
type="number"
|
||||
label="Amount"
|
||||
name="amount"
|
||||
value={amount}
|
||||
onChange={({ target }) => {
|
||||
setAmount(target.value);
|
||||
setAmountError("");
|
||||
}}
|
||||
error={amountError}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
label="Source Port"
|
||||
name="source-port"
|
||||
value={sourcePort}
|
||||
onChange={({ target }) => {
|
||||
setSourcePort(target.value);
|
||||
setSourcePortError("");
|
||||
}}
|
||||
error={sourcePortError}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
label="Source Channel"
|
||||
name="source-channel"
|
||||
value={sourceChannel}
|
||||
onChange={({ target }) => {
|
||||
setSourceChannel(target.value);
|
||||
setSourceChannelError("");
|
||||
}}
|
||||
error={sourceChannelError}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
type="datetime-local"
|
||||
list="timestamp-options"
|
||||
label="Timeout"
|
||||
name="timeout"
|
||||
value={timeout}
|
||||
onChange={({ target }) => {
|
||||
setTimeout(target.value);
|
||||
setTimeoutError("");
|
||||
}}
|
||||
error={timeoutError}
|
||||
/>
|
||||
<datalist id="timestamp-options">
|
||||
{humanTimestampOptions.map(({ label, value }) => (
|
||||
<option key={label} value={datetimeLocalFromTimestamp(Date.now() + value)}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
label="Memo"
|
||||
name="memo"
|
||||
value={memo}
|
||||
onChange={({ target }) => setMemo(target.value)}
|
||||
/>
|
||||
</div>
|
||||
<style jsx>{`
|
||||
.form-item {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
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 MsgTransferForm;
|
||||
@@ -0,0 +1,143 @@
|
||||
import SelectValidator from "@/components/SelectValidator";
|
||||
import { MsgUndelegateEncodeObject } from "@cosmjs/stargate";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MsgGetter } from "..";
|
||||
import { useChains } from "../../../../context/ChainsContext";
|
||||
import { displayCoinToBaseCoin } from "../../../../lib/coinHelpers";
|
||||
import { checkAddress, exampleAddress, trimStringsObj } from "../../../../lib/displayHelpers";
|
||||
import { MsgCodecs, MsgTypeUrls } from "../../../../types/txMsg";
|
||||
import Input from "../../../inputs/Input";
|
||||
import StackableContainer from "../../../layout/StackableContainer";
|
||||
|
||||
interface MsgUndelegateFormProps {
|
||||
readonly senderAddress: string;
|
||||
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
|
||||
readonly deleteMsg: () => void;
|
||||
}
|
||||
|
||||
const MsgUndelegateForm = ({ senderAddress, setMsgGetter, deleteMsg }: MsgUndelegateFormProps) => {
|
||||
const { chain } = useChains();
|
||||
|
||||
const [validatorAddress, setValidatorAddress] = useState("");
|
||||
const [amount, setAmount] = useState("0");
|
||||
|
||||
const [validatorAddressError, setValidatorAddressError] = useState("");
|
||||
const [amountError, setAmountError] = useState("");
|
||||
|
||||
const trimmedInputs = trimStringsObj({ validatorAddress, amount });
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
const { validatorAddress, amount } = trimmedInputs;
|
||||
|
||||
const isMsgValid = (): boolean => {
|
||||
setValidatorAddressError("");
|
||||
setAmountError("");
|
||||
|
||||
const addressErrorMsg = checkAddress(validatorAddress, chain.addressPrefix);
|
||||
if (addressErrorMsg) {
|
||||
setValidatorAddressError(
|
||||
`Invalid address for network ${chain.chainId}: ${addressErrorMsg}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!amount || Number(amount) <= 0) {
|
||||
setAmountError("Amount must be greater than 0");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
displayCoinToBaseCoin({ denom: chain.displayDenom, amount }, chain.assets);
|
||||
} catch (e: unknown) {
|
||||
setAmountError(e instanceof Error ? e.message : "Could not set decimals");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const microCoin = (() => {
|
||||
try {
|
||||
return displayCoinToBaseCoin({ denom: chain.displayDenom, amount }, chain.assets);
|
||||
} catch {
|
||||
return { denom: chain.displayDenom, amount: "0" };
|
||||
}
|
||||
})();
|
||||
|
||||
const msgValue = MsgCodecs[MsgTypeUrls.Undelegate].fromPartial({
|
||||
delegatorAddress: senderAddress,
|
||||
validatorAddress,
|
||||
amount: microCoin,
|
||||
});
|
||||
|
||||
const msg: MsgUndelegateEncodeObject = { typeUrl: MsgTypeUrls.Undelegate, value: msgValue };
|
||||
|
||||
setMsgGetter({ isMsgValid, msg });
|
||||
}, [
|
||||
chain.addressPrefix,
|
||||
chain.assets,
|
||||
chain.chainId,
|
||||
chain.displayDenom,
|
||||
senderAddress,
|
||||
setMsgGetter,
|
||||
trimmedInputs,
|
||||
]);
|
||||
|
||||
return (
|
||||
<StackableContainer lessPadding lessMargin>
|
||||
<button className="remove" onClick={() => deleteMsg()}>
|
||||
✕
|
||||
</button>
|
||||
<h2>MsgUndelegate</h2>
|
||||
<div className="form-item">
|
||||
<SelectValidator
|
||||
validatorAddress={validatorAddress}
|
||||
setValidatorAddress={setValidatorAddress}
|
||||
/>
|
||||
<Input
|
||||
label="Validator Address"
|
||||
name="validator-address"
|
||||
value={validatorAddress}
|
||||
onChange={({ target }) => {
|
||||
setValidatorAddress(target.value);
|
||||
setValidatorAddressError("");
|
||||
}}
|
||||
error={validatorAddressError}
|
||||
placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
type="number"
|
||||
label={`Amount (${chain.displayDenom})`}
|
||||
name="amount"
|
||||
value={amount}
|
||||
onChange={({ target }) => {
|
||||
setAmount(target.value);
|
||||
setAmountError("");
|
||||
}}
|
||||
error={amountError}
|
||||
/>
|
||||
</div>
|
||||
<style jsx>{`
|
||||
.form-item {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
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 MsgUndelegateForm;
|
||||
@@ -0,0 +1,133 @@
|
||||
import { MsgUpdateAdminEncodeObject } from "@cosmjs/cosmwasm-stargate";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MsgGetter } from "..";
|
||||
import { useChains } from "../../../../context/ChainsContext";
|
||||
import { checkAddress, exampleAddress, trimStringsObj } from "../../../../lib/displayHelpers";
|
||||
import { MsgCodecs, MsgTypeUrls } from "../../../../types/txMsg";
|
||||
import Input from "../../../inputs/Input";
|
||||
import StackableContainer from "../../../layout/StackableContainer";
|
||||
|
||||
interface MsgUpdateAdminFormProps {
|
||||
readonly senderAddress: string;
|
||||
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
|
||||
readonly deleteMsg: () => void;
|
||||
}
|
||||
|
||||
const MsgUpdateAdminForm = ({
|
||||
senderAddress,
|
||||
setMsgGetter,
|
||||
deleteMsg,
|
||||
}: MsgUpdateAdminFormProps) => {
|
||||
const { chain } = useChains();
|
||||
|
||||
const [contractAddress, setContractAddress] = useState("");
|
||||
const [newAdminAddress, setNewAdminAddress] = useState("");
|
||||
|
||||
const [contractAddressError, setContractAddressError] = useState("");
|
||||
const [newAdminAddressError, setNewAdminAddressError] = useState("");
|
||||
|
||||
const trimmedInputs = trimStringsObj({ contractAddress, newAdminAddress });
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
const { contractAddress, newAdminAddress } = trimmedInputs;
|
||||
|
||||
const isMsgValid = (): boolean => {
|
||||
setContractAddressError("");
|
||||
setNewAdminAddressError("");
|
||||
|
||||
const contractAddressErrorMsg = checkAddress(contractAddress, chain.addressPrefix);
|
||||
if (contractAddressErrorMsg) {
|
||||
setContractAddressError(
|
||||
`Invalid address for network ${chain.chainId}: ${contractAddressErrorMsg}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const newAdminAddressErrorMsg = checkAddress(newAdminAddress, chain.addressPrefix);
|
||||
if (newAdminAddressErrorMsg) {
|
||||
setNewAdminAddressError(
|
||||
`Invalid address for network ${chain.chainId}: ${newAdminAddressErrorMsg}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const msgValue = MsgCodecs[MsgTypeUrls.UpdateAdmin].fromPartial({
|
||||
sender: senderAddress,
|
||||
contract: contractAddress,
|
||||
newAdmin: newAdminAddress,
|
||||
});
|
||||
|
||||
const msg: MsgUpdateAdminEncodeObject = { typeUrl: MsgTypeUrls.UpdateAdmin, value: msgValue };
|
||||
|
||||
setMsgGetter({ isMsgValid, msg });
|
||||
}, [chain.addressPrefix, chain.chainId, senderAddress, setMsgGetter, trimmedInputs]);
|
||||
|
||||
return (
|
||||
<StackableContainer lessPadding lessMargin>
|
||||
<button className="remove" onClick={() => deleteMsg()}>
|
||||
✕
|
||||
</button>
|
||||
<h2>MsgUpdateAdmin</h2>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
label="Contract Address"
|
||||
name="contract-address"
|
||||
value={contractAddress}
|
||||
onChange={({ target }) => {
|
||||
setContractAddress(target.value);
|
||||
setContractAddressError("");
|
||||
}}
|
||||
error={contractAddressError}
|
||||
placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
label="New Admin"
|
||||
name="new-admin"
|
||||
value={newAdminAddress}
|
||||
onChange={({ target }) => {
|
||||
setNewAdminAddress(target.value);
|
||||
setNewAdminAddressError("");
|
||||
}}
|
||||
error={newAdminAddressError}
|
||||
placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`}
|
||||
/>
|
||||
</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 MsgUpdateAdminForm;
|
||||
@@ -0,0 +1,137 @@
|
||||
import { printVoteOption, voteOptions } from "@/lib/gov";
|
||||
import { MsgVoteEncodeObject } from "@cosmjs/stargate";
|
||||
import { longify } from "@cosmjs/stargate/build/queryclient";
|
||||
import { voteOptionFromJSON } from "cosmjs-types/cosmos/gov/v1beta1/gov";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MsgGetter } from "..";
|
||||
import { trimStringsObj } 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 selectVoteOptions = voteOptions.map((opt) => {
|
||||
const voteOptionObj = voteOptionFromJSON(opt);
|
||||
|
||||
return {
|
||||
label: printVoteOption(voteOptionObj),
|
||||
value: voteOptionObj,
|
||||
};
|
||||
});
|
||||
|
||||
interface MsgVoteFormProps {
|
||||
readonly senderAddress: string;
|
||||
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
|
||||
readonly deleteMsg: () => void;
|
||||
}
|
||||
|
||||
const MsgVoteForm = ({ senderAddress, setMsgGetter, deleteMsg }: MsgVoteFormProps) => {
|
||||
const [proposalId, setProposalId] = useState("0");
|
||||
const [selectedVote, setSelectedVote] = useState(selectVoteOptions[0]);
|
||||
|
||||
const [proposalIdError, setProposalIdError] = useState("");
|
||||
|
||||
const trimmedInputs = trimStringsObj({ proposalId });
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
const { proposalId } = trimmedInputs;
|
||||
|
||||
const isMsgValid = (): boolean => {
|
||||
setProposalIdError("");
|
||||
|
||||
if (!proposalId || Number(proposalId) <= 0 || !Number.isSafeInteger(Number(proposalId))) {
|
||||
setProposalIdError("Proposal ID must be an integer greater than 0");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
longify(proposalId);
|
||||
} catch (e: unknown) {
|
||||
setProposalIdError(e instanceof Error ? e.message : "Proposal ID is not a valid Big Int");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const proposalIdBigInt = (() => {
|
||||
try {
|
||||
return longify(proposalId);
|
||||
} catch {
|
||||
return 0n;
|
||||
}
|
||||
})();
|
||||
|
||||
const msgValue = MsgCodecs[MsgTypeUrls.Vote].fromPartial({
|
||||
voter: senderAddress,
|
||||
proposalId: proposalIdBigInt,
|
||||
option: selectedVote.value,
|
||||
});
|
||||
|
||||
const msg: MsgVoteEncodeObject = { typeUrl: MsgTypeUrls.Vote, value: msgValue };
|
||||
|
||||
setMsgGetter({ isMsgValid, msg });
|
||||
}, [selectedVote.value, senderAddress, setMsgGetter, trimmedInputs]);
|
||||
|
||||
return (
|
||||
<StackableContainer lessPadding lessMargin>
|
||||
<button className="remove" onClick={() => deleteMsg()}>
|
||||
✕
|
||||
</button>
|
||||
<h2>MsgVote</h2>
|
||||
<div className="form-item">
|
||||
<Input
|
||||
type="number"
|
||||
label="Proposal ID"
|
||||
name="proposal-id"
|
||||
value={proposalId}
|
||||
onChange={({ target }) => {
|
||||
setProposalId(target.value);
|
||||
setProposalIdError("");
|
||||
}}
|
||||
error={proposalIdError}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-item form-select">
|
||||
<label>Choose a vote:</label>
|
||||
<Select
|
||||
label="Select vote"
|
||||
name="vote-select"
|
||||
options={selectVoteOptions}
|
||||
value={selectedVote}
|
||||
onChange={(option: (typeof selectVoteOptions)[number]) => {
|
||||
setSelectedVote(option);
|
||||
}}
|
||||
/>
|
||||
</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 MsgVoteForm;
|
||||
@@ -0,0 +1,103 @@
|
||||
import SelectValidator from "@/components/SelectValidator";
|
||||
import { MsgWithdrawDelegatorRewardEncodeObject } from "@cosmjs/stargate";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MsgGetter } from "..";
|
||||
import { useChains } from "../../../../context/ChainsContext";
|
||||
import { checkAddress, exampleAddress, trimStringsObj } from "../../../../lib/displayHelpers";
|
||||
import { MsgCodecs, MsgTypeUrls } from "../../../../types/txMsg";
|
||||
import Input from "../../../inputs/Input";
|
||||
import StackableContainer from "../../../layout/StackableContainer";
|
||||
|
||||
interface MsgWithdrawDelegatorRewardFormProps {
|
||||
readonly senderAddress: string;
|
||||
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
|
||||
readonly deleteMsg: () => void;
|
||||
}
|
||||
|
||||
const MsgWithdrawDelegatorRewardForm = ({
|
||||
senderAddress,
|
||||
setMsgGetter,
|
||||
deleteMsg,
|
||||
}: MsgWithdrawDelegatorRewardFormProps) => {
|
||||
const { chain } = useChains();
|
||||
|
||||
const [validatorAddress, setValidatorAddress] = useState("");
|
||||
const [validatorAddressError, setValidatorAddressError] = useState("");
|
||||
|
||||
const trimmedInputs = trimStringsObj({ validatorAddress });
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line no-shadow
|
||||
const { validatorAddress } = trimmedInputs;
|
||||
|
||||
const isMsgValid = (): boolean => {
|
||||
setValidatorAddressError("");
|
||||
|
||||
const addressErrorMsg = checkAddress(validatorAddress, chain.addressPrefix);
|
||||
if (addressErrorMsg) {
|
||||
setValidatorAddressError(
|
||||
`Invalid address for network ${chain.chainId}: ${addressErrorMsg}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const msgValue = MsgCodecs[MsgTypeUrls.WithdrawDelegatorReward].fromPartial({
|
||||
delegatorAddress: senderAddress,
|
||||
validatorAddress,
|
||||
});
|
||||
|
||||
const msg: MsgWithdrawDelegatorRewardEncodeObject = {
|
||||
typeUrl: MsgTypeUrls.WithdrawDelegatorReward,
|
||||
value: msgValue,
|
||||
};
|
||||
|
||||
setMsgGetter({ isMsgValid, msg });
|
||||
}, [chain.addressPrefix, chain.chainId, senderAddress, setMsgGetter, trimmedInputs]);
|
||||
|
||||
return (
|
||||
<StackableContainer lessPadding lessMargin>
|
||||
<button className="remove" onClick={() => deleteMsg()}>
|
||||
✕
|
||||
</button>
|
||||
<h2>MsgWithdrawDelegatorReward</h2>
|
||||
<div className="form-item">
|
||||
<SelectValidator
|
||||
validatorAddress={validatorAddress}
|
||||
setValidatorAddress={setValidatorAddress}
|
||||
/>
|
||||
<Input
|
||||
label="Validator Address"
|
||||
name="validator-address"
|
||||
value={validatorAddress}
|
||||
onChange={({ target }) => {
|
||||
setValidatorAddress(target.value);
|
||||
setValidatorAddressError("");
|
||||
}}
|
||||
error={validatorAddressError}
|
||||
placeholder={`E.g. ${exampleAddress(0, chain.addressPrefix)}`}
|
||||
/>
|
||||
</div>
|
||||
<style jsx>{`
|
||||
.form-item {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
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 MsgWithdrawDelegatorRewardForm;
|
||||
@@ -0,0 +1,70 @@
|
||||
import { MsgGetter } from "..";
|
||||
import { MsgTypeUrl, MsgTypeUrls } from "../../../../types/txMsg";
|
||||
import MsgBeginRedelegateForm from "./MsgBeginRedelegateForm";
|
||||
import MsgCreateVestingAccountForm from "./MsgCreateVestingAccountForm";
|
||||
import MsgDelegateForm from "./MsgDelegateForm";
|
||||
import MsgExecuteContractForm from "./MsgExecuteContractForm";
|
||||
import MsgFundCommunityPoolForm from "./MsgFundCommunityPoolForm";
|
||||
import MsgInstantiateContract2Form from "./MsgInstantiateContract2Form";
|
||||
import MsgInstantiateContractForm from "./MsgInstantiateContractForm";
|
||||
import MsgMigrateContractForm from "./MsgMigrateContractForm";
|
||||
import MsgSendForm from "./MsgSendForm";
|
||||
import MsgSetWithdrawAddressForm from "./MsgSetWithdrawAddressForm";
|
||||
import MsgTransferForm from "./MsgTransferForm";
|
||||
import MsgUndelegateForm from "./MsgUndelegateForm";
|
||||
import MsgUpdateAdminForm from "./MsgUpdateAdminForm";
|
||||
import MsgVoteForm from "./MsgVoteForm";
|
||||
import MsgWithdrawDelegatorRewardForm from "./MsgWithdrawDelegatorRewardForm";
|
||||
|
||||
interface MsgFormProps {
|
||||
readonly msgType: MsgTypeUrl;
|
||||
readonly senderAddress: string;
|
||||
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
|
||||
readonly deleteMsg: () => void;
|
||||
}
|
||||
|
||||
const MsgForm = ({ msgType, ...restProps }: MsgFormProps) => {
|
||||
switch (msgType) {
|
||||
// Bank
|
||||
case MsgTypeUrls.Send:
|
||||
return <MsgSendForm {...restProps} />;
|
||||
// Staking
|
||||
case MsgTypeUrls.Delegate:
|
||||
return <MsgDelegateForm {...restProps} />;
|
||||
case MsgTypeUrls.Undelegate:
|
||||
return <MsgUndelegateForm {...restProps} />;
|
||||
case MsgTypeUrls.BeginRedelegate:
|
||||
return <MsgBeginRedelegateForm {...restProps} />;
|
||||
// Distribution
|
||||
case MsgTypeUrls.FundCommunityPool:
|
||||
return <MsgFundCommunityPoolForm {...restProps} />;
|
||||
case MsgTypeUrls.SetWithdrawAddress:
|
||||
return <MsgSetWithdrawAddressForm {...restProps} />;
|
||||
case MsgTypeUrls.WithdrawDelegatorReward:
|
||||
return <MsgWithdrawDelegatorRewardForm {...restProps} />;
|
||||
// Vesting
|
||||
case MsgTypeUrls.CreateVestingAccount:
|
||||
return <MsgCreateVestingAccountForm {...restProps} />;
|
||||
// Governance
|
||||
case MsgTypeUrls.Vote:
|
||||
return <MsgVoteForm {...restProps} />;
|
||||
// IBC
|
||||
case MsgTypeUrls.Transfer:
|
||||
return <MsgTransferForm {...restProps} />;
|
||||
// CosmWasm
|
||||
case MsgTypeUrls.InstantiateContract:
|
||||
return <MsgInstantiateContractForm {...restProps} />;
|
||||
case MsgTypeUrls.InstantiateContract2:
|
||||
return <MsgInstantiateContract2Form {...restProps} />;
|
||||
case MsgTypeUrls.UpdateAdmin:
|
||||
return <MsgUpdateAdminForm {...restProps} />;
|
||||
case MsgTypeUrls.ExecuteContract:
|
||||
return <MsgExecuteContractForm {...restProps} />;
|
||||
case MsgTypeUrls.MigrateContract:
|
||||
return <MsgMigrateContractForm {...restProps} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default MsgForm;
|
||||
Reference in New Issue
Block a user