diff --git a/components/dataViews/TransactionInfo/TxMsgVoteDetails.tsx b/components/dataViews/TransactionInfo/TxMsgVoteDetails.tsx
new file mode 100644
index 00000000..9f3e4c71
--- /dev/null
+++ b/components/dataViews/TransactionInfo/TxMsgVoteDetails.tsx
@@ -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 (
+ <>
+
+ MsgVote
+
+
+
+ {msgValue.proposalId.toString()}
+
+
+
+ {printVoteOption(msgValue.option)}
+
+
+ >
+ );
+};
+
+export default TxMsgVoteDetails;
diff --git a/components/dataViews/TransactionInfo/index.tsx b/components/dataViews/TransactionInfo/index.tsx
index d0737fa9..344b7e50 100644
--- a/components/dataViews/TransactionInfo/index.tsx
+++ b/components/dataViews/TransactionInfo/index.tsx
@@ -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 ;
case MsgTypeUrls.CreateVestingAccount:
return ;
+ case MsgTypeUrls.Vote:
+ return ;
case MsgTypeUrls.Transfer:
return ;
case MsgTypeUrls.Execute:
diff --git a/components/forms/CreateTxForm/MsgForm/MsgVoteForm.tsx b/components/forms/CreateTxForm/MsgForm/MsgVoteForm.tsx
new file mode 100644
index 00000000..7618e25e
--- /dev/null
+++ b/components/forms/CreateTxForm/MsgForm/MsgVoteForm.tsx
@@ -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 (
+
+
+ MsgVote
+
+ {
+ setProposalId(target.value);
+ setProposalIdError("");
+ }}
+ error={proposalIdError}
+ />
+
+
+
+
+
+
+ );
+};
+
+export default MsgVoteForm;
diff --git a/components/forms/CreateTxForm/MsgForm/index.tsx b/components/forms/CreateTxForm/MsgForm/index.tsx
index 82c840a0..122f12a9 100644
--- a/components/forms/CreateTxForm/MsgForm/index.tsx
+++ b/components/forms/CreateTxForm/MsgForm/index.tsx
@@ -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 ;
case MsgTypeUrls.CreateVestingAccount:
return ;
+ case MsgTypeUrls.Vote:
+ return ;
case MsgTypeUrls.Transfer:
return ;
case MsgTypeUrls.Execute:
diff --git a/components/forms/CreateTxForm/index.tsx b/components/forms/CreateTxForm/index.tsx
index 10e50a46..a9a9b9d8 100644
--- a/components/forms/CreateTxForm/index.tsx
+++ b/components/forms/CreateTxForm/index.tsx
@@ -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
+
+
+
+ -
+
+
+
diff --git a/lib/gov.ts b/lib/gov.ts
new file mode 100644
index 00000000..eed78485
--- /dev/null
+++ b/lib/gov.ts
@@ -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, " ");
+};
diff --git a/lib/txMsgHelpers.ts b/lib/txMsgHelpers.ts
index 3fa8f2c0..838077d2 100644
--- a/lib/txMsgHelpers.ts
+++ b/lib/txMsgHelpers.ts
@@ -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:
diff --git a/types/txMsg.ts b/types/txMsg.ts
index 00a4f0ce..166b96b0 100644
--- a/types/txMsg.ts
+++ b/types/txMsg.ts
@@ -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,