From 07439d2d3c8f87287095f54a0585c00618ea0cfd Mon Sep 17 00:00:00 2001 From: RodrigoAD <15104916+RodrigoAD@users.noreply.github.com> Date: Mon, 28 Feb 2022 15:00:43 +0100 Subject: [PATCH] transfer token and send uluna commands --- .../src/commands/abstract/executionWrapper.ts | 53 +++++++++--- .../src/commands/abstract/index.ts | 6 +- .../src/commands/contracts/link/transfer.ts | 83 +++++++++++-------- .../src/commands/index.ts | 2 + .../src/commands/wallet/index.ts | 3 + .../src/commands/wallet/send.ts | 60 ++++++++++++++ .../src/lib/constants.ts | 3 + .../src/commands/multisig.ts | 30 +++++-- .../gauntlet-terra-cw-plus/src/lib/types.ts | 19 ++++- .../src/commands/internal/terra.ts | 7 +- 10 files changed, 200 insertions(+), 66 deletions(-) create mode 100644 packages-ts/gauntlet-terra-contracts/src/commands/wallet/index.ts create mode 100644 packages-ts/gauntlet-terra-contracts/src/commands/wallet/send.ts 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 9919fe9c2..469506b86 100644 --- a/packages-ts/gauntlet-terra-contracts/src/commands/abstract/executionWrapper.ts +++ b/packages-ts/gauntlet-terra-contracts/src/commands/abstract/executionWrapper.ts @@ -1,8 +1,15 @@ -import AbstractCommand, { makeAbstractCommand } from '.' +import { makeAbstractCommand } from '.' import { Result } from '@chainlink/gauntlet-core' import { TerraCommand, TransactionResponse } from '@chainlink/gauntlet-terra' -import { AccAddress, MsgExecuteContract } from '@terra-money/terra.js' +import { AccAddress } from '@terra-money/terra.js' +import { logger, prompt } from '@chainlink/gauntlet-core/dist/utils' +export type BeforeExecutionContext = { + input: any + contractInput: any + commandId: string + contract: string +} export interface AbstractInstruction { examples?: string[] instruction: { @@ -13,10 +20,21 @@ export interface AbstractInstruction { makeInput: (flags: any, args: string[]) => Promise validateInput: (input: Input) => boolean makeContractInput: (input: Input) => Promise + beforeExecute?: (context: BeforeExecutionContext) => Promise afterExecute?: (response: Result) => any } -export const instructionToCommand = (instruction: AbstractInstruction) => { +export const defaultAfterExecute = async (response: Result): Promise => { + logger.success(`Execution finished at transaction: ${response.responses[0].tx.hash}`) +} + +export const defaultBeforeExecute = async (context: BeforeExecutionContext) => { + logger.loading(`Executing ${context.commandId} from contract ${context.contract}`) + logger.log('Input Params:', context.contractInput) + await prompt(`Continue?`) +} + +export const instructionToCommand = (instruction: AbstractInstruction) => { const id = `${instruction.instruction.contract}:${instruction.instruction.function}` const category = `${instruction.instruction.category}` const examples = instruction.examples || [] @@ -24,35 +42,44 @@ export const instructionToCommand = (instruction: AbstractInstruction) static id = id static category = category static examples = examples - command: AbstractCommand + + input: Input + contractInput: ContractInput constructor(flags, args) { super(flags, args) } - afterExecute = instruction.afterExecute + beforeExecute = instruction.beforeExecute || defaultBeforeExecute + afterExecute = instruction.afterExecute || defaultAfterExecute buildCommand = async (): Promise => { - const commandInput = await instruction.makeInput(this.flags, this.args) - if (!instruction.validateInput(commandInput)) { - throw new Error(`Invalid input params: ${JSON.stringify(commandInput)}`) + this.input = await instruction.makeInput(this.flags, this.args) + if (!instruction.validateInput(this.input)) { + throw new Error(`Invalid input params: ${JSON.stringify(this.input)}`) } - const input = await instruction.makeContractInput(commandInput) - const abstractCommand = await makeAbstractCommand(id, this.flags, this.args, input) + this.contractInput = await instruction.makeContractInput(this.input) + const abstractCommand = await makeAbstractCommand(id, this.flags, this.args, this.contractInput) await abstractCommand.invokeMiddlewares(abstractCommand, abstractCommand.middlewares) return abstractCommand } - makeRawTransaction = async (signer: AccAddress): Promise => { + makeRawTransaction = async (signer: AccAddress) => { const command = await this.buildCommand() return command.makeRawTransaction(signer) } execute = async (): Promise> => { const command = await this.buildCommand() + await this.beforeExecute({ + input: this.input, + contractInput: this.contractInput, + commandId: id, + contract: this.args[0], + }) let response = await command.execute() - if (this.afterExecute) { - const data = this.afterExecute(response) + const data = this.afterExecute(response) + if (data) { response = { ...response, data: { ...data } } } return response 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 bbf268635..b8233a766 100644 --- a/packages-ts/gauntlet-terra-contracts/src/commands/abstract/index.ts +++ b/packages-ts/gauntlet-terra-contracts/src/commands/abstract/index.ts @@ -153,11 +153,9 @@ export default class AbstractCommand extends TerraCommand { } abstractExecute: AbstractExecute = async (params: any, address: string) => { - logger.loading(`Executing ${this.opts.function} from contract ${this.opts.contract.id} at ${address}`) - logger.log('Input Params:', params) - await prompt(`Continue?`) + logger.debug(`Executing ${this.opts.function} from contract ${this.opts.contract.id} at ${address}`) const tx = await this.call(address, params) - logger.success(`Execution finished at tx ${tx.hash}`) + logger.debug(`Execution finished at tx ${tx.hash}`) return { responses: [ { diff --git a/packages-ts/gauntlet-terra-contracts/src/commands/contracts/link/transfer.ts b/packages-ts/gauntlet-terra-contracts/src/commands/contracts/link/transfer.ts index 85f23e6df..39a47e926 100644 --- a/packages-ts/gauntlet-terra-contracts/src/commands/contracts/link/transfer.ts +++ b/packages-ts/gauntlet-terra-contracts/src/commands/contracts/link/transfer.ts @@ -1,46 +1,57 @@ -import { TerraCommand, TransactionResponse } from '@chainlink/gauntlet-terra' -import { Result } from '@chainlink/gauntlet-core' import { BN, logger, prompt } from '@chainlink/gauntlet-core/dist/utils' import { CATEGORIES, TOKEN_DECIMALS } from '../../../lib/constants' +import { AbstractInstruction, BeforeExecutionContext, instructionToCommand } from '../../abstract/executionWrapper' +import { AccAddress } from '@terra-money/terra.js' -export default class TransferLink extends TerraCommand { - static description = 'Transfer LINK' - static examples = [ - `yarn gauntlet token:transfer --network=bombay-testnet --to=[RECEIVER] --amount=[AMOUNT_IN_TOKEN_UNITS]`, - `yarn gauntlet token:transfer --network=bombay-testnet --to=[RECEIVER] --amount=[AMOUNT_IN_TOKEN_UNITS] --link=[TOKEN_ADDRESS] --decimals=[TOKEN_DECIMALS]`, - ] +type CommandInput = { + to: string + // Units in LINK + amount: string +} - static id = 'token:transfer' - static category = CATEGORIES.LINK +type ContractInput = { + recipient: string + amount: string +} - constructor(flags, args: string[]) { - super(flags, args) +const makeCommandInput = async (flags: any): Promise => { + if (flags.input) return flags.input as CommandInput + return { + to: flags.to, + amount: flags.amount, } +} - makeRawTransaction = async () => { - throw new Error('Transfer LINK command: makeRawTransaction method not implemented') - } +const validateInput = (input: CommandInput): boolean => { + if (!AccAddress.validate(input.to)) throw new Error(`Invalid destination address`) + return true +} - execute = async () => { - const decimals = this.flags.decimals || TOKEN_DECIMALS - const link = this.flags.link || process.env.LINK - const amount = new BN(this.flags.amount).mul(new BN(10).pow(new BN(decimals))) - - await prompt(`Sending ${this.flags.amount} LINK (${amount.toString()}) to ${this.flags.to}. Continue?`) - const tx = await this.call(link, { - transfer: { - recipient: this.flags.to, - amount: amount.toString(), - }, - }) - logger.success(`LINK transferred successfully to ${this.flags.to} (txhash: ${tx.hash})`) - return { - responses: [ - { - tx, - contract: link, - }, - ], - } as Result +const makeContractInput = async (input: CommandInput): Promise => { + const amount = new BN(input.amount).mul(new BN(10).pow(new BN(TOKEN_DECIMALS))) + return { + recipient: input.to, + amount: amount.toString(), } } + +const beforeExecute = async (context: BeforeExecutionContext): Promise => { + logger.info( + `Transferring ${context.contractInput.amount} (${context.input.amount}) Tokens to ${context.contractInput.recipient}`, + ) + await prompt('Continue?') +} + +const transferToken: AbstractInstruction = { + instruction: { + category: CATEGORIES.LINK, + contract: 'cw20_base', + function: 'transfer', + }, + makeInput: makeCommandInput, + validateInput: validateInput, + makeContractInput: makeContractInput, + beforeExecute, +} + +export default instructionToCommand(transferToken) diff --git a/packages-ts/gauntlet-terra-contracts/src/commands/index.ts b/packages-ts/gauntlet-terra-contracts/src/commands/index.ts index eea44285c..30f7e77fa 100644 --- a/packages-ts/gauntlet-terra-contracts/src/commands/index.ts +++ b/packages-ts/gauntlet-terra-contracts/src/commands/index.ts @@ -8,6 +8,7 @@ import Proxy_OCR2 from './contracts/proxy_ocr2' import DeviationFlaggingValidator from './contracts/deviation_flagging_validator' import Multisig from './contracts/multisig' import CW4_GROUP from './contracts/cw4_group' +import Wallet from './wallet' export default [ Upload, @@ -20,4 +21,5 @@ export default [ ...Proxy_OCR2, ...Multisig, ...CW4_GROUP, + ...Wallet, ] diff --git a/packages-ts/gauntlet-terra-contracts/src/commands/wallet/index.ts b/packages-ts/gauntlet-terra-contracts/src/commands/wallet/index.ts new file mode 100644 index 000000000..7400c704a --- /dev/null +++ b/packages-ts/gauntlet-terra-contracts/src/commands/wallet/index.ts @@ -0,0 +1,3 @@ +import Send from './send' + +export default [Send] diff --git a/packages-ts/gauntlet-terra-contracts/src/commands/wallet/send.ts b/packages-ts/gauntlet-terra-contracts/src/commands/wallet/send.ts new file mode 100644 index 000000000..25d39129a --- /dev/null +++ b/packages-ts/gauntlet-terra-contracts/src/commands/wallet/send.ts @@ -0,0 +1,60 @@ +import { BN, logger, prompt } from '@chainlink/gauntlet-core/dist/utils' +import { defaultAfterExecute } from '../abstract/executionWrapper' +import { AccAddress, MsgSend } from '@terra-money/terra.js' +import { CATEGORIES, ULUNA_DECIMALS } from '../../lib/constants' +import { TerraCommand, TransactionResponse } from '@chainlink/gauntlet-terra' +import { Result } from '@chainlink/gauntlet-core' + +type CommandInput = { + destination: string + // Units in LUNA + amount: string +} + +export default class TransferLuna extends TerraCommand { + static description = 'Transfer Luna' + static examples = [`yarn gauntlet wallet:transfer --network=bombay-testnet`] + + static id = 'wallet:transfer' + static category = CATEGORIES.WALLET + + input: CommandInput + + constructor(flags, args: string[]) { + super(flags, args) + } + + beforeExecute = async (input: CommandInput) => { + await prompt(`Continue sending ${input.amount} uLUNA to ${input.destination}?`) + } + afterExecute = defaultAfterExecute + + makeInput = () => { + return { + destination: this.flags.to, + amount: new BN(this.flags.amount).mul(new BN(10).pow(new BN(ULUNA_DECIMALS))), + } + } + + makeRawTransaction = async (signer: AccAddress) => { + this.input = this.makeInput() + if (!AccAddress.validate(this.input.destination)) throw new Error('Invalid destination address') + return new MsgSend(signer, this.input.destination, `${this.input.amount.toString()}uluna`) + } + + execute = async () => { + const message = await this.makeRawTransaction(this.wallet.key.accAddress) + await this.beforeExecute(this.input) + const tx = await this.signAndSend([message]) + const result = { + responses: [ + { + tx, + contract: '', + }, + ], + } as Result + await this.afterExecute(result) + return result + } +} diff --git a/packages-ts/gauntlet-terra-contracts/src/lib/constants.ts b/packages-ts/gauntlet-terra-contracts/src/lib/constants.ts index bddafa5e8..ee7c03bdf 100644 --- a/packages-ts/gauntlet-terra-contracts/src/lib/constants.ts +++ b/packages-ts/gauntlet-terra-contracts/src/lib/constants.ts @@ -11,6 +11,7 @@ export const enum CATEGORIES { ACCESS_CONTROLLER = 'Access Controller', MULTISIG = 'Multisig', DEVIATION_FLAGGING_VALIDATOR = 'Devaiation Flagging Validator', + WALLET = 'Wallet', } export const DEFAULT_RELEASE_VERSION = 'local' @@ -38,3 +39,5 @@ export const CW3_FLEX_MULTISIG_CODE_IDs = { export const TOKEN_DECIMALS = 18 export const TOKEN_UNIT = new BN(10).pow(new BN(TOKEN_DECIMALS)) + +export const ULUNA_DECIMALS = 6 diff --git a/packages-ts/gauntlet-terra-cw-plus/src/commands/multisig.ts b/packages-ts/gauntlet-terra-cw-plus/src/commands/multisig.ts index 4063bedca..6db296f60 100644 --- a/packages-ts/gauntlet-terra-cw-plus/src/commands/multisig.ts +++ b/packages-ts/gauntlet-terra-cw-plus/src/commands/multisig.ts @@ -1,15 +1,15 @@ 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 { AccAddress, MsgExecuteContract, MsgSend } from '@terra-money/terra.js' import { isDeepEqual } from '../lib/utils' -import { Vote, WasmMsg, Action, State } from '../lib/types' +import { Vote, WasmMsg, Action, State, BankMsg } from '../lib/types' import { fetchProposalState } from './inspect' type ProposalAction = ( signer: AccAddress, proposalId: number, - message: MsgExecuteContract, + message: MsgExecuteContract | MsgSend, ) => Promise export const wrapCommand = (command) => { @@ -43,7 +43,7 @@ export const wrapCommand = (command) => { if (state.nextAction !== Action.CREATE) { this.require( - await this.isSameProposal(state.data, [this.toWasmMsg(message)]), + await this.isSameProposal(state.data, [this.toMsg(message)]), 'The transaction generated is different from the proposal provided', ) } @@ -51,10 +51,26 @@ export const wrapCommand = (command) => { return operations[state.nextAction](signer, Number(this.flags.proposal), message) } - isSameProposal = (proposalMsgs: WasmMsg[], generatedMsgs: WasmMsg[]) => { + isSameProposal = (proposalMsgs: (WasmMsg | BankMsg)[], generatedMsgs: (WasmMsg | BankMsg)[]) => { return isDeepEqual(proposalMsgs, generatedMsgs) } + toMsg = (message: MsgSend | MsgExecuteContract): BankMsg | WasmMsg => { + if (message instanceof MsgSend) return this.toBankMsg(message as MsgSend) + if (message instanceof MsgExecuteContract) return this.toWasmMsg(message as MsgExecuteContract) + } + + toBankMsg = (message: MsgSend): BankMsg => { + return { + bank: { + send: { + amount: message.amount.toArray().map((c) => c.toData()), + to_address: message.to_address, + }, + }, + } + } + toWasmMsg = (message: MsgExecuteContract): WasmMsg => { return { wasm: { @@ -72,7 +88,7 @@ export const wrapCommand = (command) => { const proposeInput = { propose: { description: command.id, - msgs: [this.toWasmMsg(message)], + msgs: [this.toMsg(message)], title: command.id, // TODO: Set expiration time // latest: { at_height: 7970238 }, @@ -151,6 +167,8 @@ export const wrapCommand = (command) => { } if (this.flags.execute) { + // TODO: Improve beforeExecute + // if (this.command.beforeExecute) await this.command.beforeExecute() await prompt(`Continue ${actionMessage[state.nextAction]} proposal?`) const tx = await this.signAndSend([rawTx]) let response: Result = { diff --git a/packages-ts/gauntlet-terra-cw-plus/src/lib/types.ts b/packages-ts/gauntlet-terra-cw-plus/src/lib/types.ts index 15a9c8cc4..7eb36bf97 100644 --- a/packages-ts/gauntlet-terra-cw-plus/src/lib/types.ts +++ b/packages-ts/gauntlet-terra-cw-plus/src/lib/types.ts @@ -14,19 +14,30 @@ export enum Action { NONE = 'none', } +type Coin = { + denom: string + amount: string +} + export type WasmMsg = { wasm: { execute: { contract_addr: string - funds: { - denom: string - amount: string - }[] + funds: Coin[] msg: string } } } +export type BankMsg = { + bank: { + send: { + amount: Coin[] + to_address: string + } + } +} + export type State = { threshold: number nextAction: Action diff --git a/packages-ts/gauntlet-terra/src/commands/internal/terra.ts b/packages-ts/gauntlet-terra/src/commands/internal/terra.ts index 4545bf38b..3b03bfa58 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, AccAddress, TxLog } from '@terra-money/terra.js' +import { EventsByType, MsgStoreCode, AccAddress, TxLog, MsgSend } 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,8 @@ export default abstract class TerraCommand extends WriteCommand Promise> - abstract makeRawTransaction: (signer: AccAddress) => Promise + abstract makeRawTransaction: (signer: AccAddress) => Promise + beforeExecute?: (context?) => Promise afterExecute?: (response: Result) => any constructor(flags, args) { @@ -65,7 +66,7 @@ export default abstract class TerraCommand extends WriteCommand => { + signAndSend = async (messages: MsgExecuteContract[] | MsgSend[]): Promise => { try { logger.loading('Signing transaction...') const tx = await this.wallet.createAndSignTx({