diff --git a/web3.js/src/connection.ts b/web3.js/src/connection.ts index 6f78d6890b..d9606eaebb 100644 --- a/web3.js/src/connection.ts +++ b/web3.js/src/connection.ts @@ -510,6 +510,25 @@ export type ConfirmedTransactionMeta = { err: TransactionError | null; }; +/** + * A processed transaction from the RPC API + */ +export type TransactionResponse = { + /** The slot during which the transaction was processed */ + slot: number; + /** The transaction */ + transaction: { + /** The transaction message */ + message: Message; + /** The transaction signatures */ + signatures: string[]; + }; + /** Metadata produced from the transaction */ + meta: ConfirmedTransactionMeta | null; + /** The unix timestamp of when the transaction was processed */ + blockTime?: number | null; +}; + /** * A confirmed transaction on the ledger */ @@ -596,6 +615,43 @@ export type ParsedConfirmedTransaction = { blockTime?: number | null; }; +/** + * A processed block fetched from the RPC API + */ +export type BlockResponse = { + /** 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 transaction */ + transaction: { + /** The transaction message */ + message: Message; + /** The transaction signatures */ + signatures: string[]; + }; + /** Metadata produced from the transaction */ + meta: ConfirmedTransactionMeta | null; + }>; + /** 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 ConfirmedBlock on the ledger */ @@ -1234,9 +1290,6 @@ const GetSignatureStatusesRpcResult = jsonRpcResultAndContext( */ const GetMinimumBalanceForRentExemptionRpcResult = jsonRpcResult(number()); -/** - * @internal - */ const ConfirmedTransactionResult = pick({ signatures: array(string()), message: pick({ @@ -1257,15 +1310,6 @@ const ConfirmedTransactionResult = pick({ }), }); -const TransactionFromConfirmed = coerce( - instance(Transaction), - ConfirmedTransactionResult, - result => { - const {message, signatures} = result; - return Transaction.populate(new Message(message), signatures); - }, -); - const ParsedInstructionResult = pick({ parsed: unknown(), program: string(), @@ -1395,7 +1439,7 @@ const GetConfirmedBlockRpcResult = jsonRpcResult( parentSlot: number(), transactions: array( pick({ - transaction: TransactionFromConfirmed, + transaction: ConfirmedTransactionResult, meta: nullable(ConfirmedTransactionMetaResult), }), ), @@ -1436,9 +1480,9 @@ const GetConfirmedTransactionRpcResult = jsonRpcResult( nullable( pick({ slot: number(), - transaction: TransactionFromConfirmed, meta: ConfirmedTransactionMetaResult, blockTime: optional(nullable(number())), + transaction: ConfirmedTransactionResult, }), ), ); @@ -2668,7 +2712,8 @@ export class Connection { /** * Fetch the current total currency supply of the cluster in lamports - * @deprecated Deprecated since v1.2.8. Use `Connection.getSupply()` instead. + * + * @deprecated Deprecated since v1.2.8. Please use {@link getSupply} instead. */ async getTotalSupply(commitment?: Commitment): Promise { const args = this._buildArgs([], commitment); @@ -2869,25 +2914,100 @@ export class Connection { return res.result; } + /** + * Fetch a processed block from the cluster. + */ + async getBlock( + slot: number, + opts?: {commitment?: Finality}, + ): Promise { + const args = this._buildArgsAtLeastConfirmed( + [slot], + opts && opts.commitment, + ); + const unsafeRes = await this._rpcRequest('getConfirmedBlock', args); + const res = create(unsafeRes, GetConfirmedBlockRpcResult); + + if ('error' in res) { + throw new Error('failed to get confirmed block: ' + res.error.message); + } + + const result = res.result; + if (!result) return result; + + return { + ...result, + transactions: result.transactions.map(({transaction, meta}) => { + const message = new Message(transaction.message); + return { + meta, + transaction: { + ...transaction, + message, + }, + }; + }), + }; + } + + /** + * Fetch a processed transaction from the cluster. + */ + async getTransaction( + signature: string, + opts?: {commitment?: Finality}, + ): Promise { + const args = this._buildArgsAtLeastConfirmed( + [signature], + opts && opts.commitment, + ); + const unsafeRes = await this._rpcRequest('getConfirmedTransaction', args); + const res = create(unsafeRes, GetConfirmedTransactionRpcResult); + if ('error' in res) { + throw new Error( + 'failed to get confirmed transaction: ' + res.error.message, + ); + } + + const result = res.result; + if (!result) return result; + + return { + ...result, + transaction: { + ...result.transaction, + message: new Message(result.transaction.message), + }, + }; + } + /** * Fetch a list of Transactions and transaction statuses from the cluster - * for a confirmed block + * for a confirmed block. + * + * @deprecated Deprecated since v1.13.0. Please use {@link getBlock} instead. */ async getConfirmedBlock( slot: number, commitment?: Finality, ): Promise { - const args = this._buildArgsAtLeastConfirmed([slot], commitment); - const unsafeRes = await this._rpcRequest('getConfirmedBlock', args); - const res = create(unsafeRes, GetConfirmedBlockRpcResult); - if ('error' in res) { - throw new Error('failed to get confirmed block: ' + res.error.message); - } - const result = res.result; + const result = await this.getBlock(slot, {commitment}); if (!result) { throw new Error('Confirmed block ' + slot + ' not found'); } - return result; + + return { + ...result, + transactions: result.transactions.map(({transaction, meta}) => { + return { + meta, + transaction: Transaction.populate( + transaction.message, + transaction.signatures, + ), + }; + }), + }; } /** @@ -2925,15 +3045,13 @@ export class Connection { signature: TransactionSignature, commitment?: Finality, ): Promise { - const args = this._buildArgsAtLeastConfirmed([signature], commitment); - const unsafeRes = await this._rpcRequest('getConfirmedTransaction', args); - const res = create(unsafeRes, GetConfirmedTransactionRpcResult); - if ('error' in res) { - throw new Error( - 'failed to get confirmed transaction: ' + res.error.message, - ); - } - return res.result; + const result = await this.getTransaction(signature, {commitment}); + if (!result) return result; + const {message, signatures} = result.transaction; + return { + ...result, + transaction: Transaction.populate(message, signatures), + }; } /** @@ -2994,7 +3112,8 @@ export class Connection { /** * Fetch a list of all the confirmed signatures for transactions involving an address * within a specified slot range. Max range allowed is 10,000 slots. - * @deprecated Deprecated since v1.3. Use `Connection.getConfirmedSignaturesForAddress2()` instead. + * + * @deprecated Deprecated since v1.3. Please use {@link getConfirmedSignaturesForAddress2} instead. * * @param address queried address * @param startSlot start slot, inclusive diff --git a/web3.js/test/connection.test.ts b/web3.js/test/connection.test.ts index 68bb54d292..af2cc93bad 100644 --- a/web3.js/test/connection.test.ts +++ b/web3.js/test/connection.test.ts @@ -1283,6 +1283,151 @@ describe('Connection', () => { expect(result).to.be.empty; }); + it('get transaction', async () => { + await mockRpcResponse({ + method: 'getSlot', + params: [], + value: 1, + }); + + while ((await connection.getSlot()) <= 0) { + continue; + } + + await mockRpcResponse({ + method: 'getConfirmedBlock', + params: [1], + value: { + blockTime: 1614281964, + blockhash: '57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy', + previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo', + parentSlot: 0, + transactions: [ + { + meta: { + fee: 10000, + postBalances: [499260347380, 15298080, 1, 1, 1], + preBalances: [499260357380, 15298080, 1, 1, 1], + status: {Ok: null}, + err: null, + }, + transaction: { + message: { + accountKeys: [ + 'va12u4o9DipLEB2z4fuoHszroq1U9NcAB9aooFDPJSf', + '57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy', + 'SysvarS1otHashes111111111111111111111111111', + 'SysvarC1ock11111111111111111111111111111111', + 'Vote111111111111111111111111111111111111111', + ], + header: { + numReadonlySignedAccounts: 0, + numReadonlyUnsignedAccounts: 3, + numRequiredSignatures: 2, + }, + instructions: [ + { + accounts: [1, 2, 3], + data: + '37u9WtQpcm6ULa3VtWDFAWoQc1hUvybPrA3dtx99tgHvvcE7pKRZjuGmn7VX2tC3JmYDYGG7', + programIdIndex: 4, + }, + ], + recentBlockhash: 'GeyAFFRY3WGpmam2hbgrKw4rbU2RKzfVLm5QLSeZwTZE', + }, + signatures: [ + 'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt', + '4oCEqwGrMdBeMxpzuWiukCYqSfV4DsSKXSiVVCh1iJ6pS772X7y219JZP3mgqBz5PhsvprpKyhzChjYc3VSBQXzG', + ], + }, + }, + ], + }, + }); + + // Find a block that has a transaction, usually Block 1 + let slot = 0; + let confirmedTransaction: string | undefined; + while (!confirmedTransaction) { + slot++; + const block = await connection.getBlock(slot); + if (block && block.transactions.length > 0) { + confirmedTransaction = block.transactions[0].transaction.signatures[0]; + } + } + + await mockRpcResponse({ + method: 'getConfirmedTransaction', + params: [confirmedTransaction], + value: { + slot, + transaction: { + message: { + accountKeys: [ + 'va12u4o9DipLEB2z4fuoHszroq1U9NcAB9aooFDPJSf', + '57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy', + 'SysvarS1otHashes111111111111111111111111111', + 'SysvarC1ock11111111111111111111111111111111', + 'Vote111111111111111111111111111111111111111', + ], + header: { + numReadonlySignedAccounts: 0, + numReadonlyUnsignedAccounts: 3, + numRequiredSignatures: 2, + }, + instructions: [ + { + accounts: [1, 2, 3], + data: + '37u9WtQpcm6ULa3VtWDFAWoQc1hUvybPrA3dtx99tgHvvcE7pKRZjuGmn7VX2tC3JmYDYGG7', + programIdIndex: 4, + }, + ], + recentBlockhash: 'GeyAFFRY3WGpmam2hbgrKw4rbU2RKzfVLm5QLSeZwTZE', + }, + signatures: [ + 'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt', + '4oCEqwGrMdBeMxpzuWiukCYqSfV4DsSKXSiVVCh1iJ6pS772X7y219JZP3mgqBz5PhsvprpKyhzChjYc3VSBQXzG', + ], + }, + meta: { + fee: 10000, + postBalances: [499260347380, 15298080, 1, 1, 1], + preBalances: [499260357380, 15298080, 1, 1, 1], + status: {Ok: null}, + err: null, + }, + }, + }); + + const result = await connection.getTransaction(confirmedTransaction); + + if (!result) { + expect(result).to.be.ok; + return; + } + + const resultSignature = result.transaction.signatures[0]; + expect(resultSignature).to.eq(confirmedTransaction); + + const newAddress = Keypair.generate().publicKey; + const recentSignature = await helpers.airdrop({ + connection, + address: newAddress, + amount: 1, + }); + + await mockRpcResponse({ + method: 'getConfirmedTransaction', + params: [recentSignature], + value: null, + }); + + // Signature hasn't been finalized yet + const nullResponse = await connection.getTransaction(recentSignature); + expect(nullResponse).to.be.null; + }); + it('get confirmed transaction', async () => { await mockRpcResponse({ method: 'getSlot', @@ -1535,6 +1680,121 @@ describe('Connection', () => { }); } + it('get block', async () => { + await mockRpcResponse({ + method: 'getSlot', + params: [], + value: 1, + }); + + while ((await connection.getSlot()) <= 0) { + continue; + } + + await mockRpcResponse({ + method: 'getConfirmedBlock', + params: [0], + value: { + blockTime: 1614281964, + blockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo', + previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo', + parentSlot: 0, + transactions: [], + }, + }); + + // Block 0 never has any transactions in test validator + const block0 = await connection.getBlock(0); + if (!block0) { + expect(block0).not.to.be.null; + return; + } + + const blockhash0 = block0.blockhash; + expect(block0.transactions).to.have.length(0); + expect(blockhash0).not.to.be.null; + expect(block0.previousBlockhash).not.to.be.null; + expect(block0.parentSlot).to.eq(0); + + await mockRpcResponse({ + method: 'getConfirmedBlock', + params: [1], + value: { + blockTime: 1614281964, + blockhash: '57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy', + previousBlockhash: 'H5nJ91eGag3B5ZSRHZ7zG5ZwXJ6ywCt2hyR8xCsV7xMo', + parentSlot: 0, + transactions: [ + { + meta: { + fee: 10000, + postBalances: [499260347380, 15298080, 1, 1, 1], + preBalances: [499260357380, 15298080, 1, 1, 1], + status: {Ok: null}, + err: null, + }, + transaction: { + message: { + accountKeys: [ + 'va12u4o9DipLEB2z4fuoHszroq1U9NcAB9aooFDPJSf', + '57zQNBZBEiHsCZFqsaY6h176ioXy5MsSLmcvHkEyaLGy', + 'SysvarS1otHashes111111111111111111111111111', + 'SysvarC1ock11111111111111111111111111111111', + 'Vote111111111111111111111111111111111111111', + ], + header: { + numReadonlySignedAccounts: 0, + numReadonlyUnsignedAccounts: 3, + numRequiredSignatures: 2, + }, + instructions: [ + { + accounts: [1, 2, 3], + data: + '37u9WtQpcm6ULa3VtWDFAWoQc1hUvybPrA3dtx99tgHvvcE7pKRZjuGmn7VX2tC3JmYDYGG7', + programIdIndex: 4, + }, + ], + recentBlockhash: 'GeyAFFRY3WGpmam2hbgrKw4rbU2RKzfVLm5QLSeZwTZE', + }, + signatures: [ + 'w2Zeq8YkpyB463DttvfzARD7k9ZxGEwbsEw4boEK7jDp3pfoxZbTdLFSsEPhzXhpCcjGi2kHtHFobgX49MMhbWt', + '4oCEqwGrMdBeMxpzuWiukCYqSfV4DsSKXSiVVCh1iJ6pS772X7y219JZP3mgqBz5PhsvprpKyhzChjYc3VSBQXzG', + ], + }, + }, + ], + }, + }); + + // Find a block that has a transaction, usually Block 1 + let x = 1; + while (x < 10) { + const block1 = await connection.getBlock(x); + if (block1 && block1.transactions.length >= 1) { + expect(block1.previousBlockhash).to.eq(blockhash0); + expect(block1.blockhash).not.to.be.null; + expect(block1.parentSlot).to.eq(0); + expect(block1.transactions[0].transaction).not.to.be.null; + break; + } + x++; + } + + await mockRpcResponse({ + method: 'getConfirmedBlock', + params: [Number.MAX_SAFE_INTEGER], + error: { + message: `Block not available for slot ${Number.MAX_SAFE_INTEGER}`, + }, + }); + await expect( + connection.getBlock(Number.MAX_SAFE_INTEGER), + ).to.be.rejectedWith( + `Block not available for slot ${Number.MAX_SAFE_INTEGER}`, + ); + }); + it('get confirmed block', async () => { await mockRpcResponse({ method: 'getSlot',