From e3e6932e6b2a4689ec98517d7a3652c8ee8a35db Mon Sep 17 00:00:00 2001 From: RodrigoAD <15104916+RodrigoAD@users.noreply.github.com> Date: Wed, 16 Feb 2022 14:31:05 +0100 Subject: [PATCH] multisig command improvements --- .../src/commands/multisig.ts | 134 +++++++++++++----- .../src/lib/utils.ts | 13 ++ 2 files changed, 111 insertions(+), 36 deletions(-) create mode 100644 packages-ts/gauntlet-terra-cw20-multisig/src/lib/utils.ts diff --git a/packages-ts/gauntlet-terra-cw20-multisig/src/commands/multisig.ts b/packages-ts/gauntlet-terra-cw20-multisig/src/commands/multisig.ts index b8849254d..59847785b 100644 --- a/packages-ts/gauntlet-terra-cw20-multisig/src/commands/multisig.ts +++ b/packages-ts/gauntlet-terra-cw20-multisig/src/commands/multisig.ts @@ -2,6 +2,7 @@ import { Result } from '@chainlink/gauntlet-core' import { logger, prompt } from '@chainlink/gauntlet-core/dist/utils' import { TerraCommand, TransactionResponse } from '@chainlink/gauntlet-terra' import { AccAddress, MsgExecuteContract } from '@terra-money/terra.js' +import { isDeepEqual } from '../lib/utils' type ProposalAction = ( signer: AccAddress, @@ -17,13 +18,15 @@ enum Vote { } type WasmMsg = { - execute: { - contract_addr: string - funds: { - denom: string - amount: string - }[] - msg: string + wasm: { + execute: { + contract_addr: string + funds: { + denom: string + amount: string + }[] + msg: string + } } } @@ -31,11 +34,15 @@ enum Action { CREATE = 'create', APPROVE = 'approve', EXECUTE = 'execute', + NONE = 'none', } type State = { threshold: number - proposalStatus: Action + nextAction: Action + // https://github.com/CosmWasm/cw-plus/blob/82138f9484e538913f7faf78bc292fb14407aae8/packages/cw3/src/query.rs#L75 + currentStatus?: 'pending' | 'open' | 'rejected' | 'passed' | 'executed' + data?: WasmMsg[] } export const wrapCommand = (command) => { @@ -54,7 +61,7 @@ export const wrapCommand = (command) => { this.multisig = process.env.MULTISIG_ADDRESS as AccAddress } - makeRawTransaction = async (signer: AccAddress, status?: Action) => { + makeRawTransaction = async (signer: AccAddress, state?: State) => { const message = await this.command.makeRawTransaction(this.multisig) const operations = { @@ -63,33 +70,41 @@ export const wrapCommand = (command) => { [Action.EXECUTE]: this.executeExecution, } - return operations[status](signer, Number(this.flags.proposal), message) + if (state.nextAction !== Action.CREATE) { + this.require( + await this.isSameProposal(state.data, [this.toWasmMsg(message)]), + 'The transaction generated is different from the proposal provided', + ) + } + + return operations[state.nextAction](signer, Number(this.flags.proposal), message) + } + + isSameProposal = (proposalMsgs: WasmMsg[], generatedMsgs: WasmMsg[]) => { + return isDeepEqual(proposalMsgs, generatedMsgs) } toWasmMsg = (message: MsgExecuteContract): WasmMsg => { return { - execute: { - contract_addr: message.contract, - funds: message.coins.toArray().map((c) => c.toData()), - msg: Buffer.from(message.toJSON()).toString('base64'), + wasm: { + execute: { + contract_addr: message.contract, + funds: message.coins.toArray().map((c) => c.toData()), + msg: Buffer.from(JSON.stringify(message.execute_msg)).toString('base64'), + }, }, } } - executePropose: ProposalAction = async (signer, proposalId, message) => { + executePropose: ProposalAction = async (signer, _, message) => { logger.info('Generating data for creating new proposal') const proposeInput = { propose: { description: command.id, - msgs: [ - { - wasm: this.toWasmMsg(message), - }, - ], + msgs: [this.toWasmMsg(message)], title: command.id, - // latest: { - // never: {}, - // }, + // TODO: Set expiration time + // latest: { never: {} } }, } return new MsgExecuteContract(signer, this.multisig, proposeInput) @@ -117,36 +132,72 @@ export const wrapCommand = (command) => { } fetchState = async (proposalId?: number): Promise => { - const threshold = await this.query(this.multisig, { + const thresholdState = await this.query(this.multisig, { threshold: {}, }) - if (!proposalId) + const threshold = thresholdState.absolute_count.total_weight + if (!proposalId) { return { threshold, - proposalStatus: Action.CREATE, + nextAction: Action.CREATE, } + } + const proposalState = await this.query(this.multisig, { proposal: { proposal_id: proposalId, }, }) - console.log(proposalState) + // TODO: Add owners + + const status = proposalState.status + const toNextAction = { + passed: Action.EXECUTE, + open: Action.APPROVE, + pending: Action.APPROVE, + rejected: Action.NONE, + executed: Action.NONE, + } return { threshold, - proposalStatus: Action.APPROVE, + nextAction: toNextAction[status], + currentStatus: status, + data: proposalState.msgs, } } + printPostInstructions = async (proposalId: number) => { + const state = await this.fetchState(proposalId) + // TODO: Calculate approvals left + const approvalsLeft = state.threshold - 1 + const messages = { + [Action.APPROVE]: `The proposal needs ${approvalsLeft} more approvals. Run the same command with the flag --proposal=${proposalId}`, + [Action.EXECUTE]: `The proposal reached the threshold and can be executed. Run the same command with the flag --proposal=${proposalId}`, + [Action.NONE]: `The proposal has been executed. No more actions needed`, + } + logger.line() + logger.info(`Next Actions: + ${messages[state.nextAction]} + `) + logger.line() + } + execute = async () => { let proposalId = !!this.flags.proposal && Number(this.flags.proposal) const state = await this.fetchState(proposalId) - const rawTx = await this.makeRawTransaction(this.wallet.key.accAddress, state.proposalStatus) + + if (state.nextAction === Action.NONE) { + logger.info('The proposal has already been executed/rejected') + return + } + const rawTx = await this.makeRawTransaction(this.wallet.key.accAddress, state) console.info(` Proposal State: - Threshold: ${state.threshold} - - Status: ${state.proposalStatus} + - Next Action: ${state.nextAction.toUpperCase()} + - Owners: TODO `) const actionMessage = { @@ -154,17 +205,28 @@ export const wrapCommand = (command) => { [Action.APPROVE]: 'APPROVING', [Action.EXECUTE]: 'EXECUTING', } - await prompt(`Continue ${actionMessage[state.proposalStatus]} proposal?`) + await prompt(`Continue ${actionMessage[state.nextAction]} proposal?`) const tx = await this.signAndSend([rawTx]) - if (state.proposalStatus === Action.CREATE) { - // get proposal ID from logs + if (state.nextAction === Action.CREATE) { + const proposalFromEvent = tx.events[0].wasm.proposal_id[0] + logger.success(`New proposal created with ID: ${proposalFromEvent}`) + proposalId = Number(proposalFromEvent) } - // If ID Proposal is provided, check the proposal status, and either approve or execute. - // If ID Proposal is not provided, create a new proposal + await this.printPostInstructions(proposalId) - return {} as Result + return { + responses: [ + { + tx, + contract: this.multisig, + }, + ], + data: { + proposalId, + }, + } as Result } } } diff --git a/packages-ts/gauntlet-terra-cw20-multisig/src/lib/utils.ts b/packages-ts/gauntlet-terra-cw20-multisig/src/lib/utils.ts new file mode 100644 index 000000000..074010855 --- /dev/null +++ b/packages-ts/gauntlet-terra-cw20-multisig/src/lib/utils.ts @@ -0,0 +1,13 @@ +import assert from 'assert' + +export const isDeepEqual = (a: any, b: any) => { + try { + assert.deepStrictEqual(a, b) + } catch (error) { + if (error.name === 'AssertionError') { + return false + } + throw error + } + return true +}