From 95b85e1c3a0227442e6724f17139b9a0b2090ac0 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Wed, 10 Aug 2022 22:46:58 +0100 Subject: [PATCH] feat: update getTransaction to support versioned transactions --- web3.js/src/connection.ts | 72 ++++++++++++++++++++++++++++++--- web3.js/test/connection.test.ts | 68 ++++++++++++++++++++++++++----- 2 files changed, 124 insertions(+), 16 deletions(-) diff --git a/web3.js/src/connection.ts b/web3.js/src/connection.ts index 0fb2e5a9b19b22..6244328d35a4e5 100644 --- a/web3.js/src/connection.ts +++ b/web3.js/src/connection.ts @@ -32,8 +32,12 @@ import {NonceAccount} from './nonce-account'; import {PublicKey} from './publickey'; import {Signer} from './keypair'; import {MS_PER_SLOT} from './timing'; -import {Transaction, TransactionStatus} from './transaction'; -import {Message} from './message'; +import { + Transaction, + TransactionStatus, + TransactionVersion, +} from './transaction'; +import {Message, MessageV0, VersionedMessage} from './message'; import {AddressLookupTableAccount} from './programs/address-lookup-table/state'; import assert from './utils/assert'; import {sleep} from './utils/sleep'; @@ -862,6 +866,8 @@ export type ConfirmedTransactionMeta = { postTokenBalances?: Array | null; /** The error result of transaction processing */ err: TransactionError | null; + /** The collection of addresses loaded using address lookup tables */ + loadedAddresses?: LoadedAddresses; }; /** @@ -873,7 +879,7 @@ export type TransactionResponse = { /** The transaction */ transaction: { /** The transaction message */ - message: Message; + message: VersionedMessage; /** The transaction signatures */ signatures: string[]; }; @@ -881,6 +887,8 @@ export type TransactionResponse = { meta: ConfirmedTransactionMeta | null; /** The unix timestamp of when the transaction was processed */ blockTime?: number | null; + /** The transaction version */ + version?: TransactionVersion; }; /** @@ -935,6 +943,18 @@ export type ParsedInstruction = { parsed: any; }; +/** + * A parsed address table lookup + */ +export type ParsedAddressTableLookup = { + /** Address lookup table account key */ + accountKey: PublicKey; + /** Parsed instruction info */ + writableIndexes: number[]; + /** Parsed instruction info */ + readonlyIndexes: number[]; +}; + /** * A parsed transaction message */ @@ -945,6 +965,8 @@ export type ParsedMessage = { instructions: (ParsedInstruction | PartiallyDecodedInstruction)[]; /** Recent blockhash */ recentBlockhash: string; + /** Address table lookups used to load additional accounts */ + addressTableLookups?: ParsedAddressTableLookup[] | null; }; /** @@ -976,6 +998,8 @@ export type ParsedTransactionWithMeta = { meta: ParsedTransactionMeta | null; /** The unix timestamp of when the transaction was processed */ blockTime?: number | null; + /** The version of the transaction message */ + version?: TransactionVersion; }; /** @@ -1722,6 +1746,12 @@ const GetSignatureStatusesRpcResult = jsonRpcResultAndContext( */ const GetMinimumBalanceForRentExemptionRpcResult = jsonRpcResult(number()); +const AddressTableLookupStruct = pick({ + accountKey: PublicKeyFromString, + writableIndexes: array(number()), + readonlyIndexes: array(number()), +}); + const ConfirmedTransactionResult = pick({ signatures: array(string()), message: pick({ @@ -1739,6 +1769,7 @@ const ConfirmedTransactionResult = pick({ }), ), recentBlockhash: string(), + addressTableLookups: optional(array(AddressTableLookupStruct)), }), }); @@ -1799,6 +1830,7 @@ const ParsedConfirmedTransactionResult = pick({ ), instructions: array(ParsedOrRawInstruction), recentBlockhash: string(), + addressTableLookups: optional(nullable(array(AddressTableLookupStruct))), }), }); @@ -1946,6 +1978,8 @@ const GetBlockSignaturesRpcResult = jsonRpcResult( ), ); +const TransactionVersionStruct = union([literal(0), literal('legacy')]); + /** * Expected JSON RPC response for the "getTransaction" message */ @@ -1956,6 +1990,7 @@ const GetTransactionRpcResult = jsonRpcResult( meta: ConfirmedTransactionMetaResult, blockTime: optional(nullable(number())), transaction: ConfirmedTransactionResult, + version: optional(TransactionVersionStruct), }), ), ); @@ -1970,6 +2005,7 @@ const GetParsedTransactionRpcResult = jsonRpcResult( transaction: ParsedConfirmedTransactionResult, meta: nullable(ParsedConfirmedTransactionMetaResult), blockTime: optional(nullable(number())), + version: optional(TransactionVersionStruct), }), ), ); @@ -3742,6 +3778,10 @@ export class Connection { signature: string, rawConfig?: GetTransactionConfig, ): Promise { + if (rawConfig && !('maxSupportedTransactionVersion' in rawConfig)) { + rawConfig.maxSupportedTransactionVersion = 0; + } + const {commitment, config} = extractCommitmentFromConfig(rawConfig); const args = this._buildArgsAtLeastConfirmed( [signature], @@ -3758,11 +3798,31 @@ export class Connection { const result = res.result; if (!result) return result; + const messageResponse = result.transaction.message; + let message: VersionedMessage; + if (result.version === 0) { + message = new MessageV0({ + header: messageResponse.header, + staticAccountKeys: messageResponse.accountKeys.map( + address => new PublicKey(address), + ), + recentBlockhash: messageResponse.recentBlockhash, + compiledInstructions: messageResponse.instructions.map(ix => ({ + programIdIndex: ix.programIdIndex, + accountKeyIndexes: ix.accounts, + data: bs58.decode(ix.data), + })), + addressTableLookups: messageResponse.addressTableLookups!, + }); + } else { + message = new Message(messageResponse); + } + return { ...result, transaction: { ...result.transaction, - message: new Message(result.transaction.message), + message, }, }; } @@ -3773,7 +3833,7 @@ export class Connection { async getParsedTransaction( signature: TransactionSignature, commitmentOrConfig?: GetTransactionConfig | Finality, - ): Promise { + ): Promise { const {commitment, config} = extractCommitmentFromConfig(commitmentOrConfig); const args = this._buildArgsAtLeastConfirmed( @@ -3796,7 +3856,7 @@ export class Connection { async getParsedTransactions( signatures: TransactionSignature[], commitmentOrConfig?: GetTransactionConfig | Finality, - ): Promise<(ParsedConfirmedTransaction | null)[]> { + ): Promise<(ParsedTransactionWithMeta | null)[]> { const {commitment, config} = extractCommitmentFromConfig(commitmentOrConfig); const batch = signatures.map(signature => { diff --git a/web3.js/test/connection.test.ts b/web3.js/test/connection.test.ts index 5cc8fc7d491cfc..51a2caef016dcb 100644 --- a/web3.js/test/connection.test.ts +++ b/web3.js/test/connection.test.ts @@ -4400,6 +4400,13 @@ describe('Connection', function () { const transferIxData = encodeData(SYSTEM_INSTRUCTION_LAYOUTS.Transfer, { lamports: BigInt(LAMPORTS_PER_SOL), }); + const addressTableLookups = [ + { + accountKey: lookupTableKey, + writableIndexes: [0], + readonlyIndexes: [], + }, + ]; const transaction = new VersionedTransaction( new MessageV0({ header: { @@ -4416,13 +4423,7 @@ describe('Connection', function () { data: transferIxData, }, ], - addressTableLookups: [ - { - accountKey: lookupTableKey, - writableIndexes: [0], - readonlyIndexes: [], - }, - ], + addressTableLookups, }), ); transaction.sign([payer]); @@ -4438,16 +4439,63 @@ describe('Connection', function () { blockhash, lastValidBlockHeight, }, - 'processed', + 'confirmed', ); const transferToKey = lookupTableAddresses[0]; const transferToAccount = await connection.getAccountInfo( transferToKey, - 'processed', + 'confirmed', ); expect(transferToAccount?.lamports).to.be.eq(LAMPORTS_PER_SOL); + + // fails without maxSupportedTransactionVersion option + await expect( + connection.getTransaction(signature, { + maxSupportedTransactionVersion: undefined, + commitment: 'confirmed', + }), + ).to.be.rejectedWith( + 'failed to get transaction: Transaction version (0) is not supported', + ); + + // fetch v0 transaction + { + const fetchedTransaction = await connection.getTransaction( + signature, + {commitment: 'confirmed'}, + ); + expect(fetchedTransaction).to.not.be.null; + expect(fetchedTransaction?.version).to.eq(0); + expect(fetchedTransaction?.meta?.loadedAddresses).to.eql({ + readonly: [], + writable: [lookupTableAddresses[0]], + }); + expect( + fetchedTransaction?.transaction.message.addressTableLookups, + ).to.eql(addressTableLookups); + } + + // fetch v0 parsed transaction + { + const parsedTransaction = await connection.getParsedTransaction( + signature, + { + maxSupportedTransactionVersion: 0, + commitment: 'confirmed', + }, + ); + expect(parsedTransaction).to.not.be.null; + expect(parsedTransaction?.version).to.eq(0); + expect(parsedTransaction?.meta?.loadedAddresses).to.eql({ + readonly: [], + writable: [lookupTableAddresses[0]], + }); + expect( + parsedTransaction?.transaction.message.addressTableLookups, + ).to.eql(addressTableLookups); + } } - }); + }).timeout(5 * 1000); } });