From 7b8cc13bf7f32767ac61e059f7516e068fa00e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walczak?= Date: Thu, 5 Jun 2025 12:58:04 +0200 Subject: [PATCH 01/13] feat: update `resolveEvmAddress` to handle `null` addresses and return nullable types and add tests validating address resolution logic (#3814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Walczak --- .../factories/transactionReceiptFactory.ts | 2 +- .../ethService/blockService/BlockService.ts | 5 + .../ethCommonService/CommonService.ts | 6 +- .../ethCommonService/ICommonService.ts | 2 +- .../transactionService/TransactionService.ts | 3 + .../src/lib/types/ITransactionReceipt.ts | 2 +- .../blockService/BlockService.spec.ts | 197 ++++++++++++++++++ 7 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 packages/relay/tests/lib/services/ethService/blockService/BlockService.spec.ts diff --git a/packages/relay/src/lib/factories/transactionReceiptFactory.ts b/packages/relay/src/lib/factories/transactionReceiptFactory.ts index c7d3eaa65a..3da0a6d704 100644 --- a/packages/relay/src/lib/factories/transactionReceiptFactory.ts +++ b/packages/relay/src/lib/factories/transactionReceiptFactory.ts @@ -30,7 +30,7 @@ interface IRegularTransactionReceiptParams { from: string; logs: Log[]; receiptResponse: any; - to: string; + to: string | null; } /** diff --git a/packages/relay/src/lib/services/ethService/blockService/BlockService.ts b/packages/relay/src/lib/services/ethService/blockService/BlockService.ts index 0a9b2a9f7f..e09f81680a 100644 --- a/packages/relay/src/lib/services/ethService/blockService/BlockService.ts +++ b/packages/relay/src/lib/services/ethService/blockService/BlockService.ts @@ -180,6 +180,11 @@ export class BlockService implements IBlockService { this.common.resolveEvmAddress(contractResult.from, requestDetails), this.common.resolveEvmAddress(contractResult.to, requestDetails), ]); + + if (!from) { + throw new Error(`Failed to resolve from address: ${contractResult.from}`); + } + const transactionReceiptParams: IRegularTransactionReceiptParams = { effectiveGas, from, diff --git a/packages/relay/src/lib/services/ethService/ethCommonService/CommonService.ts b/packages/relay/src/lib/services/ethService/ethCommonService/CommonService.ts index 62197bf6ab..3ba50deadb 100644 --- a/packages/relay/src/lib/services/ethService/ethCommonService/CommonService.ts +++ b/packages/relay/src/lib/services/ethService/ethCommonService/CommonService.ts @@ -420,11 +420,11 @@ export class CommonService implements ICommonService { } public async resolveEvmAddress( - address: string, + address: string | null, requestDetails: RequestDetails, searchableTypes = [constants.TYPE_CONTRACT, constants.TYPE_TOKEN, constants.TYPE_ACCOUNT], - ): Promise { - if (!address) return address; + ): Promise { + if (!address) return null; const entity = await this.mirrorNodeClient.resolveEntityType( address, diff --git a/packages/relay/src/lib/services/ethService/ethCommonService/ICommonService.ts b/packages/relay/src/lib/services/ethService/ethCommonService/ICommonService.ts index 2e9ef7d336..200b2721de 100644 --- a/packages/relay/src/lib/services/ethService/ethCommonService/ICommonService.ts +++ b/packages/relay/src/lib/services/ethService/ethCommonService/ICommonService.ts @@ -45,7 +45,7 @@ export interface ICommonService { isBlockParamValid(tag: string | null): boolean; - resolveEvmAddress(address: string, requestDetails: RequestDetails, types?: string[]): Promise; + resolveEvmAddress(address: string | null, requestDetails: RequestDetails, types?: string[]): Promise; translateBlockTag(tag: string | null, requestDetails: RequestDetails): Promise; diff --git a/packages/relay/src/lib/services/ethService/transactionService/TransactionService.ts b/packages/relay/src/lib/services/ethService/transactionService/TransactionService.ts index da69e3904d..5e086dbd23 100644 --- a/packages/relay/src/lib/services/ethService/transactionService/TransactionService.ts +++ b/packages/relay/src/lib/services/ethService/transactionService/TransactionService.ts @@ -443,6 +443,9 @@ export class TransactionService implements ITransactionService { this.common.resolveEvmAddress(receiptResponse.from, requestDetails), this.common.resolveEvmAddress(receiptResponse.to, requestDetails), ]); + if (!from) { + throw predefined.INTERNAL_ERROR(`Could not resolve from address for transaction ${receiptResponse.hash}`); + } const transactionReceiptParams: IRegularTransactionReceiptParams = { effectiveGas, from, diff --git a/packages/relay/src/lib/types/ITransactionReceipt.ts b/packages/relay/src/lib/types/ITransactionReceipt.ts index 90808b48b9..48ff299efd 100644 --- a/packages/relay/src/lib/types/ITransactionReceipt.ts +++ b/packages/relay/src/lib/types/ITransactionReceipt.ts @@ -14,7 +14,7 @@ export interface ITransactionReceipt { logsBloom: string; root: string; status: string; - to: string; + to: string | null; transactionHash: string; transactionIndex: string | null; type: string | null; diff --git a/packages/relay/tests/lib/services/ethService/blockService/BlockService.spec.ts b/packages/relay/tests/lib/services/ethService/blockService/BlockService.spec.ts new file mode 100644 index 0000000000..62a03bd7df --- /dev/null +++ b/packages/relay/tests/lib/services/ethService/blockService/BlockService.spec.ts @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: Apache-2.0 +import { expect, use } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import sinon from 'sinon'; +use(chaiAsPromised); + +import { MirrorNodeClient } from '../../../../../src/lib/clients'; +import { Log } from '../../../../../src/lib/model'; +import { BlockService } from '../../../../../src/lib/services'; +import { CommonService } from '../../../../../src/lib/services'; +import { CacheService } from '../../../../../src/lib/services/cacheService/cacheService'; +import { RequestDetails } from '../../../../../src/lib/types'; + +describe('BlockService', () => { + let blockService: BlockService; + let commonService: CommonService; + let mirrorNodeClient: MirrorNodeClient; + let cacheService: CacheService; + let logger: any; + let sandbox: sinon.SinonSandbox; + let requestDetails: RequestDetails; + + // Fixtures + const blockHashOrNumber = '0x1234'; + const mockBlock = { + number: 123, + timestamp: { + from: '1622567324.000000000', + to: '1622567325.000000000', + }, + }; + + const createMockContractResult = (overrides = {}) => ({ + hash: '0xabc123', + from: '0xoriginalFromAddress', + to: '0xoriginalToAddress', + result: 'SUCCESS', + address: '0xcontractAddress', + block_hash: '0xblockHash', + block_number: 123, + block_gas_used: 100000, + gas_used: 50000, + transaction_index: 0, + status: '0x1', + function_parameters: '0x608060405234801561001057600080fd5b50', + call_result: '0x', + ...overrides, + }); + + const createMockLogs = () => [ + new Log({ + address: '0xlogsAddress', + blockHash: '0xblockHash', + blockNumber: '0x123', + data: '0xdata', + logIndex: '0x0', + removed: false, + topics: ['0xtopic1'], + transactionHash: '0xabc123', + transactionIndex: '0x0', + }), + ]; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + logger = { + trace: sandbox.stub(), + debug: sandbox.stub(), + info: sandbox.stub(), + warn: sandbox.stub(), + error: sandbox.stub(), + fatal: sandbox.stub(), + isLevelEnabled: sandbox.stub().returns(true), + }; + + mirrorNodeClient = { + getContractResults: sandbox.stub(), + getHistoricalBlockResponse: sandbox.stub(), + getLatestBlock: sandbox.stub(), + } as unknown as MirrorNodeClient; + + commonService = { + getHistoricalBlockResponse: sinon.stub(), + getLogsWithParams: sinon.stub(), + resolveEvmAddress: sinon.stub(), + getGasPriceInWeibars: sinon.stub(), + } as unknown as CommonService; + + cacheService = { + getAsync: sandbox.stub().resolves(null), + set: sandbox.stub().resolves(), + } as unknown as CacheService; + + blockService = new BlockService( + cacheService as any, + '0x12a', + commonService as any, + mirrorNodeClient as any, + logger, + ); + + requestDetails = new RequestDetails({ + requestId: 'test-request-id', + ipAddress: '127.0.0.1', + }); + + // Common stubs for all tests + (commonService.getHistoricalBlockResponse as sinon.SinonStub).resolves(mockBlock); + (commonService.getLogsWithParams as sinon.SinonStub).resolves(createMockLogs()); + (commonService.getGasPriceInWeibars as sinon.SinonStub).resolves(100000000); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('getBlockReceipts', () => { + it('should resolve from and to addresses correctly', async () => { + // Setup + const mockContractResults = [createMockContractResult()]; + (mirrorNodeClient.getContractResults as sinon.SinonStub).resolves(mockContractResults); + + const resolvedFromAddress = '0xresolvedFromAddress'; + const resolvedToAddress = '0xresolvedToAddress'; + + (commonService.resolveEvmAddress as sinon.SinonStub) + .withArgs('0xoriginalFromAddress', requestDetails) + .resolves(resolvedFromAddress); + + (commonService.resolveEvmAddress as sinon.SinonStub) + .withArgs('0xoriginalToAddress', requestDetails) + .resolves(resolvedToAddress); + + // Execute + const receipts = await blockService.getBlockReceipts(blockHashOrNumber, requestDetails); + + // Verify + expect(receipts).to.have.length(1); + expect(receipts[0].from).to.equal(resolvedFromAddress); + expect(receipts[0].to).to.equal(resolvedToAddress); + expect((commonService.resolveEvmAddress as sinon.SinonStub).calledWith('0xoriginalFromAddress', requestDetails)) + .to.be.true; + expect((commonService.resolveEvmAddress as sinon.SinonStub).calledWith('0xoriginalToAddress', requestDetails)).to + .be.true; + }); + + it('should throw error when from address cannot be resolved', async () => { + // Setup + const mockContractResults = [createMockContractResult()]; + (mirrorNodeClient.getContractResults as sinon.SinonStub).resolves(mockContractResults); + + (commonService.resolveEvmAddress as sinon.SinonStub) + .withArgs('0xoriginalFromAddress', requestDetails, undefined) + .resolves(null); + + (commonService.resolveEvmAddress as sinon.SinonStub) + .withArgs('0xoriginalToAddress', requestDetails, undefined) + .resolves('0xresolvedToAddress'); + + // Execute & Verify + await expect(blockService.getBlockReceipts(blockHashOrNumber, requestDetails)).to.be.rejectedWith( + `Failed to resolve from address: 0xoriginalFromAddress`, + ); + }); + + it('should handle null to field for contract creation transactions', async () => { + // Setup + const mockContractResults = [ + createMockContractResult({ + to: null, + address: '0xnewlyCreatedContractAddress', + }), + ]; + + (mirrorNodeClient.getContractResults as sinon.SinonStub).resolves(mockContractResults); + + (commonService.resolveEvmAddress as sinon.SinonStub) + .withArgs('0xoriginalFromAddress', requestDetails) + .resolves('0xresolvedFromAddress'); + + (commonService.resolveEvmAddress as sinon.SinonStub).withArgs(null, requestDetails).resolves(null); + + // Execute + const receipts = await blockService.getBlockReceipts(blockHashOrNumber, requestDetails); + + // Verify + expect(receipts).to.have.length(1); + expect(receipts[0].from).to.equal('0xresolvedFromAddress'); + expect(receipts[0].to).to.equal(null); + expect(receipts[0].contractAddress).to.not.be.null; + + expect((commonService.resolveEvmAddress as sinon.SinonStub).calledWith('0xoriginalFromAddress', requestDetails)) + .to.be.true; + expect((commonService.resolveEvmAddress as sinon.SinonStub).calledWith(null, requestDetails)).to.be.true; + }); + }); +}); From 968c756e6cdcc3c6bf7e2cb94cdc523f8e45cd25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walczak?= Date: Thu, 5 Jun 2025 14:58:09 +0200 Subject: [PATCH 02/13] feat: adjust transaction receipt `to` field handling for contract creation compatibility with Ethereum JSON-RPC standards and add corresponding tests (#3814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Walczak --- .../ethService/blockService/BlockService.ts | 29 +++++++++ .../blockService/BlockService.spec.ts | 60 +++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/packages/relay/src/lib/services/ethService/blockService/BlockService.ts b/packages/relay/src/lib/services/ethService/blockService/BlockService.ts index e09f81680a..5664b539d8 100644 --- a/packages/relay/src/lib/services/ethService/blockService/BlockService.ts +++ b/packages/relay/src/lib/services/ethService/blockService/BlockService.ts @@ -175,6 +175,35 @@ export class BlockService implements IBlockService { } return null; } + + /** + * Handles the correction of transaction receipt `to` field for contract creation transactions. + * + * This logic addresses a discrepancy between Hedera and standard Ethereum behavior regarding + * the `to` field in transaction receipts. When a smart contract is deployed: + * + * 1. In standard Ethereum JSON-RPC, if the original transaction had a null `to` field + * (contract creation), the transaction receipt also reports a null `to` field. + * + * 2. Hedera Mirror Node, however, automatically populates the `to` field with the + * address of the newly created contract. + * + * The code checks if a contract was directly created by the transaction (rather than created by + * another contract) by checking if the contract's ID appears in the `created_contract_ids` array. + * If so, it resets the `to` field to null to match standard Ethereum JSON-RPC behavior. + * + * This ensures compatibility with Ethereum tooling that expects standard transaction receipt formats. + * The handling covers various scenarios: + * + * - Direct contract deployment (empty `to` field) + * - Contract creation via factory contracts + * - Method calls that don't create contracts + * - Transactions with populated `to` fields that create child contracts + */ + if (contractResult.created_contract_ids.includes(contractResult.contract_id)) { + contractResult.to = null; + } + contractResult.logs = logsByHash.get(contractResult.hash) || []; const [from, to] = await Promise.all([ this.common.resolveEvmAddress(contractResult.from, requestDetails), diff --git a/packages/relay/tests/lib/services/ethService/blockService/BlockService.spec.ts b/packages/relay/tests/lib/services/ethService/blockService/BlockService.spec.ts index 62a03bd7df..571e306731 100644 --- a/packages/relay/tests/lib/services/ethService/blockService/BlockService.spec.ts +++ b/packages/relay/tests/lib/services/ethService/blockService/BlockService.spec.ts @@ -44,6 +44,7 @@ describe('BlockService', () => { status: '0x1', function_parameters: '0x608060405234801561001057600080fd5b50', call_result: '0x', + created_contract_ids: [], ...overrides, }); @@ -84,6 +85,7 @@ describe('BlockService', () => { getLogsWithParams: sinon.stub(), resolveEvmAddress: sinon.stub(), getGasPriceInWeibars: sinon.stub(), + genericErrorHandler: sinon.stub(), } as unknown as CommonService; cacheService = { @@ -193,5 +195,63 @@ describe('BlockService', () => { .to.be.true; expect((commonService.resolveEvmAddress as sinon.SinonStub).calledWith(null, requestDetails)).to.be.true; }); + + it('should set to field to null when contract is in created_contract_ids', async () => { + // Setup + const contractId = '0.0.1234'; + const mockContractResults = [ + createMockContractResult({ + to: '0xoriginalToAddress', + contract_id: contractId, + created_contract_ids: [contractId], + }), + ]; + + (mirrorNodeClient.getContractResults as sinon.SinonStub).resolves(mockContractResults); + + (commonService.resolveEvmAddress as sinon.SinonStub) + .withArgs('0xoriginalFromAddress', requestDetails) + .resolves('0xresolvedFromAddress'); + + (commonService.resolveEvmAddress as sinon.SinonStub).withArgs(null, requestDetails).resolves(null); + + // Execute + const receipts = await blockService.getBlockReceipts(blockHashOrNumber, requestDetails); + + // Verify + expect(receipts).to.have.length(1); + expect(receipts[0].from).to.equal('0xresolvedFromAddress'); + expect(receipts[0].to).to.equal(null); + }); + + it('should keep original to field when contract is not in created_contract_ids', async () => { + // Setup + const contractId = '0.0.1234'; + const mockContractResults = [ + createMockContractResult({ + to: '0xoriginalToAddress', + contract_id: contractId, + created_contract_ids: ['0.0.5678'], // Different contract ID + }), + ]; + + (mirrorNodeClient.getContractResults as sinon.SinonStub).resolves(mockContractResults); + + (commonService.resolveEvmAddress as sinon.SinonStub) + .withArgs('0xoriginalFromAddress', requestDetails) + .resolves('0xresolvedFromAddress'); + + (commonService.resolveEvmAddress as sinon.SinonStub) + .withArgs('0xoriginalToAddress', requestDetails) + .resolves('0xresolvedToAddress'); + + // Execute + const receipts = await blockService.getBlockReceipts(blockHashOrNumber, requestDetails); + + // Verify + expect(receipts).to.have.length(1); + expect(receipts[0].from).to.equal('0xresolvedFromAddress'); + expect(receipts[0].to).to.equal('0xresolvedToAddress'); + }); }); }); From df9248fa88d23e162744ae2875a4539c20065160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walczak?= Date: Mon, 9 Jun 2025 10:45:21 +0200 Subject: [PATCH 03/13] refactor: update `resolveEvmAddress` to require non-null address (#3814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Walczak --- .../ethService/blockService/BlockService.ts | 6 +---- .../ethCommonService/CommonService.ts | 6 ++--- .../ethCommonService/ICommonService.ts | 2 +- .../transactionService/TransactionService.ts | 4 +--- .../blockService/BlockService.spec.ts | 23 +------------------ 5 files changed, 7 insertions(+), 34 deletions(-) diff --git a/packages/relay/src/lib/services/ethService/blockService/BlockService.ts b/packages/relay/src/lib/services/ethService/blockService/BlockService.ts index 5664b539d8..d9a16ba97d 100644 --- a/packages/relay/src/lib/services/ethService/blockService/BlockService.ts +++ b/packages/relay/src/lib/services/ethService/blockService/BlockService.ts @@ -207,13 +207,9 @@ export class BlockService implements IBlockService { contractResult.logs = logsByHash.get(contractResult.hash) || []; const [from, to] = await Promise.all([ this.common.resolveEvmAddress(contractResult.from, requestDetails), - this.common.resolveEvmAddress(contractResult.to, requestDetails), + contractResult.to === null ? null : this.common.resolveEvmAddress(contractResult.to, requestDetails), ]); - if (!from) { - throw new Error(`Failed to resolve from address: ${contractResult.from}`); - } - const transactionReceiptParams: IRegularTransactionReceiptParams = { effectiveGas, from, diff --git a/packages/relay/src/lib/services/ethService/ethCommonService/CommonService.ts b/packages/relay/src/lib/services/ethService/ethCommonService/CommonService.ts index 3ba50deadb..62197bf6ab 100644 --- a/packages/relay/src/lib/services/ethService/ethCommonService/CommonService.ts +++ b/packages/relay/src/lib/services/ethService/ethCommonService/CommonService.ts @@ -420,11 +420,11 @@ export class CommonService implements ICommonService { } public async resolveEvmAddress( - address: string | null, + address: string, requestDetails: RequestDetails, searchableTypes = [constants.TYPE_CONTRACT, constants.TYPE_TOKEN, constants.TYPE_ACCOUNT], - ): Promise { - if (!address) return null; + ): Promise { + if (!address) return address; const entity = await this.mirrorNodeClient.resolveEntityType( address, diff --git a/packages/relay/src/lib/services/ethService/ethCommonService/ICommonService.ts b/packages/relay/src/lib/services/ethService/ethCommonService/ICommonService.ts index 200b2721de..2e9ef7d336 100644 --- a/packages/relay/src/lib/services/ethService/ethCommonService/ICommonService.ts +++ b/packages/relay/src/lib/services/ethService/ethCommonService/ICommonService.ts @@ -45,7 +45,7 @@ export interface ICommonService { isBlockParamValid(tag: string | null): boolean; - resolveEvmAddress(address: string | null, requestDetails: RequestDetails, types?: string[]): Promise; + resolveEvmAddress(address: string, requestDetails: RequestDetails, types?: string[]): Promise; translateBlockTag(tag: string | null, requestDetails: RequestDetails): Promise; diff --git a/packages/relay/src/lib/services/ethService/transactionService/TransactionService.ts b/packages/relay/src/lib/services/ethService/transactionService/TransactionService.ts index 5e086dbd23..100bfab538 100644 --- a/packages/relay/src/lib/services/ethService/transactionService/TransactionService.ts +++ b/packages/relay/src/lib/services/ethService/transactionService/TransactionService.ts @@ -443,9 +443,7 @@ export class TransactionService implements ITransactionService { this.common.resolveEvmAddress(receiptResponse.from, requestDetails), this.common.resolveEvmAddress(receiptResponse.to, requestDetails), ]); - if (!from) { - throw predefined.INTERNAL_ERROR(`Could not resolve from address for transaction ${receiptResponse.hash}`); - } + const transactionReceiptParams: IRegularTransactionReceiptParams = { effectiveGas, from, diff --git a/packages/relay/tests/lib/services/ethService/blockService/BlockService.spec.ts b/packages/relay/tests/lib/services/ethService/blockService/BlockService.spec.ts index 571e306731..daf6003c5f 100644 --- a/packages/relay/tests/lib/services/ethService/blockService/BlockService.spec.ts +++ b/packages/relay/tests/lib/services/ethService/blockService/BlockService.spec.ts @@ -146,25 +146,6 @@ describe('BlockService', () => { .be.true; }); - it('should throw error when from address cannot be resolved', async () => { - // Setup - const mockContractResults = [createMockContractResult()]; - (mirrorNodeClient.getContractResults as sinon.SinonStub).resolves(mockContractResults); - - (commonService.resolveEvmAddress as sinon.SinonStub) - .withArgs('0xoriginalFromAddress', requestDetails, undefined) - .resolves(null); - - (commonService.resolveEvmAddress as sinon.SinonStub) - .withArgs('0xoriginalToAddress', requestDetails, undefined) - .resolves('0xresolvedToAddress'); - - // Execute & Verify - await expect(blockService.getBlockReceipts(blockHashOrNumber, requestDetails)).to.be.rejectedWith( - `Failed to resolve from address: 0xoriginalFromAddress`, - ); - }); - it('should handle null to field for contract creation transactions', async () => { // Setup const mockContractResults = [ @@ -180,8 +161,6 @@ describe('BlockService', () => { .withArgs('0xoriginalFromAddress', requestDetails) .resolves('0xresolvedFromAddress'); - (commonService.resolveEvmAddress as sinon.SinonStub).withArgs(null, requestDetails).resolves(null); - // Execute const receipts = await blockService.getBlockReceipts(blockHashOrNumber, requestDetails); @@ -193,7 +172,7 @@ describe('BlockService', () => { expect((commonService.resolveEvmAddress as sinon.SinonStub).calledWith('0xoriginalFromAddress', requestDetails)) .to.be.true; - expect((commonService.resolveEvmAddress as sinon.SinonStub).calledWith(null, requestDetails)).to.be.true; + expect((commonService.resolveEvmAddress as sinon.SinonStub).calledWith(null, requestDetails)).to.be.false; }); it('should set to field to null when contract is in created_contract_ids', async () => { From b83aa19ac63d3b1b4679270517bc056c51f8ca16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walczak?= Date: Tue, 10 Jun 2025 11:54:03 +0200 Subject: [PATCH 04/13] refactor: add tests for handling `null` to field in contract creation transactions (#3814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Walczak --- .../lib/eth/eth_getBlockReceipts.spec.ts | 102 ++++++++ .../lib/eth/eth_getTransactionReceipt.spec.ts | 23 +- .../blockService/BlockService.spec.ts | 236 ------------------ 3 files changed, 123 insertions(+), 238 deletions(-) delete mode 100644 packages/relay/tests/lib/services/ethService/blockService/BlockService.spec.ts diff --git a/packages/relay/tests/lib/eth/eth_getBlockReceipts.spec.ts b/packages/relay/tests/lib/eth/eth_getBlockReceipts.spec.ts index d476bb3f1a..acdd5223ec 100644 --- a/packages/relay/tests/lib/eth/eth_getBlockReceipts.spec.ts +++ b/packages/relay/tests/lib/eth/eth_getBlockReceipts.spec.ts @@ -214,6 +214,108 @@ describe('@ethGetBlockReceipts using MirrorNode', async function () { expect(receipts[1].transactionHash).to.equal(defaultLogs1[0].transaction_hash); expect(receipts[1].transactionHash).to.equal(defaultLogs1[1].transaction_hash); }); + + it('should handle null to field for contract creation transactions', async function () { + const contractCreationResults = { + results: [ + { + ...results[0], + to: null, + created_contract_ids: ['0.0.1234'], + contract_id: '0.0.1234', + address: '0xnewlyCreatedContractAddress', + }, + ], + links: { next: null }, + }; + + setupStandardResponses({ + [CONTRACT_RESULTS_WITH_FILTER_URL_2]: contractCreationResults, + }); + + const resolveEvmAddressStub = sinon.stub(ethImpl['common'], 'resolveEvmAddress'); + resolveEvmAddressStub.withArgs(results[0].from, sinon.match.any).resolves('0xresolvedFromAddress'); + + const receipts = await ethImpl.getBlockReceipts(BLOCK_HASH, requestDetails); + + expect(receipts).to.exist; + expect(receipts.length).to.equal(1); + expect(receipts[0].from).to.equal('0xresolvedFromAddress'); + expect(receipts[0].to).to.equal(null); + expect(receipts[0].contractAddress).to.not.equal(null); + + expect(resolveEvmAddressStub.calledWith(undefined, sinon.match.any)).to.be.false; + + resolveEvmAddressStub.restore(); + }); + + it('should set to field to null when contract is in created_contract_ids', async function () { + const contractId = '0.0.1234'; + const contractCreationResults = { + results: [ + { + ...results[0], + to: '0xoriginalToAddress', + created_contract_ids: [contractId], + contract_id: contractId, + }, + ], + links: { next: null }, + }; + + setupStandardResponses({ + [CONTRACT_RESULTS_WITH_FILTER_URL_2]: contractCreationResults, + }); + + const resolveEvmAddressStub = sinon.stub(ethImpl['common'], 'resolveEvmAddress'); + resolveEvmAddressStub.withArgs(results[0].from, sinon.match.any).resolves('0xresolvedFromAddress'); + resolveEvmAddressStub.withArgs(undefined, sinon.match.any).resolves(null); + + const receipts = await ethImpl.getBlockReceipts(BLOCK_HASH, requestDetails); + + expect(receipts).to.exist; + expect(receipts.length).to.equal(1); + expect(receipts[0].from).to.equal('0xresolvedFromAddress'); + expect(receipts[0].to).to.equal(null); + + resolveEvmAddressStub.restore(); + }); + + it('should keep original to field when contract is not in created_contract_ids', async function () { + const contractId = '0.0.1234'; + const differentContractId = '0.0.5678'; + const originalToAddress = '0xoriginalToAddress'; + const resolvedToAddress = '0xresolvedToAddress'; + + const contractResults = { + results: [ + { + ...results[0], + to: originalToAddress, + created_contract_ids: [differentContractId], + contract_id: contractId, + }, + ], + links: { next: null }, + }; + + setupStandardResponses({ + [CONTRACT_RESULTS_WITH_FILTER_URL_2]: contractResults, + }); + + const resolveEvmAddressStub = sinon.stub(ethImpl['common'], 'resolveEvmAddress'); + resolveEvmAddressStub.withArgs(results[0].from, sinon.match.any).resolves('0xresolvedFromAddress'); + resolveEvmAddressStub.withArgs(originalToAddress, sinon.match.any).resolves(resolvedToAddress); + + const receipts = await ethImpl.getBlockReceipts(BLOCK_HASH, requestDetails); + + expect(receipts).to.exist; + expect(receipts.length).to.equal(1); + expect(receipts[0].from).to.equal('0xresolvedFromAddress'); + expect(receipts[0].to).to.equal(resolvedToAddress); + + resolveEvmAddressStub.restore(); + }); }); describe('Error cases', () => { diff --git a/packages/relay/tests/lib/eth/eth_getTransactionReceipt.spec.ts b/packages/relay/tests/lib/eth/eth_getTransactionReceipt.spec.ts index 4ab8c4c9ce..a32cc7a8a1 100644 --- a/packages/relay/tests/lib/eth/eth_getTransactionReceipt.spec.ts +++ b/packages/relay/tests/lib/eth/eth_getTransactionReceipt.spec.ts @@ -315,8 +315,10 @@ describe('@ethGetTransactionReceipt eth_getTransactionReceipt tests', async func } }); - it('valid receipt on cache match', async function() { - const cacheKey = `${constants.CACHE_KEY.ETH_GET_TRANSACTION_RECEIPT.replace('eth_', '')}_${defaultDetailedContractResultByHash.hash}`; + it('valid receipt on cache match', async function () { + const cacheKey = `${constants.CACHE_KEY.ETH_GET_TRANSACTION_RECEIPT.replace('eth_', '')}_${ + defaultDetailedContractResultByHash.hash + }`; const cacheReceipt = { blockHash: defaultDetailedContractResultByHash.block_hash, blockNumber: defaultDetailedContractResultByHash.block_number, @@ -352,4 +354,21 @@ describe('@ethGetTransactionReceipt eth_getTransactionReceipt tests', async func expect(receipt.transactionHash).to.eq(cacheReceipt.transactionHash); expect(receipt.transactionIndex).to.eq(cacheReceipt.transactionIndex); }); + + it('should handle receipt with null "to" field', async function () { + const contractResultWithNullTo = { + ...defaultDetailedContractResultByHash, + to: null, + }; + + const uniqueTxHash = '0x17cad7b827375d12d73af57b6a3e84353645fd31305ea58ff52dda53ec640533'; + + restMock.onGet(`contracts/results/${uniqueTxHash}`).reply(200, JSON.stringify(contractResultWithNullTo)); + restMock.onGet(`contracts/${defaultDetailedContractResultByHash.created_contract_ids[0]}`).reply(404); + stubBlockAndFeesFunc(sandbox); + + const receipt = await ethImpl.getTransactionReceipt(uniqueTxHash, requestDetails); + expect(receipt).to.exist; + expect(receipt?.to).to.be.null; + }); }); diff --git a/packages/relay/tests/lib/services/ethService/blockService/BlockService.spec.ts b/packages/relay/tests/lib/services/ethService/blockService/BlockService.spec.ts deleted file mode 100644 index daf6003c5f..0000000000 --- a/packages/relay/tests/lib/services/ethService/blockService/BlockService.spec.ts +++ /dev/null @@ -1,236 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -import { expect, use } from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -import sinon from 'sinon'; -use(chaiAsPromised); - -import { MirrorNodeClient } from '../../../../../src/lib/clients'; -import { Log } from '../../../../../src/lib/model'; -import { BlockService } from '../../../../../src/lib/services'; -import { CommonService } from '../../../../../src/lib/services'; -import { CacheService } from '../../../../../src/lib/services/cacheService/cacheService'; -import { RequestDetails } from '../../../../../src/lib/types'; - -describe('BlockService', () => { - let blockService: BlockService; - let commonService: CommonService; - let mirrorNodeClient: MirrorNodeClient; - let cacheService: CacheService; - let logger: any; - let sandbox: sinon.SinonSandbox; - let requestDetails: RequestDetails; - - // Fixtures - const blockHashOrNumber = '0x1234'; - const mockBlock = { - number: 123, - timestamp: { - from: '1622567324.000000000', - to: '1622567325.000000000', - }, - }; - - const createMockContractResult = (overrides = {}) => ({ - hash: '0xabc123', - from: '0xoriginalFromAddress', - to: '0xoriginalToAddress', - result: 'SUCCESS', - address: '0xcontractAddress', - block_hash: '0xblockHash', - block_number: 123, - block_gas_used: 100000, - gas_used: 50000, - transaction_index: 0, - status: '0x1', - function_parameters: '0x608060405234801561001057600080fd5b50', - call_result: '0x', - created_contract_ids: [], - ...overrides, - }); - - const createMockLogs = () => [ - new Log({ - address: '0xlogsAddress', - blockHash: '0xblockHash', - blockNumber: '0x123', - data: '0xdata', - logIndex: '0x0', - removed: false, - topics: ['0xtopic1'], - transactionHash: '0xabc123', - transactionIndex: '0x0', - }), - ]; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - logger = { - trace: sandbox.stub(), - debug: sandbox.stub(), - info: sandbox.stub(), - warn: sandbox.stub(), - error: sandbox.stub(), - fatal: sandbox.stub(), - isLevelEnabled: sandbox.stub().returns(true), - }; - - mirrorNodeClient = { - getContractResults: sandbox.stub(), - getHistoricalBlockResponse: sandbox.stub(), - getLatestBlock: sandbox.stub(), - } as unknown as MirrorNodeClient; - - commonService = { - getHistoricalBlockResponse: sinon.stub(), - getLogsWithParams: sinon.stub(), - resolveEvmAddress: sinon.stub(), - getGasPriceInWeibars: sinon.stub(), - genericErrorHandler: sinon.stub(), - } as unknown as CommonService; - - cacheService = { - getAsync: sandbox.stub().resolves(null), - set: sandbox.stub().resolves(), - } as unknown as CacheService; - - blockService = new BlockService( - cacheService as any, - '0x12a', - commonService as any, - mirrorNodeClient as any, - logger, - ); - - requestDetails = new RequestDetails({ - requestId: 'test-request-id', - ipAddress: '127.0.0.1', - }); - - // Common stubs for all tests - (commonService.getHistoricalBlockResponse as sinon.SinonStub).resolves(mockBlock); - (commonService.getLogsWithParams as sinon.SinonStub).resolves(createMockLogs()); - (commonService.getGasPriceInWeibars as sinon.SinonStub).resolves(100000000); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe('getBlockReceipts', () => { - it('should resolve from and to addresses correctly', async () => { - // Setup - const mockContractResults = [createMockContractResult()]; - (mirrorNodeClient.getContractResults as sinon.SinonStub).resolves(mockContractResults); - - const resolvedFromAddress = '0xresolvedFromAddress'; - const resolvedToAddress = '0xresolvedToAddress'; - - (commonService.resolveEvmAddress as sinon.SinonStub) - .withArgs('0xoriginalFromAddress', requestDetails) - .resolves(resolvedFromAddress); - - (commonService.resolveEvmAddress as sinon.SinonStub) - .withArgs('0xoriginalToAddress', requestDetails) - .resolves(resolvedToAddress); - - // Execute - const receipts = await blockService.getBlockReceipts(blockHashOrNumber, requestDetails); - - // Verify - expect(receipts).to.have.length(1); - expect(receipts[0].from).to.equal(resolvedFromAddress); - expect(receipts[0].to).to.equal(resolvedToAddress); - expect((commonService.resolveEvmAddress as sinon.SinonStub).calledWith('0xoriginalFromAddress', requestDetails)) - .to.be.true; - expect((commonService.resolveEvmAddress as sinon.SinonStub).calledWith('0xoriginalToAddress', requestDetails)).to - .be.true; - }); - - it('should handle null to field for contract creation transactions', async () => { - // Setup - const mockContractResults = [ - createMockContractResult({ - to: null, - address: '0xnewlyCreatedContractAddress', - }), - ]; - - (mirrorNodeClient.getContractResults as sinon.SinonStub).resolves(mockContractResults); - - (commonService.resolveEvmAddress as sinon.SinonStub) - .withArgs('0xoriginalFromAddress', requestDetails) - .resolves('0xresolvedFromAddress'); - - // Execute - const receipts = await blockService.getBlockReceipts(blockHashOrNumber, requestDetails); - - // Verify - expect(receipts).to.have.length(1); - expect(receipts[0].from).to.equal('0xresolvedFromAddress'); - expect(receipts[0].to).to.equal(null); - expect(receipts[0].contractAddress).to.not.be.null; - - expect((commonService.resolveEvmAddress as sinon.SinonStub).calledWith('0xoriginalFromAddress', requestDetails)) - .to.be.true; - expect((commonService.resolveEvmAddress as sinon.SinonStub).calledWith(null, requestDetails)).to.be.false; - }); - - it('should set to field to null when contract is in created_contract_ids', async () => { - // Setup - const contractId = '0.0.1234'; - const mockContractResults = [ - createMockContractResult({ - to: '0xoriginalToAddress', - contract_id: contractId, - created_contract_ids: [contractId], - }), - ]; - - (mirrorNodeClient.getContractResults as sinon.SinonStub).resolves(mockContractResults); - - (commonService.resolveEvmAddress as sinon.SinonStub) - .withArgs('0xoriginalFromAddress', requestDetails) - .resolves('0xresolvedFromAddress'); - - (commonService.resolveEvmAddress as sinon.SinonStub).withArgs(null, requestDetails).resolves(null); - - // Execute - const receipts = await blockService.getBlockReceipts(blockHashOrNumber, requestDetails); - - // Verify - expect(receipts).to.have.length(1); - expect(receipts[0].from).to.equal('0xresolvedFromAddress'); - expect(receipts[0].to).to.equal(null); - }); - - it('should keep original to field when contract is not in created_contract_ids', async () => { - // Setup - const contractId = '0.0.1234'; - const mockContractResults = [ - createMockContractResult({ - to: '0xoriginalToAddress', - contract_id: contractId, - created_contract_ids: ['0.0.5678'], // Different contract ID - }), - ]; - - (mirrorNodeClient.getContractResults as sinon.SinonStub).resolves(mockContractResults); - - (commonService.resolveEvmAddress as sinon.SinonStub) - .withArgs('0xoriginalFromAddress', requestDetails) - .resolves('0xresolvedFromAddress'); - - (commonService.resolveEvmAddress as sinon.SinonStub) - .withArgs('0xoriginalToAddress', requestDetails) - .resolves('0xresolvedToAddress'); - - // Execute - const receipts = await blockService.getBlockReceipts(blockHashOrNumber, requestDetails); - - // Verify - expect(receipts).to.have.length(1); - expect(receipts[0].from).to.equal('0xresolvedFromAddress'); - expect(receipts[0].to).to.equal('0xresolvedToAddress'); - }); - }); -}); From 21040ae43e834a4dfea690c03d98568fee625014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walczak?= Date: Tue, 10 Jun 2025 11:54:41 +0200 Subject: [PATCH 05/13] feat: update `getTransactionReceipt` types (#3814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Walczak --- .../ethService/transactionService/ITransactionService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/relay/src/lib/services/ethService/transactionService/ITransactionService.ts b/packages/relay/src/lib/services/ethService/transactionService/ITransactionService.ts index 72d4a8a33f..53ee72af89 100644 --- a/packages/relay/src/lib/services/ethService/transactionService/ITransactionService.ts +++ b/packages/relay/src/lib/services/ethService/transactionService/ITransactionService.ts @@ -2,7 +2,7 @@ import { JsonRpcError } from '../../../errors/JsonRpcError'; import { Transaction } from '../../../model'; -import { RequestDetails } from '../../../types'; +import { ITransactionReceipt, RequestDetails } from '../../../types'; export interface ITransactionService { getTransactionByBlockHashAndIndex( @@ -19,7 +19,7 @@ export interface ITransactionService { getTransactionByHash(hash: string, requestDetails: RequestDetails): Promise; - getTransactionReceipt(hash: string, requestDetails: RequestDetails): Promise; + getTransactionReceipt(hash: string, requestDetails: RequestDetails): Promise; sendRawTransaction(transaction: string, requestDetails: RequestDetails): Promise; From a84a70c0d91f069662ad610e4a55e72eb1a90b59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walczak?= Date: Tue, 10 Jun 2025 12:41:12 +0200 Subject: [PATCH 06/13] test: add `eth_getBlockReceipts` test case to conformity (#3814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Walczak --- .../tests/acceptance/data/conformity-tests-batch-5.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/server/tests/acceptance/data/conformity-tests-batch-5.json b/packages/server/tests/acceptance/data/conformity-tests-batch-5.json index 758f3c1bbb..404697fd55 100644 --- a/packages/server/tests/acceptance/data/conformity-tests-batch-5.json +++ b/packages/server/tests/acceptance/data/conformity-tests-batch-5.json @@ -34,5 +34,9 @@ "eth_mining": { "request": "{\"id\":\"test_id\",\"jsonrpc\":\"2.0\",\"method\":\"eth_mining\",\"params\":[]}", "response": "{\"result\":false,\"jsonrpc\":\"2.0\",\"id\":\"test_id\"}" + }, + "eth_getBlockReceipts": { + "request": "{\"id\":\"test_id\",\"jsonrpc\":\"2.0\",\"method\":\"eth_getBlockReceipts\",\"params\":[\"0x20718245\"]}", + "response": "{\"result\":null,\"jsonrpc\":\"2.0\",\"id\":\"test_id\"}" } } From 6947e6e810da73b0052ff7539b03445086aee057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walczak?= Date: Wed, 11 Jun 2025 16:02:20 +0200 Subject: [PATCH 07/13] test: add test for `eth_getBlockReceipts` handling contract creation with `null` to field (#3814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Walczak --- .../tests/acceptance/rpc_batch1.spec.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/server/tests/acceptance/rpc_batch1.spec.ts b/packages/server/tests/acceptance/rpc_batch1.spec.ts index aa005a7d4e..ff456850a7 100644 --- a/packages/server/tests/acceptance/rpc_batch1.spec.ts +++ b/packages/server/tests/acceptance/rpc_batch1.spec.ts @@ -821,6 +821,40 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { ['0x', requestIdPrefix], ]); }); + + it('should execute "eth_getBlockReceipts" with contract deployment transaction showing null to field', async function () { + const contractDeployment = await Utils.deployContract( + basicContract.abi, + basicContract.bytecode, + accounts[0].wallet, + ); + const deploymentTransaction = contractDeployment.deploymentTransaction(); + if (!deploymentTransaction) { + throw new Error('Deployment transaction is null'); + } + + const deploymentReceipt = await relay.pollForValidTransactionReceipt(deploymentTransaction.hash); + + const deploymentBlock = await relay.call( + RelayCalls.ETH_ENDPOINTS.ETH_GET_BLOCK_BY_HASH, + [deploymentReceipt.blockHash, false], + requestIdPrefix, + ); + + const res = await relay.call( + RelayCalls.ETH_ENDPOINTS.ETH_GET_BLOCK_RECEIPTS, + [deploymentBlock.hash], + requestIdPrefix, + ); + + const deploymentReceiptInBlock = res.find((receipt) => receipt.transactionHash === deploymentTransaction.hash); + + expect(deploymentReceiptInBlock).to.exist; + expect(deploymentReceiptInBlock).to.have.property('to'); + expect(deploymentReceiptInBlock.to).to.be.null; + expect(deploymentReceiptInBlock.contractAddress).to.not.be.null; + expect(deploymentReceiptInBlock.contractAddress).to.equal(contractDeployment.target); + }); }); describe('Transaction related RPC Calls', () => { From 6905eb4f82e311592fbf6f532dccc6a574e1f81d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walczak?= Date: Thu, 12 Jun 2025 12:41:32 +0200 Subject: [PATCH 08/13] refactor: move `to` field correction logic for contract creation receipts to `transactionReceiptFactory` (#3814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Walczak --- .../factories/transactionReceiptFactory.ts | 31 ++++++++++++++++++- .../ethService/blockService/BlockService.ts | 28 ----------------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/packages/relay/src/lib/factories/transactionReceiptFactory.ts b/packages/relay/src/lib/factories/transactionReceiptFactory.ts index 3da0a6d704..6f7d0ef077 100644 --- a/packages/relay/src/lib/factories/transactionReceiptFactory.ts +++ b/packages/relay/src/lib/factories/transactionReceiptFactory.ts @@ -68,16 +68,45 @@ class TransactionReceiptFactory { /** * Creates a regular transaction receipt from mirror node contract result data * + * Handles the correction of transaction receipt `to` field for contract creation transactions. + * + * This logic addresses a discrepancy between Hedera and standard Ethereum behavior regarding + * the `to` field in transaction receipts. When a smart contract is deployed: + * + * 1. In standard Ethereum JSON-RPC, if the original transaction had a null `to` field + * (contract creation), the transaction receipt also reports a null `to` field. + * + * 2. Hedera Mirror Node, however, automatically populates the `to` field with the + * address of the newly created contract. + * + * The code checks if a contract was directly created by the transaction (rather than created by + * another contract) by checking if the contract's ID appears in the `created_contract_ids` array. + * If so, it resets the `to` field to null to match standard Ethereum JSON-RPC behavior. + * + * This ensures compatibility with Ethereum tooling that expects standard transaction receipt formats. + * The handling covers various scenarios: + * + * - Direct contract deployment (empty `to` field) + * - Contract creation via factory contracts + * - Method calls that don't create contracts + * - Transactions with populated `to` fields that create child contracts + * * @param params Parameters required to create a regular transaction receipt * @param resolveEvmAddressFn Function to resolve EVM addresses * @returns {ITransactionReceipt} Transaction receipt for the regular transaction */ public static createRegularReceipt(params: IRegularTransactionReceiptParams): ITransactionReceipt { - const { receiptResponse, effectiveGas, from, logs, to } = params; + const { receiptResponse, effectiveGas, from, logs } = params; + let { to } = params; // Determine contract address if it exists const contractAddress = TransactionReceiptFactory.getContractAddressFromReceipt(receiptResponse); + const { contractResult } = receiptResponse; + if (contractResult.created_contract_ids.includes(contractResult.contract_id)) { + to = null; + } + // Create the receipt object const receipt: ITransactionReceipt = { blockHash: toHash32(receiptResponse.block_hash), diff --git a/packages/relay/src/lib/services/ethService/blockService/BlockService.ts b/packages/relay/src/lib/services/ethService/blockService/BlockService.ts index d9a16ba97d..a4f800cdff 100644 --- a/packages/relay/src/lib/services/ethService/blockService/BlockService.ts +++ b/packages/relay/src/lib/services/ethService/blockService/BlockService.ts @@ -176,34 +176,6 @@ export class BlockService implements IBlockService { return null; } - /** - * Handles the correction of transaction receipt `to` field for contract creation transactions. - * - * This logic addresses a discrepancy between Hedera and standard Ethereum behavior regarding - * the `to` field in transaction receipts. When a smart contract is deployed: - * - * 1. In standard Ethereum JSON-RPC, if the original transaction had a null `to` field - * (contract creation), the transaction receipt also reports a null `to` field. - * - * 2. Hedera Mirror Node, however, automatically populates the `to` field with the - * address of the newly created contract. - * - * The code checks if a contract was directly created by the transaction (rather than created by - * another contract) by checking if the contract's ID appears in the `created_contract_ids` array. - * If so, it resets the `to` field to null to match standard Ethereum JSON-RPC behavior. - * - * This ensures compatibility with Ethereum tooling that expects standard transaction receipt formats. - * The handling covers various scenarios: - * - * - Direct contract deployment (empty `to` field) - * - Contract creation via factory contracts - * - Method calls that don't create contracts - * - Transactions with populated `to` fields that create child contracts - */ - if (contractResult.created_contract_ids.includes(contractResult.contract_id)) { - contractResult.to = null; - } - contractResult.logs = logsByHash.get(contractResult.hash) || []; const [from, to] = await Promise.all([ this.common.resolveEvmAddress(contractResult.from, requestDetails), From 3f76a61aeca1e595afbc986fcc9d15553abd8668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walczak?= Date: Fri, 13 Jun 2025 09:43:27 +0200 Subject: [PATCH 09/13] fix: correct logic for checking `created_contract_ids` in `transactionReceiptFactory` (#3814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Walczak --- packages/relay/src/lib/factories/transactionReceiptFactory.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/relay/src/lib/factories/transactionReceiptFactory.ts b/packages/relay/src/lib/factories/transactionReceiptFactory.ts index 6f7d0ef077..33e66272c9 100644 --- a/packages/relay/src/lib/factories/transactionReceiptFactory.ts +++ b/packages/relay/src/lib/factories/transactionReceiptFactory.ts @@ -102,8 +102,7 @@ class TransactionReceiptFactory { // Determine contract address if it exists const contractAddress = TransactionReceiptFactory.getContractAddressFromReceipt(receiptResponse); - const { contractResult } = receiptResponse; - if (contractResult.created_contract_ids.includes(contractResult.contract_id)) { + if (receiptResponse.created_contract_ids.includes(receiptResponse.contract_id)) { to = null; } From 4a9d4925476a9359c3cde5670871c3763d33ba50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walczak?= Date: Tue, 17 Jun 2025 11:49:35 +0200 Subject: [PATCH 10/13] test: add test for `eth_getTransactionReceipt` handling direct contract deployments with `null` to field (#3814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Walczak --- .../tests/acceptance/rpc_batch1.spec.ts | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/server/tests/acceptance/rpc_batch1.spec.ts b/packages/server/tests/acceptance/rpc_batch1.spec.ts index ff456850a7..0bc614b40c 100644 --- a/packages/server/tests/acceptance/rpc_batch1.spec.ts +++ b/packages/server/tests/acceptance/rpc_batch1.spec.ts @@ -27,6 +27,7 @@ import RelayCalls from '../../tests/helpers/constants'; import MirrorClient from '../clients/mirrorClient'; import RelayClient from '../clients/relayClient'; import ServicesClient from '../clients/servicesClient'; +import basicContractJson from '../contracts/Basic.json'; import logsContractJson from '../contracts/Logs.json'; // Local resources from contracts directory import parentContractJson from '../contracts/Parent.json'; @@ -824,20 +825,19 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { it('should execute "eth_getBlockReceipts" with contract deployment transaction showing null to field', async function () { const contractDeployment = await Utils.deployContract( - basicContract.abi, - basicContract.bytecode, + basicContractJson.abi, + basicContractJson.bytecode, accounts[0].wallet, ); - const deploymentTransaction = contractDeployment.deploymentTransaction(); - if (!deploymentTransaction) { + const basicContractTx = contractDeployment.deploymentTransaction(); + if (!basicContractTx) { throw new Error('Deployment transaction is null'); } - - const deploymentReceipt = await relay.pollForValidTransactionReceipt(deploymentTransaction.hash); + const receipt = await relay.pollForValidTransactionReceipt(basicContractTx.hash); const deploymentBlock = await relay.call( RelayCalls.ETH_ENDPOINTS.ETH_GET_BLOCK_BY_HASH, - [deploymentReceipt.blockHash, false], + [receipt.blockHash, false], requestIdPrefix, ); @@ -847,7 +847,7 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { requestIdPrefix, ); - const deploymentReceiptInBlock = res.find((receipt) => receipt.transactionHash === deploymentTransaction.hash); + const deploymentReceiptInBlock = res.find((receipt) => receipt.transactionHash === basicContractTx.hash); expect(deploymentReceiptInBlock).to.exist; expect(deploymentReceiptInBlock).to.have.property('to'); @@ -1112,6 +1112,30 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { expect(res).to.be.null; }); + it('should execute "eth_getTransactionReceipt" and set "to" field to null for direct contract deployment', async function () { + const basicContract = await Utils.deployContract( + basicContractJson.abi, + basicContractJson.bytecode, + accounts[0].wallet, + ); + + const contractDeploymentTx = basicContract.deploymentTransaction(); + if (!contractDeploymentTx) { + throw new Error('Deployment transaction is null'); + } + await relay.pollForValidTransactionReceipt(contractDeploymentTx.hash); + + const contractDeploymentReceipt = await relay.call( + RelayCalls.ETH_ENDPOINTS.ETH_GET_TRANSACTION_RECEIPT, + [contractDeploymentTx.hash], + requestIdPrefix, + ); + + expect(contractDeploymentReceipt).to.exist; + expect(contractDeploymentReceipt.contractAddress).to.not.be.null; + expect(contractDeploymentReceipt.to).to.be.null; + }); + it('should fail "eth_sendRawTransaction" for transaction with incorrect chain_id', async function () { const transaction = { ...default155TransactionData, From adba32272baf4d7c8330fbec694938a3640ee6a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walczak?= Date: Wed, 18 Jun 2025 11:08:21 +0200 Subject: [PATCH 11/13] test(fix): add case for `eth_getBlockReceipts` returning `null` on non-existent block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Walczak --- packages/server/tests/acceptance/rpc_batch1.spec.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/server/tests/acceptance/rpc_batch1.spec.ts b/packages/server/tests/acceptance/rpc_batch1.spec.ts index 0bc614b40c..ec0371733a 100644 --- a/packages/server/tests/acceptance/rpc_batch1.spec.ts +++ b/packages/server/tests/acceptance/rpc_batch1.spec.ts @@ -855,6 +855,15 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { expect(deploymentReceiptInBlock.contractAddress).to.not.be.null; expect(deploymentReceiptInBlock.contractAddress).to.equal(contractDeployment.target); }); + + it('should return null for "eth_getBlockReceipts" when block is not found', async function () { + const res = await relay.call( + RelayCalls.ETH_ENDPOINTS.ETH_GET_BLOCK_RECEIPTS, + [Address.NON_EXISTING_BLOCK_HASH], + requestIdPrefix, + ); + expect(res).to.be.null; + }); }); describe('Transaction related RPC Calls', () => { From 25dd1cfde277778b3f110759cbb3641c31501083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walczak?= Date: Mon, 23 Jun 2025 14:24:37 +0200 Subject: [PATCH 12/13] test(fix): normalize case for contractAddress comparison in contract deployment tests (#3814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Walczak --- packages/server/tests/acceptance/rpc_batch1.spec.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/server/tests/acceptance/rpc_batch1.spec.ts b/packages/server/tests/acceptance/rpc_batch1.spec.ts index ec0371733a..13fd49e415 100644 --- a/packages/server/tests/acceptance/rpc_batch1.spec.ts +++ b/packages/server/tests/acceptance/rpc_batch1.spec.ts @@ -853,7 +853,9 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { expect(deploymentReceiptInBlock).to.have.property('to'); expect(deploymentReceiptInBlock.to).to.be.null; expect(deploymentReceiptInBlock.contractAddress).to.not.be.null; - expect(deploymentReceiptInBlock.contractAddress).to.equal(contractDeployment.target); + expect(deploymentReceiptInBlock.contractAddress.toLowerCase()).to.equal( + contractDeployment.target.toString().toLowerCase(), + ); }); it('should return null for "eth_getBlockReceipts" when block is not found', async function () { From 7789fe7b2e139c843329196167f26607d290d3ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Walczak?= Date: Mon, 23 Jun 2025 14:26:58 +0200 Subject: [PATCH 13/13] test(fix): remove `eth_getBlockReceipts` case from conformity batch 5 (#3814) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Walczak --- .../tests/acceptance/data/conformity-tests-batch-5.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/server/tests/acceptance/data/conformity-tests-batch-5.json b/packages/server/tests/acceptance/data/conformity-tests-batch-5.json index 404697fd55..758f3c1bbb 100644 --- a/packages/server/tests/acceptance/data/conformity-tests-batch-5.json +++ b/packages/server/tests/acceptance/data/conformity-tests-batch-5.json @@ -34,9 +34,5 @@ "eth_mining": { "request": "{\"id\":\"test_id\",\"jsonrpc\":\"2.0\",\"method\":\"eth_mining\",\"params\":[]}", "response": "{\"result\":false,\"jsonrpc\":\"2.0\",\"id\":\"test_id\"}" - }, - "eth_getBlockReceipts": { - "request": "{\"id\":\"test_id\",\"jsonrpc\":\"2.0\",\"method\":\"eth_getBlockReceipts\",\"params\":[\"0x20718245\"]}", - "response": "{\"result\":null,\"jsonrpc\":\"2.0\",\"id\":\"test_id\"}" } }