diff --git a/gauntlet/package.json b/gauntlet/package.json index efc512fdf..2197e69fc 100644 --- a/gauntlet/package.json +++ b/gauntlet/package.json @@ -13,7 +13,8 @@ "main": "packages/gauntlet-solana-contracts/dist/index.js", "bin": "packages/gauntlet-solana-contracts/dist/index.js", "scripts": { - "gauntlet": "yarn build && node ./packages/gauntlet-solana-contracts/dist/index.js", + "gauntlet": "yarn build && node ./packages/gauntlet-solana-contracts/dist/cli.js", + "gauntlet-serum-multisig": "yarn build && node ./packages/gauntlet-serum-multisig/dist/index.js", "lint": "tsc -b ./tsconfig.json", "test": "yarn build && SKIP_PROMPTS=true jest --runInBand ./packages", "test:coverage": "yarn test --collectCoverage", diff --git a/gauntlet/packages/gauntlet-serum-multisig/LICENSE b/gauntlet/packages/gauntlet-serum-multisig/LICENSE new file mode 100644 index 000000000..0c86a8ab4 --- /dev/null +++ b/gauntlet/packages/gauntlet-serum-multisig/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Khalid Zoabi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/gauntlet/packages/gauntlet-serum-multisig/README.md b/gauntlet/packages/gauntlet-serum-multisig/README.md new file mode 100644 index 000000000..a998f719f --- /dev/null +++ b/gauntlet/packages/gauntlet-serum-multisig/README.md @@ -0,0 +1,40 @@ +# Gauntlet Serum Multisig + +## Creating a Multisig + +Example with 3 owners and threshold=2 + +`yarn gauntlet-serum-multisig create --network=local 3W37Aopzbtzczi8XWdkFTvBeSyYgXLuUkaodkq59xBCT ETqajtkz4xcsB397qTBPetprR8jMC3JszkjJJp3cjWJS QMaHW2Fpyet4ZVf7jgrGB6iirZLjwZUjN9vPKcpQrHs --threshold=2` + +## Actions + +Rest of the commands will adhere to the following flow: + +### Create + +You run a regular command, and you just replace `gauntlet` with `gauntlet-serum-multisig`. +For e.g if you have this command: `yarn gauntlet ocr2:set_billing --network=local --state=k91NrbTgTt4bo86fXN3SXqUzVvoDRiivxf2KcU1p5Gp` +it becomes: +`yarn gauntlet-serum-multisig ocr2:set_billing --network=local --state=k91NrbTgTt4bo86fXN3SXqUzVvoDRiivxf2KcU1p5Gp` + +The creator automatically signs/approves the proposal. +You get a message on console with the proposal PublicKey that you need for continuing on next actions + +### Approve/Execute + +You run a previously created command and you just append `--proposal` flag, +For e.g for the above: `yarn gauntlet-serum-multisig ocr2:set_billing --network=local --state=k91NrbTgTt4bo86fXN3SXqUzVvoDRiivxf2KcU1p5Gp --proposal=CyU1HR7Ebs4aQVQVabT6KeNFusHqov1nwCpCDs9CRZhw` + +If the threshold is met, the proposal is executed. Else, you need to repeat the action for the rest of the owners until the threshold is met. + +### Setting owners + +Example of setting one more owner to the above created multisig + +`yarn gauntlet-serum-multisig set:owners --network=local QMaHW2Fpyet4ZVf7jgrGB6iirZLjwZUjN9vPKcpQrHs ETqajtkz4xcsB397qTBPetprR8jMC3JszkjJJp3cjWJS 3W37Aopzbtzczi8XWdkFTvBeSyYgXLuUkaodkq59xBCT 8i1ZbY9S7VPV4AKVEL1xewyYFvtkrjAnffqfsCf3FoRB` + +### Setting threshold + +Example of setting threshold to 3 + +`yarn gauntlet-serum-multisig set:threshold --network=local --threshold=1` diff --git a/gauntlet/packages/gauntlet-serum-multisig/artifacts/schemas/multisig.json b/gauntlet/packages/gauntlet-serum-multisig/artifacts/schemas/multisig.json new file mode 100644 index 000000000..e69de29bb diff --git a/gauntlet/packages/gauntlet-serum-multisig/networks/.env.devnet b/gauntlet/packages/gauntlet-serum-multisig/networks/.env.devnet new file mode 100644 index 000000000..684b6c9da --- /dev/null +++ b/gauntlet/packages/gauntlet-serum-multisig/networks/.env.devnet @@ -0,0 +1,2 @@ +NODE_URL=https://api.devnet.solana.com + diff --git a/gauntlet/packages/gauntlet-serum-multisig/networks/.env.local b/gauntlet/packages/gauntlet-serum-multisig/networks/.env.local new file mode 100644 index 000000000..20974d106 --- /dev/null +++ b/gauntlet/packages/gauntlet-serum-multisig/networks/.env.local @@ -0,0 +1 @@ +NODE_URL=http://127.0.0.1:8899 \ No newline at end of file diff --git a/gauntlet/packages/gauntlet-serum-multisig/networks/.env.testnet b/gauntlet/packages/gauntlet-serum-multisig/networks/.env.testnet new file mode 100644 index 000000000..b2ca67892 --- /dev/null +++ b/gauntlet/packages/gauntlet-serum-multisig/networks/.env.testnet @@ -0,0 +1 @@ +NODE_URL=https://api.testnet.solana.com \ No newline at end of file diff --git a/gauntlet/packages/gauntlet-serum-multisig/package.json b/gauntlet/packages/gauntlet-serum-multisig/package.json new file mode 100644 index 000000000..8706d05af --- /dev/null +++ b/gauntlet/packages/gauntlet-serum-multisig/package.json @@ -0,0 +1,34 @@ +{ + "name": "@chainlink/gauntlet-serum-multisig", + "version": "0.0.1", + "description": "Gauntlet Serum Multisig", + "keywords": [ + "typescript", + "cli" + ], + "main": "./dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*", + "!dist/**/*.test.js" + ], + "scripts": { + "gauntlet": "ts-node ./src/index.ts", + "lint": "tsc", + "test": "SKIP_PROMPTS=true jest --runInBand", + "test:coverage": "yarn test --collectCoverage", + "test:ci": "yarn test --ci", + "lint:format": "yarn prettier --check ./src", + "format": "yarn prettier --write ./src", + "clean": "rm -rf ./dist/ ./bin/", + "build": "yarn clean && tsc", + "bundle": "yarn build && pkg ." + }, + "dependencies": { + "@chainlink/gauntlet-core": "0.0.7", + "@chainlink/gauntlet-solana": "*", + "@chainlink/gauntlet-solana-contracts": "*", + "@project-serum/anchor": "^0.20.0", + "@solana/web3.js": "^1.30.2" + } +} diff --git a/gauntlet/packages/gauntlet-serum-multisig/src/commands/create.ts b/gauntlet/packages/gauntlet-serum-multisig/src/commands/create.ts new file mode 100644 index 000000000..a15b75718 --- /dev/null +++ b/gauntlet/packages/gauntlet-serum-multisig/src/commands/create.ts @@ -0,0 +1,73 @@ +import { Result } from '@chainlink/gauntlet-core' +import { logger } from '@chainlink/gauntlet-core/dist/utils' +import { SolanaCommand, TransactionResponse } from '@chainlink/gauntlet-solana' +import { PublicKey, SYSVAR_RENT_PUBKEY, Keypair } from '@solana/web3.js' +import BN from 'bn.js' +import { CONTRACT_LIST, getContract } from '@chainlink/gauntlet-solana-contracts' + +type Input = { + owners: string[] + threshold: number | string +} + +const DEFAULT_MAXIMUM_SIZE = 200 + +export default class MultisigCreate extends SolanaCommand { + static id = 'create' + static category = CONTRACT_LIST.MULTISIG + + static examples = ['yarn gauntlet-serum-multisig create --network=local'] + + constructor(flags, args) { + super(flags, args) + this.requireFlag('threshold', 'Please provide multisig threshold') + } + + makeInput = (userInput: any): Input => { + if (userInput) return userInput as Input + + return { + //TODO: validate args. Maybe wrap them int PublicKey? + owners: this.args, + threshold: this.flags.threshold, + } + } + + execute = async () => { + this.require(this.args.length > 0, 'Please provide at least one owner as an argument') + const contract = getContract(CONTRACT_LIST.MULTISIG, '') + const address = contract.programId.toString() + const program = this.loadProgram(contract.idl, address) + + const input = this.makeInput(this.flags.input) + + const multisig = Keypair.generate() + + const [multisigSigner, nonce] = await PublicKey.findProgramAddress( + [multisig.publicKey.toBuffer()], + program.programId, + ) + const maximumSize = this.flags.maximumSize || DEFAULT_MAXIMUM_SIZE + const owners = input.owners.map((key) => new PublicKey(key)) + + const tx = await program.rpc.createMultisig(owners, new BN(input.threshold), nonce, { + accounts: { + multisig: multisig.publicKey, + rent: SYSVAR_RENT_PUBKEY, + }, + signers: [multisig], + instructions: [await program.account.multisig.createInstruction(multisig, maximumSize)], + }) + logger.info(`Multisig address: ${multisig.publicKey}`) + logger.info(`Multisig Signer: ${multisigSigner.toString()}`) + + return { + responses: [ + { + tx: this.wrapResponse(tx, multisig.publicKey.toString()), + contract: multisig.publicKey.toString(), + }, + ], + } as Result + } +} diff --git a/gauntlet/packages/gauntlet-serum-multisig/src/commands/index.ts b/gauntlet/packages/gauntlet-serum-multisig/src/commands/index.ts new file mode 100644 index 000000000..88350e091 --- /dev/null +++ b/gauntlet/packages/gauntlet-serum-multisig/src/commands/index.ts @@ -0,0 +1,4 @@ +import SetOwners from './setOwners' +import SetThreshold from './setThreshold' + +export default [SetOwners, SetThreshold] diff --git a/gauntlet/packages/gauntlet-serum-multisig/src/commands/multisig.ts b/gauntlet/packages/gauntlet-serum-multisig/src/commands/multisig.ts new file mode 100644 index 000000000..f76e957b7 --- /dev/null +++ b/gauntlet/packages/gauntlet-serum-multisig/src/commands/multisig.ts @@ -0,0 +1,202 @@ +import { Result } from '@chainlink/gauntlet-core' +import { TransactionResponse, SolanaCommand, RawTransaction } from '@chainlink/gauntlet-solana' +import { logger, BN } from '@chainlink/gauntlet-core/dist/utils' +import { PublicKey, SYSVAR_RENT_PUBKEY, Keypair } from '@solana/web3.js' +import { CONTRACT_LIST, getContract } from '@chainlink/gauntlet-solana-contracts' +import { Idl, Program } from '@project-serum/anchor' + +type ProposalContext = { + rawTx: RawTransaction + multisigSigner: PublicKey + proposalState: any +} + +type ProposalAction = (proposal: Keypair | PublicKey, context: ProposalContext) => Promise + +export const wrapCommand = (command) => { + return class Multisig extends SolanaCommand { + command: SolanaCommand + program: Program + multisigAddress: PublicKey + + static id = `${command.id}` + + constructor(flags, args) { + super(flags, args) + logger.info(`Running ${command.id} command using Serum Multisig`) + + this.command = new command(flags, args) + this.command.invokeMiddlewares(this.command, this.command.middlewares) + this.require(!!process.env.MULTISIG_ADDRESS, 'Please set MULTISIG_ADDRESS env var') + this.multisigAddress = new PublicKey(process.env.MULTISIG_ADDRESS) + } + + getRemainingSigners = (proposalState: any, threshold: number): number => + Number(threshold) - proposalState.signers.filter(Boolean).length + + isReadyForExecution = (proposalState: any, threshold: number): boolean => { + return this.getRemainingSigners(proposalState, threshold) <= 0 + } + + execute = async () => { + const multisig = getContract(CONTRACT_LIST.MULTISIG, '') + this.program = this.loadProgram(multisig.idl, multisig.programId.toString()) + const [multisigSigner] = await PublicKey.findProgramAddress( + [this.multisigAddress.toBuffer()], + this.program.programId, + ) + const multisigState = await this.program.account.multisig.fetch(process.env.MULTISIG_ADDRESS) + const threshold = multisigState.threshold + const owners = multisigState.owners + + logger.info(`Multisig Info: + - Address: ${this.multisigAddress.toString()} + - Signer: ${multisigSigner.toString()} + - Threshold: ${threshold.toString()} + - Owners: ${owners}`) + + // TODO: Should we support many txs? + const rawTx = (await this.command.makeRawTransaction(multisigSigner))[0] + const isCreation = !this.flags.proposal + if (isCreation) { + const proposal = Keypair.generate() + const result = await this.wrapAction(this.createProposal)(proposal, { + rawTx, + multisigSigner, + proposalState: {}, + }) + this.inspectProposalState(proposal.publicKey, threshold, owners) + return result + } + + const proposal = new PublicKey(this.flags.proposal) + const proposalState = await this.program.account.transaction.fetch(proposal) + const proposalContext = { + rawTx, + multisigSigner, + proposalState, + } + + logger.debug(`Proposal state: ${JSON.stringify(proposalState, null, 4)}`) + + const isAlreadyExecuted = proposalState.didExecute + if (isAlreadyExecuted) { + logger.info(`Proposal is already executed`) + return {} as Result + } + + if (!this.isReadyForExecution(proposalState, threshold)) { + const result = await this.wrapAction(this.approveProposal)(proposal, proposalContext) + this.inspectProposalState(proposal, threshold, owners) + return result + } + + const result = await this.wrapAction(this.executeProposal)(proposal, proposalContext) + this.inspectProposalState(proposal, threshold, owners) + return result + } + + wrapAction = (action: ProposalAction) => async (proposal: Keypair | PublicKey, context: ProposalContext) => { + try { + const tx = await action(proposal, context) + return { + responses: [ + { + tx: this.wrapResponse(tx, proposal.toString()), + contract: proposal.toString(), + }, + ], + } as Result + } catch (e) { + // known errors, defined in multisig contract. see serum_multisig.json + if (e.code >= 300 && e.code < 400) { + logger.error(e.msg) + } else { + logger.error(e) + } + return {} as Result + } + } + + createProposal: ProposalAction = async (proposal: Keypair, context): Promise => { + logger.loading(`Creating proposal`) + const txSize = 1000 + const tx = await this.program.rpc.createTransaction( + context.rawTx.programId, + context.rawTx.accounts, + context.rawTx.data, + { + accounts: { + multisig: this.multisigAddress, + transaction: proposal.publicKey, + proposer: this.wallet.payer.publicKey, + rent: SYSVAR_RENT_PUBKEY, + }, + instructions: [await this.program.account.transaction.createInstruction(proposal, txSize)], + signers: [proposal, this.wallet.payer], + }, + ) + return tx + } + + approveProposal: ProposalAction = async (proposal: PublicKey): Promise => { + logger.loading(`Approving proposal`) + const tx = await this.program.rpc.approve({ + accounts: { + multisig: this.multisigAddress, + transaction: proposal, + owner: this.wallet.publicKey, + }, + }) + return tx + } + + executeProposal: ProposalAction = async (proposal: PublicKey, context): Promise => { + logger.loading(`Executing proposal`) + + const tx = await this.program.rpc.executeTransaction({ + accounts: { + multisig: this.multisigAddress, + multisigSigner: context.multisigSigner, + transaction: proposal, + }, + remainingAccounts: context.proposalState.accounts + .map((t) => (t.pubkey.equals(context.multisigSigner) ? { ...t, isSigner: false } : t)) + .concat({ + pubkey: context.proposalState.programId, + isWritable: false, + isSigner: false, + }), + }) + logger.info(`Execution TX hash: ${tx.toString()}`) + return tx + } + + inspectProposalState = async (proposal, threshold, owners) => { + const proposalState = await this.program.account.transaction.fetch(proposal) + logger.debug('Proposal state after action:') + logger.debug(JSON.stringify(proposalState, null, 4)) + if (proposalState.didExecute == true) { + logger.info(`Proposal has been executed`) + return + } + + if (this.isReadyForExecution(proposalState, threshold)) { + logger.info( + `Threshold has been met, an owner needs to run the command once more in order to execute it, with flag --proposal=${proposal}`, + ) + return + } + // inverting the signers boolean array and filtering owners by it + const remainingEligibleSigners = owners.filter((_, i) => proposalState.signers.map((s) => !s)[i]) + logger.info( + `${this.getRemainingSigners( + proposalState, + threshold, + )} more owners should sign this proposal, using the same command with flag --proposal=${proposal}`, + ) + logger.info(`Eligible owners to sign: `) + logger.info(remainingEligibleSigners.toString()) + } + } +} diff --git a/gauntlet/packages/gauntlet-serum-multisig/src/commands/setOwners.ts b/gauntlet/packages/gauntlet-serum-multisig/src/commands/setOwners.ts new file mode 100644 index 000000000..78a12697c --- /dev/null +++ b/gauntlet/packages/gauntlet-serum-multisig/src/commands/setOwners.ts @@ -0,0 +1,52 @@ +import { SolanaCommand, RawTransaction, TransactionResponse } from '@chainlink/gauntlet-solana' +import { PublicKey } from '@solana/web3.js' +import { CONTRACT_LIST, getContract } from '@chainlink/gauntlet-solana-contracts' +import { Result } from '@chainlink/gauntlet-core' + +import BN from 'bn.js' + +export default class SetOwners extends SolanaCommand { + static id = 'set:owners' + static category = CONTRACT_LIST.MULTISIG + + static examples = [ + 'yarn gauntlet-serum-multisig set:owners --network=local --approve --tx=9Vck9Gdk8o9WhxT8bgNcfJ5gbvFBN1zPuXpf8yu8o2aq --execute AGnZeMWkdyXBiLDG2DnwuyGSviAbCGJXyk4VhvP9Y51M QMaHW2Fpyet4ZVf7jgrGB6iirZLjwZUjN9vPKcpQrHs', + ] + + constructor(flags, args) { + super(flags, args) + } + makeRawTransaction = async (signer: PublicKey) => { + const multisigAddress = new PublicKey(process.env.MULTISIG_ADDRESS || '') + const multisig = getContract(CONTRACT_LIST.MULTISIG, '') + const address = multisig.programId.toString() + const program = this.loadProgram(multisig.idl, address) + const data = program.coder.instruction.encode('set_owners', { + owners: this.args.map((a) => new PublicKey(a)), + }) + + const accounts = [ + { + pubkey: multisigAddress, + isWritable: true, + isSigner: false, + }, + { + pubkey: signer, + isWritable: false, + isSigner: true, + }, + ] + const rawTx: RawTransaction = { + data, + accounts, + programId: multisig.programId, + } + return [rawTx] + } + + //execute not needed, this command cannot be ran outside of multisig + execute = async () => { + return {} as Result + } +} diff --git a/gauntlet/packages/gauntlet-serum-multisig/src/commands/setThreshold.ts b/gauntlet/packages/gauntlet-serum-multisig/src/commands/setThreshold.ts new file mode 100644 index 000000000..501fe0dee --- /dev/null +++ b/gauntlet/packages/gauntlet-serum-multisig/src/commands/setThreshold.ts @@ -0,0 +1,55 @@ +import { SolanaCommand, RawTransaction, TransactionResponse } from '@chainlink/gauntlet-solana' +import { PublicKey } from '@solana/web3.js' +import { CONTRACT_LIST, getContract } from '@chainlink/gauntlet-solana-contracts' +import { Result } from '@chainlink/gauntlet-core' + +import BN from 'bn.js' + +export default class SetThreshold extends SolanaCommand { + static id = 'set:threshold' + static category = CONTRACT_LIST.MULTISIG + + static examples = [ + 'yarn gauntlet-serum-multisig set:threshold --network=local --threshold=2 --approve --tx=9Vck9Gdk8o9WhxT8bgNcfJ5gbvFBN1zPuXpf8yu8o2aq --execute', + ] + + constructor(flags, args) { + super(flags, args) + this.requireFlag('threshold', 'Please provide multisig threshold') + } + + makeRawTransaction = async (signer: PublicKey) => { + const multisigAddress = new PublicKey(process.env.MULTISIG_ADDRESS || '') + const multisig = getContract(CONTRACT_LIST.MULTISIG, '') + const address = multisig.programId.toString() + const program = this.loadProgram(multisig.idl, address) + const data = program.coder.instruction.encode('change_threshold', { + threshold: new BN(this.flags.threshold), + }) + + const accounts = [ + { + pubkey: multisigAddress, + isWritable: true, + isSigner: false, + }, + { + pubkey: signer, + isWritable: false, + isSigner: true, + }, + ] + const rawTx: RawTransaction = { + data, + accounts, + programId: multisig.programId, + } + + return [rawTx] + } + + //execute not needed, this command cannot be ran outside of multisig + execute = async () => { + return {} as Result + } +} diff --git a/gauntlet/packages/gauntlet-serum-multisig/src/index.ts b/gauntlet/packages/gauntlet-serum-multisig/src/index.ts new file mode 100644 index 000000000..b190e75de --- /dev/null +++ b/gauntlet/packages/gauntlet-serum-multisig/src/index.ts @@ -0,0 +1,31 @@ +import { commands } from '@chainlink/gauntlet-solana-contracts' +import { executeCLI } from '@chainlink/gauntlet-core' +import { existsSync } from 'fs' +import path from 'path' +import { io } from '@chainlink/gauntlet-core/dist/utils' +import { wrapCommand } from './commands/multisig' +import multisigSpecificCommands from './commands' +import CreateMultisig from './commands/create' + +export const multisigCommands = { + custom: [...commands.custom.concat(multisigSpecificCommands).map(wrapCommand), CreateMultisig], + loadDefaultFlags: () => ({}), + abstract: { + findPolymorphic: () => undefined, + makeCommand: () => undefined, + }, +} +;(async () => { + try { + const networkPossiblePaths = ['./networks', './packages/gauntlet-serum-multisig/networks'] + const networkPath = networkPossiblePaths.filter((networkPath) => + existsSync(path.join(process.cwd(), networkPath)), + )[0] + + const result = await executeCLI(multisigCommands, networkPath) + io.saveJSON(result, 'report') + } catch (e) { + console.log(e) + console.log('Solana Command execution error', e.message) + } +})() diff --git a/gauntlet/packages/gauntlet-serum-multisig/tsconfig.json b/gauntlet/packages/gauntlet-serum-multisig/tsconfig.json new file mode 100644 index 000000000..2c84c1fcb --- /dev/null +++ b/gauntlet/packages/gauntlet-serum-multisig/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*"], + "exclude": ["dist", "**/*.spec.ts", "**/*.test.ts"] +} diff --git a/gauntlet/packages/gauntlet-solana-contracts/package.json b/gauntlet/packages/gauntlet-solana-contracts/package.json index 02328b230..436e086a5 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/package.json +++ b/gauntlet/packages/gauntlet-solana-contracts/package.json @@ -13,7 +13,7 @@ "!dist/**/*.test.js" ], "scripts": { - "gauntlet": "ts-node ./src/index.ts", + "gauntlet": "ts-node ./src/cli.ts", "lint": "tsc", "test": "SKIP_PROMPTS=true jest --runInBand", "test:coverage": "yarn test --collectCoverage", diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/cli.ts b/gauntlet/packages/gauntlet-solana-contracts/src/cli.ts new file mode 100644 index 000000000..bd007a76c --- /dev/null +++ b/gauntlet/packages/gauntlet-solana-contracts/src/cli.ts @@ -0,0 +1,18 @@ +import { executeCLI } from '@chainlink/gauntlet-core' +import { existsSync } from 'fs' +import path from 'path' +import { io } from '@chainlink/gauntlet-core/dist/utils' +import { commands } from '.' +;(async () => { + try { + const networkPossiblePaths = ['./networks', './packages/gauntlet-solana-contracts/networks'] + const networkPath = networkPossiblePaths.filter((networkPath) => + existsSync(path.join(process.cwd(), networkPath)), + )[0] + const result = await executeCLI(commands, networkPath) + io.saveJSON(result, 'report') + } catch (e) { + console.log(e) + console.log('Solana Command execution error', e.message) + } +})() diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/accessController/initialize.ts b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/accessController/initialize.ts index d51cee8b5..ff019050f 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/accessController/initialize.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/accessController/initialize.ts @@ -7,7 +7,9 @@ export default class Initialize extends SolanaCommand { static id = 'access_controller:initialize' static category = CONTRACT_LIST.ACCESS_CONTROLLER - static examples = ['yarn gauntlet access_controller:initialize --network=devnet'] + static examples = [ + 'yarn gauntlet access_controller:initialize --network=devnet 8cMfJYzeFS2ELDSCMZK65ib9zF6DmtEqFS7sNe9dZzct', + ] constructor(flags, args) { super(flags, args) diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/setBilling.ts b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/setBilling.ts index d50b5eba5..40ae74599 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/setBilling.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/setBilling.ts @@ -1,7 +1,7 @@ import { Result } from '@chainlink/gauntlet-core' +import { SolanaCommand, TransactionResponse, RawTransaction } from '@chainlink/gauntlet-solana' +import { AccountMeta, PublicKey, Transaction, TransactionInstruction } from '@solana/web3.js' import { logger, BN, prompt } from '@chainlink/gauntlet-core/dist/utils' -import { SolanaCommand, TransactionResponse } from '@chainlink/gauntlet-solana' -import { PublicKey } from '@solana/web3.js' import { CONTRACT_LIST, getContract } from '../../../lib/contracts' import { getRDD } from '../../../lib/rdd' @@ -9,7 +9,6 @@ type Input = { observationPaymentGjuels: number | string transmissionPaymentGjuels: number | string } - export default class SetBilling extends SolanaCommand { static id = 'ocr2:set_billing' static category = CONTRACT_LIST.OCR_2 @@ -36,39 +35,74 @@ export default class SetBilling extends SolanaCommand { this.requireFlag('state', 'Provide a valid state address') } - execute = async () => { + makeRawTransaction = async (signer: PublicKey) => { const ocr2 = getContract(CONTRACT_LIST.OCR_2, '') const address = ocr2.programId.toString() const program = this.loadProgram(ocr2.idl, address) - const state = new PublicKey(this.flags.state) + const input = this.makeInput(this.flags.input) const info = await program.account.state.fetch(state) const billingAC = new PublicKey(info.config.billingAccessController) - + logger.loading('Generating billing tx information...') logger.log('Billing information:', input) await prompt('Continue setting billing?') + const data = program.coder.instruction.encode('set_billing', { + observationPaymentGjuels: new BN(input.observationPaymentGjuels), + transmissionPaymentGjuels: new BN(input.transmissionPaymentGjuels), + }) - const tx = await program.rpc.setBilling( - new BN(input.observationPaymentGjuels), - new BN(input.transmissionPaymentGjuels), + const accounts: AccountMeta[] = [ { - accounts: { - state: state, - authority: this.wallet.payer.publicKey, - accessController: billingAC, - }, - signers: [this.wallet.payer], + pubkey: state, + isSigner: false, + isWritable: true, + }, + { + pubkey: signer, + isSigner: true, + isWritable: false, }, + { + pubkey: billingAC, + isSigner: false, + isWritable: false, + }, + ] + + const rawTx: RawTransaction = { + data, + accounts, + programId: ocr2.programId, + } + + return [rawTx] + } + + execute = async () => { + const rawTx = await this.makeRawTransaction(this.wallet.payer.publicKey) + const tx = rawTx.reduce( + (tx, meta) => + tx.add( + new TransactionInstruction({ + programId: meta.programId, + keys: meta.accounts, + data: meta.data, + }), + ), + new Transaction(), ) - logger.success(`Billing set on tx ${tx}`) + logger.loading('Sending tx...') + const txhash = await this.provider.send(tx, [this.wallet.payer]) + logger.success(`Billing set on tx hash: ${txhash}`) + return { responses: [ { - tx: this.wrapResponse(tx, state.toString()), - contract: state.toString(), + tx: this.wrapResponse(txhash, this.flags.state), + contract: this.flags.state, }, ], } as Result diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/index.ts b/gauntlet/packages/gauntlet-solana-contracts/src/index.ts index 45c7debde..b59e60d9c 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/index.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/index.ts @@ -1,12 +1,9 @@ -import { executeCLI } from '@chainlink/gauntlet-core' -import { existsSync } from 'fs' -import path from 'path' -import { io } from '@chainlink/gauntlet-core/dist/utils' import Solana from './commands' import { makeAbstractCommand } from './commands/abstract' import { defaultFlags } from './lib/args' +export { CONTRACT_LIST, getContract } from './lib/contracts' -const commands = { +export const commands = { custom: [...Solana], loadDefaultFlags: () => defaultFlags, abstract: { @@ -14,16 +11,3 @@ const commands = { makeCommand: makeAbstractCommand, }, } -;(async () => { - try { - const networkPossiblePaths = ['./networks', './packages/gauntlet-solana-contracts/networks'] - const networkPath = networkPossiblePaths.filter((networkPath) => - existsSync(path.join(process.cwd(), networkPath)), - )[0] - const result = await executeCLI(commands, networkPath) - io.saveJSON(result, 'report') - } catch (e) { - console.log(e) - console.log('Solana Command execution error', e.message) - } -})() diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/lib/contracts.ts b/gauntlet/packages/gauntlet-solana-contracts/src/lib/contracts.ts index fb13b6ee2..e01ba23e8 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/lib/contracts.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/lib/contracts.ts @@ -22,12 +22,14 @@ export enum CONTRACT_LIST { FLAGS = 'flags', STORE = 'store', TOKEN = 'token', + MULTISIG = 'serum_multisig', } export const CONTRACT_ENV_NAMES = { [CONTRACT_LIST.ACCESS_CONTROLLER]: 'PROGRAM_ID_ACCESS_CONTROLLER', [CONTRACT_LIST.OCR_2]: 'PROGRAM_ID_OCR2', [CONTRACT_LIST.STORE]: 'PROGRAM_ID_STORE', + [CONTRACT_LIST.MULTISIG]: 'PROGRAM_ID_MULTISIG', } export const getContract = (name: CONTRACT_LIST, version: string): Contract => ({ diff --git a/gauntlet/packages/gauntlet-solana/src/commands/internal/solana.ts b/gauntlet/packages/gauntlet-solana/src/commands/internal/solana.ts index 3761e497c..c99f9d993 100644 --- a/gauntlet/packages/gauntlet-solana/src/commands/internal/solana.ts +++ b/gauntlet/packages/gauntlet-solana/src/commands/internal/solana.ts @@ -1,13 +1,14 @@ import { Result, WriteCommand } from '@chainlink/gauntlet-core' -import { BpfLoader, BPF_LOADER_PROGRAM_ID, Keypair, LAMPORTS_PER_SOL } from '@solana/web3.js' +import { BpfLoader, BPF_LOADER_PROGRAM_ID, Keypair, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js' import { withProvider, withWallet, withNetwork } from '../middlewares' -import { TransactionResponse } from '../types' +import { RawTransaction, TransactionResponse } from '../types' import { Idl, Program, Provider, Wallet } from '@project-serum/anchor' export default abstract class SolanaCommand extends WriteCommand { wallet: typeof Wallet provider: Provider abstract execute: () => Promise> + makeRawTransaction: (signer: PublicKey) => Promise constructor(flags, args) { super(flags, args) diff --git a/gauntlet/packages/gauntlet-solana/src/commands/types.ts b/gauntlet/packages/gauntlet-solana/src/commands/types.ts index 869c720ee..91875d550 100644 --- a/gauntlet/packages/gauntlet-solana/src/commands/types.ts +++ b/gauntlet/packages/gauntlet-solana/src/commands/types.ts @@ -1,3 +1,5 @@ +import { AccountMeta, PublicKey } from '@solana/web3.js' + export type TransactionResponse = { hash: string address?: string @@ -5,3 +7,9 @@ export type TransactionResponse = { tx?: any states?: Record } + +export type RawTransaction = { + data: Buffer + accounts: AccountMeta[] + programId: PublicKey +} diff --git a/gauntlet/packages/gauntlet-solana/src/index.ts b/gauntlet/packages/gauntlet-solana/src/index.ts index 0b13af3d0..20194ae3f 100644 --- a/gauntlet/packages/gauntlet-solana/src/index.ts +++ b/gauntlet/packages/gauntlet-solana/src/index.ts @@ -1,6 +1,6 @@ import SolanaCommand from './commands/internal/solana' import { waitExecute } from './lib/execute' -import { TransactionResponse } from './commands/types' +import { TransactionResponse, RawTransaction } from './commands/types' import * as constants from './lib/constants' -export { SolanaCommand, waitExecute, TransactionResponse, constants } +export { SolanaCommand, waitExecute, TransactionResponse, constants, RawTransaction } diff --git a/gauntlet/tsconfig.json b/gauntlet/tsconfig.json index 0d2c05971..f77357bef 100644 --- a/gauntlet/tsconfig.json +++ b/gauntlet/tsconfig.json @@ -8,6 +8,9 @@ }, { "path": "./packages/gauntlet-solana-contracts" + }, + { + "path": "./packages/gauntlet-serum-multisig" } ] }