Skip to content

Commit

Permalink
Merge pull request #215 from cosmos/feat/msg-vote
Browse files Browse the repository at this point in the history
Add MsgVote
  • Loading branch information
abefernan authored Jun 11, 2024
2 parents 0ebf5c9 + 9e300a7 commit c71d25a
Showing 8 changed files with 233 additions and 4 deletions.
51 changes: 51 additions & 0 deletions components/dataViews/TransactionInfo/TxMsgVoteDetails.tsx
Original file line number Diff line number Diff line change
@@ -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;
3 changes: 3 additions & 0 deletions components/dataViews/TransactionInfo/index.tsx
Original file line number Diff line number Diff line change
@@ -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:
137 changes: 137 additions & 0 deletions components/forms/CreateTxForm/MsgForm/MsgVoteForm.tsx
Original file line number Diff line number Diff line change
@@ -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;
3 changes: 3 additions & 0 deletions components/forms/CreateTxForm/MsgForm/index.tsx
Original file line number Diff line number Diff line change
@@ -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:
19 changes: 15 additions & 4 deletions components/forms/CreateTxForm/index.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 19 additions & 0 deletions lib/gov.ts
Original file line number Diff line number Diff line change
@@ -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, " ");
};
2 changes: 2 additions & 0 deletions lib/txMsgHelpers.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions types/txMsg.ts
Original file line number Diff line number Diff line change
@@ -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,

0 comments on commit c71d25a

Please sign in to comment.