diff --git a/packages-ts/gauntlet-terra-contracts/src/commands/abstract/executionWrapper.ts b/packages-ts/gauntlet-terra-contracts/src/commands/abstract/executionWrapper.ts index edbd6ac22..00766832b 100644 --- a/packages-ts/gauntlet-terra-contracts/src/commands/abstract/executionWrapper.ts +++ b/packages-ts/gauntlet-terra-contracts/src/commands/abstract/executionWrapper.ts @@ -1,7 +1,7 @@ import AbstractCommand, { makeAbstractCommand } from '.' import { Result } from '@chainlink/gauntlet-core' import { TerraCommand, TransactionResponse } from '@chainlink/gauntlet-terra' -import { MsgExecuteContract } from '@terra-money/terra.js' +import { AccAddress, MsgExecuteContract } from '@terra-money/terra.js' export interface AbstractInstruction { instruction: { @@ -38,9 +38,9 @@ export const instructionToCommand = (instruction: AbstractInstruction) return abstractCommand } - makeRawTransaction = async (): Promise => { + makeRawTransaction = async (signer: AccAddress): Promise => { const command = await this.buildCommand() - return command.makeRawTransaction() + return command.makeRawTransaction(signer) } execute = async (): Promise> => { diff --git a/packages-ts/gauntlet-terra-contracts/src/commands/abstract/index.ts b/packages-ts/gauntlet-terra-contracts/src/commands/abstract/index.ts index 810fb0168..f04616ab3 100644 --- a/packages-ts/gauntlet-terra-contracts/src/commands/abstract/index.ts +++ b/packages-ts/gauntlet-terra-contracts/src/commands/abstract/index.ts @@ -1,5 +1,5 @@ import { Result } from '@chainlink/gauntlet-core' -import { MsgExecuteContract } from '@terra-money/terra.js' +import { AccAddress, MsgExecuteContract } from '@terra-money/terra.js' import { logger, prompt } from '@chainlink/gauntlet-core/dist/utils' import { TransactionResponse, TerraCommand } from '@chainlink/gauntlet-terra' import { Contract, CONTRACT_LIST, getContract, TerraABI, TERRA_OPERATIONS } from '../../lib/contracts' @@ -126,9 +126,9 @@ export default class AbstractCommand extends TerraCommand { this.contracts = [this.opts.contract.id] } - makeRawTransaction = async (): Promise => { + makeRawTransaction = async (signer: AccAddress): Promise => { const address = this.args[0] - return new MsgExecuteContract(this.wallet.key.accAddress, address, this.params) + return new MsgExecuteContract(signer, address, this.params) } abstractDeploy: AbstractExecute = async (params: any) => { 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 ff9690b15..b8849254d 100644 --- a/packages-ts/gauntlet-terra-cw20-multisig/src/commands/multisig.ts +++ b/packages-ts/gauntlet-terra-cw20-multisig/src/commands/multisig.ts @@ -1,11 +1,47 @@ import { Result } from '@chainlink/gauntlet-core' -import { logger } from '@chainlink/gauntlet-core/dist/utils' +import { logger, prompt } from '@chainlink/gauntlet-core/dist/utils' import { TerraCommand, TransactionResponse } from '@chainlink/gauntlet-terra' -import { MsgExecuteContract } from '@terra-money/terra.js' +import { AccAddress, MsgExecuteContract } from '@terra-money/terra.js' + +type ProposalAction = ( + signer: AccAddress, + proposalId: number, + message: MsgExecuteContract, +) => Promise + +enum Vote { + YES = 'yes', + NO = 'no', + ABS = 'abstain', + VETO = 'veto', +} + +type WasmMsg = { + execute: { + contract_addr: string + funds: { + denom: string + amount: string + }[] + msg: string + } +} + +enum Action { + CREATE = 'create', + APPROVE = 'approve', + EXECUTE = 'execute', +} + +type State = { + threshold: number + proposalStatus: Action +} export const wrapCommand = (command) => { return class Multisig extends TerraCommand { command: TerraCommand + multisig: AccAddress static id = `${command.id}:multisig` @@ -13,18 +49,120 @@ export const wrapCommand = (command) => { super(flags, args) this.command = new command(flags, args) + + if (!AccAddress.validate(process.env.MULTISIG_ADDRESS)) throw new Error(`Invalid Multisig wallet address`) + this.multisig = process.env.MULTISIG_ADDRESS as AccAddress + } + + makeRawTransaction = async (signer: AccAddress, status?: Action) => { + const message = await this.command.makeRawTransaction(this.multisig) + + const operations = { + [Action.CREATE]: this.executePropose, + [Action.APPROVE]: this.executeApproval, + [Action.EXECUTE]: this.executeExecution, + } + + return operations[status](signer, Number(this.flags.proposal), message) + } + + 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'), + }, + } + } + + executePropose: ProposalAction = async (signer, proposalId, message) => { + logger.info('Generating data for creating new proposal') + const proposeInput = { + propose: { + description: command.id, + msgs: [ + { + wasm: this.toWasmMsg(message), + }, + ], + title: command.id, + // latest: { + // never: {}, + // }, + }, + } + return new MsgExecuteContract(signer, this.multisig, proposeInput) + } + + executeApproval: ProposalAction = async (signer, proposalId) => { + logger.info(`Generating data for approving proposal ${proposalId}`) + const approvalInput = { + vote: { + vote: Vote.YES, + proposal_id: proposalId, + }, + } + return new MsgExecuteContract(signer, this.multisig, approvalInput) } - makeRawTransaction = async () => { - // TODO: Replace with Mulstig tx message - return {} as MsgExecuteContract + executeExecution: ProposalAction = async (signer, proposalId) => { + logger.info(`Generating data for executing proposal ${proposalId}`) + const executeInput = { + execute: { + proposal_id: proposalId, + }, + } + return new MsgExecuteContract(signer, this.multisig, executeInput) + } + + fetchState = async (proposalId?: number): Promise => { + const threshold = await this.query(this.multisig, { + threshold: {}, + }) + if (!proposalId) + return { + threshold, + proposalStatus: Action.CREATE, + } + const proposalState = await this.query(this.multisig, { + proposal: { + proposal_id: proposalId, + }, + }) + console.log(proposalState) + + return { + threshold, + proposalStatus: Action.APPROVE, + } } 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) + + console.info(` + Proposal State: + - Threshold: ${state.threshold} + - Status: ${state.proposalStatus} + `) + + const actionMessage = { + [Action.CREATE]: 'CREATING', + [Action.APPROVE]: 'APPROVING', + [Action.EXECUTE]: 'EXECUTING', + } + await prompt(`Continue ${actionMessage[state.proposalStatus]} proposal?`) + const tx = await this.signAndSend([rawTx]) + + if (state.proposalStatus === Action.CREATE) { + // get proposal ID from logs + } + // If ID Proposal is provided, check the proposal status, and either approve or execute. // If ID Proposal is not provided, create a new proposal - const message = await this.command.makeRawTransaction() - logger.log('Command data:', message) return {} as Result } diff --git a/packages-ts/gauntlet-terra/src/commands/internal/terra.ts b/packages-ts/gauntlet-terra/src/commands/internal/terra.ts index a00f9162d..cbc5eb673 100644 --- a/packages-ts/gauntlet-terra/src/commands/internal/terra.ts +++ b/packages-ts/gauntlet-terra/src/commands/internal/terra.ts @@ -1,6 +1,6 @@ import { Result, WriteCommand } from '@chainlink/gauntlet-core' import { logger } from '@chainlink/gauntlet-core/dist/utils' -import { EventsByType, MsgStoreCode, TxLog } from '@terra-money/terra.js' +import { EventsByType, MsgStoreCode, AccAddress, TxLog } from '@terra-money/terra.js' import { SignMode } from '@terra-money/terra.proto/cosmos/tx/signing/v1beta1/signing' import { withProvider, withWallet, withCodeIds, withNetwork } from '../middlewares' @@ -23,7 +23,7 @@ export default abstract class TerraCommand extends WriteCommand Promise> - abstract makeRawTransaction: () => Promise + abstract makeRawTransaction: (signer: AccAddress) => Promise constructor(flags, args) { super(flags, args) @@ -64,6 +64,25 @@ export default abstract class TerraCommand extends WriteCommand => { + try { + const tx = await this.wallet.createAndSignTx({ + msgs: messages, + ...(this.wallet.key instanceof LedgerKey && { + signMode: SignMode.SIGN_MODE_LEGACY_AMINO_JSON, + }), + }) + + const res = await this.provider.tx.broadcast(tx) + + logger.debug(res) + return this.wrapResponse(res) + } catch (e) { + const details = e.response.data + throw new Error(details.message) + } + } + async call(address, input) { const msg = new MsgExecuteContract(this.wallet.key.accAddress, address, input)