Skip to content

Commit 87f6955

Browse files
authored
chore: cherry pick feat: added DEPENDENT_SERVICE_IMMATURE_RECORDS predefined error for immature records scenarios (#3394) (#3415)
feat: added DEPENDENT_SERVICE_IMMATURE_RECORDS predefined error for immature records scenarios (#3394) * feat: added DEPENDENT_SERVICE_IMMATURE_RECORDS predefined error for immature records scenarios * test: fixed unit tests for the new DEPENDENT_SERVICE_IMMATURE_RECORDS scenarios --------- Signed-off-by: Logan Nguyen <logan.nguyen@swirldslabs.com>
1 parent 18d3acd commit 87f6955

File tree

11 files changed

+75
-99
lines changed

11 files changed

+75
-99
lines changed

packages/relay/src/lib/clients/mirrorNodeClient.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,8 @@ export class MirrorNodeClient {
783783
let contractResult = await this[methodName](...args);
784784

785785
for (let i = 0; i < mirrorNodeRequestRetryCount; i++) {
786+
const isLastAttempt = i === mirrorNodeRequestRetryCount - 1;
787+
786788
if (contractResult) {
787789
const contractObjects = Array.isArray(contractResult) ? contractResult : [contractResult];
788790

@@ -802,10 +804,15 @@ export class MirrorNodeClient {
802804
requestDetails.formattedRequestId
803805
} Contract result contains nullable transaction_index or block_number, or block_hash is an empty hex (0x): contract_result=${JSON.stringify(
804806
contractObject,
805-
)}. Retrying after a delay of ${mirrorNodeRetryDelay} ms `,
807+
)}. ${!isLastAttempt ? `Retrying after a delay of ${mirrorNodeRetryDelay} ms.` : ``}`,
806808
);
807809
}
808810

811+
// If immature records persist after the final polling attempt, throw the DEPENDENT_SERVICE_IMMATURE_RECORDS error.
812+
if (isLastAttempt) {
813+
throw predefined.DEPENDENT_SERVICE_IMMATURE_RECORDS;
814+
}
815+
809816
foundImmatureRecord = true;
810817
break;
811818
}
@@ -965,6 +972,7 @@ export class MirrorNodeClient {
965972
);
966973

967974
for (let i = 0; i < mirrorNodeRequestRetryCount; i++) {
975+
const isLastAttempt = i === mirrorNodeRequestRetryCount - 1;
968976
if (logResults) {
969977
let foundImmatureRecord = false;
970978

@@ -981,12 +989,17 @@ export class MirrorNodeClient {
981989
this.logger.debug(
982990
`${
983991
requestDetails.formattedRequestId
984-
} Contract result log contains undefined transaction_index, block_number, index, or block_hash is an empty hex (0x): log=${JSON.stringify(
992+
} Contract result log contains nullable transaction_index, block_number, index, or block_hash is an empty hex (0x): log=${JSON.stringify(
985993
log,
986-
)}. Retrying after a delay of ${mirrorNodeRetryDelay} ms.`,
994+
)}. ${!isLastAttempt ? `Retrying after a delay of ${mirrorNodeRetryDelay} ms.` : ``}`,
987995
);
988996
}
989997

998+
// If immature records persist after the final polling attempt, throw the DEPENDENT_SERVICE_IMMATURE_RECORDS error.
999+
if (isLastAttempt) {
1000+
throw predefined.DEPENDENT_SERVICE_IMMATURE_RECORDS;
1001+
}
1002+
9901003
foundImmatureRecord = true;
9911004
break;
9921005
}

packages/relay/src/lib/errors/JsonRpcError.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ export const predefined = {
4747
data,
4848
});
4949
},
50+
DEPENDENT_SERVICE_IMMATURE_RECORDS: new JsonRpcError({
51+
code: -32015,
52+
message: 'Dependent service returned immature records',
53+
}),
5054
GAS_LIMIT_TOO_HIGH: (gasLimit, maxGas) =>
5155
new JsonRpcError({
5256
code: -32005,

packages/relay/src/lib/eth.ts

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1937,8 +1937,6 @@ export class EthImpl implements Eth {
19371937

19381938
if (!contractResults[0]) return null;
19391939

1940-
this.handleImmatureContractResultRecord(contractResults[0], requestDetails);
1941-
19421940
const resolvedToAddress = await this.resolveEvmAddress(contractResults[0].to, requestDetails);
19431941
const resolvedFromAddress = await this.resolveEvmAddress(contractResults[0].from, requestDetails, [
19441942
constants.TYPE_ACCOUNT,
@@ -2233,8 +2231,6 @@ export class EthImpl implements Eth {
22332231
return this.createTransactionFromLog(syntheticLogs[0]);
22342232
}
22352233

2236-
this.handleImmatureContractResultRecord(contractResult, requestDetails);
2237-
22382234
const fromAddress = await this.resolveEvmAddress(contractResult.from, requestDetails, [constants.TYPE_ACCOUNT]);
22392235
const toAddress = await this.resolveEvmAddress(contractResult.to, requestDetails);
22402236
contractResult.chain_id = contractResult.chain_id || this.chain;
@@ -2327,8 +2323,6 @@ export class EthImpl implements Eth {
23272323
);
23282324
return receipt;
23292325
} else {
2330-
this.handleImmatureContractResultRecord(receiptResponse, requestDetails);
2331-
23322326
const effectiveGas = await this.getCurrentGasPriceForBlock(receiptResponse.block_hash, requestDetails);
23332327
// support stricter go-eth client which requires the transaction hash property on logs
23342328
const logs = receiptResponse.logs.map((log) => {
@@ -2570,8 +2564,6 @@ export class EthImpl implements Eth {
25702564
// prepare transactionArray
25712565
let transactionArray: any[] = [];
25722566
for (const contractResult of contractResults) {
2573-
this.handleImmatureContractResultRecord(contractResult, requestDetails);
2574-
25752567
// there are several hedera-specific validations that occur right before entering the evm
25762568
// if a transaction has reverted there, we should not include that tx in the block response
25772569
if (Utils.isRevertedDueToHederaSpecificValidation(contractResult)) {
@@ -2841,32 +2833,4 @@ export class EthImpl implements Eth {
28412833
const exchangeRateInCents = currentNetworkExchangeRate.cent_equivalent / currentNetworkExchangeRate.hbar_equivalent;
28422834
return exchangeRateInCents;
28432835
}
2844-
2845-
/**
2846-
* Checks if a contract result record is immature by validating required fields.
2847-
* An immature record can be characterized by:
2848-
* - `transaction_index` being null/undefined
2849-
* - `block_number` being null/undefined
2850-
* - `block_hash` being '0x' (empty hex)
2851-
*
2852-
* @param {any} record - The contract result record to validate
2853-
* @param {RequestDetails} requestDetails - Details used for logging and tracking the request
2854-
* @throws {Error} If the record is missing required fields
2855-
*/
2856-
private handleImmatureContractResultRecord(record: any, requestDetails: RequestDetails) {
2857-
if (record.transaction_index == null || record.block_number == null || record.block_hash === EthImpl.emptyHex) {
2858-
if (this.logger.isLevelEnabled('debug')) {
2859-
this.logger.debug(
2860-
`${
2861-
requestDetails.formattedRequestId
2862-
} Contract result is missing required fields: block_number, transaction_index, or block_hash is an empty hex (0x). contractResult=${JSON.stringify(
2863-
record,
2864-
)}`,
2865-
);
2866-
}
2867-
throw predefined.INTERNAL_ERROR(
2868-
`The contract result response from the remote Mirror Node server is missing required fields. `,
2869-
);
2870-
}
2871-
}
28722836
}

packages/relay/src/lib/services/ethService/ethCommonService/index.ts

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services'
2222
import * as _ from 'lodash';
2323
import { Logger } from 'pino';
2424

25-
import { nullableNumberTo0x, numberTo0x, parseNumericEnvVar, toHash32 } from '../../../../formatters';
25+
import { numberTo0x, parseNumericEnvVar, toHash32 } from '../../../../formatters';
2626
import { MirrorNodeClient } from '../../../clients';
2727
import constants from '../../../constants';
2828
import { JsonRpcError, predefined } from '../../../errors/JsonRpcError';
@@ -346,26 +346,6 @@ export class CommonService implements ICommonService {
346346

347347
const logs: Log[] = [];
348348
for (const log of logResults) {
349-
if (
350-
log.transaction_index == null ||
351-
log.block_number == null ||
352-
log.index == null ||
353-
log.block_hash === EthImpl.emptyHex
354-
) {
355-
if (this.logger.isLevelEnabled('debug')) {
356-
this.logger.debug(
357-
`${
358-
requestDetails.formattedRequestId
359-
} Log entry is missing required fields: block_number, index, or block_hash is an empty hex (0x). log=${JSON.stringify(
360-
log,
361-
)}`,
362-
);
363-
}
364-
throw predefined.INTERNAL_ERROR(
365-
`The log entry from the remote Mirror Node server is missing required fields. `,
366-
);
367-
}
368-
369349
logs.push(
370350
new Log({
371351
address: log.address,

packages/relay/tests/lib/eth/eth_getBlockByHash.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ describe('@ethGetBlockByHash using MirrorNode', async function () {
393393
expect.fail('should have thrown an error');
394394
} catch (error) {
395395
expect(error).to.exist;
396-
expect(error.message).to.include('The log entry from the remote Mirror Node server is missing required fields');
396+
expect(error).to.eq(predefined.DEPENDENT_SERVICE_IMMATURE_RECORDS);
397397
}
398398
}
399399
});

packages/relay/tests/lib/eth/eth_getBlockByNumber.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,7 @@ describe('@ethGetBlockByNumber using MirrorNode', async function () {
638638
expect.fail('should have thrown an error');
639639
} catch (error) {
640640
expect(error).to.exist;
641-
expect(error.message).to.include('The log entry from the remote Mirror Node server is missing required fields');
641+
expect(error).to.eq(predefined.DEPENDENT_SERVICE_IMMATURE_RECORDS);
642642
}
643643
}
644644
});

packages/relay/tests/lib/eth/eth_getLogs.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import chaiAsPromised from 'chai-as-promised';
2424
import { ethers } from 'ethers';
2525
import sinon from 'sinon';
2626

27-
import { Eth } from '../../../src';
27+
import { Eth, predefined } from '../../../src';
2828
import { SDKClient } from '../../../src/lib/clients';
2929
import { CacheService } from '../../../src/lib/services/cacheService/cacheService';
3030
import HAPIService from '../../../src/lib/services/hapiService/hapiService';
@@ -195,7 +195,7 @@ describe('@ethGetLogs using MirrorNode', async function () {
195195
expect.fail('should have thrown an error');
196196
} catch (error) {
197197
expect(error).to.exist;
198-
expect(error.message).to.include('The log entry from the remote Mirror Node server is missing required fields.');
198+
expect(error).to.eq(predefined.DEPENDENT_SERVICE_IMMATURE_RECORDS);
199199
}
200200
});
201201

packages/relay/tests/lib/eth/eth_getTransactionByHash.spec.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
NO_TRANSACTIONS,
3535
} from './eth-config';
3636
import { generateEthTestEnv } from './eth-helpers';
37+
import { predefined } from '../../../src';
3738

3839
use(chaiAsPromised);
3940

@@ -233,9 +234,7 @@ describe('@ethGetTransactionByHash eth_getTransactionByHash tests', async functi
233234
expect.fail('should have thrown an error');
234235
} catch (error) {
235236
expect(error).to.exist;
236-
expect(error.message).to.include(
237-
'The contract result response from the remote Mirror Node server is missing required fields.',
238-
);
237+
expect(error).to.eq(predefined.DEPENDENT_SERVICE_IMMATURE_RECORDS);
239238
}
240239
});
241240

@@ -252,9 +251,7 @@ describe('@ethGetTransactionByHash eth_getTransactionByHash tests', async functi
252251
expect.fail('should have thrown an error');
253252
} catch (error) {
254253
expect(error).to.exist;
255-
expect(error.message).to.include(
256-
'The contract result response from the remote Mirror Node server is missing required fields.',
257-
);
254+
expect(error).to.eq(predefined.DEPENDENT_SERVICE_IMMATURE_RECORDS);
258255
}
259256
});
260257

@@ -273,9 +270,7 @@ describe('@ethGetTransactionByHash eth_getTransactionByHash tests', async functi
273270
expect.fail('should have thrown an error');
274271
} catch (error) {
275272
expect(error).to.exist;
276-
expect(error.message).to.include(
277-
'The contract result response from the remote Mirror Node server is missing required fields.',
278-
);
273+
expect(error).to.eq(predefined.DEPENDENT_SERVICE_IMMATURE_RECORDS);
279274
}
280275
});
281276

packages/relay/tests/lib/eth/eth_getTransactionReceipt.spec.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import RelayAssertions from '../../assertions';
2929
import { defaultErrorMessageHex } from '../../helpers';
3030
import { DEFAULT_BLOCK, EMPTY_LOGS_RESPONSE } from './eth-config';
3131
import { generateEthTestEnv } from './eth-helpers';
32+
import { predefined } from '../../../src';
3233

3334
use(chaiAsPromised);
3435

@@ -313,9 +314,7 @@ describe('@ethGetTransactionReceipt eth_getTransactionReceipt tests', async func
313314
expect.fail('should have thrown an error');
314315
} catch (error) {
315316
expect(error).to.exist;
316-
expect(error.message).to.include(
317-
'The contract result response from the remote Mirror Node server is missing required fields.',
318-
);
317+
expect(error).to.eq(predefined.DEPENDENT_SERVICE_IMMATURE_RECORDS);
319318
}
320319
});
321320

packages/relay/tests/lib/mirrorNodeClient.spec.ts

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -686,8 +686,8 @@ describe('MirrorNodeClient', async function () {
686686

687687
it('`getContractResultsWithRetry` should return immature records after exhausting maximum retry attempts', async () => {
688688
const hash = '0x2a563af33c4871b51a8b108aa2fe1dd5280a30dfb7236170ae5e5e7957eb6393';
689-
// Mock 11 sequential calls that return immature records - as default polling counts (10)
690-
[...Array(11)].reduce((mockChain) => {
689+
// Mock 10 sequential calls that return immature records - equals to the default polling counts (10) - should throw an error at the last polling attempt
690+
[...Array(10)].reduce((mockChain) => {
691691
return mockChain.onGet(`contracts/results/${hash}`).replyOnce(200, {
692692
...detailedContractResult,
693693
transaction_index: null,
@@ -696,17 +696,19 @@ describe('MirrorNodeClient', async function () {
696696
});
697697
}, mock);
698698

699-
const result = await mirrorNodeInstance.getContractResultWithRetry(
700-
mirrorNodeInstance.getContractResult.name,
701-
[hash, requestDetails],
702-
requestDetails,
703-
);
699+
try {
700+
await mirrorNodeInstance.getContractResultWithRetry(
701+
mirrorNodeInstance.getContractResult.name,
702+
[hash, requestDetails],
703+
requestDetails,
704+
);
705+
expect.fail('should have thrown an error');
706+
} catch (error) {
707+
expect(error).to.exist;
708+
expect(error).to.eq(predefined.DEPENDENT_SERVICE_IMMATURE_RECORDS);
709+
}
704710

705-
expect(result).to.exist;
706-
expect(result.transaction_index).equal(null);
707-
expect(result.block_number).equal(null);
708-
expect(result.block_hash).equal('0x');
709-
expect(mock.history.get.length).to.eq(11);
711+
expect(mock.history.get.length).to.eq(10);
710712
});
711713

712714
it('`getContractResults` detailed', async () => {
@@ -861,22 +863,20 @@ describe('MirrorNodeClient', async function () {
861863
});
862864

863865
it('`getContractResultsLogsWithRetry` should return immature records after exhausting maximum retry attempts', async () => {
864-
// Mock 11 sequential calls that return immature records - greater than default polling counts (10)
865-
[...Array(11)].reduce((mockChain) => {
866+
// Mock 10 sequential calls that return immature records - equals to the default polling counts (10) - should throw an error at the last polling attempt
867+
[...Array(10)].reduce((mockChain) => {
866868
return mockChain.onGet(`contracts/results/logs?limit=100&order=asc`).replyOnce(200, {
867869
logs: [{ ...log, transaction_index: null, block_number: null, index: null, block_hash: '0x' }],
868870
});
869871
}, mock);
870872

871-
const expectedLog = { ...log, transaction_index: null, block_number: null, index: null, block_hash: '0x' };
872-
873-
const results = await mirrorNodeInstance.getContractResultsLogsWithRetry(requestDetails);
874-
875-
expect(results).to.exist;
876-
expect(results.length).to.gt(0);
877-
const logObject = results[0];
878-
expect(logObject).to.deep.eq(expectedLog);
879-
expect(mock.history.get.length).to.eq(11);
873+
try {
874+
await mirrorNodeInstance.getContractResultsLogsWithRetry(requestDetails);
875+
} catch (error) {
876+
expect(error).to.exist;
877+
expect(error).to.eq(predefined.DEPENDENT_SERVICE_IMMATURE_RECORDS);
878+
}
879+
expect(mock.history.get.length).to.eq(10);
880880
});
881881

882882
it('`getContractResultsLogsByAddress` ', async () => {

0 commit comments

Comments
 (0)