@@ -0,0 +1,51 @@
|
||||
import { printVoteOption } from "@/lib/gov";
|
||||
import { MsgVote } from "cosmjs-types/cosmos/gov/v1beta1/tx";
|
||||
|
||||
interface TxMsgVoteDetailsProps {
|
||||
readonly msgValue: MsgVote;
|
||||
}
|
||||
|
||||
const TxMsgVoteDetails = ({ msgValue }: TxMsgVoteDetailsProps) => {
|
||||
return (
|
||||
<>
|
||||
<li>
|
||||
<h3>MsgVote</h3>
|
||||
</li>
|
||||
<li>
|
||||
<label>Proposal ID:</label>
|
||||
<div>{msgValue.proposalId.toString()}</div>
|
||||
</li>
|
||||
<li>
|
||||
<label>Voted:</label>
|
||||
<div>{printVoteOption(msgValue.option)}</div>
|
||||
</li>
|
||||
<style jsx>{`
|
||||
li:not(:has(h3)) {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
li + li:nth-child(2) {
|
||||
margin-top: 25px;
|
||||
}
|
||||
li + li {
|
||||
margin-top: 10px;
|
||||
}
|
||||
li div {
|
||||
padding: 3px 6px;
|
||||
}
|
||||
label {
|
||||
font-size: 12px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 3px 6px;
|
||||
border-radius: 5px;
|
||||
display: block;
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TxMsgVoteDetails;
|
||||
@@ -17,6 +17,7 @@ import TxMsgSetWithdrawAddressDetails from "./TxMsgSetWithdrawAddressDetails";
|
||||
import TxMsgTransferDetails from "./TxMsgTransferDetails";
|
||||
import TxMsgUndelegateDetails from "./TxMsgUndelegateDetails";
|
||||
import TxMsgUpdateAdminDetails from "./TxMsgUpdateAdminDetails";
|
||||
import TxMsgVoteDetails from "./TxMsgVoteDetails";
|
||||
|
||||
const TxMsgDetails = ({ typeUrl, value: msgValue }: EncodeObject) => {
|
||||
switch (typeUrl) {
|
||||
@@ -34,6 +35,8 @@ const TxMsgDetails = ({ typeUrl, value: msgValue }: EncodeObject) => {
|
||||
return <TxMsgSetWithdrawAddressDetails msgValue={msgValue} />;
|
||||
case MsgTypeUrls.CreateVestingAccount:
|
||||
return <TxMsgCreateVestingAccountDetails msgValue={msgValue} />;
|
||||
case MsgTypeUrls.Vote:
|
||||
return <TxMsgVoteDetails msgValue={msgValue} />;
|
||||
case MsgTypeUrls.Transfer:
|
||||
return <TxMsgTransferDetails msgValue={msgValue} />;
|
||||
case MsgTypeUrls.Execute:
|
||||
|
||||
@@ -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 fromAddress: string;
|
||||
readonly setMsgGetter: (msgGetter: MsgGetter) => void;
|
||||
readonly deleteMsg: () => void;
|
||||
}
|
||||
|
||||
const MsgVoteForm = ({ fromAddress, 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: fromAddress,
|
||||
proposalId: proposalIdBigInt,
|
||||
option: selectedVote.value,
|
||||
});
|
||||
|
||||
const msg: MsgVoteEncodeObject = { typeUrl: MsgTypeUrls.Vote, value: msgValue };
|
||||
|
||||
setMsgGetter({ isMsgValid, msg });
|
||||
}, [fromAddress, selectedVote.value, 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;
|
||||
@@ -13,6 +13,7 @@ import MsgSetWithdrawAddressForm from "./MsgSetWithdrawAddressForm";
|
||||
import MsgTransferForm from "./MsgTransferForm";
|
||||
import MsgUndelegateForm from "./MsgUndelegateForm";
|
||||
import MsgUpdateAdminForm from "./MsgUpdateAdminForm";
|
||||
import MsgVoteForm from "./MsgVoteForm";
|
||||
|
||||
interface MsgFormProps {
|
||||
readonly msgType: MsgTypeUrl;
|
||||
@@ -37,6 +38,8 @@ const MsgForm = ({ msgType, senderAddress, ...restProps }: MsgFormProps) => {
|
||||
return <MsgSetWithdrawAddressForm delegatorAddress={senderAddress} {...restProps} />;
|
||||
case MsgTypeUrls.CreateVestingAccount:
|
||||
return <MsgCreateVestingAccountForm fromAddress={senderAddress} {...restProps} />;
|
||||
case MsgTypeUrls.Vote:
|
||||
return <MsgVoteForm fromAddress={senderAddress} {...restProps} />;
|
||||
case MsgTypeUrls.Transfer:
|
||||
return <MsgTransferForm fromAddress={senderAddress} {...restProps} />;
|
||||
case MsgTypeUrls.Execute:
|
||||
|
||||
@@ -2,7 +2,7 @@ import { loadValidators } from "@/context/ChainsContext/helpers";
|
||||
import { toastError, toastSuccess } from "@/lib/utils";
|
||||
import { EncodeObject } from "@cosmjs/proto-signing";
|
||||
import { Account, calculateFee } from "@cosmjs/stargate";
|
||||
import { assert } from "@cosmjs/utils";
|
||||
import { assert, sleep } from "@cosmjs/utils";
|
||||
import { NextRouter, withRouter } from "next/router";
|
||||
import { useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -61,6 +61,9 @@ const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormPro
|
||||
|
||||
const createTx = async () => {
|
||||
const loadingToastId = toast.loading("Creating transaction");
|
||||
setProcessing(true);
|
||||
// If it fails too fast, toast.dismiss does not work
|
||||
await sleep(500);
|
||||
|
||||
try {
|
||||
assert(typeof accountOnChain.accountNumber === "number", "accountNumber missing");
|
||||
@@ -70,15 +73,15 @@ const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormPro
|
||||
.filter(({ isMsgValid }) => isMsgValid())
|
||||
.map(({ msg }) => exportMsgToJson(msg));
|
||||
|
||||
if (!msgs.length || msgs.length !== msgTypes.length) return;
|
||||
if (!msgs.length || msgs.length !== msgTypes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Number.isSafeInteger(gasLimit) || gasLimit <= 0) {
|
||||
setGasLimitError("gas limit must be a positive integer");
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
|
||||
const tx: DbTransaction = {
|
||||
accountNumber: accountOnChain.accountNumber,
|
||||
sequence: accountOnChain.sequence,
|
||||
@@ -180,6 +183,14 @@ const CreateTxForm = ({ router, senderAddress, accountOnChain }: CreateTxFormPro
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="btn-cluster">
|
||||
<label>Governance</label>
|
||||
<ul>
|
||||
<li>
|
||||
<Button label="Vote" onClick={() => addMsgType(MsgTypeUrls.Vote)} />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="btn-cluster">
|
||||
<label>IBC</label>
|
||||
<ul>
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { VoteOption, voteOptionToJSON } from "cosmjs-types/cosmos/gov/v1beta1/gov";
|
||||
|
||||
const voteOptionPrefix = "VOTE_OPTION_";
|
||||
|
||||
export const voteOptions = Object.keys(VoteOption).filter(
|
||||
(key) => isNaN(Number(key)) && key.startsWith(voteOptionPrefix),
|
||||
);
|
||||
|
||||
export const printVoteOption = (voteOption: VoteOption): string => {
|
||||
const voteStr = voteOptionToJSON(voteOption);
|
||||
|
||||
if (!voteStr.startsWith(voteOptionPrefix)) {
|
||||
return "Unrecognized";
|
||||
}
|
||||
|
||||
const voteNoPrefix = voteStr.split(voteOptionPrefix)[1];
|
||||
|
||||
return voteNoPrefix.charAt(0) + voteNoPrefix.slice(1).toLowerCase().replace(/_/g, " ");
|
||||
};
|
||||
@@ -20,6 +20,8 @@ const gasOfMsg = (msgType: MsgTypeUrl): number => {
|
||||
return 400_000;
|
||||
case MsgTypeUrls.CreateVestingAccount:
|
||||
return 100_000;
|
||||
case MsgTypeUrls.Vote:
|
||||
return 100_000;
|
||||
case MsgTypeUrls.Transfer:
|
||||
return 180_000;
|
||||
case MsgTypeUrls.Execute:
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
MsgSetWithdrawAddress,
|
||||
MsgWithdrawDelegatorReward,
|
||||
} from "cosmjs-types/cosmos/distribution/v1beta1/tx";
|
||||
import { MsgVote } from "cosmjs-types/cosmos/gov/v1beta1/tx";
|
||||
import {
|
||||
MsgBeginRedelegate,
|
||||
MsgDelegate,
|
||||
@@ -26,6 +27,7 @@ export const MsgTypeUrls = {
|
||||
Delegate: "/cosmos.staking.v1beta1.MsgDelegate",
|
||||
Undelegate: "/cosmos.staking.v1beta1.MsgUndelegate",
|
||||
CreateVestingAccount: "/cosmos.vesting.v1beta1.MsgCreateVestingAccount",
|
||||
Vote: "/cosmos.gov.v1beta1.MsgVote",
|
||||
Transfer: "/ibc.applications.transfer.v1.MsgTransfer",
|
||||
Execute: "/cosmwasm.wasm.v1.MsgExecuteContract",
|
||||
Instantiate: "/cosmwasm.wasm.v1.MsgInstantiateContract",
|
||||
@@ -44,6 +46,7 @@ export const MsgCodecs = {
|
||||
[MsgTypeUrls.Delegate]: MsgDelegate,
|
||||
[MsgTypeUrls.Undelegate]: MsgUndelegate,
|
||||
[MsgTypeUrls.CreateVestingAccount]: MsgCreateVestingAccount,
|
||||
[MsgTypeUrls.Vote]: MsgVote,
|
||||
[MsgTypeUrls.Transfer]: MsgTransfer,
|
||||
[MsgTypeUrls.Execute]: MsgExecuteContract,
|
||||
[MsgTypeUrls.Instantiate]: MsgInstantiateContract,
|
||||
|
||||
Reference in New Issue
Block a user