diff --git a/gauntlet/packages/gauntlet-serum-multisig/src/commands/create.ts b/gauntlet/packages/gauntlet-serum-multisig/src/commands/create.ts index f2d68333d..2bae29296 100644 --- a/gauntlet/packages/gauntlet-serum-multisig/src/commands/create.ts +++ b/gauntlet/packages/gauntlet-serum-multisig/src/commands/create.ts @@ -1,5 +1,5 @@ import { Result } from '@chainlink/gauntlet-core' -import { logger, BN } from '@chainlink/gauntlet-core/dist/utils' +import { logger, BN, prompt } from '@chainlink/gauntlet-core/dist/utils' import { SolanaCommand, TransactionResponse } from '@chainlink/gauntlet-solana' import { PublicKey, SYSVAR_RENT_PUBKEY, Keypair } from '@solana/web3.js' import { CONTRACT_LIST, getContract } from '@chainlink/gauntlet-solana-contracts' @@ -9,8 +9,6 @@ type Input = { threshold: number | string } -const DEFAULT_MAXIMUM_SIZE = 200 - export default class MultisigCreate extends SolanaCommand { static id = 'create' static category = CONTRACT_LIST.MULTISIG @@ -48,17 +46,34 @@ export default class MultisigCreate extends SolanaCommand { [multisig.publicKey.toBuffer()], program.programId, ) - const maximumSize = this.flags.maximumSize || DEFAULT_MAXIMUM_SIZE + const maxOwners = this.flags.maxOwners || 30 const owners = input.owners.map((key) => new PublicKey(key)) + // SIZE IN BYTES + const OWNER_LENGTH = 32 + const EXTRA = 2 + const NONCE_LENGTH = 1 + const THRESHOLD_LENGTH = 8 + const SEQ_LENGTH = 4 + + const TOTAL_TO_ALLOCATE = (OWNER_LENGTH + EXTRA) * maxOwners + THRESHOLD_LENGTH + NONCE_LENGTH + SEQ_LENGTH + + const threshold = new BN(input.threshold) + await prompt( + `A new multisig will be created with threshold ${threshold.toNumber()} and owners ${owners.map((o) => + o.toString(), + )}. Continue?`, + ) + 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)], + instructions: [await program.account.multisig.createInstruction(multisig, TOTAL_TO_ALLOCATE)], }) + logger.success('New multisig created') logger.info(`Multisig address: ${multisig.publicKey}`) logger.info(`Multisig Signer: ${multisigSigner.toString()}`) diff --git a/gauntlet/packages/gauntlet-serum-multisig/src/commands/inspect.ts b/gauntlet/packages/gauntlet-serum-multisig/src/commands/inspect.ts new file mode 100644 index 000000000..0c3797d90 --- /dev/null +++ b/gauntlet/packages/gauntlet-serum-multisig/src/commands/inspect.ts @@ -0,0 +1,44 @@ +import { Result } from '@chainlink/gauntlet-core' +import { logger } from '@chainlink/gauntlet-core/dist/utils' +import { SolanaCommand, TransactionResponse } from '@chainlink/gauntlet-solana' +import { PublicKey } from '@solana/web3.js' +import { CONTRACT_LIST, getContract } from '@chainlink/gauntlet-solana-contracts' + +export default class MultisigInspect extends SolanaCommand { + static id = 'multisig:inspect' + static category = CONTRACT_LIST.MULTISIG + + static examples = ['yarn gauntlet-serum-multisig multisig:inspect --network=local --state=MULTISIG_ACCOUNT'] + + constructor(flags, args) { + super(flags, args) + this.requireFlag('state', 'Please provide multisig state address') + } + + execute = async () => { + const multisig = getContract(CONTRACT_LIST.MULTISIG, '') + const program = this.loadProgram(multisig.idl, multisig.programId.toString()) + + const state = new PublicKey(this.flags.state) + const multisigState = await program.account.multisig.fetch(state) + const [multisigSigner] = await PublicKey.findProgramAddress([state.toBuffer()], program.programId) + const threshold = multisigState.threshold + const owners = multisigState.owners + + logger.info(`Multisig Info: + - ProgramID: ${program.programId.toString()} + - Address: ${state.toString()} + - Signer: ${multisigSigner.toString()} + - Threshold: ${threshold.toString()} + - Owners: ${owners}`) + + return { + responses: [ + { + tx: this.wrapResponse('', state.toString()), + contract: state.toString(), + }, + ], + } as Result + } +} diff --git a/gauntlet/packages/gauntlet-serum-multisig/src/commands/multisig.ts b/gauntlet/packages/gauntlet-serum-multisig/src/commands/multisig.ts index 4c2e29609..7d025d18c 100644 --- a/gauntlet/packages/gauntlet-serum-multisig/src/commands/multisig.ts +++ b/gauntlet/packages/gauntlet-serum-multisig/src/commands/multisig.ts @@ -1,10 +1,10 @@ -import { Result } from '@chainlink/gauntlet-core' -import { TransactionResponse, SolanaCommand, RawTransaction } from '@chainlink/gauntlet-solana' +import { SolanaCommand, RawTransaction } from '@chainlink/gauntlet-solana' import { logger, BN, prompt } from '@chainlink/gauntlet-core/dist/utils' -import { PublicKey, SYSVAR_RENT_PUBKEY, Keypair, Transaction } from '@solana/web3.js' -import { CONTRACT_LIST, getContract } from '@chainlink/gauntlet-solana-contracts' -import { ProgramError, parseIdlErrors, Idl, Program } from '@project-serum/anchor' +import { PublicKey, SYSVAR_RENT_PUBKEY, Keypair, AccountMeta, SystemProgram } from '@solana/web3.js' +import { CONTRACT_LIST, getContract, makeTx } from '@chainlink/gauntlet-solana-contracts' +import { Idl, Program } from '@project-serum/anchor' import { MAX_BUFFER_SIZE } from '../lib/constants' +import { isDeepEqual } from '../lib/utils' type ProposalContext = { rawTx: RawTransaction @@ -12,7 +12,7 @@ type ProposalContext = { proposalState: any } -type ProposalAction = (proposal: PublicKey, context: ProposalContext) => Promise +type ProposalAction = (proposal: PublicKey, signer: PublicKey, context: ProposalContext) => Promise export const wrapCommand = (command) => { return class Multisig extends SolanaCommand { @@ -27,27 +27,78 @@ export const wrapCommand = (command) => { logger.info(`Running ${command.id} command using Serum Multisig`) this.command = new command({ ...flags, bufferSize: MAX_BUFFER_SIZE }, 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 () => { + // TODO: Command underneath will try to load its own provider and wallet if invoke middlewares, but we should be able to specify which ones to use, in an obvious better way + this.command.provider = this.provider + this.command.wallet = this.wallet + const multisig = getContract(CONTRACT_LIST.MULTISIG, '') this.program = this.loadProgram(multisig.idl, multisig.programId.toString()) + + const signer = this.wallet.publicKey + const rawTxs = await this.makeRawTransaction(signer) + // If proposal is not provided, we are at creation time, and a new proposal acc should have been created + const proposal = new PublicKey(this.flags.proposal || rawTxs[0].accounts[1].pubkey) + + if (this.flags.execute) { + await prompt('CREATION,APPROVAL or EXECUTION TX will be executed. Continue?') + logger.loading(`Executing action...`) + const txhash = await this.sendTxWithIDL(this.signAndSendRawTx, this.program.idl)(rawTxs) + await this.inspectProposalState(proposal) + return { + responses: [ + { + tx: this.wrapResponse(txhash, this.multisigAddress.toString()), + contract: this.multisigAddress.toString(), + }, + ], + } + } + + const latestSlot = await this.provider.connection.getSlot() + const recentBlock = await this.provider.connection.getBlock(latestSlot) + const tx = makeTx(rawTxs, { + recentBlockhash: recentBlock.blockhash, + feePayer: signer, + }) + + const msgData = tx.serializeMessage().toString('base64') + logger.line() + logger.success( + `Message generated with blockhash ID: ${recentBlock.blockhash.toString()} (${new Date( + recentBlock.blockTime * 1000, + ).toLocaleString()}). MESSAGE DATA:`, + ) + logger.log() + logger.log(msgData) + logger.log() + logger.line() + + return { + responses: [ + { + tx: this.wrapResponse('', this.multisigAddress.toString()), + contract: this.multisigAddress.toString(), + data: { + transactionData: msgData, + }, + }, + ], + } + } + + makeRawTransaction = async (signer: PublicKey): Promise => { + logger.info(`Generating transaction data using ${signer.toString()} account as signer`) + + const multisigState = await this.program.account.multisig.fetch(this.multisigAddress) 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 @@ -62,141 +113,198 @@ export const wrapCommand = (command) => { await this.showExecutionInstructions(rawTxs, instructionIndex) const rawTx = rawTxs[instructionIndex] - const isCreation = !this.flags.proposal + // First step should be creating the proposal account. If no proposal flag is provided, proceed to create it + const proposal = this.flags.proposal ? new PublicKey(this.flags.proposal) : await this.createProposalAcount() + + const proposalState = await this.fetchState(proposal) + const isCreation = !proposalState if (isCreation) { - const proposal = await this.createProposalAcount() - const result = await this.wrapAction(this.createProposal)(proposal, { + return await this.createProposal(proposal, signer, { rawTx, multisigSigner, proposalState: {}, }) - this.inspectProposalState(proposal, 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 { - responses: [ - { - tx: this.wrapResponse('', proposal.toString()), - contract: proposal.toString(), - }, - ], - } - } + if (isAlreadyExecuted) throw new Error('Proposal is already executed') + + this.require( + await this.isSameProposal(proposal, rawTx), + 'The transaction generated is different from the proposal provided', + ) if (!this.isReadyForExecution(proposalState, threshold)) { - const result = await this.wrapAction(this.approveProposal)(proposal, proposalContext) - this.inspectProposalState(proposal, threshold, owners) - return result + return await this.approveProposal(proposal, signer, proposalContext) } - const result = await this.wrapAction(this.executeProposal)(proposal, proposalContext) - return result + return await this.executeProposal(proposal, signer, proposalContext) } - wrapAction = (action: ProposalAction) => async ( - proposal: PublicKey, - context: ProposalContext, - ): Promise> => { - const tx = await action(proposal, context) - return { - responses: [ - { - tx: this.wrapResponse(tx, proposal.toString()), - contract: proposal.toString(), - }, - ], + 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 + } + + fetchState = async (proposal: PublicKey): Promise => { + try { + return await this.program.account.transaction.fetch(proposal) + } catch (e) { + logger.info('Proposal state not found. Should be empty at CREATION time') + return } } + isSameProposal = async (proposal: PublicKey, rawTx: RawTransaction): Promise => { + const state = await this.fetchState(proposal) + if (!state) { + logger.error('Proposal state does not exist. Considering the proposal as different') + return false + } + const isSameData = Buffer.compare(state.data, rawTx.data) === 0 + const isSameProgramId = new PublicKey(state.programId).toString() === rawTx.programId.toString() + const isSameAccounts = isDeepEqual(state.accounts, rawTx.accounts) + return isSameData && isSameProgramId && isSameAccounts + } + createProposalAcount = async (): Promise => { - logger.log('Creating proposal account') + await prompt('A new proposal account will be created. Continue?') + logger.log('Creating proposal account...') const proposal = Keypair.generate() const txSize = 1300 // Space enough - const proposalAccount = await this.program.account.transaction.createInstruction(proposal, txSize) - const accountTx = new Transaction().add(proposalAccount) - await this.provider.send(accountTx, [proposal, this.wallet.payer]) + const proposalInstruction = await SystemProgram.createAccount({ + fromPubkey: this.wallet.publicKey, + newAccountPubkey: proposal.publicKey, + space: txSize, + lamports: await this.provider.connection.getMinimumBalanceForRentExemption(txSize), + programId: this.program.programId, + }) + const rawTx: RawTransaction = { + data: proposalInstruction.data, + accounts: proposalInstruction.keys, + programId: proposalInstruction.programId, + } + await this.signAndSendRawTx([rawTx], [proposal]) logger.success(`Proposal account created at: ${proposal.publicKey.toString()}`) return proposal.publicKey } - createProposal: ProposalAction = async (proposal: PublicKey, context): Promise => { - logger.loading(`Creating proposal for ${command.id}`) - const tx = await this.program.rpc.createTransaction( - context.rawTx.programId, - context.rawTx.accounts, - context.rawTx.data, + createProposal: ProposalAction = async (proposal: PublicKey, signer, context): Promise => { + logger.loading(`Generating proposal CREATION data for ${command.id}`) + + const data = this.program.coder.instruction.encode('createTransaction', { + pid: context.rawTx.programId, + accs: context.rawTx.accounts, + data: context.rawTx.data, + }) + const accounts: AccountMeta[] = [ { - accounts: { - multisig: this.multisigAddress, - transaction: proposal, - proposer: this.wallet.payer.publicKey, - rent: SYSVAR_RENT_PUBKEY, - }, - signers: [this.wallet.payer], + pubkey: this.multisigAddress, + isWritable: false, + isSigner: false, }, - ) - return tx + { + pubkey: proposal, + isWritable: true, + isSigner: false, + }, + { + pubkey: signer, + isWritable: false, + isSigner: true, + }, + { + pubkey: SYSVAR_RENT_PUBKEY, + isWritable: false, + isSigner: false, + }, + ] + const rawTx: RawTransaction = { + data, + accounts, + programId: this.program.programId, + } + return [rawTx] } - approveProposal: ProposalAction = async (proposal: PublicKey): Promise => { - logger.loading(`Approving proposal for ${command.id}`) - const tx = await this.program.rpc.approve({ - accounts: { - multisig: this.multisigAddress, - transaction: proposal, - owner: this.wallet.publicKey, + approveProposal: ProposalAction = async (proposal: PublicKey, signer): Promise => { + logger.loading(`Generating proposal APPROVAL data for ${command.id}`) + + const data = this.program.coder.instruction.encode('approve', {}) + const accounts: AccountMeta[] = [ + { + pubkey: this.multisigAddress, + isWritable: false, + isSigner: false, }, - }) - return tx + { + pubkey: proposal, + isWritable: true, + isSigner: false, + }, + { + pubkey: signer, + isWritable: false, + isSigner: true, + }, + ] + const rawTx: RawTransaction = { + data, + accounts, + programId: this.program.programId, + } + return [rawTx] } - executeProposal: ProposalAction = async (proposal: PublicKey, context): Promise => { - logger.loading(`Executing proposal for ${command.id}`) - // get the command's starting word to map it to the respective IDL(ocr2, store etc) - const { idl } = getContract(command.id.split(':')[0], '') - try { - 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, - }), + executeProposal: ProposalAction = async (proposal: PublicKey, signer, context): Promise => { + logger.loading(`Generating proposal EXECUTION data for ${command.id}`) + + const data = this.program.coder.instruction.encode('executeTransaction', {}) + const 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 - } catch (err) { - // Translate IDL error - const idlErrors = parseIdlErrors(idl) - let translatedErr = ProgramError.parse(err, idlErrors) - if (translatedErr === null) { - throw err - } - throw translatedErr + const accounts: AccountMeta[] = [ + { + pubkey: this.multisigAddress, + isWritable: false, + isSigner: false, + }, + { + pubkey: context.multisigSigner, + isWritable: false, + isSigner: false, + }, + { + pubkey: proposal, + isWritable: true, + isSigner: false, + }, + ...remainingAccounts, + ] + const rawTx: RawTransaction = { + data, + accounts, + programId: this.program.programId, } + return [rawTx] } - inspectProposalState = async (proposal, threshold, owners) => { + inspectProposalState = async (proposal) => { const proposalState = await this.program.account.transaction.fetch(proposal) + const multisigState = await this.program.account.multisig.fetch(this.multisigAddress) + const threshold = multisigState.threshold + const owners = multisigState.owners + logger.debug('Proposal state after action:') logger.debug(JSON.stringify(proposalState, null, 4)) if (proposalState.didExecute == true) { @@ -229,8 +337,6 @@ export const wrapCommand = (command) => { )}) zero-indexed transactions. Currently running ${instructionIndex + 1} of ${rawTxs.length}. `) - - await prompt('Continue?') } } } diff --git a/gauntlet/packages/gauntlet-serum-multisig/src/commands/setOwners.ts b/gauntlet/packages/gauntlet-serum-multisig/src/commands/setOwners.ts index 6522413a6..a404071a1 100644 --- a/gauntlet/packages/gauntlet-serum-multisig/src/commands/setOwners.ts +++ b/gauntlet/packages/gauntlet-serum-multisig/src/commands/setOwners.ts @@ -2,14 +2,13 @@ import { SolanaCommand, RawTransaction, TransactionResponse } from '@chainlink/g import { PublicKey } from '@solana/web3.js' import { CONTRACT_LIST, getContract } from '@chainlink/gauntlet-solana-contracts' import { Result } from '@chainlink/gauntlet-core' +import { logger } from '@chainlink/gauntlet-core/dist/utils' export default class SetOwners extends SolanaCommand { - static id = 'set:owners' + static id = 'multisig:set_owners' static category = CONTRACT_LIST.MULTISIG - static examples = [ - 'yarn gauntlet-serum-multisig set:owners --network=local --approve --tx=9Vck9Gdk8o9WhxT8bgNcfJ5gbvFBN1zPuXpf8yu8o2aq --execute AGnZeMWkdyXBiLDG2DnwuyGSviAbCGJXyk4VhvP9Y51M QMaHW2Fpyet4ZVf7jgrGB6iirZLjwZUjN9vPKcpQrHs', - ] + static examples = ['yarn gauntlet-serum-multisig multisig:set_owners --network=local'] constructor(flags, args) { super(flags, args) @@ -19,8 +18,13 @@ export default class SetOwners extends SolanaCommand { const multisig = getContract(CONTRACT_LIST.MULTISIG, '') const address = multisig.programId.toString() const program = this.loadProgram(multisig.idl, address) + + const owners = this.args.map((a) => new PublicKey(a)) + + logger.info(`Generating data for new owners: ${owners.map((o) => o.toString())}`) + const data = program.coder.instruction.encode('set_owners', { - owners: this.args.map((a) => new PublicKey(a)), + owners, }) const accounts = [ diff --git a/gauntlet/packages/gauntlet-serum-multisig/src/commands/setThreshold.ts b/gauntlet/packages/gauntlet-serum-multisig/src/commands/setThreshold.ts index 56f2601be..fef4613f3 100644 --- a/gauntlet/packages/gauntlet-serum-multisig/src/commands/setThreshold.ts +++ b/gauntlet/packages/gauntlet-serum-multisig/src/commands/setThreshold.ts @@ -2,15 +2,13 @@ import { SolanaCommand, RawTransaction, TransactionResponse } from '@chainlink/g import { PublicKey } from '@solana/web3.js' import { CONTRACT_LIST, getContract } from '@chainlink/gauntlet-solana-contracts' import { Result } from '@chainlink/gauntlet-core' -import { BN } from '@chainlink/gauntlet-core/dist/utils' +import { BN, logger } from '@chainlink/gauntlet-core/dist/utils' export default class SetThreshold extends SolanaCommand { - static id = 'set:threshold' + static id = 'multisig:change_threshold' static category = CONTRACT_LIST.MULTISIG - static examples = [ - 'yarn gauntlet-serum-multisig set:threshold --network=local --threshold=2 --approve --tx=9Vck9Gdk8o9WhxT8bgNcfJ5gbvFBN1zPuXpf8yu8o2aq --execute', - ] + static examples = ['yarn gauntlet-serum-multisig multisig:change_threshold --network=local --threshold=2 [OWNERS...]'] constructor(flags, args) { super(flags, args) @@ -22,8 +20,12 @@ export default class SetThreshold extends SolanaCommand { const multisig = getContract(CONTRACT_LIST.MULTISIG, '') const address = multisig.programId.toString() const program = this.loadProgram(multisig.idl, address) + + const threshold = new BN(this.flags.threshold) + logger.info(`Generating data for new threshold: ${threshold.toNumber()}`) + const data = program.coder.instruction.encode('change_threshold', { - threshold: new BN(this.flags.threshold), + threshold, }) const accounts = [ diff --git a/gauntlet/packages/gauntlet-serum-multisig/src/index.ts b/gauntlet/packages/gauntlet-serum-multisig/src/index.ts index b190e75de..0472381ab 100644 --- a/gauntlet/packages/gauntlet-serum-multisig/src/index.ts +++ b/gauntlet/packages/gauntlet-serum-multisig/src/index.ts @@ -6,9 +6,10 @@ import { io } from '@chainlink/gauntlet-core/dist/utils' import { wrapCommand } from './commands/multisig' import multisigSpecificCommands from './commands' import CreateMultisig from './commands/create' +import MultisigInspect from './commands/inspect' export const multisigCommands = { - custom: [...commands.custom.concat(multisigSpecificCommands).map(wrapCommand), CreateMultisig], + custom: [...commands.custom.concat(multisigSpecificCommands).map(wrapCommand), CreateMultisig, MultisigInspect], loadDefaultFlags: () => ({}), abstract: { findPolymorphic: () => undefined, diff --git a/gauntlet/packages/gauntlet-serum-multisig/src/lib/utils.ts b/gauntlet/packages/gauntlet-serum-multisig/src/lib/utils.ts new file mode 100644 index 000000000..074010855 --- /dev/null +++ b/gauntlet/packages/gauntlet-serum-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 +} diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/accessController/read.ts b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/accessController/read.ts index da98c331b..75fa61e6d 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/accessController/read.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/accessController/read.ts @@ -21,7 +21,10 @@ export default class ReadState extends SolanaCommand { // read could be abstract. account.accessController is just the name of the account that can be got form the camelcase(schema.accounts[x].name) const data = await program.account.accessController.fetch(state) - console.log(data) + console.log(` + - Owner: ${new PublicKey(data.owner).toString()} + - Proposed Owner: ${new PublicKey(data.proposedOwner).toString()} + `) return {} as Result } } diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/initialize.ts b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/initialize.ts index db9bd0f35..fdf857896 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/initialize.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/initialize.ts @@ -1,6 +1,6 @@ import { Result } from '@chainlink/gauntlet-core' -import { SolanaCommand, TransactionResponse } from '@chainlink/gauntlet-solana' -import { Keypair, PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } from '@solana/web3.js' +import { RawTransaction, SolanaCommand, TransactionResponse } from '@chainlink/gauntlet-solana' +import { AccountMeta, Keypair, PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } from '@solana/web3.js' import { ASSOCIATED_TOKEN_PROGRAM_ID, Token, TOKEN_PROGRAM_ID } from '@solana/spl-token' import { CONTRACT_LIST, getContract } from '../../../lib/contracts' import { utils } from '@project-serum/anchor' @@ -39,28 +39,23 @@ export default class Initialize extends SolanaCommand { this.requireFlag('billingAccessController', 'Provide a --billingAccessController flag with a valid address') } - execute = async () => { + makeRawTransaction = async (signer: PublicKey, state?: PublicKey): Promise => { + if (!state) throw new Error('State account is required') + const ocr2 = getContract(CONTRACT_LIST.OCR_2, '') const address = ocr2.programId.toString() const program = this.loadProgram(ocr2.idl, address) // STATE ACCOUNTS - const state = Keypair.generate() - const owner = this.wallet.payer const input = this.makeInput(this.flags.input) // ARGS const [vaultAuthority, vaultNonce] = await PublicKey.findProgramAddress( - [Buffer.from(utils.bytes.utf8.encode('vault')), state.publicKey.toBuffer()], + [Buffer.from(utils.bytes.utf8.encode('vault')), state.toBuffer()], program.programId, ) - const [storeAuthority, _storeNonce] = await PublicKey.findProgramAddress( - [Buffer.from(utils.bytes.utf8.encode('store')), state.publicKey.toBuffer()], - program.programId, - ) - - const linkPublicKey = new PublicKey(this.flags.link) + const linkPublicKey = new PublicKey(this.flags.link || process.env.LINK) const requesterAccessController = new PublicKey(this.flags.requesterAccessController) const billingAccessController = new PublicKey(this.flags.billingAccessController) @@ -76,47 +71,132 @@ export default class Initialize extends SolanaCommand { true, ) - const accounts = { - state: state.publicKey, - transmissions: transmissions, - payer: this.provider.wallet.publicKey, - owner: owner.publicKey, - tokenMint: linkPublicKey, - tokenVault, - vaultAuthority, - requesterAccessController, - billingAccessController, - rent: SYSVAR_RENT_PUBKEY, - systemProgram: SystemProgram.programId, - tokenProgram: TOKEN_PROGRAM_ID, - associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, - } - - Object.entries(accounts).map(([k, v]) => console.log('KEY:', k, '=', v.toString())) - console.log(` - - Min Answer: ${minAnswer.toString()} - - Max Answer: ${maxAnswer.toString()} - - Vault Nonce: ${vaultNonce} - `) - - logger.log('Feed information:', input) - await prompt('Continue initializing OCR 2 feed?') - - const txHash = await program.rpc.initialize(vaultNonce, minAnswer, maxAnswer, { - accounts, - signers: [owner, state], - instructions: [await program.account.state.createInstruction(state)], + const data = program.coder.instruction.encode('initialize', { + nonce: vaultNonce, + minAnswer, + maxAnswer, }) + const accounts: AccountMeta[] = [ + { + pubkey: state, + isWritable: true, + isSigner: false, + }, + { + pubkey: transmissions, + isWritable: false, + isSigner: false, + }, + { + pubkey: signer, + isWritable: false, + isSigner: false, + }, + { + pubkey: signer, + isWritable: false, + isSigner: true, + }, + { + pubkey: linkPublicKey, + isWritable: false, + isSigner: false, + }, + { + pubkey: tokenVault, + isWritable: true, + isSigner: false, + }, + { + pubkey: vaultAuthority, + isWritable: false, + isSigner: false, + }, + { + pubkey: requesterAccessController, + isWritable: false, + isSigner: false, + }, + { + pubkey: billingAccessController, + isWritable: false, + isSigner: false, + }, + { + pubkey: SYSVAR_RENT_PUBKEY, + isWritable: false, + isSigner: false, + }, + { + pubkey: SystemProgram.programId, + isWritable: false, + isSigner: false, + }, + { + pubkey: TOKEN_PROGRAM_ID, + isWritable: false, + isSigner: false, + }, + { + pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, + isWritable: false, + isSigner: false, + }, + ] + console.log(` STATE ACCOUNTS: - - State: ${state.publicKey} + - State: ${state?.toString()} - Transmissions: ${transmissions} - - StoreAuthority: ${storeAuthority.toString()} - Payer: ${this.provider.wallet.publicKey} - - Owner: ${owner.publicKey} + - Owner: ${signer.toString()} `) + const defaultAccountSize = new BN(program.account.state.size) + const feedCreationInstruction = await SystemProgram.createAccount({ + fromPubkey: signer, + newAccountPubkey: state, + space: defaultAccountSize.toNumber(), + lamports: await this.provider.connection.getMinimumBalanceForRentExemption(defaultAccountSize.toNumber()), + programId: program.programId, + }) + + const rawTxs: RawTransaction[] = [ + { + data: feedCreationInstruction.data, + accounts: feedCreationInstruction.keys, + programId: feedCreationInstruction.programId, + }, + { + data, + accounts, + programId: program.programId, + }, + ] + + return rawTxs + } + + execute = async () => { + const ocr2 = getContract(CONTRACT_LIST.OCR_2, '') + const address = ocr2.programId.toString() + const program = this.loadProgram(ocr2.idl, address) + + const state = Keypair.generate() + const rawTx = await this.makeRawTransaction(this.wallet.publicKey, state.publicKey) + await prompt(`Start initializing ocr2 feed?`) + + const txhash = await this.sendTxWithIDL(this.signAndSendRawTx, program.idl)(rawTx, [state]) + logger.success(`Feed initialized on tx ${txhash}`) + + const transmissions = rawTx[1].accounts[1].pubkey + + const [storeAuthority, _storeNonce] = await PublicKey.findProgramAddress( + [Buffer.from(utils.bytes.utf8.encode('store')), state.publicKey.toBuffer()], + program.programId, + ) + return { data: { state: state.publicKey.toString(), @@ -125,7 +205,7 @@ export default class Initialize extends SolanaCommand { }, responses: [ { - tx: this.wrapResponse(txHash, address, { + tx: this.wrapResponse(txhash, address, { state: state.publicKey.toString(), transmissions: transmissions.toString(), }), diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/offchainConfig/begin.ts b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/offchainConfig/begin.ts index a65d40c64..976205121 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/offchainConfig/begin.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/offchainConfig/begin.ts @@ -55,14 +55,10 @@ export default class BeginOffchainConfig extends SolanaCommand { } execute = async () => { - const contract = getContract(CONTRACT_LIST.OCR_2, '') - const rawTx = await this.makeRawTransaction(this.wallet.payer.publicKey) - const tx = makeTx(rawTx) - logger.debug(tx) + const rawTx = await this.makeRawTransaction(this.wallet.publicKey) const version = new BN(2) await prompt(`Begin setting Offchain config version ${version.toString()}?`) - logger.loading('Sending tx...') - const txhash = await this.sendTx(tx, [this.wallet.payer], contract.idl) + const txhash = await this.signAndSendRawTx(rawTx) logger.success(`Begin set offchain config on tx hash: ${txhash}`) return { diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/offchainConfig/commit.ts b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/offchainConfig/commit.ts index 5978fdc72..29e1eb924 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/offchainConfig/commit.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/offchainConfig/commit.ts @@ -25,7 +25,6 @@ export default class CommitOffchainConfig extends SolanaCommand { const program = this.loadProgram(ocr2.idl, address) const state = new PublicKey(this.flags.state) - const owner = this.wallet.payer const data = program.coder.instruction.encode('commit_offchain_config', {}) @@ -52,13 +51,9 @@ export default class CommitOffchainConfig extends SolanaCommand { } execute = async () => { - const contract = getContract(CONTRACT_LIST.OCR_2, '') - const rawTx = await this.makeRawTransaction(this.wallet.payer.publicKey) - const tx = makeTx(rawTx) - logger.debug(tx) + const rawTx = await this.makeRawTransaction(this.wallet.publicKey) await prompt(`Commit Offchain config?`) - logger.loading('Sending tx...') - const txhash = await this.sendTx(tx, [this.wallet.payer], contract.idl) + const txhash = await this.signAndSendRawTx(rawTx) logger.success(`Committing offchain config on tx ${txhash}`) return { diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/offchainConfig/resetPending.ts b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/offchainConfig/resetPending.ts index 2ced9e29f..8bdc87d90 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/offchainConfig/resetPending.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/offchainConfig/resetPending.ts @@ -1,7 +1,7 @@ import { Result } from '@chainlink/gauntlet-core' import { logger, prompt } from '@chainlink/gauntlet-core/dist/utils' -import { SolanaCommand, TransactionResponse } from '@chainlink/gauntlet-solana' -import { PublicKey } from '@solana/web3.js' +import { RawTransaction, SolanaCommand, TransactionResponse } from '@chainlink/gauntlet-solana' +import { AccountMeta, PublicKey } from '@solana/web3.js' import { CONTRACT_LIST, getContract } from '../../../../lib/contracts' export default class ResetPendingOffchainConfig extends SolanaCommand { @@ -15,13 +15,12 @@ export default class ResetPendingOffchainConfig extends SolanaCommand { this.require(!!this.flags.state, 'Please provide flags with "state"') } - execute = async () => { + makeRawTransaction = async (signer: PublicKey): Promise => { 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 owner = this.wallet.payer const info = await program.account.state.fetch(state) console.log(info.config.pendingOffchainConfig) @@ -30,22 +29,47 @@ export default class ResetPendingOffchainConfig extends SolanaCommand { 'pending offchain config version is already in reset state', ) - await prompt(`Reset pending offchain config?`) + const data = program.coder.instruction.encode('reset_pending_offchain_config', {}) + + const accounts: AccountMeta[] = [ + { + pubkey: state, + isWritable: true, + isSigner: false, + }, + { + pubkey: signer, + isWritable: false, + isSigner: true, + }, + ] - const tx = await program.rpc.resetPendingOffchainConfig({ - accounts: { - state: state, - authority: owner.publicKey, + return [ + { + data, + accounts, + programId: program.programId, }, - }) + ] + } + + execute = async () => { + const ocr2 = getContract(CONTRACT_LIST.OCR_2, '') + const address = ocr2.programId.toString() + const program = this.loadProgram(ocr2.idl, address) + + const rawTx = await this.makeRawTransaction(this.wallet.publicKey) + await prompt(`Continue Reset pending offchain config?`) + + const txhash = await this.sendTxWithIDL(this.signAndSendRawTx, program.idl)(rawTx) - logger.success(`Reset pending offchain config on tx ${tx}`) + logger.success(`Reset pending offchain config on tx ${txhash}`) return { responses: [ { - tx: this.wrapResponse(tx, state.toString(), { state: state.toString() }), - contract: state.toString(), + tx: this.wrapResponse(txhash, this.flags.state, { state: this.flags.state }), + contract: this.flags.state, }, ], } as Result diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/offchainConfig/write.ts b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/offchainConfig/write.ts index ce130fc7d..9d6c17745 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/offchainConfig/write.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/offchainConfig/write.ts @@ -2,7 +2,7 @@ import { Result } from '@chainlink/gauntlet-core' import { logger, prompt, time, BN } from '@chainlink/gauntlet-core/dist/utils' import { Proto, sharedSecretEncryptions } from '@chainlink/gauntlet-core/dist/crypto' -import { SolanaCommand, TransactionResponse } from '@chainlink/gauntlet-solana' +import { RawTransaction, SolanaCommand, TransactionResponse } from '@chainlink/gauntlet-solana' import { AccountMeta, PublicKey } from '@solana/web3.js' import { MAX_TRANSACTION_BYTES, ORACLES_MAX_LENGTH } from '../../../../lib/constants' import { CONTRACT_LIST, getContract } from '../../../../lib/contracts' @@ -192,7 +192,7 @@ export default class WriteOffchainConfig extends SolanaCommand { return true } - makeRawTransaction = async (signer: PublicKey) => { + makeRawTransaction = async (signer: PublicKey): Promise => { const ocr2 = getContract(CONTRACT_LIST.OCR_2, '') const address = ocr2.programId.toString() const program = this.loadProgram(ocr2.idl, address) @@ -244,10 +244,9 @@ export default class WriteOffchainConfig extends SolanaCommand { } execute = async () => { - const contract = getContract(CONTRACT_LIST.OCR_2, '') const state = new PublicKey(this.flags.state) - const rawTx = await this.makeRawTransaction(this.wallet.payer.publicKey) + const rawTx = await this.makeRawTransaction(this.wallet.publicKey) const startingPoint = new BN(this.flags.instruction || 0).toNumber() await prompt(`Start writing offchain config from ${startingPoint}/${rawTx.length - 1}?`) @@ -255,8 +254,7 @@ export default class WriteOffchainConfig extends SolanaCommand { const txs: string[] = [] for (let i = startingPoint; i < rawTx.length; i++) { logger.loading(`Sending ${i}/${rawTx.length - 1}...`) - const tx = makeTx([rawTx[i]]) - const txhash = await this.sendTx(tx, [this.wallet.payer], contract.idl) + const txhash = await this.signAndSendRawTx([rawTx[i]]) txs.push(txhash) } logger.success(`Last tx Write offchain config set on tx ${txs[txs.length - 1]}`) diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/payRemaining.ts b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/payRemaining.ts index 274b39c08..d481fb9db 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/payRemaining.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/payRemaining.ts @@ -104,13 +104,9 @@ export default class PayRemaining extends SolanaCommand { } execute = async () => { - const contract = getContract(CONTRACT_LIST.OCR_2, '') - const rawTx = await this.makeRawTransaction(this.wallet.payer.publicKey) - const tx = makeTx(rawTx) - logger.debug(tx) + const rawTx = await this.makeRawTransaction(this.wallet.publicKey) await prompt(`Pay remaining on ${this.flags.state.toString()}?`) - logger.loading('Sending tx...') - const txhash = await this.sendTx(tx, [this.wallet.payer], contract.idl) + const txhash = await this.signAndSendRawTx(rawTx) logger.success(`Remaining oracles paid on tx ${txhash}`) return { diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/read.ts b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/read.ts index 3272eb75b..d700216b9 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/read.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/read.ts @@ -35,6 +35,7 @@ export default class ReadState extends SolanaCommand { // read could be abstract. account.accessController is just the name of the account that can be got form the camelcase(schema.accounts[x].name) const data = await program.account.state.fetch(state) + console.log('OWNER:', new PublicKey(data.config.owner).toString()) console.log('DATA:', data) // Get the necessary bytes const offchainBuffer = Buffer.from(data.config.offchainConfig.xs).slice( 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 553791b0f..8fe18801b 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 @@ -81,13 +81,10 @@ export default class SetBilling extends SolanaCommand { } execute = async () => { - const contract = getContract(CONTRACT_LIST.OCR_2, '') - const rawTx = await this.makeRawTransaction(this.wallet.payer.publicKey) - const tx = makeTx(rawTx) - logger.debug(tx) + const rawTx = await this.makeRawTransaction(this.wallet.publicKey) await prompt('Continue setting billing?') logger.loading('Sending tx...') - const txhash = await this.sendTx(tx, [this.wallet.payer], contract.idl) + const txhash = await this.signAndSendRawTx(rawTx) logger.success(`Billing set on tx hash: ${txhash}`) return { diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/setConfig.ts b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/setConfig.ts index 530a357a8..2648b5d4b 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/setConfig.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/setConfig.ts @@ -95,13 +95,9 @@ export default class SetConfig extends SolanaCommand { } execute = async () => { - const contract = getContract(CONTRACT_LIST.OCR_2, '') - const rawTx = await this.makeRawTransaction(this.wallet.payer.publicKey) - const tx = makeTx(rawTx) - logger.debug(tx) + const rawTx = await this.makeRawTransaction(this.wallet.publicKey) await prompt(`Continue setting config on ${this.flags.state.toString()}?`) - logger.loading('Sending tx...') - const txhash = await this.sendTx(tx, [this.wallet.payer], contract.idl) + const txhash = await this.signAndSendRawTx(rawTx) logger.success(`Config set on tx ${txhash}`) return { diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/setPayees.ts b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/setPayees.ts index 7531e9adf..5c10e1e59 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/setPayees.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ocr2/setPayees.ts @@ -54,7 +54,10 @@ export default class SetPayees extends SolanaCommand { const link = new PublicKey(this.flags.link || process.env.LINK) const info = await program.account.state.fetch(state) - const token = new Token(this.provider.connection, link, TOKEN_PROGRAM_ID, this.wallet.payer) + const token = new Token(this.provider.connection, link, TOKEN_PROGRAM_ID, { + publicKey: signer, + secretKey: Buffer.from([]), + }) this.flags.TESTING_ONLY_IGNORE_PAYEE_VALIDATION && logger.warn('TESTING_ONLY_IGNORE_PAYEE_VALIDATION flag is enabled') @@ -121,14 +124,11 @@ export default class SetPayees extends SolanaCommand { } execute = async () => { - const contract = getContract(CONTRACT_LIST.OCR_2, '') - const rawTx = await this.makeRawTransaction(this.wallet.payer.publicKey) - const tx = makeTx(rawTx) - logger.debug(tx) + const rawTx = await this.makeRawTransaction(this.wallet.publicKey) await prompt('Continue setting payees?') - logger.loading('Sending tx...') - const txhash = await this.sendTx(tx, [this.wallet.payer], contract.idl) + const txhash = await this.signAndSendRawTx(rawTx) logger.success(`Payees set on tx hash: ${txhash}`) + return { responses: [ { diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ownership/acceptOwnership.ts b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ownership/acceptOwnership.ts index 77d36a4b3..87c7b6ca1 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ownership/acceptOwnership.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ownership/acceptOwnership.ts @@ -4,7 +4,6 @@ import { SolanaCommand, TransactionResponse, RawTransaction } from '@chainlink/g import { AccountMeta, PublicKey } from '@solana/web3.js' import { CONTRACT_LIST, getContract } from '../../../lib/contracts' import { SolanaConstructor } from '../../../lib/types' -import { makeTx } from '../../../lib/utils' export const makeAcceptOwnershipCommand = (contractId: CONTRACT_LIST): SolanaConstructor => { return class AcceptOwnership extends SolanaCommand { @@ -52,13 +51,13 @@ export const makeAcceptOwnershipCommand = (contractId: CONTRACT_LIST): SolanaCon execute = async () => { const contract = getContract(contractId, '') - const rawTx = await this.makeRawTransaction(this.wallet.payer.publicKey) - const tx = makeTx(rawTx) - logger.debug(tx) + const address = contract.programId.toString() + const program = this.loadProgram(contract.idl, address) + + const rawTx = await this.makeRawTransaction(this.wallet.publicKey) await prompt(`Accepting ownership of ${contractId} state (${this.flags.state.toString()}). Continue?`) - logger.loading('Sending tx...') - const txhash = await this.sendTx(tx, [this.wallet.payer], contract.idl) - logger.success(`Accepted ownership on tx ${txhash}`) + const txhash = await this.sendTxWithIDL(this.signAndSendRawTx, program.idl)(rawTx) + logger.success(`Accepted ownership on tx hash: ${txhash}`) return { responses: [ { diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ownership/transferOwnership.ts b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ownership/transferOwnership.ts index cc0fe2a17..d2eda98a3 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ownership/transferOwnership.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/ownership/transferOwnership.ts @@ -4,7 +4,6 @@ import { SolanaCommand, TransactionResponse, RawTransaction } from '@chainlink/g import { AccountMeta, PublicKey } from '@solana/web3.js' import { CONTRACT_LIST, getContract } from '../../../lib/contracts' import { SolanaConstructor } from '../../../lib/types' -import { makeTx } from '../../../lib/utils' export const makeTransferOwnershipCommand = (contractId: CONTRACT_LIST): SolanaConstructor => { return class TransferOwnership extends SolanaCommand { @@ -58,16 +57,12 @@ export const makeTransferOwnershipCommand = (contractId: CONTRACT_LIST): SolanaC execute = async () => { const contract = getContract(contractId, '') - const rawTx = await this.makeRawTransaction(this.wallet.payer.publicKey) - const tx = makeTx(rawTx) - await prompt( - `Transfering ownership of ${contractId} state (${this.flags.state.toString()}) to: (${new PublicKey( - this.flags.to, - ).toString()}). Continue?`, - ) - logger.debug(tx) - logger.loading('Sending tx...') - const txhash = await this.sendTx(tx, [this.wallet.payer], contract.idl) + const address = contract.programId.toString() + const program = this.loadProgram(contract.idl, address) + + const rawTx = await this.makeRawTransaction(this.wallet.publicKey) + await prompt(`Transferring ownership of ${contractId} state (${this.flags.state.toString()}). Continue?`) + const txhash = await this.sendTxWithIDL(this.signAndSendRawTx, program.idl)(rawTx) logger.success(`Ownership transferred to ${new PublicKey(this.flags.to)} on tx ${txhash}`) return { responses: [ diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/store/createFeed.ts b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/store/createFeed.ts index 1f11d4482..d05cc18fd 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/store/createFeed.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/store/createFeed.ts @@ -1,7 +1,6 @@ -import { Result } from '@chainlink/gauntlet-core' import { logger, BN, prompt } from '@chainlink/gauntlet-core/dist/utils' -import { SolanaCommand, TransactionResponse } from '@chainlink/gauntlet-solana' -import { Keypair, PublicKey } from '@solana/web3.js' +import { RawTransaction, SolanaCommand } from '@chainlink/gauntlet-solana' +import { AccountMeta, Keypair, PublicKey, SystemProgram } from '@solana/web3.js' import { CONTRACT_LIST, getContract } from '../../../lib/contracts' import { getRDD } from '../../../lib/rdd' @@ -38,16 +37,15 @@ export default class CreateFeed extends SolanaCommand { super(flags, args) } - execute = async () => { + makeRawTransaction = async (signer: PublicKey, feed?: PublicKey): Promise => { + if (!feed) throw new Error('Feed account is required') const storeProgram = getContract(CONTRACT_LIST.STORE, '') const address = storeProgram.programId.toString() const program = this.loadProgram(storeProgram.idl, address) const input = this.makeInput(this.flags.input) - const owner = this.wallet.payer const store = new PublicKey(input.store) - const feed = Keypair.generate() const granularity = new BN(input.granularity) const liveLength = new BN(input.liveLength) @@ -70,25 +68,65 @@ export default class CreateFeed extends SolanaCommand { - Total Length: ${feedAccountLength.toNumber()} `) - await prompt('Continue creating new Transmissions Feed?') - logger.loading(`Creating feed...`) + const data = program.coder.instruction.encode('create_feed', { + description, + decimals, + granularity, + liveLength, + }) - const tx = await program.rpc.createFeed(description, decimals, granularity, liveLength, { - accounts: { - store: store, - feed: feed.publicKey, - authority: owner.publicKey, + const accounts: AccountMeta[] = [ + { + pubkey: store, + isWritable: false, + isSigner: false, + }, + { + pubkey: feed, + isWritable: true, + isSigner: false, + }, + { + pubkey: signer, + isWritable: false, + isSigner: true, }, - signers: [owner, feed], - instructions: [await program.account.transmissions.createInstruction(feed, feedAccountLength.toNumber())], + ] + + const transmissionsCreationInstruction = await SystemProgram.createAccount({ + fromPubkey: signer, + newAccountPubkey: feed, + space: feedAccountLength.toNumber(), + lamports: await this.provider.connection.getMinimumBalanceForRentExemption(feedAccountLength.toNumber()), + programId: program.programId, }) - logger.success(`Created feed on tx ${tx}`) - logger.info(` - STATE ACCOUNTS: - - Store: ${store} - - Feed/Transmissions: ${feed.publicKey} - `) + return [ + { + data: transmissionsCreationInstruction.data, + accounts: transmissionsCreationInstruction.keys, + programId: transmissionsCreationInstruction.programId, + }, + { + data, + accounts, + programId: program.programId, + }, + ] + } + + execute = async () => { + const storeProgram = getContract(CONTRACT_LIST.STORE, '') + const address = storeProgram.programId.toString() + const program = this.loadProgram(storeProgram.idl, address) + + const feed = Keypair.generate() + + const rawTxs = await this.makeRawTransaction(this.wallet.publicKey, feed.publicKey) + await prompt('Continue creating new Transmissions Feed?') + + const txhash = await this.sendTxWithIDL(this.signAndSendRawTx, program.idl)(rawTxs, [feed]) + logger.success(`Transmissions feed created on tx ${txhash}`) return { data: { @@ -96,13 +134,13 @@ export default class CreateFeed extends SolanaCommand { }, responses: [ { - tx: this.wrapResponse(tx, feed.publicKey.toString(), { + tx: this.wrapResponse(txhash, feed.toString(), { state: feed.toString(), - transmissions: feed.publicKey.toString(), + transmissions: feed.toString(), }), - contract: feed.publicKey.toString(), + contract: feed.toString(), }, ], - } as Result + } } } diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/store/index.ts b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/store/index.ts index 0ed092f45..41495c643 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/store/index.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/store/index.ts @@ -7,6 +7,7 @@ import { makeAcceptOwnershipCommand } from '../ownership/acceptOwnership' import { makeTransferOwnershipCommand } from '../ownership/transferOwnership' import { CONTRACT_LIST } from '../../../lib/contracts' import { makeUpgradeProgramCommand } from '../../abstract/upgrade' +import Inspect from './inspect' export default [ Initialize, @@ -14,6 +15,7 @@ export default [ SetValidatorConfig, SetWriter, SetLoweringAccessController, + Inspect, makeAcceptOwnershipCommand(CONTRACT_LIST.STORE), makeTransferOwnershipCommand(CONTRACT_LIST.STORE), makeUpgradeProgramCommand(CONTRACT_LIST.STORE), diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/store/initialize.ts b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/store/initialize.ts index 3924a11c2..8e5b1092d 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/store/initialize.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/store/initialize.ts @@ -20,13 +20,13 @@ export default class Initialize extends SolanaCommand { const state = Keypair.generate() const accessController = new PublicKey(this.flags.accessController) - const owner = this.wallet.payer + const owner = this.wallet.publicKey console.log(`Initializing store contract with State at ${state.publicKey}...`) const txHash = await program.rpc.initialize({ accounts: { store: state.publicKey, - owner: owner.publicKey, + owner: owner, loweringAccessController: accessController, }, signers: [state], @@ -39,7 +39,7 @@ export default class Initialize extends SolanaCommand { STATE ACCOUNTS: - State: ${state.publicKey} - Payer: ${this.provider.wallet.publicKey} - - Owner: ${owner.publicKey} + - Owner: ${owner} `) return { diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/store/setWriter.ts b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/store/setWriter.ts index 21f238211..24742416a 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/store/setWriter.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/commands/contracts/store/setWriter.ts @@ -50,6 +50,10 @@ export default class SetWriter extends SolanaCommand { const ocr2State = new PublicKey(this.flags.ocrState) const feedState = new PublicKey(input.transmissions) + logger.info( + `Generating data for setting store writer on Store (${storeState.toString()}) and Feed (${feedState.toString()})`, + ) + const [storeAuthority, _storeNonce] = await PublicKey.findProgramAddress( [Buffer.from(utils.bytes.utf8.encode('store')), ocr2State.toBuffer()], ocr2Program.programId, @@ -87,16 +91,8 @@ export default class SetWriter extends SolanaCommand { } execute = async () => { - const contract = getContract(CONTRACT_LIST.STORE, '') - const rawTx = await this.makeRawTransaction(this.wallet.payer.publicKey) - const tx = makeTx(rawTx) - logger.debug(tx) - const input = this.makeInput(this.flags.input) - const storeState = new PublicKey(input.store || this.flags.state) - const feedState = new PublicKey(input.transmissions) - logger.info(`Setting store writer on Store (${storeState.toString()}) and Feed (${feedState.toString()})`) - logger.loading('Sending tx...') - const txhash = await this.sendTx(tx, [this.wallet.payer], contract.idl) + const rawTx = await this.makeRawTransaction(this.wallet.publicKey) + const txhash = await this.signAndSendRawTx(rawTx) logger.success(`Writer set on tx hash: ${txhash}`) return { diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/index.ts b/gauntlet/packages/gauntlet-solana-contracts/src/index.ts index b59e60d9c..be1abebdd 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/index.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/index.ts @@ -1,10 +1,12 @@ +import { SendRawTx } from '@chainlink/gauntlet-solana' import Solana from './commands' import { makeAbstractCommand } from './commands/abstract' import { defaultFlags } from './lib/args' export { CONTRACT_LIST, getContract } from './lib/contracts' +export { makeTx } from './lib/utils' export const commands = { - custom: [...Solana], + custom: [...Solana, SendRawTx], loadDefaultFlags: () => defaultFlags, abstract: { findPolymorphic: () => undefined, diff --git a/gauntlet/packages/gauntlet-solana-contracts/src/lib/utils.ts b/gauntlet/packages/gauntlet-solana-contracts/src/lib/utils.ts index b54ad2133..e996035bb 100644 --- a/gauntlet/packages/gauntlet-solana-contracts/src/lib/utils.ts +++ b/gauntlet/packages/gauntlet-solana-contracts/src/lib/utils.ts @@ -1,4 +1,4 @@ -import { Transaction, TransactionInstruction } from '@solana/web3.js' +import { Transaction, TransactionCtorFields, TransactionInstruction } from '@solana/web3.js' import { RawTransaction } from '@chainlink/gauntlet-solana' import * as BufferLayout from '@solana/buffer-layout' @@ -12,7 +12,7 @@ export const divideIntoChunks = (arr: Array | Buffer, chunkSize: number): a return chunks } -export const makeTx = (rawTx: RawTransaction[]): Transaction => { +export const makeTx = (rawTx: RawTransaction[], opts?: TransactionCtorFields): Transaction => { return rawTx.reduce( (tx, meta) => tx.add( @@ -22,7 +22,7 @@ export const makeTx = (rawTx: RawTransaction[]): Transaction => { data: meta.data, }), ), - new Transaction(), + new Transaction(opts), ) } diff --git a/gauntlet/packages/gauntlet-solana/package.json b/gauntlet/packages/gauntlet-solana/package.json index 851647419..e8bbde926 100644 --- a/gauntlet/packages/gauntlet-solana/package.json +++ b/gauntlet/packages/gauntlet-solana/package.json @@ -26,6 +26,8 @@ }, "dependencies": { "@chainlink/gauntlet-core": "0.0.7", + "@ledgerhq/hw-app-solana": "^6.20.0", + "@ledgerhq/hw-transport-node-hid": "^6.20.0", "@project-serum/anchor": "^0.20.1", "@project-serum/borsh": "^0.2.2", "@solana/web3.js": "^1.30.2" diff --git a/gauntlet/packages/gauntlet-solana/src/commands/internal/sendRawTx.ts b/gauntlet/packages/gauntlet-solana/src/commands/internal/sendRawTx.ts new file mode 100644 index 000000000..f4e46f396 --- /dev/null +++ b/gauntlet/packages/gauntlet-solana/src/commands/internal/sendRawTx.ts @@ -0,0 +1,37 @@ +import { Result } from '@chainlink/gauntlet-core' +import { logger, prompt } from '@chainlink/gauntlet-core/dist/utils' +import { Message, sendAndConfirmRawTransaction, Transaction } from '@solana/web3.js' +import SolanaCommand from './solana' +import { TransactionResponse } from '../types' + +export default class SendRawTx extends SolanaCommand { + constructor(flags, args) { + super(flags, args) + + this.requireFlag('message', 'Include a base64 encoded message') + this.requireFlag('signature', 'Include a base58 encoded signature') + } + + execute = async () => { + const msg = Message.from(Buffer.from(this.flags.message, 'base64')) + const signature = this.flags.signature + + logger.log('Message', msg) + logger.log('Signature:', signature) + const transaction = Transaction.populate(msg, [signature]) + + await prompt('Continue sending transaction?') + const txHash = await sendAndConfirmRawTransaction(this.provider.connection, transaction.serialize()) + + logger.success(`Transaction sent with tx hash: ${txHash}`) + + return { + responses: [ + { + tx: this.wrapResponse(txHash, ''), + contract: '', + }, + ], + } as Result + } +} diff --git a/gauntlet/packages/gauntlet-solana/src/commands/internal/solana.ts b/gauntlet/packages/gauntlet-solana/src/commands/internal/solana.ts index 53fa880ef..f1b341eed 100644 --- a/gauntlet/packages/gauntlet-solana/src/commands/internal/solana.ts +++ b/gauntlet/packages/gauntlet-solana/src/commands/internal/solana.ts @@ -7,12 +7,17 @@ import { LAMPORTS_PER_SOL, PublicKey, TransactionSignature, + sendAndConfirmRawTransaction, } from '@solana/web3.js' import { withProvider, withWallet, withNetwork } from '../middlewares' import { RawTransaction, TransactionResponse } from '../types' -import { ProgramError, parseIdlErrors, Idl, Program, Provider, Wallet } from '@project-serum/anchor' +import { ProgramError, parseIdlErrors, Idl, Program, Provider } from '@project-serum/anchor' +import { SolanaWallet } from '../wallet' +import { logger } from '@chainlink/gauntlet-core/dist/utils' +import { makeTx } from '../../lib/utils' + export default abstract class SolanaCommand extends WriteCommand { - wallet: typeof Wallet + wallet: SolanaWallet provider: Provider abstract execute: () => Promise> makeRawTransaction: (signer: PublicKey) => Promise @@ -63,6 +68,36 @@ export default abstract class SolanaCommand extends WriteCommand => { + const recentBlockhash = (await this.provider.connection.getRecentBlockhash()).blockhash + const tx = makeTx(rawTxs, { + recentBlockhash, + feePayer: this.wallet.publicKey, + }) + if (extraSigners) { + tx.sign(...extraSigners) + } + const signedTx = await this.wallet.signTransaction(tx) + logger.loading('Sending tx...') + return await sendAndConfirmRawTransaction(this.provider.connection, signedTx.serialize()) + } + + sendTxWithIDL = (sendAction: (...args: any) => Promise, idl: Idl) => async ( + ...args + ): Promise => { + try { + return await sendAction(...args) + } catch (e) { + // Translate IDL error + const idlErrors = parseIdlErrors(idl) + let translatedErr = ProgramError.parse(e, idlErrors) + if (translatedErr === null) { + throw e + } + throw translatedErr + } + } + sendTx = async (tx: Transaction, signers: Keypair[], idl: Idl): Promise => { try { return await this.provider.send(tx, signers) diff --git a/gauntlet/packages/gauntlet-solana/src/commands/middlewares.ts b/gauntlet/packages/gauntlet-solana/src/commands/middlewares.ts index f62f78796..fa80f3af3 100644 --- a/gauntlet/packages/gauntlet-solana/src/commands/middlewares.ts +++ b/gauntlet/packages/gauntlet-solana/src/commands/middlewares.ts @@ -1,9 +1,11 @@ import { Middleware, Next } from '@chainlink/gauntlet-core' -import { assertions } from '@chainlink/gauntlet-core/dist/utils' +import { boolean } from '@chainlink/gauntlet-core/dist/lib/args' +import { assertions, logger } from '@chainlink/gauntlet-core/dist/utils' import { Provider } from '@project-serum/anchor' import { Connection, Keypair } from '@solana/web3.js' +import { DEFAULT_DERIVATION_PATH } from '../lib/constants' import SolanaCommand from './internal/solana' -const { Wallet } = require('@project-serum/anchor') // module exported dynamically from anchor +import { LedgerWallet, LocalWallet } from './wallet' const isValidURL = (url: string) => { var pattern = new RegExp('^(https?)://') @@ -20,11 +22,21 @@ export const withProvider: Middleware = (c: SolanaCommand, next: Next) => { return next() } -export const withWallet: Middleware = (c: SolanaCommand, next: Next) => { +export const withWallet: Middleware = async (c: SolanaCommand, next: Next) => { + if (c.flags.withLedger || boolean(process.env.WITH_LEDGER)) { + logger.info('Loading Ledger wallet') + const path = c.flags.ledgerPath || DEFAULT_DERIVATION_PATH + c.wallet = await LedgerWallet.create(path) + console.info(`Operator address is ${c.wallet.publicKey}`) + return next() + } + + logger.info('Loading Local wallet') const rawPK = process.env.PRIVATE_KEY assertions.assert(!!rawPK, `Missing PRIVATE_KEY, please add one`) - c.wallet = new Wallet(Keypair.fromSecretKey(Uint8Array.from(JSON.parse(rawPK)))) + const keypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(rawPK))) + c.wallet = await LocalWallet.create(keypair) console.info(`Operator address is ${c.wallet.publicKey}`) return next() } diff --git a/gauntlet/packages/gauntlet-solana/src/commands/wallet.ts b/gauntlet/packages/gauntlet-solana/src/commands/wallet.ts new file mode 100644 index 000000000..4195c1631 --- /dev/null +++ b/gauntlet/packages/gauntlet-solana/src/commands/wallet.ts @@ -0,0 +1,96 @@ +const { Wallet } = require('@project-serum/anchor') +import { Wallet as SerumWallet } from '@project-serum/anchor' +import { Keypair, PublicKey, Transaction } from '@solana/web3.js' +import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' +import Solana from '@ledgerhq/hw-app-solana' +import { logger } from '@chainlink/gauntlet-core/dist/utils' + +export enum WalletTypes { + LOCAL = 'local', + LEDGER = 'ledger', +} + +export abstract class SolanaWallet { + static create: (...args) => Promise + + abstract signTransaction: (tx: Transaction) => Promise + abstract signAllTransactions: (txs: Transaction[]) => Promise + abstract publicKey: PublicKey + abstract payer: Keypair + + abstract type: () => WalletTypes +} + +export class LocalWallet extends SolanaWallet { + wallet: typeof SerumWallet + + private constructor(wallet: typeof SerumWallet) { + super() + this.wallet = wallet + } + + static create = async (keypair: Keypair) => { + const wallet = new Wallet(keypair) + return new LocalWallet(wallet) + } + + signTransaction = (tx: Transaction) => this.wallet.signTransaction(tx) + signAllTransactions = (txs: Transaction[]) => this.wallet.signAllTransactions(txs) + + get publicKey() { + return this.wallet.payer.publicKey + } + + get payer() { + return this.wallet.payer + } + + type = () => WalletTypes.LOCAL +} + +export class LedgerWallet extends SolanaWallet { + publicKey: PublicKey + wallet: Solana + path: string + + private constructor(solanaLW: Solana, pubKey: PublicKey, path: string) { + super() + + this.wallet = solanaLW + this.publicKey = pubKey + this.path = path + } + + static create = async (path: string) => { + try { + const transport = await TransportNodeHid.create() + const solana = new Solana(transport) + const { address } = await solana.getAddress(path, false) + const pubkey = new PublicKey(address) + logger.info(`Ledger: Using ${pubkey.toString()}, derivationPath: ${path} (can be overridden with --ledgerPath)`) + return new LedgerWallet(solana, pubkey, path) + } catch (e) { + logger.error('Ledger: Could not access ledger. Is it unlocked and Solana app open?') + throw e + } + } + + signTransaction = async (tx: Transaction) => { + logger.info(`Ledger: Request to sign message`) + const msg = tx.serializeMessage() + const { signature } = await this.wallet.signTransaction(this.path, msg) + tx.addSignature(this.publicKey, signature) + return tx + } + + signAllTransactions = async (txs: Transaction[]) => { + logger.warn('Signing multiple transactions with Ledger') + return Promise.all(txs.map(this.signTransaction)) + } + + get payer(): Keypair { + throw new Error('Payer method not available on Ledger') + } + + type = () => WalletTypes.LEDGER +} diff --git a/gauntlet/packages/gauntlet-solana/src/index.ts b/gauntlet/packages/gauntlet-solana/src/index.ts index 20194ae3f..4e1c50cf7 100644 --- a/gauntlet/packages/gauntlet-solana/src/index.ts +++ b/gauntlet/packages/gauntlet-solana/src/index.ts @@ -1,6 +1,8 @@ import SolanaCommand from './commands/internal/solana' +import SendRawTx from './commands/internal/sendRawTx' import { waitExecute } from './lib/execute' import { TransactionResponse, RawTransaction } from './commands/types' import * as constants from './lib/constants' +import * as utils from './lib/utils' -export { SolanaCommand, waitExecute, TransactionResponse, constants, RawTransaction } +export { SolanaCommand, SendRawTx, waitExecute, TransactionResponse, constants, utils, RawTransaction } diff --git a/gauntlet/packages/gauntlet-solana/src/lib/constants.ts b/gauntlet/packages/gauntlet-solana/src/lib/constants.ts index f9c101cb3..216cbfe04 100644 --- a/gauntlet/packages/gauntlet-solana/src/lib/constants.ts +++ b/gauntlet/packages/gauntlet-solana/src/lib/constants.ts @@ -1 +1,3 @@ export const ADDRESS_ZERO = 'solana_address_zero' + +export const DEFAULT_DERIVATION_PATH = "44'/501'/0'/0/0" diff --git a/gauntlet/packages/gauntlet-solana/src/lib/utils.ts b/gauntlet/packages/gauntlet-solana/src/lib/utils.ts new file mode 100644 index 000000000..15117a6c5 --- /dev/null +++ b/gauntlet/packages/gauntlet-solana/src/lib/utils.ts @@ -0,0 +1,16 @@ +import { Transaction, TransactionInstruction, TransactionCtorFields } from '@solana/web3.js' +import { RawTransaction } from '..' + +export const makeTx = (rawTx: RawTransaction[], opts?: TransactionCtorFields): Transaction => { + return rawTx.reduce( + (tx, meta) => + tx.add( + new TransactionInstruction({ + programId: meta.programId, + keys: meta.accounts, + data: meta.data, + }), + ), + new Transaction(opts), + ) +} diff --git a/gauntlet/yarn.lock b/gauntlet/yarn.lock index b9759ff59..f1e9a4162 100644 --- a/gauntlet/yarn.lock +++ b/gauntlet/yarn.lock @@ -734,6 +734,70 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@ledgerhq/devices@^6.20.0": + version "6.20.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-6.20.0.tgz#4280aaa5dc16f821ecab9ee8ae882299411ba5b7" + integrity sha512-WehM7HGdb+nSUzyUlz1t2qJ8Tg4I+rQkOJJsx0/Dpjkx6/+1hHcX6My/apPuwh39qahqwYhjszq0H1YzGDS0Yg== + dependencies: + "@ledgerhq/errors" "^6.10.0" + "@ledgerhq/logs" "^6.10.0" + rxjs "6" + semver "^7.3.5" + +"@ledgerhq/errors@^6.10.0": + version "6.10.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.10.0.tgz#dda9127b65f653fbb2f74a55e8f0e550d69de6e4" + integrity sha512-fQFnl2VIXh9Yd41lGjReCeK+Q2hwxQJvLZfqHnKqWapTz68NHOv5QcI0OHuZVNEbv0xhgdLhi5b65kgYeQSUVg== + +"@ledgerhq/hw-app-solana@^6.20.0": + version "6.20.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-app-solana/-/hw-app-solana-6.20.0.tgz#95f173f1a27d2106c7ba8549e7ac9b52facf4bae" + integrity sha512-LkQUg58KfhBDqXgIfTR3EL+/oxnBDFzCgKSv7RZZtGbPe5ZXeHd6LajaYbNppoVbfapB7nnLRuV831hxlH2bcQ== + dependencies: + "@ledgerhq/errors" "^6.10.0" + "@ledgerhq/hw-transport" "^6.20.0" + "@ledgerhq/logs" "^6.10.0" + bip32-path "^0.4.2" + +"@ledgerhq/hw-transport-node-hid-noevents@^6.20.0": + version "6.20.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid-noevents/-/hw-transport-node-hid-noevents-6.20.0.tgz#9baa5023a7191de41ca96ef7ab345225ab84d05d" + integrity sha512-JeY41pMwr5qHkghocLzTXMxZtui8dEEm4Hdx2UcjNuP1KglVwJ2U3IJgF/cjBy6OkkeL8o+kqeJTSQ9XJ/hVVg== + dependencies: + "@ledgerhq/devices" "^6.20.0" + "@ledgerhq/errors" "^6.10.0" + "@ledgerhq/hw-transport" "^6.20.0" + "@ledgerhq/logs" "^6.10.0" + node-hid "2.1.1" + +"@ledgerhq/hw-transport-node-hid@^6.20.0": + version "6.20.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-node-hid/-/hw-transport-node-hid-6.20.0.tgz#b42b19acf58ca91c141ee8640d376f19bbd8ce83" + integrity sha512-xqTs0VyEPqCFZv0Lj9bLLbfmYbwTePOF6FkaYZ4XM/HDF3hhUCfiFBPnY8KgzQH5cCq/kVTSQqw2o0PR7UTEDw== + dependencies: + "@ledgerhq/devices" "^6.20.0" + "@ledgerhq/errors" "^6.10.0" + "@ledgerhq/hw-transport" "^6.20.0" + "@ledgerhq/hw-transport-node-hid-noevents" "^6.20.0" + "@ledgerhq/logs" "^6.10.0" + lodash "^4.17.21" + node-hid "2.1.1" + usb "^1.7.0" + +"@ledgerhq/hw-transport@^6.20.0": + version "6.20.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-6.20.0.tgz#16e84c99fca2d10f637c0e36c87088322479a488" + integrity sha512-5KS0Y6CbWRDOv3FgNIfk53ViQOIZqMxAw0RuOexreW5GMwuYfK7ddGi4142qcu7YrxkGo7cNe42wBbx1hdXl0Q== + dependencies: + "@ledgerhq/devices" "^6.20.0" + "@ledgerhq/errors" "^6.10.0" + events "^3.3.0" + +"@ledgerhq/logs@^6.10.0": + version "6.10.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-6.10.0.tgz#c012c1ecc1a0e53d50e6af381618dca5268461c1" + integrity sha512-lLseUPEhSFUXYTKj6q7s2O3s2vW2ebgA11vMAlKodXGf5AFw4zUoEbTz9CoFOC9jS6xY4Qr8BmRnxP/odT4Uuw== + "@manypkg/find-root@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@manypkg/find-root/-/find-root-1.1.0.tgz#a62d8ed1cd7e7d4c11d9d52a8397460b5d4ad29f" @@ -1409,6 +1473,18 @@ better-path-resolve@1.0.0: dependencies: is-windows "^1.0.0" +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +bip32-path@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/bip32-path/-/bip32-path-0.4.2.tgz#5db0416ad6822712f077836e2557b8697c0c7c99" + integrity sha1-XbBBataCJxLwd4NuJVe4aXwMfJk= + bl@^4.0.3: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -2167,6 +2243,11 @@ eventemitter3@^4.0.7: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + exec-sh@^0.3.2: version "0.3.6" resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.6.tgz#ff264f9e325519a60cb5e273692943483cca63bc" @@ -2331,6 +2412,11 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" @@ -3582,7 +3668,7 @@ lodash.startcase@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.startcase/-/lodash.startcase-4.4.0.tgz#9436e34ed26093ed7ffae1936144350915d9add8" integrity sha1-lDbjTtJgk+1/+uGTYUQ1CRXZrdg= -lodash@4.x, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.7.0: +lodash@4.x, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3848,7 +3934,7 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -node-abi@^2.7.0: +node-abi@^2.21.0, node-abi@^2.7.0: version "2.30.1" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.30.1.tgz#c437d4b1fe0e285aaf290d45b45d4d7afedac4cf" integrity sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w== @@ -3860,6 +3946,16 @@ node-addon-api@^2.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== +node-addon-api@^3.0.2: + version "3.2.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" + integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== + +node-addon-api@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" + integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== + node-fetch@2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" @@ -3877,6 +3973,15 @@ node-gyp-build@^4.2.0, node-gyp-build@^4.3.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== +node-hid@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/node-hid/-/node-hid-2.1.1.tgz#f83c8aa0bb4e6758b5f7383542477da93f67359d" + integrity sha512-Skzhqow7hyLZU93eIPthM9yjot9lszg9xrKxESleEs05V2NcbUptZc5HFqzjOkSmL0sFlZFr3kmvaYebx06wrw== + dependencies: + bindings "^1.5.0" + node-addon-api "^3.0.2" + prebuild-install "^6.0.0" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -4233,6 +4338,25 @@ prebuild-install@6.0.1: tunnel-agent "^0.6.0" which-pm-runs "^1.0.0" +prebuild-install@^6.0.0: + version "6.1.4" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.4.tgz#ae3c0142ad611d58570b89af4986088a4937e00f" + integrity sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ== + dependencies: + detect-libc "^1.0.3" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^2.21.0" + npmlog "^4.0.1" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^3.0.3" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + preferred-pm@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/preferred-pm/-/preferred-pm-3.0.3.tgz#1b6338000371e3edbce52ef2e4f65eb2e73586d6" @@ -4518,6 +4642,13 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +rxjs@6: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + safe-buffer@^5.0.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -5139,6 +5270,11 @@ tslib@2.1.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== +tslib@^1.9.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + tslib@^2.0.3: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" @@ -5245,6 +5381,14 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= +usb@^1.7.0: + version "1.9.2" + resolved "https://registry.yarnpkg.com/usb/-/usb-1.9.2.tgz#fb6b36f744ecc707a196c45a6ec72442cb6f2b73" + integrity sha512-dryNz030LWBPAf6gj8vyq0Iev3vPbCLHCT8dBw3gQRXRzVNsIdeuU+VjPp3ksmSPkeMAl1k+kQ14Ij0QHyeiAg== + dependencies: + node-addon-api "^4.2.0" + node-gyp-build "^4.3.0" + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"