From 5b69ce7da7330a799adfde4c8a229253a251e2d7 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 11 Nov 2020 18:14:33 +0100 Subject: [PATCH 01/11] scripts: Add CW3 contract to Launchpad Wasm --- scripts/launchpad/contracts/checksums.sha256 | 1 + scripts/launchpad/contracts/cw3_fixed_multisig.wasm | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 scripts/launchpad/contracts/cw3_fixed_multisig.wasm diff --git a/scripts/launchpad/contracts/checksums.sha256 b/scripts/launchpad/contracts/checksums.sha256 index 6dcac988..e9600aba 100644 --- a/scripts/launchpad/contracts/checksums.sha256 +++ b/scripts/launchpad/contracts/checksums.sha256 @@ -1,3 +1,4 @@ +1a4a376ef1099ad3edc33aa1d3105e4621bc49e44b1ac0a449d7b6912e40fb0a cw3_fixed_multisig.wasm ebc2b11e2afa50d5dcd4234840cd581e948a59d888bb8d651598bba3732cd8ee cw-nameservice.wasm d04368320ad55089384adb171aaea39e43d710d7608829adba0300ed30aa2988 cw_erc20.wasm 3defc33a41f58c71d38b176d521c411d8e74d26403fde7660486930c7579a016 hackatom.wasm diff --git a/scripts/launchpad/contracts/cw3_fixed_multisig.wasm b/scripts/launchpad/contracts/cw3_fixed_multisig.wasm new file mode 100644 index 00000000..61772386 --- /dev/null +++ b/scripts/launchpad/contracts/cw3_fixed_multisig.wasm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a4a376ef1099ad3edc33aa1d3105e4621bc49e44b1ac0a449d7b6912e40fb0a +size 247712 From 7c5dd3e4670df87bb7e7fcfed036bc5cb5f4cb7a Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 11 Nov 2020 18:33:20 +0100 Subject: [PATCH 02/11] scripts: Add CW3 deployment to Launchpad Wasm --- scripts/launchpad/deploy_cw3.js | 91 +++++++++++++++++++++++++++++++++ scripts/launchpad/init.sh | 1 + 2 files changed, 92 insertions(+) create mode 100755 scripts/launchpad/deploy_cw3.js diff --git a/scripts/launchpad/deploy_cw3.js b/scripts/launchpad/deploy_cw3.js new file mode 100755 index 00000000..411f4837 --- /dev/null +++ b/scripts/launchpad/deploy_cw3.js @@ -0,0 +1,91 @@ +#!/usr/bin/env node + +/* eslint-disable @typescript-eslint/naming-convention */ +const { SigningCosmWasmClient } = require("@cosmjs/cosmwasm"); +const { Secp256k1HdWallet } = require("@cosmjs/launchpad"); +const fs = require("fs"); + +const httpUrl = "http://localhost:1317"; +const alice = { + mnemonic: "enlist hip relief stomach skate base shallow young switch frequent cry park", + address0: "cosmos14qemq0vw6y3gc3u3e0aty2e764u4gs5le3hada", + address1: "cosmos1hhg2rlu9jscacku2wwckws7932qqqu8x3gfgw0", + address2: "cosmos1xv9tklw7d82sezh9haa573wufgy59vmwe6xxe5", + address3: "cosmos17yg9mssjenmc3jkqth6ulcwj9cxujrxxzezwta", + address4: "cosmos1f7j7ryulwjfe9ljplvhtcaxa6wqgula3etktce", +}; + +const codeMeta = { + source: "https://crates.io/api/v1/crates/cw3-fixed-multisig/0.3.1/download", + builder: "cosmwasm/rust-optimizer:0.10.4", +}; + +const initData = [ + { + admin: alice.address0, + label: "Multisig (1/3)", + initMsg: { + voters: [ + { addr: alice.address0, weight: 1 }, + { addr: alice.address1, weight: 1 }, + { addr: alice.address2, weight: 1 }, + ], + required_weight: 1, + max_voting_period: { height: 12345 }, + }, + }, + { + admin: alice.address0, + label: "Multisig (2/3)", + initMsg: { + voters: [ + { addr: alice.address0, weight: 1 }, + { addr: alice.address1, weight: 1 }, + { addr: alice.address2, weight: 1 }, + ], + required_weight: 2, + max_voting_period: { height: 12345 }, + }, + }, + { + admin: alice.address0, + label: "Multisig (uneven weights)", + initMsg: { + voters: [ + { addr: alice.address0, weight: 1 }, + { addr: alice.address1, weight: 2 }, + { addr: alice.address2, weight: 3 }, + ], + required_weight: 3, + max_voting_period: { height: 12345 }, + }, + }, +]; + +async function main() { + const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic); + const client = new SigningCosmWasmClient(httpUrl, alice.address0, wallet); + + const wasm = fs.readFileSync(__dirname + "/contracts/cw3_fixed_multisig.wasm"); + const uploadReceipt = await client.upload(wasm, codeMeta, "Upload CW3 fixed multisig contract"); + console.info(`Upload succeeded. Receipt: ${JSON.stringify(uploadReceipt)}`); + + for (const { admin, initMsg, label } of initData) { + const { contractAddress } = await client.instantiate(uploadReceipt.codeId, initMsg, label, { + memo: `Create a CW3 instance for ${initMsg.symbol}`, + admin: admin, + }); + console.info(`Contract instantiated for ${label} at ${contractAddress}`); + } +} + +main().then( + () => { + console.info("All done, let the coins flow."); + process.exit(0); + }, + (error) => { + console.error(error); + process.exit(1); + }, +); diff --git a/scripts/launchpad/init.sh b/scripts/launchpad/init.sh index 617001f2..7c49cb3e 100755 --- a/scripts/launchpad/init.sh +++ b/scripts/launchpad/init.sh @@ -26,4 +26,5 @@ SCRIPT_DIR="$(realpath "$(dirname "$0")")" ) "$SCRIPT_DIR/deploy_hackatom.js" "$SCRIPT_DIR/deploy_erc20.js" +"$SCRIPT_DIR/deploy_cw3.js" # "$SCRIPT_DIR/deploy_nameservice.js" From ef869866224e3d0edc4873b216ff4a2f4369531c Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 11 Nov 2020 18:55:14 +0100 Subject: [PATCH 03/11] launchpad: Add CW3 test utils --- packages/cosmwasm/src/testutils.spec.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/cosmwasm/src/testutils.spec.ts b/packages/cosmwasm/src/testutils.spec.ts index 12ce2198..f9f12ef6 100644 --- a/packages/cosmwasm/src/testutils.spec.ts +++ b/packages/cosmwasm/src/testutils.spec.ts @@ -91,6 +91,18 @@ export const deployedErc20 = { ], }; +/** Deployed as part of scripts/launchpad/init.sh */ +export const deployedCw3 = { + codeId: 3, + source: "https://crates.io/api/v1/crates/cw3-fixed-multisig/0.3.1/download", + builder: "cosmwasm/rust-optimizer:0.10.4", + instances: [ + "cosmos1xqeym28j9xgv0p93pwwt6qcxf9tdvf9zddufdw", // Multisig (1/3) + "cosmos1jka38ckju8cpjap00jf9xdvdyttz9caujtd6t5", // Multisig (2/3) + "cosmos12dnl585uxzddjw9hw4ca694f054shgpgr4zg80", // Multisig (uneven weights) + ], +}; + export const launchpad = { endpoint: "http://localhost:1317", chainId: "testing", @@ -119,6 +131,16 @@ export function pendingWithoutErc20(): void { } } +export function cw3Enabled(): boolean { + return !!process.env.CW3_ENABLED; +} + +export function pendingWithoutCw3(): void { + if (!cw3Enabled()) { + return pending("Set CW3_ENABLED to enable CW3-based tests"); + } +} + /** Returns first element. Throws if array has a different length than 1. */ export function fromOneElementArray(elements: ArrayLike): T { if (elements.length !== 1) throw new Error(`Expected exactly one element but got ${elements.length}`); From bd762847b88355d16c544795ebbb927dd67a78d1 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 17 Nov 2020 13:13:53 +0100 Subject: [PATCH 04/11] scripts: Give CW3 contracts some funds --- scripts/launchpad/deploy_cw3.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/launchpad/deploy_cw3.js b/scripts/launchpad/deploy_cw3.js index 411f4837..38efddab 100755 --- a/scripts/launchpad/deploy_cw3.js +++ b/scripts/launchpad/deploy_cw3.js @@ -75,6 +75,12 @@ async function main() { memo: `Create a CW3 instance for ${initMsg.symbol}`, admin: admin, }); + await client.sendTokens(contractAddress, [ + { + amount: "1000", + denom: "ucosm", + }, + ]); console.info(`Contract instantiated for ${label} at ${contractAddress}`); } } From 794d87a2658a7dedbca3c44d7db787b78dbc9a8f Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 17 Nov 2020 13:51:31 +0100 Subject: [PATCH 05/11] cosmwasm: Add Cw3CosmWasmClient class --- packages/cosmwasm/src/cw3cosmwasmclient.ts | 76 +++++++++++++++++++ .../cosmwasm/types/cw3cosmwasmclient.d.ts | 38 ++++++++++ 2 files changed, 114 insertions(+) create mode 100644 packages/cosmwasm/src/cw3cosmwasmclient.ts create mode 100644 packages/cosmwasm/types/cw3cosmwasmclient.d.ts diff --git a/packages/cosmwasm/src/cw3cosmwasmclient.ts b/packages/cosmwasm/src/cw3cosmwasmclient.ts new file mode 100644 index 00000000..5252ed50 --- /dev/null +++ b/packages/cosmwasm/src/cw3cosmwasmclient.ts @@ -0,0 +1,76 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { BroadcastMode, GasLimits, GasPrice, OfflineSigner } from "@cosmjs/launchpad"; + +import { CosmWasmFeeTable, ExecuteResult, SigningCosmWasmClient } from "./signingcosmwasmclient"; + +export type Expiration = + | { + readonly at_height: number; + } + | { + readonly at_time: number; + }; + +export enum Vote { + Yes = "yes", + No = "no", + Abstain = "abstain", + Veto = "veto", +} + +export class Cw3CosmWasmClient extends SigningCosmWasmClient { + private readonly cw3ContractAddress: string; + + public constructor( + apiUrl: string, + senderAddress: string, + signer: OfflineSigner, + cw3ContractAddress: string, + gasPrice?: GasPrice, + gasLimits?: Partial>, + broadcastMode?: BroadcastMode, + ) { + super(apiUrl, senderAddress, signer, gasPrice, gasLimits, broadcastMode); + this.cw3ContractAddress = cw3ContractAddress; + } + + public createMultisigProposal( + title: string, + description: string, + msgs: ReadonlyArray>, + earliest?: Expiration, + latest?: Expiration, + memo = "", + ): Promise { + const handleMsg = { + propose: { + title: title, + description: description, + msgs: msgs, + earliest: earliest, + latest: latest, + }, + }; + return this.execute(this.cw3ContractAddress, handleMsg, memo); + } + + public voteMultisigProposal(proposalId: number, vote: Vote, memo = ""): Promise { + const handleMsg = { + vote: { + proposal_id: proposalId, + vote: vote, + }, + }; + return this.execute(this.cw3ContractAddress, handleMsg, memo); + } + + public executeMultisigProposal(proposalId: number, memo = ""): Promise { + const handleMsg = { execute: { proposal_id: proposalId } }; + return this.execute(this.cw3ContractAddress, handleMsg, memo); + } + + public closeMultisigProposal(proposalId: number, memo = ""): Promise { + const handleMsg = { close: { proposal_id: proposalId } }; + return this.execute(this.cw3ContractAddress, handleMsg, memo); + } +} diff --git a/packages/cosmwasm/types/cw3cosmwasmclient.d.ts b/packages/cosmwasm/types/cw3cosmwasmclient.d.ts new file mode 100644 index 00000000..c30b639b --- /dev/null +++ b/packages/cosmwasm/types/cw3cosmwasmclient.d.ts @@ -0,0 +1,38 @@ +import { BroadcastMode, GasLimits, GasPrice, OfflineSigner } from "@cosmjs/launchpad"; +import { CosmWasmFeeTable, ExecuteResult, SigningCosmWasmClient } from "./signingcosmwasmclient"; +export declare type Expiration = + | { + readonly at_height: number; + } + | { + readonly at_time: number; + }; +export declare enum Vote { + Yes = "yes", + No = "no", + Abstain = "abstain", + Veto = "veto", +} +export declare class Cw3CosmWasmClient extends SigningCosmWasmClient { + private readonly cw3ContractAddress; + constructor( + apiUrl: string, + senderAddress: string, + signer: OfflineSigner, + cw3ContractAddress: string, + gasPrice?: GasPrice, + gasLimits?: Partial>, + broadcastMode?: BroadcastMode, + ); + createMultisigProposal( + title: string, + description: string, + msgs: ReadonlyArray>, + earliest?: Expiration, + latest?: Expiration, + memo?: string, + ): Promise; + voteMultisigProposal(proposalId: number, vote: Vote, memo?: string): Promise; + executeMultisigProposal(proposalId: number, memo?: string): Promise; + closeMultisigProposal(proposalId: number, memo?: string): Promise; +} From 09c5968437e332618a5bf50638052d895e1fcb49 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Tue, 17 Nov 2020 13:51:48 +0100 Subject: [PATCH 06/11] cosmwasm: Add Cw3CosmWasmClient tests --- .../cosmwasm/src/cw3cosmwasmclient.spec.ts | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 packages/cosmwasm/src/cw3cosmwasmclient.spec.ts diff --git a/packages/cosmwasm/src/cw3cosmwasmclient.spec.ts b/packages/cosmwasm/src/cw3cosmwasmclient.spec.ts new file mode 100644 index 00000000..d40f0926 --- /dev/null +++ b/packages/cosmwasm/src/cw3cosmwasmclient.spec.ts @@ -0,0 +1,187 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { makeCosmoshubPath, Secp256k1HdWallet } from "@cosmjs/launchpad"; +import { assert, sleep } from "@cosmjs/utils"; + +import { Cw3CosmWasmClient, Vote } from "./cw3cosmwasmclient"; +import { + alice, + deployedCw3, + launchpad, + makeRandomAddress, + pendingWithoutCw3, + pendingWithoutLaunchpad, +} from "./testutils.spec"; + +describe("Cw3CosmWasmClient", () => { + describe("constructor", () => { + it("can be constructed", async () => { + const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic); + const client = new Cw3CosmWasmClient( + launchpad.endpoint, + alice.address0, + wallet, + deployedCw3.instances[0], + ); + expect(client).toBeTruthy(); + }); + }); + + describe("queries", () => { + it("works", async () => { + pendingWithoutLaunchpad(); + pendingWithoutCw3(); + const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic); + const client = new Cw3CosmWasmClient( + launchpad.endpoint, + alice.address0, + wallet, + deployedCw3.instances[0], + ); + const result = await client.queryContractSmart(deployedCw3.instances[0], { threshold: {} }); + expect(result).toEqual({ absolute_count: { weight_needed: 1, total_weight: 3 } }); + }); + }); + + describe("Proposal lifecycle", () => { + it("proposal is accepted (proposer has enough weight alone)", async () => { + pendingWithoutLaunchpad(); + pendingWithoutCw3(); + const contractAddress = deployedCw3.instances[0]; + const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic); + const client = new Cw3CosmWasmClient(launchpad.endpoint, alice.address0, wallet, contractAddress); + const toAddress = makeRandomAddress(); + const msg = { + bank: { + send: { + from_address: contractAddress, + to_address: toAddress, + amount: [ + { + amount: "1", + denom: "ucosm", + }, + ], + }, + }, + }; + const { logs } = await client.createMultisigProposal( + "My proposal", + "A proposal to propose proposing proposals", + [msg], + ); + const wasmEvents = logs[0].events.find((event) => event.type === "wasm"); + assert(wasmEvents, "Wasm events not found in logs"); + const proposalIdAttribute = wasmEvents.attributes.find((log) => log.key === "proposal_id"); + assert(proposalIdAttribute, "Proposal ID not found in logs"); + const proposalId = parseInt(proposalIdAttribute.value, 10); + + const executeResult = await client.executeMultisigProposal(proposalId); + expect(executeResult).toBeTruthy(); + }); + + it("proposal is accepted (proposer does not have enough weight alone)", async () => { + pendingWithoutLaunchpad(); + pendingWithoutCw3(); + const contractAddress = deployedCw3.instances[1]; + const proposerWallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic); + const proposer = new Cw3CosmWasmClient( + launchpad.endpoint, + alice.address0, + proposerWallet, + contractAddress, + ); + const voterWallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic, makeCosmoshubPath(1)); + const voter = new Cw3CosmWasmClient(launchpad.endpoint, alice.address1, voterWallet, contractAddress); + const toAddress = makeRandomAddress(); + const msg = { + bank: { + send: { + from_address: contractAddress, + to_address: toAddress, + amount: [ + { + amount: "1", + denom: "ucosm", + }, + ], + }, + }, + }; + const { logs } = await proposer.createMultisigProposal( + "My proposal", + "A proposal to propose proposing proposals", + [msg], + ); + const wasmEvents = logs[0].events.find((event) => event.type === "wasm"); + assert(wasmEvents, "Wasm events not found in logs"); + const proposalIdAttribute = wasmEvents.attributes.find((log) => log.key === "proposal_id"); + assert(proposalIdAttribute, "Proposal ID not found in logs"); + const proposalId = parseInt(proposalIdAttribute.value, 10); + + const voteResult = await voter.voteMultisigProposal(proposalId, Vote.Yes); + expect(voteResult).toBeTruthy(); + + const executeResult = await proposer.executeMultisigProposal(proposalId); + expect(executeResult).toBeTruthy(); + }); + + it("proposal is rejected", async () => { + pendingWithoutLaunchpad(); + pendingWithoutCw3(); + const contractAddress = deployedCw3.instances[1]; + const proposerWallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic); + const proposer = new Cw3CosmWasmClient( + launchpad.endpoint, + alice.address0, + proposerWallet, + contractAddress, + ); + const voter1Wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic, makeCosmoshubPath(1)); + const voter1 = new Cw3CosmWasmClient(launchpad.endpoint, alice.address1, voter1Wallet, contractAddress); + const voter2Wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic, makeCosmoshubPath(2)); + const voter2 = new Cw3CosmWasmClient(launchpad.endpoint, alice.address2, voter2Wallet, contractAddress); + const toAddress = makeRandomAddress(); + const msg = { + bank: { + send: { + from_address: contractAddress, + to_address: toAddress, + amount: [ + { + amount: "1", + denom: "ucosm", + }, + ], + }, + }, + }; + const currentHeight = await proposer.getHeight(); + const { logs } = await proposer.createMultisigProposal( + "My proposal", + "A proposal to propose proposing proposals", + [msg], + { + at_height: currentHeight, + }, + { + at_height: currentHeight + 5, + }, + ); + const wasmEvents = logs[0].events.find((event) => event.type === "wasm"); + assert(wasmEvents, "Wasm events not found in logs"); + const proposalIdAttribute = wasmEvents.attributes.find((log) => log.key === "proposal_id"); + assert(proposalIdAttribute, "Proposal ID not found in logs"); + const proposalId = parseInt(proposalIdAttribute.value, 10); + + const vote1Result = await voter1.voteMultisigProposal(proposalId, Vote.Abstain); + expect(vote1Result).toBeTruthy(); + const vote2Result = await voter2.voteMultisigProposal(proposalId, Vote.No); + expect(vote2Result).toBeTruthy(); + + await sleep(2000); + + const closeResult = await proposer.closeMultisigProposal(proposalId); + expect(closeResult).toBeTruthy(); + }); + }); +}); From bb8f2d856fde974ec86d401e9410fa06f72e77ce Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 18 Nov 2020 13:33:59 +0100 Subject: [PATCH 07/11] cosmwasm: Add query methods to Cw3CosmWasmClient --- packages/cosmwasm/src/cw3cosmwasmclient.ts | 99 +++++++++++++++++++ .../cosmwasm/types/cw3cosmwasmclient.d.ts | 42 ++++++++ 2 files changed, 141 insertions(+) diff --git a/packages/cosmwasm/src/cw3cosmwasmclient.ts b/packages/cosmwasm/src/cw3cosmwasmclient.ts index 5252ed50..f2344a3e 100644 --- a/packages/cosmwasm/src/cw3cosmwasmclient.ts +++ b/packages/cosmwasm/src/cw3cosmwasmclient.ts @@ -18,6 +18,43 @@ export enum Vote { Veto = "veto", } +export interface ThresholdResult { + readonly absolute_count: { + readonly weight_needed: number; + readonly total_weight: number; + }; +} + +export interface ProposalResult { + readonly id: number; + readonly title: string; + readonly description: string; + readonly msgs: ReadonlyArray>; + readonly expires: Expiration; + readonly status: string; +} + +export interface ProposalsResult { + readonly proposals: readonly ProposalResult[]; +} + +export interface VoteResult { + readonly vote: Vote; +} + +export interface VotesResult { + readonly votes: ReadonlyArray<{ readonly vote: Vote; readonly voter: string; readonly weight: number }>; +} + +export interface VoterResult { + readonly addr: string; + readonly weight: number; +} + +export interface VotersResult { + readonly voters: readonly VoterResult[]; +} + export class Cw3CosmWasmClient extends SigningCosmWasmClient { private readonly cw3ContractAddress: string; @@ -34,6 +71,68 @@ export class Cw3CosmWasmClient extends SigningCosmWasmClient { this.cw3ContractAddress = cw3ContractAddress; } + public getThreshold(): Promise { + return this.queryContractSmart(this.cw3ContractAddress, { threshold: {} }); + } + + public getProposal(proposalId: number): Promise { + return this.queryContractSmart(this.cw3ContractAddress, { proposal: { proposal_id: proposalId } }); + } + + public listProposals(startAfter?: number, limit?: number): Promise { + return this.queryContractSmart(this.cw3ContractAddress, { + list_proposals: { + start_after: startAfter, + limit: limit, + }, + }); + } + + public reverseProposals(startBefore?: number, limit?: number): Promise { + return this.queryContractSmart(this.cw3ContractAddress, { + reverse_proposals: { + start_before: startBefore, + limit: limit, + }, + }); + } + + public getVote(proposalId: number, voter: string): Promise { + return this.queryContractSmart(this.cw3ContractAddress, { + vote: { + proposal_id: proposalId, + voter: voter, + }, + }); + } + + public listVotes(proposalId: number, startAfter?: string, limit?: number): Promise { + return this.queryContractSmart(this.cw3ContractAddress, { + list_votes: { + proposal_id: proposalId, + start_after: startAfter, + limit: limit, + }, + }); + } + + public getVoter(address: string): Promise { + return this.queryContractSmart(this.cw3ContractAddress, { + voter: { + address: address, + }, + }); + } + + public listVoters(startAfter?: string, limit?: number): Promise { + return this.queryContractSmart(this.cw3ContractAddress, { + list_voters: { + start_after: startAfter, + limit: limit, + }, + }); + } + public createMultisigProposal( title: string, description: string, diff --git a/packages/cosmwasm/types/cw3cosmwasmclient.d.ts b/packages/cosmwasm/types/cw3cosmwasmclient.d.ts index c30b639b..72171efe 100644 --- a/packages/cosmwasm/types/cw3cosmwasmclient.d.ts +++ b/packages/cosmwasm/types/cw3cosmwasmclient.d.ts @@ -13,6 +13,40 @@ export declare enum Vote { Abstain = "abstain", Veto = "veto", } +export interface ThresholdResult { + readonly absolute_count: { + readonly weight_needed: number; + readonly total_weight: number; + }; +} +export interface ProposalResult { + readonly id: number; + readonly title: string; + readonly description: string; + readonly msgs: ReadonlyArray>; + readonly expires: Expiration; + readonly status: string; +} +export interface ProposalsResult { + readonly proposals: readonly ProposalResult[]; +} +export interface VoteResult { + readonly vote: Vote; +} +export interface VotesResult { + readonly votes: ReadonlyArray<{ + readonly vote: Vote; + readonly voter: string; + readonly weight: number; + }>; +} +export interface VoterResult { + readonly addr: string; + readonly weight: number; +} +export interface VotersResult { + readonly voters: readonly VoterResult[]; +} export declare class Cw3CosmWasmClient extends SigningCosmWasmClient { private readonly cw3ContractAddress; constructor( @@ -24,6 +58,14 @@ export declare class Cw3CosmWasmClient extends SigningCosmWasmClient { gasLimits?: Partial>, broadcastMode?: BroadcastMode, ); + getThreshold(): Promise; + getProposal(proposalId: number): Promise; + listProposals(startAfter?: number, limit?: number): Promise; + reverseProposals(startBefore?: number, limit?: number): Promise; + getVote(proposalId: number, voter: string): Promise; + listVotes(proposalId: number, startAfter?: string, limit?: number): Promise; + getVoter(address: string): Promise; + listVoters(startAfter?: string, limit?: number): Promise; createMultisigProposal( title: string, description: string, From f90cf793ba573e4f97977ca979aa7b50d01542e2 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 18 Nov 2020 13:34:21 +0100 Subject: [PATCH 08/11] cosmwasm: Add tests for Cw3CosmWasmClient query methods --- .../cosmwasm/src/cw3cosmwasmclient.spec.ts | 157 +++++++++++++++++- 1 file changed, 155 insertions(+), 2 deletions(-) diff --git a/packages/cosmwasm/src/cw3cosmwasmclient.spec.ts b/packages/cosmwasm/src/cw3cosmwasmclient.spec.ts index d40f0926..4cb35059 100644 --- a/packages/cosmwasm/src/cw3cosmwasmclient.spec.ts +++ b/packages/cosmwasm/src/cw3cosmwasmclient.spec.ts @@ -27,9 +27,48 @@ describe("Cw3CosmWasmClient", () => { }); describe("queries", () => { - it("works", async () => { + const contractAddress = deployedCw3.instances[0]; + const toAddress = makeRandomAddress(); + const msg = { + bank: { + send: { + from_address: contractAddress, + to_address: toAddress, + amount: [ + { + amount: "1", + denom: "ucosm", + }, + ], + }, + }, + }; + let proposalId: number; + let expirationHeight: number; + + beforeAll(async () => { + const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic); + const client = new Cw3CosmWasmClient(launchpad.endpoint, alice.address0, wallet, contractAddress); + const currentHeight = await client.getHeight(); + expirationHeight = currentHeight + 1; + const { logs } = await client.createMultisigProposal( + "My proposal", + "A proposal to propose proposing proposals", + [msg], + undefined, + { at_height: expirationHeight }, + ); + const wasmEvents = logs[0].events.find((event) => event.type === "wasm"); + assert(wasmEvents, "Wasm events not found in logs"); + const proposalIdAttribute = wasmEvents.attributes.find((log) => log.key === "proposal_id"); + assert(proposalIdAttribute, "Proposal ID not found in logs"); + proposalId = parseInt(proposalIdAttribute.value, 10); + }); + + it("getThreshold", async () => { pendingWithoutLaunchpad(); pendingWithoutCw3(); + const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic); const client = new Cw3CosmWasmClient( launchpad.endpoint, @@ -37,9 +76,123 @@ describe("Cw3CosmWasmClient", () => { wallet, deployedCw3.instances[0], ); - const result = await client.queryContractSmart(deployedCw3.instances[0], { threshold: {} }); + const result = await client.getThreshold(); + expect(result).toEqual({ absolute_count: { weight_needed: 1, total_weight: 3 } }); }); + + it("getProposal", async () => { + pendingWithoutLaunchpad(); + pendingWithoutCw3(); + + const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic); + const client = new Cw3CosmWasmClient(launchpad.endpoint, alice.address0, wallet, contractAddress); + const result = await client.getProposal(proposalId); + + expect(result).toEqual({ + id: proposalId, + title: "My proposal", + description: "A proposal to propose proposing proposals", + msgs: [msg], + expires: { at_height: expirationHeight }, + status: "passed", + }); + }); + + it("listProposals", async () => { + pendingWithoutLaunchpad(); + pendingWithoutCw3(); + + const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic); + const client = new Cw3CosmWasmClient(launchpad.endpoint, alice.address0, wallet, contractAddress); + const result = await client.listProposals(proposalId - 1, 1); + + expect(result).toEqual({ + proposals: [ + { + id: proposalId, + title: "My proposal", + description: "A proposal to propose proposing proposals", + msgs: [msg], + expires: { at_height: expirationHeight }, + status: "passed", + }, + ], + }); + }); + + it("reverseProposals", async () => { + pendingWithoutLaunchpad(); + pendingWithoutCw3(); + + const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic); + const client = new Cw3CosmWasmClient(launchpad.endpoint, alice.address0, wallet, contractAddress); + const result = await client.reverseProposals(undefined, 1); + + expect(result).toEqual({ + proposals: [ + { + id: proposalId, + title: "My proposal", + description: "A proposal to propose proposing proposals", + msgs: [msg], + expires: { at_height: expirationHeight }, + status: "passed", + }, + ], + }); + }); + + it("getVote", async () => { + pendingWithoutLaunchpad(); + pendingWithoutCw3(); + + const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic); + const client = new Cw3CosmWasmClient(launchpad.endpoint, alice.address0, wallet, contractAddress); + const result = await client.getVote(proposalId, alice.address0); + + expect(result).toEqual({ vote: Vote.Yes }); + }); + + it("listVotes", async () => { + pendingWithoutLaunchpad(); + pendingWithoutCw3(); + + const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic); + const client = new Cw3CosmWasmClient(launchpad.endpoint, alice.address0, wallet, contractAddress); + const result = await client.listVotes(proposalId); + + expect(result).toEqual({ votes: [{ voter: alice.address0, vote: Vote.Yes, weight: 1 }] }); + }); + + it("getVoter", async () => { + pendingWithoutLaunchpad(); + pendingWithoutCw3(); + + const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic); + const client = new Cw3CosmWasmClient(launchpad.endpoint, alice.address0, wallet, contractAddress); + const result = await client.getVoter(alice.address0); + + expect(result).toEqual({ addr: alice.address0, weight: 1 }); + }); + + it("listVoters", async () => { + pendingWithoutLaunchpad(); + pendingWithoutCw3(); + + const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic); + const client = new Cw3CosmWasmClient(launchpad.endpoint, alice.address0, wallet, contractAddress); + const result = await client.listVoters(); + + expect(result.voters.length).toEqual(3); + expect(result.voters).toEqual( + jasmine.arrayContaining([ + { addr: alice.address0, weight: 1 }, + { addr: alice.address1, weight: 1 }, + { addr: alice.address2, weight: 1 }, + ]), + ); + }); }); describe("Proposal lifecycle", () => { From 2ef2dd891b769a90ac8a7be874114ee20b4d9435 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 18 Nov 2020 13:39:13 +0100 Subject: [PATCH 09/11] cosmwasm: Export Cw3CosmWasmClient etc --- packages/cosmwasm/src/index.ts | 12 ++++++++++++ packages/cosmwasm/types/index.d.ts | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/cosmwasm/src/index.ts b/packages/cosmwasm/src/index.ts index e5eee5a5..7bc9232b 100644 --- a/packages/cosmwasm/src/index.ts +++ b/packages/cosmwasm/src/index.ts @@ -16,6 +16,18 @@ export { SearchTxQuery, SearchTxFilter, } from "./cosmwasmclient"; +export { + Cw3CosmWasmClient, + Expiration, + ProposalResult, + ProposalsResult, + ThresholdResult, + Vote, + VoteResult, + VotesResult, + VoterResult, + VotersResult, +} from "./cw3cosmwasmclient"; export { ExecuteResult, CosmWasmFeeTable, diff --git a/packages/cosmwasm/types/index.d.ts b/packages/cosmwasm/types/index.d.ts index e5eee5a5..7bc9232b 100644 --- a/packages/cosmwasm/types/index.d.ts +++ b/packages/cosmwasm/types/index.d.ts @@ -16,6 +16,18 @@ export { SearchTxQuery, SearchTxFilter, } from "./cosmwasmclient"; +export { + Cw3CosmWasmClient, + Expiration, + ProposalResult, + ProposalsResult, + ThresholdResult, + Vote, + VoteResult, + VotesResult, + VoterResult, + VotersResult, +} from "./cw3cosmwasmclient"; export { ExecuteResult, CosmWasmFeeTable, From 75091a92ed87fd4e7a74ec1d07fc7bf0fb5be7d8 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 18 Nov 2020 13:44:45 +0100 Subject: [PATCH 10/11] cosmwasm: Refactor CW3 proposal tests to use query method --- .../cosmwasm/src/cw3cosmwasmclient.spec.ts | 39 ++++++------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/packages/cosmwasm/src/cw3cosmwasmclient.spec.ts b/packages/cosmwasm/src/cw3cosmwasmclient.spec.ts index 4cb35059..f9e7035f 100644 --- a/packages/cosmwasm/src/cw3cosmwasmclient.spec.ts +++ b/packages/cosmwasm/src/cw3cosmwasmclient.spec.ts @@ -217,17 +217,9 @@ describe("Cw3CosmWasmClient", () => { }, }, }; - const { logs } = await client.createMultisigProposal( - "My proposal", - "A proposal to propose proposing proposals", - [msg], - ); - const wasmEvents = logs[0].events.find((event) => event.type === "wasm"); - assert(wasmEvents, "Wasm events not found in logs"); - const proposalIdAttribute = wasmEvents.attributes.find((log) => log.key === "proposal_id"); - assert(proposalIdAttribute, "Proposal ID not found in logs"); - const proposalId = parseInt(proposalIdAttribute.value, 10); - + await client.createMultisigProposal("My proposal", "A proposal to propose proposing proposals", [msg]); + const { proposals } = await client.reverseProposals(undefined, 1); + const proposalId = proposals[0].id; const executeResult = await client.executeMultisigProposal(proposalId); expect(executeResult).toBeTruthy(); }); @@ -260,17 +252,11 @@ describe("Cw3CosmWasmClient", () => { }, }, }; - const { logs } = await proposer.createMultisigProposal( - "My proposal", - "A proposal to propose proposing proposals", - [msg], - ); - const wasmEvents = logs[0].events.find((event) => event.type === "wasm"); - assert(wasmEvents, "Wasm events not found in logs"); - const proposalIdAttribute = wasmEvents.attributes.find((log) => log.key === "proposal_id"); - assert(proposalIdAttribute, "Proposal ID not found in logs"); - const proposalId = parseInt(proposalIdAttribute.value, 10); - + await proposer.createMultisigProposal("My proposal", "A proposal to propose proposing proposals", [ + msg, + ]); + const { proposals } = await voter.reverseProposals(undefined, 1); + const proposalId = proposals[0].id; const voteResult = await voter.voteMultisigProposal(proposalId, Vote.Yes); expect(voteResult).toBeTruthy(); @@ -309,7 +295,7 @@ describe("Cw3CosmWasmClient", () => { }, }; const currentHeight = await proposer.getHeight(); - const { logs } = await proposer.createMultisigProposal( + await proposer.createMultisigProposal( "My proposal", "A proposal to propose proposing proposals", [msg], @@ -320,11 +306,8 @@ describe("Cw3CosmWasmClient", () => { at_height: currentHeight + 5, }, ); - const wasmEvents = logs[0].events.find((event) => event.type === "wasm"); - assert(wasmEvents, "Wasm events not found in logs"); - const proposalIdAttribute = wasmEvents.attributes.find((log) => log.key === "proposal_id"); - assert(proposalIdAttribute, "Proposal ID not found in logs"); - const proposalId = parseInt(proposalIdAttribute.value, 10); + const { proposals } = await voter1.reverseProposals(undefined, 1); + const proposalId = proposals[0].id; const vote1Result = await voter1.voteMultisigProposal(proposalId, Vote.Abstain); expect(vote1Result).toBeTruthy(); From 246da8f4d79d8ced403660df88b714c6584098d3 Mon Sep 17 00:00:00 2001 From: willclarktech Date: Wed, 18 Nov 2020 13:55:22 +0100 Subject: [PATCH 11/11] cosmwasm: Refactor CW3 query pagination options --- .../cosmwasm/src/cw3cosmwasmclient.spec.ts | 10 +++---- packages/cosmwasm/src/cw3cosmwasmclient.ts | 30 ++++++++++++++++--- .../cosmwasm/types/cw3cosmwasmclient.d.ts | 24 ++++++++++++--- 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/packages/cosmwasm/src/cw3cosmwasmclient.spec.ts b/packages/cosmwasm/src/cw3cosmwasmclient.spec.ts index f9e7035f..3b2888f5 100644 --- a/packages/cosmwasm/src/cw3cosmwasmclient.spec.ts +++ b/packages/cosmwasm/src/cw3cosmwasmclient.spec.ts @@ -105,7 +105,7 @@ describe("Cw3CosmWasmClient", () => { const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic); const client = new Cw3CosmWasmClient(launchpad.endpoint, alice.address0, wallet, contractAddress); - const result = await client.listProposals(proposalId - 1, 1); + const result = await client.listProposals({ startAfter: proposalId - 1, limit: 1 }); expect(result).toEqual({ proposals: [ @@ -127,7 +127,7 @@ describe("Cw3CosmWasmClient", () => { const wallet = await Secp256k1HdWallet.fromMnemonic(alice.mnemonic); const client = new Cw3CosmWasmClient(launchpad.endpoint, alice.address0, wallet, contractAddress); - const result = await client.reverseProposals(undefined, 1); + const result = await client.reverseProposals({ limit: 1 }); expect(result).toEqual({ proposals: [ @@ -218,7 +218,7 @@ describe("Cw3CosmWasmClient", () => { }, }; await client.createMultisigProposal("My proposal", "A proposal to propose proposing proposals", [msg]); - const { proposals } = await client.reverseProposals(undefined, 1); + const { proposals } = await client.reverseProposals({ limit: 1 }); const proposalId = proposals[0].id; const executeResult = await client.executeMultisigProposal(proposalId); expect(executeResult).toBeTruthy(); @@ -255,7 +255,7 @@ describe("Cw3CosmWasmClient", () => { await proposer.createMultisigProposal("My proposal", "A proposal to propose proposing proposals", [ msg, ]); - const { proposals } = await voter.reverseProposals(undefined, 1); + const { proposals } = await voter.reverseProposals({ limit: 1 }); const proposalId = proposals[0].id; const voteResult = await voter.voteMultisigProposal(proposalId, Vote.Yes); expect(voteResult).toBeTruthy(); @@ -306,7 +306,7 @@ describe("Cw3CosmWasmClient", () => { at_height: currentHeight + 5, }, ); - const { proposals } = await voter1.reverseProposals(undefined, 1); + const { proposals } = await voter1.reverseProposals({ limit: 1 }); const proposalId = proposals[0].id; const vote1Result = await voter1.voteMultisigProposal(proposalId, Vote.Abstain); diff --git a/packages/cosmwasm/src/cw3cosmwasmclient.ts b/packages/cosmwasm/src/cw3cosmwasmclient.ts index f2344a3e..bfde5a36 100644 --- a/packages/cosmwasm/src/cw3cosmwasmclient.ts +++ b/packages/cosmwasm/src/cw3cosmwasmclient.ts @@ -55,6 +55,21 @@ export interface VotersResult { readonly voters: readonly VoterResult[]; } +interface StartBeforeNumberPaginationOptions { + readonly startBefore?: number; + readonly limit?: number; +} + +interface StartAfterNumberPaginationOptions { + readonly startAfter?: number; + readonly limit?: number; +} + +interface StartAfterStringPaginationOptions { + readonly startAfter?: string; + readonly limit?: number; +} + export class Cw3CosmWasmClient extends SigningCosmWasmClient { private readonly cw3ContractAddress: string; @@ -79,7 +94,9 @@ export class Cw3CosmWasmClient extends SigningCosmWasmClient { return this.queryContractSmart(this.cw3ContractAddress, { proposal: { proposal_id: proposalId } }); } - public listProposals(startAfter?: number, limit?: number): Promise { + public listProposals({ startAfter, limit }: StartAfterNumberPaginationOptions = {}): Promise< + ProposalsResult + > { return this.queryContractSmart(this.cw3ContractAddress, { list_proposals: { start_after: startAfter, @@ -88,7 +105,9 @@ export class Cw3CosmWasmClient extends SigningCosmWasmClient { }); } - public reverseProposals(startBefore?: number, limit?: number): Promise { + public reverseProposals({ startBefore, limit }: StartBeforeNumberPaginationOptions = {}): Promise< + ProposalsResult + > { return this.queryContractSmart(this.cw3ContractAddress, { reverse_proposals: { start_before: startBefore, @@ -106,7 +125,10 @@ export class Cw3CosmWasmClient extends SigningCosmWasmClient { }); } - public listVotes(proposalId: number, startAfter?: string, limit?: number): Promise { + public listVotes( + proposalId: number, + { startAfter, limit }: StartAfterStringPaginationOptions = {}, + ): Promise { return this.queryContractSmart(this.cw3ContractAddress, { list_votes: { proposal_id: proposalId, @@ -124,7 +146,7 @@ export class Cw3CosmWasmClient extends SigningCosmWasmClient { }); } - public listVoters(startAfter?: string, limit?: number): Promise { + public listVoters({ startAfter, limit }: StartAfterStringPaginationOptions = {}): Promise { return this.queryContractSmart(this.cw3ContractAddress, { list_voters: { start_after: startAfter, diff --git a/packages/cosmwasm/types/cw3cosmwasmclient.d.ts b/packages/cosmwasm/types/cw3cosmwasmclient.d.ts index 72171efe..529f34f8 100644 --- a/packages/cosmwasm/types/cw3cosmwasmclient.d.ts +++ b/packages/cosmwasm/types/cw3cosmwasmclient.d.ts @@ -47,6 +47,18 @@ export interface VoterResult { export interface VotersResult { readonly voters: readonly VoterResult[]; } +interface StartBeforeNumberPaginationOptions { + readonly startBefore?: number; + readonly limit?: number; +} +interface StartAfterNumberPaginationOptions { + readonly startAfter?: number; + readonly limit?: number; +} +interface StartAfterStringPaginationOptions { + readonly startAfter?: string; + readonly limit?: number; +} export declare class Cw3CosmWasmClient extends SigningCosmWasmClient { private readonly cw3ContractAddress; constructor( @@ -60,12 +72,15 @@ export declare class Cw3CosmWasmClient extends SigningCosmWasmClient { ); getThreshold(): Promise; getProposal(proposalId: number): Promise; - listProposals(startAfter?: number, limit?: number): Promise; - reverseProposals(startBefore?: number, limit?: number): Promise; + listProposals({ startAfter, limit }?: StartAfterNumberPaginationOptions): Promise; + reverseProposals({ startBefore, limit }?: StartBeforeNumberPaginationOptions): Promise; getVote(proposalId: number, voter: string): Promise; - listVotes(proposalId: number, startAfter?: string, limit?: number): Promise; + listVotes( + proposalId: number, + { startAfter, limit }?: StartAfterStringPaginationOptions, + ): Promise; getVoter(address: string): Promise; - listVoters(startAfter?: string, limit?: number): Promise; + listVoters({ startAfter, limit }?: StartAfterStringPaginationOptions): Promise; createMultisigProposal( title: string, description: string, @@ -78,3 +93,4 @@ export declare class Cw3CosmWasmClient extends SigningCosmWasmClient { executeMultisigProposal(proposalId: number, memo?: string): Promise; closeMultisigProposal(proposalId: number, memo?: string): Promise; } +export {};