From 07b7bb7ec5fe06eee1dd1e6776210b6fbde6ad26 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 27 Jul 2021 15:37:36 +0200 Subject: [PATCH 1/7] Add gov extension --- CHANGELOG.md | 1 + packages/stargate/src/index.ts | 4 + packages/stargate/src/queries/gov.spec.ts | 0 packages/stargate/src/queries/gov.ts | 105 ++++++++++++++++++++++ packages/stargate/src/queries/index.ts | 1 + packages/stargate/src/queries/utils.ts | 10 +++ 6 files changed, 121 insertions(+) create mode 100644 packages/stargate/src/queries/gov.spec.ts create mode 100644 packages/stargate/src/queries/gov.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 04645cf8..7b0ffa0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to - @cosmjs/faucet: Add new configuration variable `FAUCET_PATH_PATTERN` to configure the HD path of the faucet accounts ([#832]). - @cosmjs/cosmwasm-stargate: Add field `ibcPortId` to `Contract` ([#836]). +- @cosmjs/stargate: Add `GovExtension` for query client. [#832]: https://github.com/cosmos/cosmjs/issues/832 [#836]: https://github.com/cosmos/cosmjs/issues/836 diff --git a/packages/stargate/src/index.ts b/packages/stargate/src/index.ts index fc01db0b..807c35bb 100644 --- a/packages/stargate/src/index.ts +++ b/packages/stargate/src/index.ts @@ -60,12 +60,16 @@ export { createPagination, createProtobufRpcClient, DistributionExtension, + GovExtension, + GovParamsType, + GovProposalId, IbcExtension, ProtobufRpcClient, QueryClient, setupAuthExtension, setupBankExtension, setupDistributionExtension, + setupGovExtension, setupIbcExtension, setupStakingExtension, StakingExtension, diff --git a/packages/stargate/src/queries/gov.spec.ts b/packages/stargate/src/queries/gov.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/stargate/src/queries/gov.ts b/packages/stargate/src/queries/gov.ts new file mode 100644 index 00000000..cb37bc22 --- /dev/null +++ b/packages/stargate/src/queries/gov.ts @@ -0,0 +1,105 @@ +import { Uint64 } from "@cosmjs/math"; +import { ProposalStatus } from "cosmjs-types/cosmos/gov/v1beta1/gov"; +import { + QueryClientImpl, + QueryDepositResponse, + QueryDepositsResponse, + QueryParamsResponse, + QueryProposalResponse, + QueryProposalsResponse, + QueryTallyResultResponse, + QueryVoteResponse, + QueryVotesResponse, +} from "cosmjs-types/cosmos/gov/v1beta1/query"; +import Long from "long"; + +import { QueryClient } from "./queryclient"; +import { createProtobufRpcClient, longify } from "./utils"; + +export type GovParamsType = "deposit" | "tallying" | "voting"; + +export type GovProposalId = string | number | Long | Uint64; + +export interface GovExtension { + readonly gov: { + readonly params: (parametersType: GovParamsType) => Promise; + readonly proposals: ( + proposalStatus: ProposalStatus, + depositor: string, + voter: string, + ) => Promise; + readonly proposal: (proposalId: GovProposalId) => Promise; + readonly deposits: (proposalId: GovProposalId) => Promise; + readonly deposit: (proposalId: GovProposalId, depositorAddress: string) => Promise; + readonly tally: (proposalId: GovProposalId) => Promise; + readonly votes: (proposalId: GovProposalId) => Promise; + readonly vote: (proposalId: GovProposalId, voterAddress: string) => Promise; + }; +} + +export function setupGovExtension(base: QueryClient): GovExtension { + const rpc = createProtobufRpcClient(base); + + // Use this service to get easy typed access to query methods + // This cannot be used for proof verification + const queryService = new QueryClientImpl(rpc); + + return { + gov: { + params: async (parametersType: GovParamsType) => { + const response = await queryService.Params({ paramsType: parametersType }); + return response; + }, + proposals: async (proposalStatus: ProposalStatus, depositorAddress: string, voterAddress: string) => { + // TODO: pagination + const response = await queryService.Proposals({ + proposalStatus, + depositor: depositorAddress, + voter: voterAddress, + pagination: undefined, + }); + return response; + }, + proposal: async (proposalId: GovProposalId) => { + const response = await queryService.Proposal({ proposalId: longify(proposalId) }); + return response; + }, + deposits: async (proposalId: GovProposalId) => { + // TODO: pagination + const response = await queryService.Deposits({ + proposalId: longify(proposalId), + pagination: undefined, + }); + return response; + }, + deposit: async (proposalId: GovProposalId, depositorAddress: string) => { + const response = await queryService.Deposit({ + proposalId: longify(proposalId), + depositor: depositorAddress, + }); + return response; + }, + tally: async (proposalId: GovProposalId) => { + const response = await queryService.TallyResult({ + proposalId: longify(proposalId), + }); + return response; + }, + votes: async (proposalId: GovProposalId) => { + // TODO: pagination + const response = await queryService.Votes({ + proposalId: longify(proposalId), + pagination: undefined, + }); + return response; + }, + vote: async (proposalId: GovProposalId, voterAddress: string) => { + const response = await queryService.Vote({ + proposalId: longify(proposalId), + voter: voterAddress, + }); + return response; + }, + }, + }; +} diff --git a/packages/stargate/src/queries/index.ts b/packages/stargate/src/queries/index.ts index 31d220bb..cc8b1609 100644 --- a/packages/stargate/src/queries/index.ts +++ b/packages/stargate/src/queries/index.ts @@ -7,6 +7,7 @@ export { QueryClient } from "./queryclient"; export { AuthExtension, setupAuthExtension } from "./auth"; export { BankExtension, setupBankExtension } from "./bank"; export { DistributionExtension, setupDistributionExtension } from "./distribution"; +export { setupGovExtension, GovExtension, GovProposalId, GovParamsType } from "./gov"; export { IbcExtension, setupIbcExtension } from "./ibc"; export { setupStakingExtension, StakingExtension } from "./staking"; export { createPagination, createProtobufRpcClient, ProtobufRpcClient } from "./utils"; diff --git a/packages/stargate/src/queries/utils.ts b/packages/stargate/src/queries/utils.ts index fe8144bf..247446a6 100644 --- a/packages/stargate/src/queries/utils.ts +++ b/packages/stargate/src/queries/utils.ts @@ -1,4 +1,5 @@ import { Bech32 } from "@cosmjs/encoding"; +import { Uint64 } from "@cosmjs/math"; import { PageRequest } from "cosmjs-types/cosmos/base/query/v1beta1/pagination"; import Long from "long"; @@ -36,3 +37,12 @@ export function createProtobufRpcClient(base: QueryClient): ProtobufRpcClient { }, }; } + +/** + * Takes a uint64 value as string, number, Long or Uint64 and returns an unsigned Long instance + * of it. + */ +export function longify(value: string | number | Long | Uint64): Long { + const checkedValue = Uint64.fromString(value.toString()); + return Long.fromBytesBE([...checkedValue.toBytesBigEndian()], true); +} From 1b8eed047943459f2d3cb98c759088ed447183df Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 27 Jul 2021 16:30:31 +0200 Subject: [PATCH 2/7] Add support for `MsgSubmitProposal` and `MsgVote` --- CHANGELOG.md | 1 + packages/stargate/src/encodeobjects.ts | 21 +++++++++++++++++++ packages/stargate/src/index.ts | 4 ++++ .../stargate/src/signingstargateclient.ts | 3 +++ 4 files changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b0ffa0e..02900b7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to configure the HD path of the faucet accounts ([#832]). - @cosmjs/cosmwasm-stargate: Add field `ibcPortId` to `Contract` ([#836]). - @cosmjs/stargate: Add `GovExtension` for query client. +- @cosmjs/stargate: Add support for `MsgSubmitProposal` and `MsgVote`. [#832]: https://github.com/cosmos/cosmjs/issues/832 [#836]: https://github.com/cosmos/cosmjs/issues/836 diff --git a/packages/stargate/src/encodeobjects.ts b/packages/stargate/src/encodeobjects.ts index 88209a79..ba0b606f 100644 --- a/packages/stargate/src/encodeobjects.ts +++ b/packages/stargate/src/encodeobjects.ts @@ -1,6 +1,7 @@ import { EncodeObject } from "@cosmjs/proto-signing"; import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx"; import { MsgWithdrawDelegatorReward } from "cosmjs-types/cosmos/distribution/v1beta1/tx"; +import { MsgSubmitProposal, MsgVote } from "cosmjs-types/cosmos/gov/v1beta1/tx"; import { MsgDelegate, MsgUndelegate } from "cosmjs-types/cosmos/staking/v1beta1/tx"; import { MsgTransfer } from "cosmjs-types/ibc/applications/transfer/v1/tx"; @@ -59,3 +60,23 @@ export function isMsgTransferEncodeObject( ): encodeObject is MsgTransferEncodeObject { return (encodeObject as MsgTransferEncodeObject).typeUrl === "/ibc.applications.transfer.v1.MsgTransfer"; } + +export interface MsgSubmitProposalEncodeObject extends EncodeObject { + readonly typeUrl: "/cosmos.gov.v1beta1.MsgSubmitProposal"; + readonly value: Partial; +} + +export function isMsgSubmitProposalEncodeObject( + encodeObject: EncodeObject, +): encodeObject is MsgSubmitProposalEncodeObject { + return (encodeObject as MsgSubmitProposalEncodeObject).typeUrl === "/cosmos.gov.v1beta1.MsgSubmitProposal"; +} + +export interface MsgVoteEncodeObject extends EncodeObject { + readonly typeUrl: "/cosmos.gov.v1beta1.MsgVote"; + readonly value: Partial; +} + +export function isMsgVoteEncodeObject(encodeObject: EncodeObject): encodeObject is MsgVoteEncodeObject { + return (encodeObject as MsgVoteEncodeObject).typeUrl === "/cosmos.gov.v1beta1.MsgVote"; +} diff --git a/packages/stargate/src/index.ts b/packages/stargate/src/index.ts index 807c35bb..103c83db 100644 --- a/packages/stargate/src/index.ts +++ b/packages/stargate/src/index.ts @@ -42,13 +42,17 @@ export { AminoConverter, AminoTypes } from "./aminotypes"; export { isMsgDelegateEncodeObject, isMsgSendEncodeObject, + isMsgSubmitProposalEncodeObject, isMsgTransferEncodeObject, isMsgUndelegateEncodeObject, + isMsgVoteEncodeObject, isMsgWithdrawDelegatorRewardEncodeObject, MsgDelegateEncodeObject, MsgSendEncodeObject, + MsgSubmitProposalEncodeObject, MsgTransferEncodeObject, MsgUndelegateEncodeObject, + MsgVoteEncodeObject, MsgWithdrawDelegatorRewardEncodeObject, } from "./encodeobjects"; export { calculateFee, GasPrice } from "./fee"; diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index e1853d4c..3e3778c0 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -22,6 +22,7 @@ import { MsgWithdrawDelegatorReward, MsgWithdrawValidatorCommission, } from "cosmjs-types/cosmos/distribution/v1beta1/tx"; +import { MsgSubmitProposal, MsgVote } from "cosmjs-types/cosmos/gov/v1beta1/tx"; import { MsgBeginRedelegate, MsgCreateValidator, @@ -75,6 +76,8 @@ export const defaultRegistryTypes: ReadonlyArray<[string, GeneratedType]> = [ ["/cosmos.distribution.v1beta1.MsgSetWithdrawAddress", MsgSetWithdrawAddress], ["/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", MsgWithdrawDelegatorReward], ["/cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission", MsgWithdrawValidatorCommission], + ["/cosmos.gov.v1beta1.MsgSubmitProposal", MsgSubmitProposal], + ["/cosmos.gov.v1beta1.MsgVote", MsgVote], ["/cosmos.staking.v1beta1.MsgBeginRedelegate", MsgBeginRedelegate], ["/cosmos.staking.v1beta1.MsgCreateValidator", MsgCreateValidator], ["/cosmos.staking.v1beta1.MsgDelegate", MsgDelegate], From b985d384cf35bc8eacdcdd9ab1a2d52e6432fdf6 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 27 Jul 2021 17:25:42 +0200 Subject: [PATCH 3/7] Add GovExtension testing --- packages/stargate/src/queries/gov.spec.ts | 291 ++++++++++++++++++++++ 1 file changed, 291 insertions(+) diff --git a/packages/stargate/src/queries/gov.spec.ts b/packages/stargate/src/queries/gov.spec.ts index e69de29b..b699d2a6 100644 --- a/packages/stargate/src/queries/gov.spec.ts +++ b/packages/stargate/src/queries/gov.spec.ts @@ -0,0 +1,291 @@ +import { coins } from "@cosmjs/amino"; +import { toAscii } from "@cosmjs/encoding"; +import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; +import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; +import { assert, sleep } from "@cosmjs/utils"; +import { ProposalStatus, TextProposal, VoteOption } from "cosmjs-types/cosmos/gov/v1beta1/gov"; +import { Any } from "cosmjs-types/google/protobuf/any"; +import Long from "long"; + +import { MsgSubmitProposalEncodeObject, MsgVoteEncodeObject } from "../encodeobjects"; +import { SigningStargateClient } from "../signingstargateclient"; +import { assertIsBroadcastTxSuccess } from "../stargateclient"; +import { + defaultSigningClientOptions, + faucet, + nonNegativeIntegerMatcher, + pendingWithoutSimapp, + simapp, + simappEnabled, +} from "../testutils.spec"; +import { GovExtension, setupGovExtension } from "./gov"; +import { QueryClient } from "./queryclient"; +import { longify } from "./utils"; + +async function makeClientWithGov(rpcUrl: string): Promise<[QueryClient & GovExtension, Tendermint34Client]> { + const tmClient = await Tendermint34Client.connect(rpcUrl); + return [QueryClient.withExtensions(tmClient, setupGovExtension), tmClient]; +} + +describe("GovExtension", () => { + const defaultFee = { + amount: coins(25000, "ucosm"), + gas: "1500000", // 1.5 million + }; + const textProposal = TextProposal.fromPartial({ + title: "Test Proposal", + description: "This proposal proposes to test whether this proposal passes", + }); + const minDeposit = coins(10000000, "ustake"); + const initialDeposit = coins(12300000, "ustake"); + let proposalId: string; + + beforeAll(async () => { + if (simappEnabled()) { + const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic); + const [firstAccount] = await wallet.getAccounts(); + const client = await SigningStargateClient.connectWithSigner( + simapp.tendermintUrl, + wallet, + defaultSigningClientOptions, + ); + + const proposalMsg: MsgSubmitProposalEncodeObject = { + typeUrl: "/cosmos.gov.v1beta1.MsgSubmitProposal", + value: { + content: Any.fromPartial({ + typeUrl: "/cosmos.gov.v1beta1.TextProposal", + value: Uint8Array.from(TextProposal.encode(textProposal).finish()), + }), + proposer: faucet.address0, + initialDeposit: initialDeposit, + }, + }; + const proposalResult = await client.signAndBroadcast( + firstAccount.address, + [proposalMsg], + defaultFee, + "Test proposal for simd", + ); + assertIsBroadcastTxSuccess(proposalResult); + const logs = JSON.parse(proposalResult.rawLog || ""); + proposalId = logs[0].events + .find(({ type }: any) => type === "submit_proposal") + .attributes.find(({ key }: any) => key === "proposal_id").value; + assert(proposalId.match(nonNegativeIntegerMatcher)); + + const voteMsg: MsgVoteEncodeObject = { + typeUrl: "/cosmos.gov.v1beta1.MsgVote", + value: { + proposalId: longify(proposalId), + voter: faucet.address0, + option: VoteOption.VOTE_OPTION_YES, + }, + }; + const voteMemo = "Test vote for simd"; + await client.signAndBroadcast(firstAccount.address, [voteMsg], defaultFee, voteMemo); + + await sleep(75); // wait until transactions are indexed + } + }); + + describe("params", () => { + it("works for deposit", async () => { + pendingWithoutSimapp(); + const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl); + + const response = await client.gov.params("deposit"); + expect(response).toEqual( + jasmine.objectContaining({ + depositParams: { + minDeposit: minDeposit, + maxDepositPeriod: { + seconds: Long.fromNumber(172800, false), + nanos: 0, + }, + }, + }), + ); + + tmClient.disconnect(); + }); + + it("works for tallying", async () => { + pendingWithoutSimapp(); + const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl); + + const response = await client.gov.params("tallying"); + expect(response).toEqual( + jasmine.objectContaining({ + tallyParams: { + // Why the f*** are we getting binary values here? + quorum: toAscii("334000000000000000"), // 0.334 + threshold: toAscii("500000000000000000"), // 0.5 + vetoThreshold: toAscii("334000000000000000"), // 0.334 + }, + }), + ); + + tmClient.disconnect(); + }); + + it("works for voting", async () => { + pendingWithoutSimapp(); + const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl); + + const response = await client.gov.params("voting"); + expect(response).toEqual( + jasmine.objectContaining({ + votingParams: { + votingPeriod: { + seconds: Long.fromNumber(172800, false), + nanos: 0, + }, + }, + }), + ); + + tmClient.disconnect(); + }); + }); + + describe("proposals", () => { + it("works", async () => { + pendingWithoutSimapp(); + const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl); + + const response = await client.gov.proposals( + ProposalStatus.PROPOSAL_STATUS_VOTING_PERIOD, + faucet.address0, + faucet.address0, + ); + expect(response.proposals.length).toBeGreaterThanOrEqual(1); + expect(response.proposals[response.proposals.length - 1]).toEqual({ + content: Any.fromPartial({ + typeUrl: "/cosmos.gov.v1beta1.TextProposal", + value: Uint8Array.from(TextProposal.encode(textProposal).finish()), + }), + proposalId: longify(proposalId), + status: ProposalStatus.PROPOSAL_STATUS_VOTING_PERIOD, + finalTallyResult: { yes: "0", abstain: "0", no: "0", noWithVeto: "0" }, + submitTime: jasmine.any(Date), + depositEndTime: jasmine.any(Date), + totalDeposit: initialDeposit, + votingStartTime: jasmine.any(Date), + votingEndTime: jasmine.any(Date), + }); + + tmClient.disconnect(); + }); + }); + + describe("proposal", () => { + it("works", async () => { + pendingWithoutSimapp(); + const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl); + + const response = await client.gov.proposal(proposalId); + expect(response.proposal).toEqual({ + content: Any.fromPartial({ + typeUrl: "/cosmos.gov.v1beta1.TextProposal", + value: Uint8Array.from(TextProposal.encode(textProposal).finish()), + }), + proposalId: longify(proposalId), + status: ProposalStatus.PROPOSAL_STATUS_VOTING_PERIOD, + finalTallyResult: { yes: "0", abstain: "0", no: "0", noWithVeto: "0" }, + submitTime: jasmine.any(Date), + depositEndTime: jasmine.any(Date), + totalDeposit: initialDeposit, + votingStartTime: jasmine.any(Date), + votingEndTime: jasmine.any(Date), + }); + + tmClient.disconnect(); + }); + }); + + describe("deposits", () => { + it("works", async () => { + pendingWithoutSimapp(); + const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl); + + const response = await client.gov.deposits(proposalId); + expect(response.deposits).toEqual([ + { + proposalId: longify(proposalId), + depositor: faucet.address0, + amount: initialDeposit, + }, + ]); + + tmClient.disconnect(); + }); + }); + + describe("deposit", () => { + it("works", async () => { + pendingWithoutSimapp(); + const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl); + + const response = await client.gov.deposit(proposalId, faucet.address0); + expect(response.deposit).toEqual({ + proposalId: longify(proposalId), + depositor: faucet.address0, + amount: initialDeposit, + }); + + tmClient.disconnect(); + }); + }); + + describe("tally", () => { + it("works", async () => { + pendingWithoutSimapp(); + const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl); + + const response = await client.gov.tally(proposalId); + expect(response.tally).toEqual({ + yes: "0", + abstain: "0", + no: "0", + noWithVeto: "0", + }); + + tmClient.disconnect(); + }); + }); + + describe("votes", () => { + it("works", async () => { + pendingWithoutSimapp(); + const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl); + + const response = await client.gov.votes(proposalId); + expect(response.votes).toEqual([ + { + proposalId: longify(proposalId), + voter: faucet.address0, + option: VoteOption.VOTE_OPTION_YES, + }, + ]); + + tmClient.disconnect(); + }); + }); + + describe("vote", () => { + it("works", async () => { + pendingWithoutSimapp(); + const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl); + + const response = await client.gov.vote(proposalId, faucet.address0); + expect(response.vote).toEqual({ + voter: faucet.address0, + proposalId: longify(proposalId), + option: VoteOption.VOTE_OPTION_YES, + }); + + tmClient.disconnect(); + }); + }); +}); From 869ba7e23c3f0a028ae8f95fa94e147022f6980b Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Tue, 27 Jul 2021 17:34:48 +0200 Subject: [PATCH 4/7] Pull out govMinDeposit --- packages/stargate/src/queries/gov.spec.ts | 3 +-- packages/stargate/src/testutils.spec.ts | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stargate/src/queries/gov.spec.ts b/packages/stargate/src/queries/gov.spec.ts index b699d2a6..b33929f8 100644 --- a/packages/stargate/src/queries/gov.spec.ts +++ b/packages/stargate/src/queries/gov.spec.ts @@ -36,7 +36,6 @@ describe("GovExtension", () => { title: "Test Proposal", description: "This proposal proposes to test whether this proposal passes", }); - const minDeposit = coins(10000000, "ustake"); const initialDeposit = coins(12300000, "ustake"); let proposalId: string; @@ -98,7 +97,7 @@ describe("GovExtension", () => { expect(response).toEqual( jasmine.objectContaining({ depositParams: { - minDeposit: minDeposit, + minDeposit: simapp.govMinDeposit, maxDepositPeriod: { seconds: Long.fromNumber(172800, false), nanos: 0, diff --git a/packages/stargate/src/testutils.spec.ts b/packages/stargate/src/testutils.spec.ts index cccc2d78..510a99bd 100644 --- a/packages/stargate/src/testutils.spec.ts +++ b/packages/stargate/src/testutils.spec.ts @@ -61,6 +61,7 @@ export const simapp = { denomFee: "ucosm", blockTime: 1_000, // ms totalSupply: 21000000000, // ucosm + govMinDeposit: coins(10000000, "ustake"), }; export const slowSimapp = { From 9ddfe6e679f4ed618d31422abd7f3b56c2938917 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 28 Jul 2021 18:40:22 +0200 Subject: [PATCH 5/7] Fix and improve gov testing --- packages/stargate/src/queries/gov.spec.ts | 117 +++++++++++++++++----- packages/stargate/src/stargateclient.ts | 39 +++++++- 2 files changed, 125 insertions(+), 31 deletions(-) diff --git a/packages/stargate/src/queries/gov.spec.ts b/packages/stargate/src/queries/gov.spec.ts index b33929f8..e426adff 100644 --- a/packages/stargate/src/queries/gov.spec.ts +++ b/packages/stargate/src/queries/gov.spec.ts @@ -1,4 +1,4 @@ -import { coins } from "@cosmjs/amino"; +import { coin, coins, makeCosmoshubPath } from "@cosmjs/amino"; import { toAscii } from "@cosmjs/encoding"; import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; @@ -7,7 +7,11 @@ import { ProposalStatus, TextProposal, VoteOption } from "cosmjs-types/cosmos/go import { Any } from "cosmjs-types/google/protobuf/any"; import Long from "long"; -import { MsgSubmitProposalEncodeObject, MsgVoteEncodeObject } from "../encodeobjects"; +import { + MsgDelegateEncodeObject, + MsgSubmitProposalEncodeObject, + MsgVoteEncodeObject, +} from "../encodeobjects"; import { SigningStargateClient } from "../signingstargateclient"; import { assertIsBroadcastTxSuccess } from "../stargateclient"; import { @@ -17,6 +21,7 @@ import { pendingWithoutSimapp, simapp, simappEnabled, + validator, } from "../testutils.spec"; import { GovExtension, setupGovExtension } from "./gov"; import { QueryClient } from "./queryclient"; @@ -37,12 +42,19 @@ describe("GovExtension", () => { description: "This proposal proposes to test whether this proposal passes", }); const initialDeposit = coins(12300000, "ustake"); + const delegationVoter1 = coin(424242, "ustake"); + const delegationVoter2 = coin(777, "ustake"); + const voter1Address = faucet.address1; + const voter2Address = faucet.address2; let proposalId: string; beforeAll(async () => { if (simappEnabled()) { - const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic); - const [firstAccount] = await wallet.getAccounts(); + const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic, { + // Use address 1 and 2 instead of 0 to avoid conflicts with other delegation tests + // This must match `voterAddress` above. + hdPaths: [makeCosmoshubPath(1), makeCosmoshubPath(2)], + }); const client = await SigningStargateClient.connectWithSigner( simapp.tendermintUrl, wallet, @@ -56,12 +68,12 @@ describe("GovExtension", () => { typeUrl: "/cosmos.gov.v1beta1.TextProposal", value: Uint8Array.from(TextProposal.encode(textProposal).finish()), }), - proposer: faucet.address0, + proposer: voter1Address, initialDeposit: initialDeposit, }, }; const proposalResult = await client.signAndBroadcast( - firstAccount.address, + voter1Address, [proposalMsg], defaultFee, "Test proposal for simd", @@ -73,18 +85,65 @@ describe("GovExtension", () => { .attributes.find(({ key }: any) => key === "proposal_id").value; assert(proposalId.match(nonNegativeIntegerMatcher)); - const voteMsg: MsgVoteEncodeObject = { - typeUrl: "/cosmos.gov.v1beta1.MsgVote", - value: { - proposalId: longify(proposalId), - voter: faucet.address0, - option: VoteOption.VOTE_OPTION_YES, - }, - }; - const voteMemo = "Test vote for simd"; - await client.signAndBroadcast(firstAccount.address, [voteMsg], defaultFee, voteMemo); + // Voter 1 + { + // My vote only counts when I delegate + if (!(await client.getDelegation(voter1Address, validator.validatorAddress))) { + const msgDelegate: MsgDelegateEncodeObject = { + typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", + value: { + delegatorAddress: voter1Address, + validatorAddress: validator.validatorAddress, + amount: delegationVoter1, + }, + }; + const result = await client.signAndBroadcast(voter1Address, [msgDelegate], defaultFee); + assertIsBroadcastTxSuccess(result); + } + + const voteMsg: MsgVoteEncodeObject = { + typeUrl: "/cosmos.gov.v1beta1.MsgVote", + value: { + proposalId: longify(proposalId), + voter: voter1Address, + option: VoteOption.VOTE_OPTION_YES, + }, + }; + const voteResult = await client.signAndBroadcast(voter1Address, [voteMsg], defaultFee); + assertIsBroadcastTxSuccess(voteResult); + } + + // Voter 2 + { + // My vote only counts when I delegate + if (!(await client.getDelegation(voter2Address, validator.validatorAddress))) { + const msgDelegate: MsgDelegateEncodeObject = { + typeUrl: "/cosmos.staking.v1beta1.MsgDelegate", + value: { + delegatorAddress: voter2Address, + validatorAddress: validator.validatorAddress, + amount: delegationVoter2, + }, + }; + const result = await client.signAndBroadcast(voter2Address, [msgDelegate], defaultFee); + assertIsBroadcastTxSuccess(result); + } + + const voteMsg: MsgVoteEncodeObject = { + typeUrl: "/cosmos.gov.v1beta1.MsgVote", + value: { + proposalId: longify(proposalId), + voter: voter2Address, + option: VoteOption.VOTE_OPTION_NO_WITH_VETO, + }, + }; + const voteResult = await client.signAndBroadcast(voter2Address, [voteMsg], defaultFee); + assertIsBroadcastTxSuccess(voteResult); + } await sleep(75); // wait until transactions are indexed + + client.disconnect(); } }); @@ -155,8 +214,8 @@ describe("GovExtension", () => { const response = await client.gov.proposals( ProposalStatus.PROPOSAL_STATUS_VOTING_PERIOD, - faucet.address0, - faucet.address0, + voter1Address, + voter1Address, ); expect(response.proposals.length).toBeGreaterThanOrEqual(1); expect(response.proposals[response.proposals.length - 1]).toEqual({ @@ -212,7 +271,7 @@ describe("GovExtension", () => { expect(response.deposits).toEqual([ { proposalId: longify(proposalId), - depositor: faucet.address0, + depositor: voter1Address, amount: initialDeposit, }, ]); @@ -226,10 +285,10 @@ describe("GovExtension", () => { pendingWithoutSimapp(); const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl); - const response = await client.gov.deposit(proposalId, faucet.address0); + const response = await client.gov.deposit(proposalId, voter1Address); expect(response.deposit).toEqual({ proposalId: longify(proposalId), - depositor: faucet.address0, + depositor: voter1Address, amount: initialDeposit, }); @@ -244,10 +303,10 @@ describe("GovExtension", () => { const response = await client.gov.tally(proposalId); expect(response.tally).toEqual({ - yes: "0", + yes: delegationVoter1.amount, abstain: "0", no: "0", - noWithVeto: "0", + noWithVeto: delegationVoter2.amount, }); tmClient.disconnect(); @@ -261,9 +320,15 @@ describe("GovExtension", () => { const response = await client.gov.votes(proposalId); expect(response.votes).toEqual([ + // why is vote 2 first? { proposalId: longify(proposalId), - voter: faucet.address0, + voter: voter2Address, + option: VoteOption.VOTE_OPTION_NO_WITH_VETO, + }, + { + proposalId: longify(proposalId), + voter: voter1Address, option: VoteOption.VOTE_OPTION_YES, }, ]); @@ -277,9 +342,9 @@ describe("GovExtension", () => { pendingWithoutSimapp(); const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl); - const response = await client.gov.vote(proposalId, faucet.address0); + const response = await client.gov.vote(proposalId, voter1Address); expect(response.vote).toEqual({ - voter: faucet.address0, + voter: voter1Address, proposalId: longify(proposalId), option: VoteOption.VOTE_OPTION_YES, }); diff --git a/packages/stargate/src/stargateclient.ts b/packages/stargate/src/stargateclient.ts index 5138dc3c..a0ac1e59 100644 --- a/packages/stargate/src/stargateclient.ts +++ b/packages/stargate/src/stargateclient.ts @@ -7,7 +7,15 @@ import { MsgData } from "cosmjs-types/cosmos/base/abci/v1beta1/abci"; import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin"; import { Account, accountFromAny } from "./accounts"; -import { AuthExtension, BankExtension, QueryClient, setupAuthExtension, setupBankExtension } from "./queries"; +import { + AuthExtension, + BankExtension, + QueryClient, + setupAuthExtension, + setupBankExtension, + setupStakingExtension, + StakingExtension, +} from "./queries"; import { isSearchByHeightQuery, isSearchBySentFromOrToQuery, @@ -138,7 +146,7 @@ export interface PrivateStargateClient { export class StargateClient { private readonly tmClient: Tendermint34Client | undefined; - private readonly queryClient: (QueryClient & AuthExtension & BankExtension) | undefined; + private readonly queryClient: (QueryClient & AuthExtension & BankExtension & StakingExtension) | undefined; private chainId: string | undefined; public static async connect(endpoint: string): Promise { @@ -149,7 +157,12 @@ export class StargateClient { protected constructor(tmClient: Tendermint34Client | undefined) { if (tmClient) { this.tmClient = tmClient; - this.queryClient = QueryClient.withExtensions(tmClient, setupAuthExtension, setupBankExtension); + this.queryClient = QueryClient.withExtensions( + tmClient, + setupAuthExtension, + setupBankExtension, + setupStakingExtension, + ); } } @@ -166,11 +179,11 @@ export class StargateClient { return this.tmClient; } - protected getQueryClient(): (QueryClient & AuthExtension & BankExtension) | undefined { + protected getQueryClient(): (QueryClient & AuthExtension & BankExtension & StakingExtension) | undefined { return this.queryClient; } - protected forceGetQueryClient(): QueryClient & AuthExtension & BankExtension { + protected forceGetQueryClient(): QueryClient & AuthExtension & BankExtension & StakingExtension { if (!this.queryClient) { throw new Error("Query client not available. You cannot use online functionality in offline mode."); } @@ -254,6 +267,22 @@ export class StargateClient { return this.forceGetQueryClient().bank.allBalances(address); } + public async getDelegation(delegatorAddress: string, validatorAddress: string): Promise { + let delegatedAmount: Coin | undefined; + try { + delegatedAmount = ( + await this.forceGetQueryClient().staking.delegation(delegatorAddress, validatorAddress) + ).delegationResponse?.balance; + } catch (e) { + if (e.toString().includes("key not found")) { + // ignore, `delegatedAmount` remains undefined + } else { + throw e; + } + } + return delegatedAmount || null; + } + public async getTx(id: string): Promise { const results = await this.txsQuery(`tx.hash='${id}'`); return results[0] ?? null; From 5a5bb16b6e3f922a219889b9399d0c9772ef83b5 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 28 Jul 2021 23:21:30 +0200 Subject: [PATCH 6/7] Improve some pagination code --- packages/cosmwasm-stargate/src/queries/wasm.ts | 12 ++++++------ packages/stargate/src/queries/utils.ts | 7 +++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/cosmwasm-stargate/src/queries/wasm.ts b/packages/cosmwasm-stargate/src/queries/wasm.ts index 229a0cb8..0f13060d 100644 --- a/packages/cosmwasm-stargate/src/queries/wasm.ts +++ b/packages/cosmwasm-stargate/src/queries/wasm.ts @@ -85,10 +85,10 @@ export function setupWasmExtension(base: QueryClient): WasmExtension { return queryService.Code(request); }, listContractsByCodeId: async (id: number, paginationKey?: Uint8Array) => { - const pagination = { + const request = { + codeId: Long.fromNumber(id), pagination: createPagination(paginationKey), }; - const request = { ...pagination, codeId: Long.fromNumber(id) }; return queryService.ContractsByCode(request); }, getContractInfo: async (address: string) => { @@ -97,18 +97,18 @@ export function setupWasmExtension(base: QueryClient): WasmExtension { }, getContractCodeHistory: async (address: string, paginationKey?: Uint8Array) => { - const pagination = { + const request = { + address: address, pagination: createPagination(paginationKey), }; - const request = { ...pagination, address: address }; return queryService.ContractHistory(request); }, getAllContractState: async (address: string, paginationKey?: Uint8Array) => { - const pagination = { + const request = { + address: address, pagination: createPagination(paginationKey), }; - const request = { ...pagination, address: address }; return queryService.AllContractState(request); }, diff --git a/packages/stargate/src/queries/utils.ts b/packages/stargate/src/queries/utils.ts index 247446a6..983494f8 100644 --- a/packages/stargate/src/queries/utils.ts +++ b/packages/stargate/src/queries/utils.ts @@ -14,6 +14,13 @@ export function toAccAddress(address: string): Uint8Array { return Bech32.decode(address).data; } +/** + * If paginationKey is set, return a `PageRequest` with the given key. + * If paginationKey is unset, return `undefined`. + * + * Use this with a query response's pagination next key to + * request the next page. + */ export function createPagination(paginationKey?: Uint8Array): PageRequest | undefined { return paginationKey ? { From 86251a9ce561e3cc45f4ea2a10619bbc348d894b Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Wed, 28 Jul 2021 23:21:52 +0200 Subject: [PATCH 7/7] Add pagination keys for gov queries --- packages/stargate/src/queries/gov.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/stargate/src/queries/gov.ts b/packages/stargate/src/queries/gov.ts index cb37bc22..7e9c9d52 100644 --- a/packages/stargate/src/queries/gov.ts +++ b/packages/stargate/src/queries/gov.ts @@ -14,7 +14,7 @@ import { import Long from "long"; import { QueryClient } from "./queryclient"; -import { createProtobufRpcClient, longify } from "./utils"; +import { createPagination, createProtobufRpcClient, longify } from "./utils"; export type GovParamsType = "deposit" | "tallying" | "voting"; @@ -50,13 +50,17 @@ export function setupGovExtension(base: QueryClient): GovExtension { const response = await queryService.Params({ paramsType: parametersType }); return response; }, - proposals: async (proposalStatus: ProposalStatus, depositorAddress: string, voterAddress: string) => { - // TODO: pagination + proposals: async ( + proposalStatus: ProposalStatus, + depositorAddress: string, + voterAddress: string, + paginationKey?: Uint8Array, + ) => { const response = await queryService.Proposals({ proposalStatus, depositor: depositorAddress, voter: voterAddress, - pagination: undefined, + pagination: createPagination(paginationKey), }); return response; }, @@ -64,11 +68,10 @@ export function setupGovExtension(base: QueryClient): GovExtension { const response = await queryService.Proposal({ proposalId: longify(proposalId) }); return response; }, - deposits: async (proposalId: GovProposalId) => { - // TODO: pagination + deposits: async (proposalId: GovProposalId, paginationKey?: Uint8Array) => { const response = await queryService.Deposits({ proposalId: longify(proposalId), - pagination: undefined, + pagination: createPagination(paginationKey), }); return response; }, @@ -85,11 +88,10 @@ export function setupGovExtension(base: QueryClient): GovExtension { }); return response; }, - votes: async (proposalId: GovProposalId) => { - // TODO: pagination + votes: async (proposalId: GovProposalId, paginationKey?: Uint8Array) => { const response = await queryService.Votes({ proposalId: longify(proposalId), - pagination: undefined, + pagination: createPagination(paginationKey), }); return response; },