From 204f2724128a31358e6f78f02834bb6bb7c92141 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Tue, 6 Sep 2022 22:53:42 -0500 Subject: [PATCH] feat: support versioned txs in `sendTransaction` and `simulateTransaction` (#27528) * feat: support versioned txs in sendTransaction and simulateTransaction * chore: add docs for simulation config parameters --- web3.js/src/connection.ts | 108 +++++++++++++++++++++++++++++++- web3.js/test/connection.test.ts | 77 +++++++++++++++++++++++ 2 files changed, 183 insertions(+), 2 deletions(-) diff --git a/web3.js/src/connection.ts b/web3.js/src/connection.ts index 28555fbf34..5789b27f24 100644 --- a/web3.js/src/connection.ts +++ b/web3.js/src/connection.ts @@ -36,6 +36,7 @@ import { Transaction, TransactionStatus, TransactionVersion, + VersionedTransaction, } from './transaction'; import {Message, MessageHeader, MessageV0, VersionedMessage} from './message'; import {AddressLookupTableAccount} from './programs/address-lookup-table/state'; @@ -801,6 +802,22 @@ export type TransactionReturnData = { data: [string, TransactionReturnDataEncoding]; }; +export type SimulateTransactionConfig = { + /** Optional parameter used to enable signature verification before simulation */ + sigVerify?: boolean; + /** Optional parameter used to replace the simulated transaction's recent blockhash with the latest blockhash */ + replaceRecentBlockhash?: boolean; + /** Optional parameter used to set the commitment level when selecting the latest block */ + commitment?: Commitment; + /** Optional parameter used to specify a list of account addresses to return post simulation state for */ + accounts?: { + encoding: 'base64'; + addresses: string[]; + }; + /** Optional parameter used to specify the minimum block slot that can be used for simulation */ + minContextSlot?: number; +}; + export type SimulatedTransactionResponse = { err: TransactionError | string | null; logs: Array | null; @@ -4625,12 +4642,58 @@ export class Connection { /** * Simulate a transaction + * + * @deprecated Instead, call {@link simulateTransaction} with {@link + * VersionedTransaction} and {@link SimulateTransactionConfig} parameters */ - async simulateTransaction( + simulateTransaction( transactionOrMessage: Transaction | Message, signers?: Array, includeAccounts?: boolean | Array, + ): Promise>; + + /** + * Simulate a transaction + */ + // eslint-disable-next-line no-dupe-class-members + simulateTransaction( + transaction: VersionedTransaction, + config?: SimulateTransactionConfig, + ): Promise>; + + /** + * Simulate a transaction + */ + // eslint-disable-next-line no-dupe-class-members + async simulateTransaction( + transactionOrMessage: VersionedTransaction | Transaction | Message, + configOrSigners?: SimulateTransactionConfig | Array, + includeAccounts?: boolean | Array, ): Promise> { + if ('message' in transactionOrMessage) { + const versionedTx = transactionOrMessage; + const wireTransaction = versionedTx.serialize(); + const encodedTransaction = + Buffer.from(wireTransaction).toString('base64'); + if (Array.isArray(configOrSigners) || includeAccounts !== undefined) { + throw new Error('Invalid arguments'); + } + + const config: any = configOrSigners || {}; + config.encoding = 'base64'; + if (!('commitment' in config)) { + config.commitment = this.commitment; + } + + const args = [encodedTransaction, config]; + const unsafeRes = await this._rpcRequest('simulateTransaction', args); + const res = create(unsafeRes, SimulatedTransactionResponseStruct); + if ('error' in res) { + throw new Error('failed to simulate transaction: ' + res.error.message); + } + return res.result; + } + let transaction; if (transactionOrMessage instanceof Transaction) { let originalTx: Transaction = transactionOrMessage; @@ -4645,6 +4708,11 @@ export class Connection { transaction._message = transaction._json = undefined; } + if (configOrSigners !== undefined && !Array.isArray(configOrSigners)) { + throw new Error('Invalid arguments'); + } + + const signers = configOrSigners; if (transaction.nonceInfo && signers) { transaction.sign(...signers); } else { @@ -4731,12 +4799,48 @@ export class Connection { /** * Sign and send a transaction + * + * @deprecated Instead, call {@link sendTransaction} with a {@link + * VersionedTransaction} */ - async sendTransaction( + sendTransaction( transaction: Transaction, signers: Array, options?: SendOptions, + ): Promise; + + /** + * Send a signed transaction + */ + // eslint-disable-next-line no-dupe-class-members + sendTransaction( + transaction: VersionedTransaction, + options?: SendOptions, + ): Promise; + + /** + * Sign and send a transaction + */ + // eslint-disable-next-line no-dupe-class-members + async sendTransaction( + transaction: VersionedTransaction | Transaction, + signersOrOptions?: Array | SendOptions, + options?: SendOptions, ): Promise { + if ('message' in transaction) { + if (signersOrOptions && Array.isArray(signersOrOptions)) { + throw new Error('Invalid arguments'); + } + + const wireTransaction = transaction.serialize(); + return await this.sendRawTransaction(wireTransaction, options); + } + + if (signersOrOptions === undefined || !Array.isArray(signersOrOptions)) { + throw new Error('Invalid arguments'); + } + + const signers = signersOrOptions; if (transaction.nonceInfo) { transaction.sign(...signers); } else { diff --git a/web3.js/test/connection.test.ts b/web3.js/test/connection.test.ts index f32195ec99..c36798b25f 100644 --- a/web3.js/test/connection.test.ts +++ b/web3.js/test/connection.test.ts @@ -3813,6 +3813,83 @@ describe('Connection', function () { const {value} = await connection.getStakeMinimumDelegation(); expect(value).to.be.a('number'); }); + + it('sendTransaction', async () => { + const connection = new Connection(url, 'confirmed'); + const payer = Keypair.generate(); + + await helpers.airdrop({ + connection, + address: payer.publicKey, + amount: LAMPORTS_PER_SOL, + }); + + const recentBlockhash = await ( + await helpers.latestBlockhash({connection}) + ).blockhash; + + const versionedTx = new VersionedTransaction( + new Message({ + header: { + numRequiredSignatures: 1, + numReadonlySignedAccounts: 0, + numReadonlyUnsignedAccounts: 0, + }, + recentBlockhash, + instructions: [], + accountKeys: [payer.publicKey.toBase58()], + }), + ); + + versionedTx.sign([payer]); + await connection.sendTransaction(versionedTx); + }); + + it('simulateTransaction', async () => { + const connection = new Connection(url, 'confirmed'); + const payer = Keypair.generate(); + + await helpers.airdrop({ + connection, + address: payer.publicKey, + amount: LAMPORTS_PER_SOL, + }); + + const recentBlockhash = await ( + await helpers.latestBlockhash({connection}) + ).blockhash; + + const versionedTx = new VersionedTransaction( + new Message({ + header: { + numRequiredSignatures: 1, + numReadonlySignedAccounts: 0, + numReadonlyUnsignedAccounts: 0, + }, + recentBlockhash, + instructions: [], + accountKeys: [payer.publicKey.toBase58()], + }), + ); + + const response = await connection.simulateTransaction(versionedTx, { + accounts: { + encoding: 'base64', + addresses: [payer.publicKey.toBase58()], + }, + }); + expect(response.value.err).to.be.null; + expect(response.value.accounts).to.eql([ + { + data: ['', 'base64'], + executable: false, + lamports: LAMPORTS_PER_SOL - 5000, + owner: SystemProgram.programId.toBase58(), + rentEpoch: 0, + }, + ]); + }); + it('simulate transaction with message', async () => { connection._commitment = 'confirmed';