diff --git a/packages/sdk38/src/index.ts b/packages/sdk38/src/index.ts index 4233a729..d9dd90ce 100644 --- a/packages/sdk38/src/index.ts +++ b/packages/sdk38/src/index.ts @@ -28,6 +28,16 @@ export { BlockResponse, BroadcastMode, EncodeTxResponse, + GovExtension, + GovParametersResponse, + GovProposalsResponse, + GovProposalResponse, + GovProposerResponse, + GovDepositsResponse, + GovDepositResponse, + GovTallyResponse, + GovVotesResponse, + GovVoteResponse, LcdApiArray, LcdClient, MintAnnualProvisionsResponse, @@ -40,6 +50,7 @@ export { SearchTxsResponse, setupAuthExtension, setupBankExtension, + setupGovExtension, setupMintExtension, setupSlashingExtension, setupSupplyExtension, diff --git a/packages/sdk38/src/lcdapi/gov.spec.ts b/packages/sdk38/src/lcdapi/gov.spec.ts new file mode 100644 index 00000000..594be5d5 --- /dev/null +++ b/packages/sdk38/src/lcdapi/gov.spec.ts @@ -0,0 +1,304 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { assert, sleep } from "@cosmjs/utils"; + +import { coins } from "../coins"; +import { isPostTxFailure } from "../cosmosclient"; +import { makeSignBytes } from "../encoding"; +import { SigningCosmosClient } from "../signingcosmosclient"; +import { + dateTimeStampMatcher, + faucet, + nonNegativeIntegerMatcher, + pendingWithoutWasmd, + wasmd, + wasmdEnabled, +} from "../testutils.spec"; +import { Secp256k1Wallet } from "../wallet"; +import { GovExtension, GovParametersType, setupGovExtension } from "./gov"; +import { LcdClient } from "./lcdclient"; + +function makeGovClient(apiUrl: string): LcdClient & GovExtension { + return LcdClient.withExtensions({ apiUrl }, setupGovExtension); +} + +describe("GovExtension", () => { + const defaultFee = { + amount: coins(25000, "ucosm"), + gas: "1500000", // 1.5 million + }; + let proposalId: string; + + beforeAll(async () => { + if (wasmdEnabled()) { + const wallet = await Secp256k1Wallet.fromMnemonic(faucet.mnemonic); + const client = new SigningCosmosClient(wasmd.endpoint, faucet.address, wallet, {}); + + const chainId = await client.getChainId(); + const proposalMsg = { + type: "cosmos-sdk/MsgSubmitProposal", + value: { + content: { + type: "cosmos-sdk/TextProposal", + value: { + description: "This proposal proposes to test whether this proposal passes", + title: "Test Proposal", + }, + }, + proposer: faucet.address, + initial_deposit: coins(25000000, "ustake"), + }, + }; + const proposalMemo = "Test proposal for wasmd"; + const { accountNumber: proposalAccountNumber, sequence: proposalSequence } = await client.getNonce(); + const proposalSignBytes = makeSignBytes( + [proposalMsg], + defaultFee, + chainId, + proposalMemo, + proposalAccountNumber, + proposalSequence, + ); + const proposalSignature = await wallet.sign(faucet.address, proposalSignBytes); + const proposalTx = { + msg: [proposalMsg], + fee: defaultFee, + memo: proposalMemo, + signatures: [proposalSignature], + }; + + const proposalReceipt = await client.postTx(proposalTx); + assert(!isPostTxFailure(proposalReceipt)); + proposalId = proposalReceipt.logs[0].events + .find(({ type }) => type === "submit_proposal")! + .attributes.find(({ key }) => key === "proposal_id")!.value; + + const voteMsg = { + type: "cosmos-sdk/MsgVote", + value: { + proposal_id: proposalId, + voter: faucet.address, + option: "Yes", + }, + }; + const voteMemo = "Test vote for wasmd"; + const { accountNumber: voteAccountNumber, sequence: voteSequence } = await client.getNonce(); + const voteSignBytes = makeSignBytes( + [voteMsg], + defaultFee, + chainId, + voteMemo, + voteAccountNumber, + voteSequence, + ); + const voteSignature = await wallet.sign(faucet.address, voteSignBytes); + const voteTx = { + msg: [voteMsg], + fee: defaultFee, + memo: voteMemo, + signatures: [voteSignature], + }; + await client.postTx(voteTx); + + await sleep(75); // wait until transactions are indexed + } + }); + + describe("parameters", () => { + it("works for deposit", async () => { + pendingWithoutWasmd(); + const client = makeGovClient(wasmd.endpoint); + const paramsType = GovParametersType.Deposit; + const response = await client.gov.parameters(paramsType); + expect(response).toEqual({ + height: jasmine.stringMatching(nonNegativeIntegerMatcher), + result: { + min_deposit: [{ denom: "ustake", amount: "10000000" }], + max_deposit_period: "172800000000000", + }, + }); + }); + + it("works for tallying", async () => { + pendingWithoutWasmd(); + const client = makeGovClient(wasmd.endpoint); + const paramsType = GovParametersType.Tallying; + const response = await client.gov.parameters(paramsType); + expect(response).toEqual({ + height: jasmine.stringMatching(nonNegativeIntegerMatcher), + result: { + quorum: "0.334000000000000000", + threshold: "0.500000000000000000", + veto: "0.334000000000000000", + }, + }); + }); + + it("works for voting", async () => { + pendingWithoutWasmd(); + const client = makeGovClient(wasmd.endpoint); + const paramsType = GovParametersType.Voting; + const response = await client.gov.parameters(paramsType); + expect(response).toEqual({ + height: jasmine.stringMatching(nonNegativeIntegerMatcher), + result: { + voting_period: "172800000000000", + }, + }); + }); + }); + + describe("proposals", () => { + it("works", async () => { + pendingWithoutWasmd(); + const client = makeGovClient(wasmd.endpoint); + const response = await client.gov.proposals(); + expect(response.height).toMatch(nonNegativeIntegerMatcher); + expect(response.result.length).toBeGreaterThanOrEqual(1); + expect(response.result[response.result.length - 1]).toEqual({ + content: { + type: "cosmos-sdk/TextProposal", + value: { + title: "Test Proposal", + description: "This proposal proposes to test whether this proposal passes", + }, + }, + id: proposalId, + proposal_status: "VotingPeriod", + final_tally_result: { yes: "0", abstain: "0", no: "0", no_with_veto: "0" }, + submit_time: jasmine.stringMatching(dateTimeStampMatcher), + deposit_end_time: jasmine.stringMatching(dateTimeStampMatcher), + total_deposit: [{ denom: "ustake", amount: "25000000" }], + voting_start_time: jasmine.stringMatching(dateTimeStampMatcher), + voting_end_time: jasmine.stringMatching(dateTimeStampMatcher), + }); + }); + }); + + describe("proposal", () => { + it("works", async () => { + pendingWithoutWasmd(); + const client = makeGovClient(wasmd.endpoint); + const response = await client.gov.proposal(proposalId); + expect(response).toEqual({ + height: jasmine.stringMatching(nonNegativeIntegerMatcher), + result: { + content: { + type: "cosmos-sdk/TextProposal", + value: { + title: "Test Proposal", + description: "This proposal proposes to test whether this proposal passes", + }, + }, + id: proposalId, + proposal_status: "VotingPeriod", + final_tally_result: { yes: "0", abstain: "0", no: "0", no_with_veto: "0" }, + submit_time: jasmine.stringMatching(dateTimeStampMatcher), + deposit_end_time: jasmine.stringMatching(dateTimeStampMatcher), + total_deposit: [{ denom: "ustake", amount: "25000000" }], + voting_start_time: jasmine.stringMatching(dateTimeStampMatcher), + voting_end_time: jasmine.stringMatching(dateTimeStampMatcher), + }, + }); + }); + }); + + describe("proposer", () => { + it("works", async () => { + pendingWithoutWasmd(); + const client = makeGovClient(wasmd.endpoint); + const response = await client.gov.proposer(proposalId); + expect(response).toEqual({ + height: jasmine.stringMatching(nonNegativeIntegerMatcher), + result: { + proposal_id: proposalId, + proposer: faucet.address, + }, + }); + }); + }); + + describe("deposits", () => { + it("works", async () => { + pendingWithoutWasmd(); + const client = makeGovClient(wasmd.endpoint); + const response = await client.gov.deposits(proposalId); + expect(response).toEqual({ + height: jasmine.stringMatching(nonNegativeIntegerMatcher), + result: [ + { + proposal_id: proposalId, + depositor: faucet.address, + amount: [{ denom: "ustake", amount: "25000000" }], + }, + ], + }); + }); + }); + + describe("deposit", () => { + it("works", async () => { + pendingWithoutWasmd(); + const client = makeGovClient(wasmd.endpoint); + const response = await client.gov.deposit(proposalId, faucet.address); + expect(response).toEqual({ + height: jasmine.stringMatching(nonNegativeIntegerMatcher), + result: { + proposal_id: proposalId, + depositor: faucet.address, + amount: [{ denom: "ustake", amount: "25000000" }], + }, + }); + }); + }); + + describe("tally", () => { + it("works", async () => { + pendingWithoutWasmd(); + const client = makeGovClient(wasmd.endpoint); + const response = await client.gov.tally(proposalId); + expect(response).toEqual({ + height: jasmine.stringMatching(nonNegativeIntegerMatcher), + result: { + yes: "0", + abstain: "0", + no: "0", + no_with_veto: "0", + }, + }); + }); + }); + + describe("votes", () => { + it("works", async () => { + pendingWithoutWasmd(); + const client = makeGovClient(wasmd.endpoint); + const response = await client.gov.votes(proposalId); + expect(response).toEqual({ + height: jasmine.stringMatching(nonNegativeIntegerMatcher), + result: [ + { + proposal_id: proposalId, + voter: faucet.address, + option: "Yes", + }, + ], + }); + }); + }); + + describe("vote", () => { + it("works", async () => { + pendingWithoutWasmd(); + const client = makeGovClient(wasmd.endpoint); + const response = await client.gov.vote(proposalId, faucet.address); + expect(response).toEqual({ + height: jasmine.stringMatching(nonNegativeIntegerMatcher), + result: { + voter: faucet.address, + proposal_id: proposalId, + option: "Yes", + }, + }); + }); + }); +}); diff --git a/packages/sdk38/src/lcdapi/gov.ts b/packages/sdk38/src/lcdapi/gov.ts new file mode 100644 index 00000000..f7b46635 --- /dev/null +++ b/packages/sdk38/src/lcdapi/gov.ts @@ -0,0 +1,150 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { Coin } from "../coins"; +import { LcdClient } from "./lcdclient"; + +export enum GovParametersType { + Deposit = "deposit", + Tallying = "tallying", + Voting = "voting", +} + +export interface GovParametersDepositResponse { + readonly height: string; + readonly result: { + readonly min_deposit: readonly Coin[]; + readonly max_deposit_period: string; + }; +} + +export interface GovParametersTallyingResponse { + readonly height: string; + readonly result: { + readonly quorum: string; + readonly threshold: string; + readonly veto: string; + }; +} + +export interface GovParametersVotingResponse { + readonly height: string; + readonly result: { + readonly voting_period: string; + }; +} + +export type GovParametersResponse = + | GovParametersDepositResponse + | GovParametersTallyingResponse + | GovParametersVotingResponse; + +export interface Tally { + readonly yes: string; + readonly abstain: string; + readonly no: string; + readonly no_with_veto: string; +} + +export interface Proposal { + readonly id: string; + readonly proposal_status: string; + readonly final_tally_result: Tally; + readonly submit_time: string; + readonly total_deposit: readonly Coin[]; + readonly deposit_end_time: string; + readonly voting_start_time: string; + readonly voting_end_time: string; + readonly content: { + readonly type: string; + readonly value: { + readonly title: string; + readonly description: string; + }; + }; +} + +export interface GovProposalsResponse { + readonly height: string; + readonly result: readonly Proposal[]; +} + +export interface GovProposalResponse { + readonly height: string; + readonly result: Proposal; +} + +export interface GovProposerResponse { + readonly height: string; + readonly result: { + readonly proposal_id: string; + readonly proposer: string; + }; +} + +export interface Deposit { + readonly amount: readonly Coin[]; + readonly proposal_id: string; + readonly depositor: string; +} + +export interface GovDepositsResponse { + readonly height: string; + readonly result: readonly Deposit[]; +} + +export interface GovDepositResponse { + readonly height: string; + readonly result: Deposit; +} + +export interface GovTallyResponse { + readonly height: string; + readonly result: Tally; +} + +export interface Vote { + readonly voter: string; + readonly proposal_id: string; + readonly option: string; +} + +export interface GovVotesResponse { + readonly height: string; + readonly result: readonly Vote[]; +} + +export interface GovVoteResponse { + readonly height: string; + readonly result: Vote; +} + +export interface GovExtension { + readonly gov: { + readonly parameters: (parametersType: GovParametersType) => Promise; + readonly proposals: () => Promise; + readonly proposal: (proposalId: string) => Promise; + readonly proposer: (proposalId: string) => Promise; + readonly deposits: (proposalId: string) => Promise; + readonly deposit: (proposalId: string, depositorAddress: string) => Promise; + readonly tally: (proposalId: string) => Promise; + readonly votes: (proposalId: string) => Promise; + readonly vote: (proposalId: string, voterAddress: string) => Promise; + }; +} + +export function setupGovExtension(base: LcdClient): GovExtension { + return { + gov: { + parameters: async (parametersType: GovParametersType) => base.get(`/gov/parameters/${parametersType}`), + proposals: async () => base.get("/gov/proposals"), + proposal: async (proposalId: string) => base.get(`/gov/proposals/${proposalId}`), + proposer: async (proposalId: string) => base.get(`/gov/proposals/${proposalId}/proposer`), + deposits: async (proposalId: string) => base.get(`/gov/proposals/${proposalId}/deposits`), + deposit: async (proposalId: string, depositorAddress: string) => + base.get(`/gov/proposals/${proposalId}/deposits/${depositorAddress}`), + tally: async (proposalId: string) => base.get(`/gov/proposals/${proposalId}/tally`), + votes: async (proposalId: string) => base.get(`/gov/proposals/${proposalId}/votes`), + vote: async (proposalId: string, voterAddress: string) => + base.get(`/gov/proposals/${proposalId}/votes/${voterAddress}`), + }, + }; +} diff --git a/packages/sdk38/src/lcdapi/index.ts b/packages/sdk38/src/lcdapi/index.ts index 11ef100e..80bd53b7 100644 --- a/packages/sdk38/src/lcdapi/index.ts +++ b/packages/sdk38/src/lcdapi/index.ts @@ -4,6 +4,19 @@ export { AuthExtension, AuthAccountsResponse, setupAuthExtension } from "./auth"; export { BankBalancesResponse, BankExtension, setupBankExtension } from "./bank"; +export { + GovExtension, + GovParametersResponse, + GovProposalsResponse, + GovProposalResponse, + GovProposerResponse, + GovDepositsResponse, + GovDepositResponse, + GovTallyResponse, + GovVotesResponse, + GovVoteResponse, + setupGovExtension, +} from "./gov"; export { MintAnnualProvisionsResponse, MintExtension, diff --git a/packages/sdk38/src/testutils.spec.ts b/packages/sdk38/src/testutils.spec.ts index 005fdc26..b6c80a73 100644 --- a/packages/sdk38/src/testutils.spec.ts +++ b/packages/sdk38/src/testutils.spec.ts @@ -17,6 +17,7 @@ export const tendermintIdMatcher = /^[0-9A-F]{64}$/; export const tendermintOptionalIdMatcher = /^([0-9A-F]{64}|)$/; export const tendermintAddressMatcher = /^[0-9A-F]{40}$/; export const tendermintShortHashMatcher = /^[0-9a-f]{40}$/; +export const dateTimeStampMatcher = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]+)?Z$/; export const semverMatcher = /^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/; // https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32 diff --git a/packages/sdk38/types/index.d.ts b/packages/sdk38/types/index.d.ts index 4fd2a02e..4f22dbbc 100644 --- a/packages/sdk38/types/index.d.ts +++ b/packages/sdk38/types/index.d.ts @@ -26,6 +26,16 @@ export { BlockResponse, BroadcastMode, EncodeTxResponse, + GovExtension, + GovParametersResponse, + GovProposalsResponse, + GovProposalResponse, + GovProposerResponse, + GovDepositsResponse, + GovDepositResponse, + GovTallyResponse, + GovVotesResponse, + GovVoteResponse, LcdApiArray, LcdClient, MintAnnualProvisionsResponse, @@ -38,6 +48,7 @@ export { SearchTxsResponse, setupAuthExtension, setupBankExtension, + setupGovExtension, setupMintExtension, setupSlashingExtension, setupSupplyExtension, diff --git a/packages/sdk38/types/lcdapi/gov.d.ts b/packages/sdk38/types/lcdapi/gov.d.ts new file mode 100644 index 00000000..b7f3f8fd --- /dev/null +++ b/packages/sdk38/types/lcdapi/gov.d.ts @@ -0,0 +1,114 @@ +import { Coin } from "../coins"; +import { LcdClient } from "./lcdclient"; +export declare enum GovParametersType { + Deposit = "deposit", + Tallying = "tallying", + Voting = "voting", +} +export interface GovParametersDepositResponse { + readonly height: string; + readonly result: { + readonly min_deposit: readonly Coin[]; + readonly max_deposit_period: string; + }; +} +export interface GovParametersTallyingResponse { + readonly height: string; + readonly result: { + readonly quorum: string; + readonly threshold: string; + readonly veto: string; + }; +} +export interface GovParametersVotingResponse { + readonly height: string; + readonly result: { + readonly voting_period: string; + }; +} +export declare type GovParametersResponse = + | GovParametersDepositResponse + | GovParametersTallyingResponse + | GovParametersVotingResponse; +export interface Tally { + readonly yes: string; + readonly abstain: string; + readonly no: string; + readonly no_with_veto: string; +} +export interface Proposal { + readonly id: string; + readonly proposal_status: string; + readonly final_tally_result: Tally; + readonly submit_time: string; + readonly total_deposit: readonly Coin[]; + readonly deposit_end_time: string; + readonly voting_start_time: string; + readonly voting_end_time: string; + readonly content: { + readonly type: string; + readonly value: { + readonly title: string; + readonly description: string; + }; + }; +} +export interface GovProposalsResponse { + readonly height: string; + readonly result: readonly Proposal[]; +} +export interface GovProposalResponse { + readonly height: string; + readonly result: Proposal; +} +export interface GovProposerResponse { + readonly height: string; + readonly result: { + readonly proposal_id: string; + readonly proposer: string; + }; +} +export interface Deposit { + readonly amount: readonly Coin[]; + readonly proposal_id: string; + readonly depositor: string; +} +export interface GovDepositsResponse { + readonly height: string; + readonly result: readonly Deposit[]; +} +export interface GovDepositResponse { + readonly height: string; + readonly result: Deposit; +} +export interface GovTallyResponse { + readonly height: string; + readonly result: Tally; +} +export interface Vote { + readonly voter: string; + readonly proposal_id: string; + readonly option: string; +} +export interface GovVotesResponse { + readonly height: string; + readonly result: readonly Vote[]; +} +export interface GovVoteResponse { + readonly height: string; + readonly result: Vote; +} +export interface GovExtension { + readonly gov: { + readonly parameters: (parametersType: GovParametersType) => Promise; + readonly proposals: () => Promise; + readonly proposal: (proposalId: string) => Promise; + readonly proposer: (proposalId: string) => Promise; + readonly deposits: (proposalId: string) => Promise; + readonly deposit: (proposalId: string, depositorAddress: string) => Promise; + readonly tally: (proposalId: string) => Promise; + readonly votes: (proposalId: string) => Promise; + readonly vote: (proposalId: string, voterAddress: string) => Promise; + }; +} +export declare function setupGovExtension(base: LcdClient): GovExtension; diff --git a/packages/sdk38/types/lcdapi/index.d.ts b/packages/sdk38/types/lcdapi/index.d.ts index d22a29b4..b9f984ac 100644 --- a/packages/sdk38/types/lcdapi/index.d.ts +++ b/packages/sdk38/types/lcdapi/index.d.ts @@ -1,5 +1,18 @@ export { AuthExtension, AuthAccountsResponse, setupAuthExtension } from "./auth"; export { BankBalancesResponse, BankExtension, setupBankExtension } from "./bank"; +export { + GovExtension, + GovParametersResponse, + GovProposalsResponse, + GovProposalResponse, + GovProposerResponse, + GovDepositsResponse, + GovDepositResponse, + GovTallyResponse, + GovVotesResponse, + GovVoteResponse, + setupGovExtension, +} from "./gov"; export { MintAnnualProvisionsResponse, MintExtension,