From 332c02e18cfe6cec3fdd90d5db19a68ee66d59a9 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Tue, 11 Oct 2022 15:25:16 +0800 Subject: [PATCH] feat: add getParsedBlock method to Connection --- web3.js/src/connection.ts | 88 +++++++ web3.js/test/connection.test.ts | 402 +++++++++++++++++--------------- 2 files changed, 302 insertions(+), 188 deletions(-) diff --git a/web3.js/src/connection.ts b/web3.js/src/connection.ts index 2741dea65e0294..a03b7bdab38d48 100644 --- a/web3.js/src/connection.ts +++ b/web3.js/src/connection.ts @@ -1143,6 +1143,40 @@ export type BlockResponse = { blockTime: number | null; }; +/** + * A block with parsed transactions + */ +export type ParsedBlockResponse = { + /** Blockhash of this block */ + blockhash: Blockhash; + /** Blockhash of this block's parent */ + previousBlockhash: Blockhash; + /** Slot index of this block's parent */ + parentSlot: number; + /** Vector of transactions with status meta and original message */ + transactions: Array<{ + /** The details of the transaction */ + transaction: ParsedTransaction; + /** Metadata produced from the transaction */ + meta: ParsedTransactionMeta | null; + /** The transaction version */ + version?: TransactionVersion; + }>; + /** Vector of block rewards */ + rewards?: Array<{ + /** Public key of reward recipient */ + pubkey: string; + /** Reward value in lamports */ + lamports: number; + /** Account balance after reward is applied */ + postBalance: number | null; + /** Type of reward received */ + rewardType: string | null; + }>; + /** The unix timestamp of when the block was processed */ + blockTime: number | null; +}; + /** * A processed block fetched from the RPC API */ @@ -2081,6 +2115,38 @@ const GetBlockRpcResult = jsonRpcResult( ), ); +/** + * Expected parsed JSON RPC response for the "getBlock" message + */ +const GetParsedBlockRpcResult = jsonRpcResult( + nullable( + pick({ + blockhash: string(), + previousBlockhash: string(), + parentSlot: number(), + transactions: array( + pick({ + transaction: ParsedConfirmedTransactionResult, + meta: nullable(ParsedConfirmedTransactionMetaResult), + version: optional(TransactionVersionStruct), + }), + ), + rewards: optional( + array( + pick({ + pubkey: string(), + lamports: number(), + postBalance: nullable(number()), + rewardType: nullable(string()), + }), + ), + ), + blockTime: nullable(number()), + blockHeight: nullable(number()), + }), + ), +); + /** * Expected JSON RPC response for the "getConfirmedBlock" message * @@ -3878,6 +3944,28 @@ export class Connection { }; } + /** + * Fetch parsed transaction details for a confirmed or finalized block + */ + async getParsedBlock( + slot: number, + rawConfig?: GetVersionedBlockConfig, + ): Promise { + const {commitment, config} = extractCommitmentFromConfig(rawConfig); + const args = this._buildArgsAtLeastConfirmed( + [slot], + commitment as Finality, + 'jsonParsed', + config, + ); + const unsafeRes = await this._rpcRequest('getBlock', args); + const res = create(unsafeRes, GetParsedBlockRpcResult); + if ('error' in res) { + throw new SolanaJSONRPCError(res.error, 'failed to get block'); + } + return res.result; + } + /* * Returns the current block height of the node */ diff --git a/web3.js/test/connection.test.ts b/web3.js/test/connection.test.ts index a5f8724a2cd44b..2b3ddddd905c01 100644 --- a/web3.js/test/connection.test.ts +++ b/web3.js/test/connection.test.ts @@ -4330,7 +4330,7 @@ describe('Connection', function () { .fill(0) .map(() => Keypair.generate().publicKey); - describe('address lookup table program', () => { + describe.only('address lookup table program', () => { const connection = new Connection(url); const payer = Keypair.generate(); @@ -4413,214 +4413,240 @@ describe('Connection', function () { }); }); - describe('v0 transaction', () => { - const connection = new Connection(url); - const payer = Keypair.generate(); + describe + .only('v0 transaction', () => { + const connection = new Connection(url); + const payer = Keypair.generate(); - before(async () => { - await helpers.airdrop({ - connection, - address: payer.publicKey, - amount: 10 * LAMPORTS_PER_SOL, + before(async () => { + await helpers.airdrop({ + connection, + address: payer.publicKey, + amount: 10 * LAMPORTS_PER_SOL, + }); }); - }); - - // wait for lookup table to be usable - before(async () => { - const lookupTableResponse = await connection.getAddressLookupTable( - lookupTableKey, - { - commitment: 'processed', - }, - ); - const lookupTableAccount = lookupTableResponse.value; - if (!lookupTableAccount) { - expect(lookupTableAccount).to.be.ok; - return; - } + // wait for lookup table to be usable + before(async () => { + const lookupTableResponse = await connection.getAddressLookupTable( + lookupTableKey, + { + commitment: 'processed', + }, + ); - // eslint-disable-next-line no-constant-condition - while (true) { - const latestSlot = await connection.getSlot('confirmed'); - if (latestSlot > lookupTableAccount.state.lastExtendedSlot) { - break; - } else { - console.log('Waiting for next slot...'); - await sleep(500); + const lookupTableAccount = lookupTableResponse.value; + if (!lookupTableAccount) { + expect(lookupTableAccount).to.be.ok; + return; } - } - }); - let signature; - let addressTableLookups; - it('send and confirm', async () => { - const {blockhash, lastValidBlockHeight} = - await connection.getLatestBlockhash(); - const transferIxData = encodeData(SYSTEM_INSTRUCTION_LAYOUTS.Transfer, { - lamports: BigInt(LAMPORTS_PER_SOL), + // eslint-disable-next-line no-constant-condition + while (true) { + const latestSlot = await connection.getSlot('confirmed'); + if (latestSlot > lookupTableAccount.state.lastExtendedSlot) { + break; + } else { + console.log('Waiting for next slot...'); + await sleep(500); + } + } }); - addressTableLookups = [ - { - accountKey: lookupTableKey, - writableIndexes: [0], - readonlyIndexes: [], - }, - ]; - const transaction = new VersionedTransaction( - new MessageV0({ - header: { - numRequiredSignatures: 1, - numReadonlySignedAccounts: 0, - numReadonlyUnsignedAccounts: 1, + + let signature; + let addressTableLookups; + it('send and confirm', async () => { + const {blockhash, lastValidBlockHeight} = + await connection.getLatestBlockhash(); + const transferIxData = encodeData( + SYSTEM_INSTRUCTION_LAYOUTS.Transfer, + { + lamports: BigInt(LAMPORTS_PER_SOL), }, - staticAccountKeys: [payer.publicKey, SystemProgram.programId], - recentBlockhash: blockhash, - compiledInstructions: [ - { - programIdIndex: 1, - accountKeyIndexes: [0, 2], - data: transferIxData, + ); + addressTableLookups = [ + { + accountKey: lookupTableKey, + writableIndexes: [0], + readonlyIndexes: [], + }, + ]; + const transaction = new VersionedTransaction( + new MessageV0({ + header: { + numRequiredSignatures: 1, + numReadonlySignedAccounts: 0, + numReadonlyUnsignedAccounts: 1, }, - ], - addressTableLookups, - }), - ); - transaction.sign([payer]); - signature = bs58.encode(transaction.signatures[0]); - const serializedTransaction = transaction.serialize(); - await connection.sendRawTransaction(serializedTransaction, { - preflightCommitment: 'confirmed', + staticAccountKeys: [payer.publicKey, SystemProgram.programId], + recentBlockhash: blockhash, + compiledInstructions: [ + { + programIdIndex: 1, + accountKeyIndexes: [0, 2], + data: transferIxData, + }, + ], + addressTableLookups, + }), + ); + transaction.sign([payer]); + signature = bs58.encode(transaction.signatures[0]); + const serializedTransaction = transaction.serialize(); + await connection.sendRawTransaction(serializedTransaction, { + preflightCommitment: 'confirmed', + }); + + await connection.confirmTransaction( + { + signature, + blockhash, + lastValidBlockHeight, + }, + 'confirmed', + ); + + const transferToKey = lookupTableAddresses[0]; + const transferToAccount = await connection.getAccountInfo( + transferToKey, + 'confirmed', + ); + expect(transferToAccount?.lamports).to.be.eq(LAMPORTS_PER_SOL); }); - await connection.confirmTransaction( - { - signature, - blockhash, - lastValidBlockHeight, - }, - 'confirmed', - ); + it('getTransaction (failure)', async () => { + await expect( + connection.getTransaction(signature, { + commitment: 'confirmed', + }), + ).to.be.rejectedWith( + 'failed to get transaction: Transaction version (0) is not supported', + ); + }); - const transferToKey = lookupTableAddresses[0]; - const transferToAccount = await connection.getAccountInfo( - transferToKey, - 'confirmed', - ); - expect(transferToAccount?.lamports).to.be.eq(LAMPORTS_PER_SOL); - }); + let transactionSlot; + it('getTransaction', async () => { + // fetch v0 transaction + const fetchedTransaction = await connection.getTransaction( + signature, + { + commitment: 'confirmed', + maxSupportedTransactionVersion: 0, + }, + ); + if (fetchedTransaction === null) { + expect(fetchedTransaction).to.not.be.null; + return; + } + transactionSlot = fetchedTransaction.slot; + expect(fetchedTransaction.version).to.eq(0); + expect(fetchedTransaction.meta?.loadedAddresses).to.eql({ + readonly: [], + writable: [lookupTableAddresses[0]], + }); + expect(fetchedTransaction.meta?.computeUnitsConsumed).to.not.be + .undefined; + expect( + fetchedTransaction.transaction.message.addressTableLookups, + ).to.eql(addressTableLookups); + }); - it('getTransaction (failure)', async () => { - await expect( - connection.getTransaction(signature, { - commitment: 'confirmed', - }), - ).to.be.rejectedWith( - 'failed to get transaction: Transaction version (0) is not supported', - ); - }); + it('getParsedTransaction (failure)', async () => { + await expect( + connection.getParsedTransaction(signature, { + commitment: 'confirmed', + }), + ).to.be.rejectedWith( + 'failed to get transaction: Transaction version (0) is not supported', + ); + }); - let transactionSlot; - it('getTransaction', async () => { - // fetch v0 transaction - const fetchedTransaction = await connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, + it('getParsedTransaction', async () => { + const parsedTransaction = await connection.getParsedTransaction( + signature, + { + commitment: 'confirmed', + maxSupportedTransactionVersion: 0, + }, + ); + expect(parsedTransaction).to.not.be.null; + expect(parsedTransaction?.version).to.eq(0); + // loaded addresses are not returned for parsed transactions + expect(parsedTransaction?.meta?.loadedAddresses).to.be.undefined; + expect(parsedTransaction?.meta?.computeUnitsConsumed).to.not.be + .undefined; + expect( + parsedTransaction?.transaction.message.addressTableLookups, + ).to.eql(addressTableLookups); + expect(parsedTransaction?.transaction.message.accountKeys).to.eql([ + { + pubkey: payer.publicKey, + signer: true, + writable: true, + source: 'transaction', + }, + { + pubkey: SystemProgram.programId, + signer: false, + writable: false, + source: 'transaction', + }, + { + pubkey: lookupTableAddresses[0], + signer: false, + writable: true, + source: 'lookupTable', + }, + ]); }); - if (fetchedTransaction === null) { - expect(fetchedTransaction).to.not.be.null; - return; - } - transactionSlot = fetchedTransaction.slot; - expect(fetchedTransaction.version).to.eq(0); - expect(fetchedTransaction.meta?.loadedAddresses).to.eql({ - readonly: [], - writable: [lookupTableAddresses[0]], + + it('getBlock (failure)', async () => { + await expect( + connection.getBlock(transactionSlot, { + maxSupportedTransactionVersion: undefined, + commitment: 'confirmed', + }), + ).to.be.rejectedWith( + 'failed to get confirmed block: Transaction version (0) is not supported', + ); }); - expect(fetchedTransaction.meta?.computeUnitsConsumed).to.not.be - .undefined; - expect( - fetchedTransaction.transaction.message.addressTableLookups, - ).to.eql(addressTableLookups); - }); - it('getParsedTransaction (failure)', async () => { - await expect( - connection.getParsedTransaction(signature, { + it('getBlock', async () => { + const block = await connection.getBlock(transactionSlot, { + maxSupportedTransactionVersion: 0, commitment: 'confirmed', - }), - ).to.be.rejectedWith( - 'failed to get transaction: Transaction version (0) is not supported', - ); - }); + }); + expect(block).to.not.be.null; + if (block === null) throw new Error(); // unreachable + + let foundTx = false; + for (const tx of block.transactions) { + if (tx.transaction.signatures[0] === signature) { + foundTx = true; + expect(tx.version).to.eq(0); + } + } + expect(foundTx).to.be.true; + }); - it('getParsedTransaction', async () => { - const parsedTransaction = await connection.getParsedTransaction( - signature, - { - commitment: 'confirmed', + it('getParsedBlock', async () => { + const block = await connection.getParsedBlock(transactionSlot, { maxSupportedTransactionVersion: 0, - }, - ); - expect(parsedTransaction).to.not.be.null; - expect(parsedTransaction?.version).to.eq(0); - // loaded addresses are not returned for parsed transactions - expect(parsedTransaction?.meta?.loadedAddresses).to.be.undefined; - expect(parsedTransaction?.meta?.computeUnitsConsumed).to.not.be - .undefined; - expect( - parsedTransaction?.transaction.message.addressTableLookups, - ).to.eql(addressTableLookups); - expect(parsedTransaction?.transaction.message.accountKeys).to.eql([ - { - pubkey: payer.publicKey, - signer: true, - writable: true, - source: 'transaction', - }, - { - pubkey: SystemProgram.programId, - signer: false, - writable: false, - source: 'transaction', - }, - { - pubkey: lookupTableAddresses[0], - signer: false, - writable: true, - source: 'lookupTable', - }, - ]); - }); - - it('getBlock (failure)', async () => { - await expect( - connection.getBlock(transactionSlot, { - maxSupportedTransactionVersion: undefined, commitment: 'confirmed', - }), - ).to.be.rejectedWith( - 'failed to get confirmed block: Transaction version (0) is not supported', - ); - }); - - it('getBlock', async () => { - const block = await connection.getBlock(transactionSlot, { - maxSupportedTransactionVersion: 0, - commitment: 'confirmed', - }); - expect(block).to.not.be.null; - if (block === null) throw new Error(); // unreachable - - let foundTx = false; - for (const tx of block.transactions) { - if (tx.transaction.signatures[0] === signature) { - foundTx = true; - expect(tx.version).to.eq(0); + }); + expect(block).to.not.be.null; + if (block === null) throw new Error(); // unreachable + + let foundTx = false; + for (const tx of block.transactions) { + if (tx.transaction.signatures[0] === signature) { + foundTx = true; + expect(tx.version).to.eq(0); + } } - } - expect(foundTx).to.be.true; - }); - }).timeout(5 * 1000); + expect(foundTx).to.be.true; + }); + }) + .timeout(5 * 1000); } });