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 df11a0ca..71a41dbf 100644 --- a/packages-ts/gauntlet-terra-contracts/src/commands/abstract/index.ts +++ b/packages-ts/gauntlet-terra-contracts/src/commands/abstract/index.ts @@ -1,9 +1,12 @@ import { Result } from '@chainlink/gauntlet-core' import { logger, prompt } from '@chainlink/gauntlet-core/dist/utils' -import { TransactionResponse, TerraCommand } from '@chainlink/gauntlet-terra' +import { TransactionResponse, TerraCommand, RawTransaction } from '@chainlink/gauntlet-terra' import { Contract, CONTRACT_LIST, getContract, TerraABI, TERRA_OPERATIONS } from '../../lib/contracts' import { DEFAULT_RELEASE_VERSION } from '../../lib/constants' import schema from '../../lib/schema' +import findPolymorphic from './polymorphic' + +export { findPolymorphic } export interface AbstractOpts { contract: Contract @@ -181,6 +184,32 @@ export default class AbstractCommand extends TerraCommand { } } + // create and sign transaction, without executing + makeRawTransaction = async (): Promise => { + const operations = { + [TERRA_OPERATIONS.DEPLOY]: this.abstractPrepareDeploy, + [TERRA_OPERATIONS.EXECUTE]: this.abstractPrepareCall, + [TERRA_OPERATIONS.QUERY]: () => { + throw Error('makeRawTransaction: cannot make a tx from a query commmand') + }, + // TODO: [TERRA_OPERATIONS.UPLOAD]: this.abstractPrepareUpload, + } + + return await operations[this.opts.action](this.params, this.args[0]) + } + + abstractPrepareDeploy = async (params: any) => { + const codeId = this.codeIds[this.opts.contract.id] + this.require(!!codeId, `Code Id for contract ${this.opts.contract.id} not found`) + return await this.prepareDeploy(codeId, params) + } + + abstractPrepareCall = async (params: any, address: string) => { + return await this.prepareCall(address, { + [this.opts.function]: params, + }) + } + execute = async () => { const operations = { [TERRA_OPERATIONS.DEPLOY]: this.abstractDeploy, diff --git a/packages-ts/gauntlet-terra-contracts/src/commands/abstract/polymorphic.ts b/packages-ts/gauntlet-terra-contracts/src/commands/abstract/polymorphic.ts new file mode 100644 index 00000000..37cde568 --- /dev/null +++ b/packages-ts/gauntlet-terra-contracts/src/commands/abstract/polymorphic.ts @@ -0,0 +1,64 @@ +import { TerraCommand, TransactionResponse } from '@chainlink/gauntlet-terra' +import { MultisigTerraCommand } from '../contracts/multisig' +import { Result, Command } from '@chainlink/gauntlet-core' +import { logger } from '@chainlink/gauntlet-core/dist/utils' +import DeployLink from '../../commands/contracts/link/deploy' +import AbstractCommand from '.' +import { makeAbstractCommand } from '.' + +class EmptyCommand extends Command { + constructor() { + super({ help: false }, []) + } +} + +type ICommandConstructor = (flags: any, args: string[]) => void + +export default (commands: any, slug: string): Command => { + const slugs: string[] = slug.split(':') + if (slugs.length < 3) { + throw Error(`Command ${slug} not found`) + } + const op: string = slugs.pop()! + const instruction = slugs.join(':') + + const commandType = commands[instruction] ? commands[instruction] : AbstractCommand + + switch (op) { + case 'multisig': + case 'propose': + case 'vote': + case 'execute': + case 'approve': // vote yes, then execute if threshold is reached + class WrappedCommand extends MultisigTerraCommand { + static id = instruction + static commandType = commandType + + constructor(flags, args) { + super(flags, args) + if (commandType === AbstractCommand) { + throw Error(`Command ${instruction} not found`) + // TODO: get this working for abstract commands. Something like: + // this.command = await makeAbstractCommand(instruction, flags, args) + } else { + this.command = new commandType(flags, args) + } + } + + multisigOp = () => { + return op + } + } + + // This is a temporary workaround for a bug in the type specification for findPolymorphic in @gauntlet-core. + // At runtime, it's used as a constructor. It must be callable and able to construct an actual command object + // when passed flags & args, (ie, it should be a class). But it's declared as if it were an instance of a type + // satisfying ICommand, which will cause typescript to reject it during compilation if it's a class satisfying + // the ICommand interface. The only way to satisfy both constraints is for it to look like an instance at compile + // time, and a class at runtime. The workaround is to return something that is both at once: add all properties + // of an instance of EmtpyCommand to the class WrappedCommand and return the resulting hybrid. + return Object.assign(WrappedCommand, new EmptyCommand()) + default: + throw Error(`Command ${slugs.join(':')} not found`) + } +} diff --git a/packages-ts/gauntlet-terra-contracts/src/commands/contracts/link/deploy.ts b/packages-ts/gauntlet-terra-contracts/src/commands/contracts/link/deploy.ts index c644a4e9..49f5e2df 100644 --- a/packages-ts/gauntlet-terra-contracts/src/commands/contracts/link/deploy.ts +++ b/packages-ts/gauntlet-terra-contracts/src/commands/contracts/link/deploy.ts @@ -1,4 +1,4 @@ -import { TerraCommand, TransactionResponse } from '@chainlink/gauntlet-terra' +import { TerraCommand, TransactionResponse, RawTransaction } from '@chainlink/gauntlet-terra' import { Result } from '@chainlink/gauntlet-core' import { logger, prompt } from '@chainlink/gauntlet-core/dist/utils' import { CATEGORIES, CW20_BASE_CODE_IDs } from '../../../lib/constants' @@ -19,9 +19,8 @@ export default class DeployLink extends TerraCommand { super(flags, args) } - execute = async () => { - await prompt(`Begin deploying LINK Token?`) - const deploy = await this.deploy(CW20_BASE_CODE_IDs[this.flags.network], { + genParams = () => { + return { name: 'ChainLink Token', symbol: 'LINK', decimals: 18, @@ -36,7 +35,18 @@ export default class DeployLink extends TerraCommand { mint: { minter: this.wallet.key.accAddress, }, - }) + } + } + + makeRawTransaction = async (): Promise => { + const codeId = CW20_BASE_CODE_IDs[this.flags.network] + this.require(!!codeId, `Code Id for link token contract not found`) + return await this.prepareDeploy(CW20_BASE_CODE_IDs[this.flags.network], this.genParams()) + } + + execute = async () => { + await prompt(`Begin deploying LINK Token?`) + const deploy = await this.deploy(CW20_BASE_CODE_IDs[this.flags.network], this.genParams()) const result = await this.provider.wasm.contractQuery(deploy.address!, { token_info: {} }) logger.success(`LINK token successfully deployed at ${deploy.address} (txhash: ${deploy.hash})`) logger.debug(result) diff --git a/packages-ts/gauntlet-terra-contracts/src/commands/contracts/multisig/index.ts b/packages-ts/gauntlet-terra-contracts/src/commands/contracts/multisig/index.ts index ba1d8907..ab5a0ca2 100644 --- a/packages-ts/gauntlet-terra-contracts/src/commands/contracts/multisig/index.ts +++ b/packages-ts/gauntlet-terra-contracts/src/commands/contracts/multisig/index.ts @@ -1,4 +1,6 @@ import { CreateGroup } from './group' import { CreateWallet } from './wallet' +import { MultisigTerraCommand } from './multisig' export default [CreateGroup, CreateWallet] +export { MultisigTerraCommand } diff --git a/packages-ts/gauntlet-terra-contracts/src/commands/contracts/multisig/multisig.ts b/packages-ts/gauntlet-terra-contracts/src/commands/contracts/multisig/multisig.ts new file mode 100644 index 00000000..96eed9cb --- /dev/null +++ b/packages-ts/gauntlet-terra-contracts/src/commands/contracts/multisig/multisig.ts @@ -0,0 +1,67 @@ +// multisig.ts +// +// For now, propose, vote, and execute functionality are all combined into one CONTRACT:COMMAND::multisig meta-command +// This is parallel to how things are implemented in Solana. The execute happens automatically as soon as the last +// vote required to exceeed the threshold is cast. And the difference between propose and vote is distinguished by +// whether the --proposal=PROPOSAL_HASH flag is passed. Later, we may want to split this into CONTRACT::COMMAND::propose, +// CONTRACT::COMMAND::vote, and CONTRACT::COMMAND::execute. We may also want to add CONTRACT::COMMAND::close, to +// abort a proposal early (before it expires), disallowing any further voting on it. + +import { logger, prompt } from '@chainlink/gauntlet-core/dist/utils' +import { Result, ICommand } from '@chainlink/gauntlet-core' +import { TerraCommand, RawTransaction, TransactionResponse } from '@chainlink/gauntlet-terra' +import { CATEGORIES } from '../../../lib/constants' +import { CONTRACT_LIST, Contract, getContract, TERRA_OPERATIONS } from '../../../lib/contracts' +import AbstractCommand from '../../abstract' + +type ProposalContext = { + rawTx: RawTransaction + multisigSigner: string + proposalState: any +} + +type StringGetter = () => string +export type ICommandConstructor = (flags: any, args: string[]) => void + +abstract class MultisigTerraCommand extends TerraCommand { + static category = CATEGORIES.MULTISIG + + commandType: typeof TerraCommand + multisigOp: StringGetter + + command: TerraCommand + multisigAddress: string + multisigContract: Promise + + constructor(flags, args) { + super(flags, args) + } + + postConstructor(flags, args) { + // Called after child constructor + logger.debug(`Running ${this.commandType} in multisig mode`) + + this.command.invokeMiddlewares(this.command, this.command.middlewares) + this.require(!!process.env.MULTISIG_WALLET, 'Please set MULTISIG_WALLET env var') + this.multisigContract = getContract(CONTRACT_LIST.MULTISIG, flags.version) + this.multisigAddress = process.env.MULTISIG_WALLET! + } + + execute = async (): Promise> => { + const tx = this.command.makeRawTransaction() + console.debug(tx) + + return { + responses: [], + } as Result + } +} + +export const wrapCommand = (command) => { + return class CustomCommand extends MultisigTerraCommand { + static id = `${command.id}:multisig` + static category = CATEGORIES.MULTISIG + } +} + +export { MultisigTerraCommand } diff --git a/packages-ts/gauntlet-terra-contracts/src/index.ts b/packages-ts/gauntlet-terra-contracts/src/index.ts index b1e589e0..498640de 100644 --- a/packages-ts/gauntlet-terra-contracts/src/index.ts +++ b/packages-ts/gauntlet-terra-contracts/src/index.ts @@ -2,14 +2,14 @@ import { executeCLI } from '@chainlink/gauntlet-core' import { existsSync } from 'fs' import path from 'path' import Terra from './commands' -import { makeAbstractCommand } from './commands/abstract' +import { makeAbstractCommand, findPolymorphic } from './commands/abstract' import { defaultFlags } from './lib/args' const commands = { custom: [...Terra], loadDefaultFlags: () => defaultFlags, abstract: { - findPolymorphic: () => undefined, + findPolymorphic: findPolymorphic, makeCommand: makeAbstractCommand, }, } diff --git a/packages-ts/gauntlet-terra-contracts/src/lib/constants.ts b/packages-ts/gauntlet-terra-contracts/src/lib/constants.ts index 20cb82fe..7f803cd2 100644 --- a/packages-ts/gauntlet-terra-contracts/src/lib/constants.ts +++ b/packages-ts/gauntlet-terra-contracts/src/lib/constants.ts @@ -21,15 +21,3 @@ export const CW20_BASE_CODE_IDs = { local: 32, 'bombay-testnet': 148, } - -export const CW4_GROUP_CODE_IDs = { - mainnet: -1, - local: -1, - 'bombay-testnet': 36895, -} - -export const CW3_FLEX_MULTISIG_CODE_IDs = { - mainnet: -1, - local: -1, - 'bombay-testnet': 36059, -} diff --git a/packages-ts/gauntlet-terra-contracts/src/lib/schema.ts b/packages-ts/gauntlet-terra-contracts/src/lib/schema.ts index 1d116a89..c511621c 100644 --- a/packages-ts/gauntlet-terra-contracts/src/lib/schema.ts +++ b/packages-ts/gauntlet-terra-contracts/src/lib/schema.ts @@ -3,6 +3,11 @@ import JTD from 'ajv/dist/jtd' const ajv = new Ajv().addFormat('uint8', (value: any) => !isNaN(value)) +ajv.addFormat('uint32', { + type: 'number', + validate: (x) => !isNaN(x), +}) + ajv.addFormat('uint64', { type: 'number', validate: (x) => !isNaN(x), @@ -14,6 +19,5 @@ ajv.addFormat('uint32', { }) export default ajv - const jtd = new JTD() export { jtd } diff --git a/packages-ts/gauntlet-terra/src/commands/internal/terra.ts b/packages-ts/gauntlet-terra/src/commands/internal/terra.ts index 72a140bf..67b1210b 100644 --- a/packages-ts/gauntlet-terra/src/commands/internal/terra.ts +++ b/packages-ts/gauntlet-terra/src/commands/internal/terra.ts @@ -1,4 +1,5 @@ import { Result, WriteCommand } from '@chainlink/gauntlet-core' +import { RawTransaction } from '../types' import { logger } from '@chainlink/gauntlet-core/dist/utils' import { EventsByType, MsgStoreCode, TxLog } from '@terra-money/terra.js' import { SignMode } from '@terra-money/terra.proto/cosmos/tx/signing/v1beta1/signing' @@ -29,6 +30,10 @@ export default abstract class TerraCommand extends WriteCommand => { + throw Error('makeRawTransaction: not implemented!') + } + parseResponseValue(receipt: any, eventType: string, attributeType: string) { try { const parsed = JSON.parse(receipt?.raw_log) @@ -63,53 +68,63 @@ export default abstract class TerraCommand extends WriteCommand