Skip to content

Commit 01c893a

Browse files
fix: Adds retry to send raw transaction (#3161) (#3208)
* Adds retry to eth send raw transaction * Adds mirror node check before retry and expands SDKClientError object to include transactionId * Adds unit tests * Adds improvements * adds relevant comment for hardcoded values * removes retry and adds mirror node check in the sdk client * Adds unit tests * Addresses PR comments * Changes the error thrown when timeout occurs to SDKClientError * Ensure other error types are thrown --------- Signed-off-by: Konstantina Blazhukova <konstantina.blajukova@gmail.com> Signed-off-by: Eric Badiere <ebadiere@gmail.com> Co-authored-by: konstantinabl <konstantina.blajukova@gmail.com>
1 parent 90bf36a commit 01c893a

File tree

6 files changed

+158
-16
lines changed

6 files changed

+158
-16
lines changed

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

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ import {
6969
ITransactionRecordMetric,
7070
RequestDetails,
7171
} from '../types';
72+
import { MirrorNodeClient } from './mirrorNodeClient';
73+
import { Registry } from 'prom-client';
7274

7375
const _ = require('lodash');
7476

@@ -122,6 +124,14 @@ export class SDKClient {
122124
*/
123125
private readonly hbarLimitService: HbarLimitService;
124126

127+
/**
128+
* An instance of the MirrorNodeClient
129+
* @private
130+
* @readonly
131+
* @type {MirrorNodeClient}
132+
*/
133+
private readonly mirrorNodeClient: MirrorNodeClient;
134+
125135
/**
126136
* Constructs an instance of the SDKClient and initializes various services and settings.
127137
*
@@ -136,6 +146,7 @@ export class SDKClient {
136146
cacheService: CacheService,
137147
eventEmitter: EventEmitter,
138148
hbarLimitService: HbarLimitService,
149+
register: Registry,
139150
) {
140151
this.clientMain = clientMain;
141152

@@ -151,6 +162,13 @@ export class SDKClient {
151162
this.hbarLimitService = hbarLimitService;
152163
this.maxChunks = Number(ConfigService.get('FILE_APPEND_MAX_CHUNKS')) || 20;
153164
this.fileAppendChunkSize = Number(ConfigService.get('FILE_APPEND_CHUNK_SIZE')) || 5120;
165+
this.mirrorNodeClient = new MirrorNodeClient(
166+
// @ts-ignore
167+
ConfigService.get('MIRROR_NODE_URL') || '',
168+
logger,
169+
register,
170+
cacheService,
171+
);
154172
}
155173

156174
/**
@@ -441,19 +459,61 @@ export class SDKClient {
441459
Hbar.fromTinybars(Math.floor(networkGasPriceInTinyBars * constants.MAX_GAS_PER_SEC)),
442460
);
443461

444-
return {
445-
fileId,
446-
txResponse: await this.executeTransaction(
462+
let txResponse;
463+
try {
464+
txResponse = await this.executeTransaction(
447465
ethereumTransaction,
448466
callerName,
449467
interactingEntity,
450468
requestDetails,
451469
true,
452470
originalCallerAddress,
453-
),
471+
);
472+
} catch (e: any) {
473+
if (e instanceof SDKClientError && (e.isConnectionDropped() || e.isTimeoutExceeded())) {
474+
const isFailed = await this.isFailedTransaction(requestDetails, e.transactionId);
475+
if (isFailed) {
476+
throw e;
477+
}
478+
txResponse = { transactionId: e.transactionId };
479+
} else {
480+
throw e;
481+
}
482+
}
483+
484+
return {
485+
fileId,
486+
txResponse,
454487
};
455488
}
456489

490+
/**
491+
* Checks if a transaction has failed by querying the mirror node.
492+
*
493+
* @param {RequestDetails} requestDetails - The details of the request.
494+
* @param {string} [transactionId] - The ID of the transaction to check.
495+
* @returns {Promise<boolean>} - A promise that resolves to `true` if the transaction has failed, `false` otherwise.
496+
*/
497+
async isFailedTransaction(requestDetails: RequestDetails, transactionId?: string): Promise<boolean> {
498+
const retryCount = 5;
499+
try {
500+
const transaction = await this.mirrorNodeClient.repeatedRequest(
501+
this.mirrorNodeClient.getTransactionById.name,
502+
[transactionId, requestDetails],
503+
retryCount,
504+
requestDetails,
505+
);
506+
const isFailed = transaction !== null;
507+
return isFailed;
508+
} catch (e: any) {
509+
this.logger.error(
510+
e,
511+
`${requestDetails.formattedRequestId} Failed to check if transaction ${transactionId} is failed`,
512+
);
513+
return true;
514+
}
515+
}
516+
457517
/**
458518
* Submits a contract call query to a smart contract on the Hedera network.
459519
* @param {string} to - The address of the contract to call, in either Solidity or EVM format.
@@ -724,7 +784,7 @@ export class SDKClient {
724784
throw e;
725785
}
726786

727-
const sdkClientError = new SDKClientError(e, e.message);
787+
const sdkClientError = new SDKClientError(e, e.message, transaction.transactionId?.toString());
728788

729789
// Throw WRONG_NONCE error as more error handling logic for WRONG_NONCE is awaited in eth.sendRawTransactionErrorHandler().
730790
if (sdkClientError.status && sdkClientError.status === Status.WrongNonce) {
@@ -737,9 +797,13 @@ export class SDKClient {
737797
);
738798

739799
if (!transactionResponse) {
740-
throw predefined.INTERNAL_ERROR(
741-
`${requestDetails.formattedRequestId} Transaction execution returns a null value: transactionId=${transaction.transactionId}, callerName=${callerName}, txConstructorName=${txConstructorName}`,
742-
);
800+
if (sdkClientError.isConnectionDropped() || sdkClientError.isTimeoutExceeded()) {
801+
throw sdkClientError;
802+
} else {
803+
throw predefined.INTERNAL_ERROR(
804+
`${requestDetails.formattedRequestId} Transaction execution returns a null value: transactionId=${transaction.transactionId}, callerName=${callerName}, txConstructorName=${txConstructorName}`,
805+
);
806+
}
743807
}
744808
return transactionResponse;
745809
} finally {

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,27 @@ import { Status } from '@hashgraph/sdk';
2323
export class SDKClientError extends Error {
2424
public status: Status = Status.Unknown;
2525
private validNetworkError: boolean = false;
26+
private failedTransactionId: string | undefined;
2627

27-
constructor(e: any, message?: string) {
28+
constructor(e: any, message?: string, transactionId?: string) {
2829
super(e?.status?._code ? e.message : message);
2930

3031
if (e?.status?._code) {
3132
this.validNetworkError = true;
3233
this.status = e.status;
3334
}
34-
35+
this.failedTransactionId = transactionId || '';
3536
Object.setPrototypeOf(this, SDKClientError.prototype);
3637
}
3738

3839
get statusCode(): number {
3940
return this.status._code;
4041
}
4142

43+
get transactionId(): string | undefined {
44+
return this.failedTransactionId;
45+
}
46+
4247
public isValidNetworkError(): boolean {
4348
return this.validNetworkError;
4449
}

packages/relay/src/lib/services/hapiService/hapiService.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ export default class HAPIService {
208208
this.clientMain = this.initClient(logger, this.hederaNetwork);
209209

210210
this.cacheService = cacheService;
211-
this.client = this.initSDKClient(logger);
211+
this.client = this.initSDKClient(logger, register);
212212

213213
const currentDateNow = Date.now();
214214
// @ts-ignore
@@ -287,7 +287,7 @@ export default class HAPIService {
287287
.inc(1);
288288

289289
this.clientMain = this.initClient(this.logger, this.hederaNetwork);
290-
this.client = this.initSDKClient(this.logger);
290+
this.client = this.initSDKClient(this.logger, this.register);
291291
this.resetCounters();
292292
}
293293

@@ -306,13 +306,14 @@ export default class HAPIService {
306306
* @param {Logger} logger
307307
* @returns SDK Client
308308
*/
309-
private initSDKClient(logger: Logger): SDKClient {
309+
private initSDKClient(logger: Logger, register: Registry): SDKClient {
310310
return new SDKClient(
311311
this.clientMain,
312312
logger.child({ name: `consensus-node` }),
313313
this.cacheService,
314314
this.eventEmitter,
315315
this.hbarLimitService,
316+
register,
316317
);
317318
}
318319

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

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,23 +35,35 @@ import {
3535
import { HbarLimitService } from '../../../src/lib/services/hbarLimitService';
3636
import { EventEmitter } from 'events';
3737
import pino from 'pino';
38-
import { SDKClient } from '../../../src/lib/clients';
38+
import { MirrorNodeClient, SDKClient } from '../../../src/lib/clients';
3939
import { ACCOUNT_ADDRESS_1, DEFAULT_NETWORK_FEES, MAX_GAS_LIMIT_HEX, NO_TRANSACTIONS } from './eth-config';
40-
import { JsonRpcError, predefined } from '../../../src';
40+
import { Eth, JsonRpcError, predefined } from '../../../src';
4141
import RelayAssertions from '../../assertions';
4242
import { getRequestId, mockData, overrideEnvsInMochaDescribe, signTransaction } from '../../helpers';
4343
import { generateEthTestEnv } from './eth-helpers';
4444
import { SDKClientError } from '../../../src/lib/errors/SDKClientError';
4545
import { RequestDetails } from '../../../src/lib/types';
46+
import MockAdapter from 'axios-mock-adapter';
47+
import HAPIService from '../../../src/lib/services/hapiService/hapiService';
48+
import { CacheService } from '../../../src/lib/services/cacheService/cacheService';
49+
import * as utils from '../../../src/formatters';
4650

4751
use(chaiAsPromised);
4852

4953
let sdkClientStub: sinon.SinonStubbedInstance<SDKClient>;
54+
let mirrorNodeStub: sinon.SinonStubbedInstance<MirrorNodeClient>;
5055
let getSdkClientStub: sinon.SinonStub;
56+
let formatTransactionIdWithoutQueryParamsStub: sinon.SinonStub;
5157

5258
describe('@ethSendRawTransaction eth_sendRawTransaction spec', async function () {
5359
this.timeout(10000);
54-
let { restMock, hapiServiceInstance, ethImpl, cacheService } = generateEthTestEnv();
60+
const {
61+
restMock,
62+
hapiServiceInstance,
63+
ethImpl,
64+
cacheService,
65+
}: { restMock: MockAdapter; hapiServiceInstance: HAPIService; ethImpl: Eth; cacheService: CacheService } =
66+
generateEthTestEnv();
5567

5668
const requestDetails = new RequestDetails({ requestId: 'eth_sendRawTransactionTest', ipAddress: '0.0.0.0' });
5769

@@ -62,6 +74,7 @@ describe('@ethSendRawTransaction eth_sendRawTransaction spec', async function ()
6274
await cacheService.clear(requestDetails);
6375
restMock.reset();
6476
sdkClientStub = sinon.createStubInstance(SDKClient);
77+
mirrorNodeStub = sinon.createStubInstance(MirrorNodeClient);
6578
getSdkClientStub = sinon.stub(hapiServiceInstance, 'getSDKClient').returns(sdkClientStub);
6679
restMock.onGet('network/fees').reply(200, DEFAULT_NETWORK_FEES);
6780
});
@@ -281,5 +294,62 @@ describe('@ethSendRawTransaction eth_sendRawTransaction spec', async function ()
281294
[signed, getRequestId()],
282295
);
283296
});
297+
298+
it('should call mirror node upon time out and return successful if found', async function () {
299+
const transactionId = '0.0.902';
300+
const contractResultEndpoint = `contracts/results/${transactionId}`;
301+
formatTransactionIdWithoutQueryParamsStub = sinon.stub(utils, 'formatTransactionIdWithoutQueryParams');
302+
formatTransactionIdWithoutQueryParamsStub.returns(transactionId);
303+
304+
restMock.onGet(contractResultEndpoint).reply(200, { hash: ethereumHash });
305+
306+
sdkClientStub.submitEthereumTransaction.restore();
307+
mirrorNodeStub.repeatedRequest = sinon.stub();
308+
mirrorNodeStub.getTransactionById = sinon.stub();
309+
sdkClientStub.deleteFile.resolves();
310+
sdkClientStub.createFile.resolves(new FileId(0, 0, 5644));
311+
sdkClientStub.executeTransaction
312+
.onCall(0)
313+
.throws(new SDKClientError({ status: 21 }, 'timeout exceeded', transactionId));
314+
sdkClientStub.isFailedTransaction.resolves(false);
315+
const signed = await signTransaction(transaction);
316+
317+
const resultingHash = await ethImpl.sendRawTransaction(signed, requestDetails);
318+
expect(resultingHash).to.equal(ethereumHash);
319+
});
320+
321+
it('should call mirror node upon time out and throw error if not found', async function () {
322+
sdkClientStub.submitEthereumTransaction.restore();
323+
mirrorNodeStub.repeatedRequest = sinon.stub();
324+
mirrorNodeStub.getTransactionById = sinon.stub();
325+
326+
sdkClientStub.createFile.resolves(new FileId(0, 0, 5644));
327+
sdkClientStub.executeTransaction.onCall(0).throws(new SDKClientError({ status: 21 }, 'timeout exceeded'));
328+
sdkClientStub.isFailedTransaction.resolves(true);
329+
const signed = await signTransaction(transaction);
330+
331+
const response = (await ethImpl.sendRawTransaction(signed, requestDetails)) as JsonRpcError;
332+
console.log(response);
333+
expect(response).to.be.instanceOf(JsonRpcError);
334+
expect(response.message).to.include('timeout exceeded');
335+
sinon.assert.called(sdkClientStub.isFailedTransaction);
336+
});
337+
338+
it('should call mirror node upon connection dropped and throw error if not found', async function () {
339+
sdkClientStub.submitEthereumTransaction.restore();
340+
mirrorNodeStub.repeatedRequest = sinon.stub();
341+
mirrorNodeStub.getTransactionById = sinon.stub();
342+
343+
sdkClientStub.createFile.resolves(new FileId(0, 0, 5644));
344+
sdkClientStub.executeTransaction.onCall(0).throws(new SDKClientError({ status: 21 }, 'Connection dropped'));
345+
sdkClientStub.isFailedTransaction.resolves(true);
346+
const signed = await signTransaction(transaction);
347+
348+
const response = (await ethImpl.sendRawTransaction(signed, requestDetails)) as JsonRpcError;
349+
console.log(response);
350+
expect(response).to.be.instanceOf(JsonRpcError);
351+
expect(response.message).to.include('Connection dropped');
352+
sinon.assert.called(sdkClientStub.isFailedTransaction);
353+
});
284354
});
285355
});

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ describe('SdkClient', async function () {
137137
new CacheService(logger.child({ name: `cache` }), registry),
138138
eventEmitter,
139139
hbarLimitService,
140+
register,
140141
);
141142

142143
instance = axios.create({

packages/relay/tests/lib/services/metricService/metricService.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ describe('Metric Service', function () {
163163
new CacheService(logger.child({ name: `cache` }), registry),
164164
eventEmitter,
165165
hbarLimitService,
166+
register,
166167
);
167168
// Init new MetricService instance
168169
metricService = new MetricService(logger, sdkClient, mirrorNodeClient, registry, eventEmitter, hbarLimitService);

0 commit comments

Comments
 (0)