diff --git a/src.ts/_tests/test-providers-data.ts b/src.ts/_tests/test-providers-data.ts index 56cfc50958..ef7d32b217 100644 --- a/src.ts/_tests/test-providers-data.ts +++ b/src.ts/_tests/test-providers-data.ts @@ -245,6 +245,90 @@ describe("Test Provider Transaction operations", function() { }); }); +describe("Test getBlockReceipts", function() { + forEach("test getBlockReceipts(block)", testBlock, (providerName, test) => { + // Skip blocks without transactions + if (test.transactions.length === 0) { return null; } + + // Skip providers that may not support eth_getBlockReceipts + // Etherscan doesn't support many RPC methods + if (providerName === "EtherscanProvider") { return null; } + + return async (provider) => { + // Only test with JsonRpcProvider instances + if (!(provider instanceof JsonRpcProvider)) { return; } + + const block = await provider.getBlock(test.number); + assert.ok(block != null, "block != null"); + if (block == null) { return; } + + const receipts = await provider.getBlockReceipts(block.number); + + // Verify receipt count matches transaction count + assert.equal(receipts.length, block.transactions.length, + "receipt count matches transaction count"); + + // Verify each receipt matches its transaction + for (let i = 0; i < receipts.length; i++) { + const receipt = receipts[i]; + const txHash = block.transactions[i]; + + assert.equal(receipt.hash, txHash, `receipt[${ i }].hash matches`); + assert.equal(receipt.blockNumber, block.number, `receipt[${ i }].blockNumber`); + assert.equal(receipt.blockHash, block.hash, `receipt[${ i }].blockHash`); + } + + // If we have testReceipt data for any of these transactions, + // validate against it + for (const receipt of receipts) { + // Find matching test receipt data + const networkName = (await provider.getNetwork()).name; + const testReceiptData = testReceipt[networkName as TestBlockchainNetwork]?.find( + (tr) => tr.hash === receipt.hash + ); + + if (testReceiptData) { + // Handle provider-specific quirks + let adjustedTest = testReceiptData; + if (providerName === "CloudflareProvider" || + providerName === "AnkrProvider" || + providerName === "PocketProvider" || + providerName === "BlockscoutProvider") { + adjustedTest = Object.assign({ }, adjustedTest, { root: undefined }); + } + + assertReceipt(receipt, adjustedTest); + } + } + }; + }); + + // Test with a specific block that has known receipts + it("test getBlockReceipts with known receipts", async function() { + this.timeout(60000); + + const provider = getProvider("JsonRpcProvider", "mainnet"); + if (provider == null || !(provider instanceof JsonRpcProvider)) { this.skip(); } + assert.ok(provider != null, "provider != null"); + if (provider == null) { return; } + + // Use a block number from testReceipt data + const testReceiptData = testReceipt.mainnet[0]; // legacy receipt + const block = await provider.getBlock(testReceiptData.blockNumber); + assert.ok(block != null, "block != null"); + if (block == null) { return; } + + const receipts = await provider.getBlockReceipts(testReceiptData.blockNumber); + + // Find the specific receipt we're testing + const receipt = receipts.find((r) => r.hash === testReceiptData.hash); + assert.ok(receipt != null, "found expected receipt"); + + // Validate using existing assertReceipt + assertReceipt(receipt, testReceiptData); + }); +}); + describe("Test Networks", function() { const networks = [ "mainnet", "sepolia", "holesky", diff --git a/src.ts/providers/abstract-provider.ts b/src.ts/providers/abstract-provider.ts index 0d75b435cc..5bf27f8868 100644 --- a/src.ts/providers/abstract-provider.ts +++ b/src.ts/providers/abstract-provider.ts @@ -397,6 +397,9 @@ export type PerformActionRequest = { } | { method: "getTransactionReceipt", hash: string +} | { + method: "getBlockReceipts", + blockTag: BlockTag } | { method: "getTransactionResult", hash: string @@ -1160,6 +1163,34 @@ export class AbstractProvider implements Provider { return this._wrapTransactionReceipt(params, network); } + /** + * Resolves to all transaction receipts for %%blockTag%%. + */ + async getBlockReceipts(blockTag: BlockTag | string): Promise> { + let blockTagStr: string; + if (isHexString(blockTag, 32)) { + // If it's a block hash, we need to get the block number first + const block = await this.getBlock(blockTag); + if (block == null) { return [ ]; } + blockTagStr = toQuantity(block.number); + } else { + const tag = this._getBlockTag(blockTag); + blockTagStr = typeof(tag) === "string" ? tag : await tag; + } + + const { network, params } = await resolveProperties({ + network: this.getNetwork(), + params: this.#perform({ method: "getBlockReceipts", blockTag: blockTagStr }) + }); + + if (params == null || !Array.isArray(params)) { return [ ]; } + + // Wrap raw receipts into proper TransactionReceipt objects + return params.map((r: any) => { + return this._wrapTransactionReceipt(formatTransactionReceipt(r), network); + }); + } + async getTransactionResult(hash: string): Promise { const { result } = await resolveProperties({ network: this.getNetwork(), diff --git a/src.ts/providers/provider-jsonrpc.ts b/src.ts/providers/provider-jsonrpc.ts index 301fa26571..8a0154ee00 100644 --- a/src.ts/providers/provider-jsonrpc.ts +++ b/src.ts/providers/provider-jsonrpc.ts @@ -958,6 +958,12 @@ export abstract class JsonRpcApiProvider extends AbstractProvider { args: [ req.hash ] }; + case "getBlockReceipts": + return { + method: "eth_getBlockReceipts", + args: [ req.blockTag ] + }; + case "call": return { method: "eth_call", @@ -1332,4 +1338,4 @@ function spelunkMessage(value: any): Array { const result: Array = [ ]; _spelunkMessage(value, result); return result; -} +} \ No newline at end of file diff --git a/src.ts/providers/provider.ts b/src.ts/providers/provider.ts index 1e1e64bba4..1b0893aafb 100644 --- a/src.ts/providers/provider.ts +++ b/src.ts/providers/provider.ts @@ -2084,6 +2084,11 @@ export interface Provider extends ContractRunner, EventEmitterable; + /** + * Resolves to all transaction receipts for %%blockTag%%. + */ + getBlockReceipts(blockTag: BlockTag | string): Promise>; + /** * Resolves to the result returned by the executions of %%hash%%. *