Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Adds retry to send raw transaction (#3161) #3208

Merged
merged 1 commit into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 72 additions & 8 deletions packages/relay/src/lib/clients/sdkClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@
ITransactionRecordMetric,
RequestDetails,
} from '../types';
import { MirrorNodeClient } from './mirrorNodeClient';
import { Registry } from 'prom-client';

const _ = require('lodash');

Expand Down Expand Up @@ -122,6 +124,14 @@
*/
private readonly hbarLimitService: HbarLimitService;

/**
* An instance of the MirrorNodeClient
* @private
* @readonly
* @type {MirrorNodeClient}
*/
private readonly mirrorNodeClient: MirrorNodeClient;

/**
* Constructs an instance of the SDKClient and initializes various services and settings.
*
Expand All @@ -136,6 +146,7 @@
cacheService: CacheService,
eventEmitter: EventEmitter,
hbarLimitService: HbarLimitService,
register: Registry,
) {
this.clientMain = clientMain;

Expand All @@ -151,6 +162,13 @@
this.hbarLimitService = hbarLimitService;
this.maxChunks = Number(ConfigService.get('FILE_APPEND_MAX_CHUNKS')) || 20;
this.fileAppendChunkSize = Number(ConfigService.get('FILE_APPEND_CHUNK_SIZE')) || 5120;
this.mirrorNodeClient = new MirrorNodeClient(
// @ts-ignore
ConfigService.get('MIRROR_NODE_URL') || '',
logger,
register,
cacheService,
);
}

/**
Expand Down Expand Up @@ -441,19 +459,61 @@
Hbar.fromTinybars(Math.floor(networkGasPriceInTinyBars * constants.MAX_GAS_PER_SEC)),
);

return {
fileId,
txResponse: await this.executeTransaction(
let txResponse;
try {
txResponse = await this.executeTransaction(
ethereumTransaction,
callerName,
interactingEntity,
requestDetails,
true,
originalCallerAddress,
),
);
} catch (e: any) {
if (e instanceof SDKClientError && (e.isConnectionDropped() || e.isTimeoutExceeded())) {
const isFailed = await this.isFailedTransaction(requestDetails, e.transactionId);
if (isFailed) {
throw e;
}
txResponse = { transactionId: e.transactionId };
} else {
throw e;

Check warning on line 480 in packages/relay/src/lib/clients/sdkClient.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L480 was not covered by tests
}
}

return {
fileId,
txResponse,
};
}

/**
* Checks if a transaction has failed by querying the mirror node.
*
* @param {RequestDetails} requestDetails - The details of the request.
* @param {string} [transactionId] - The ID of the transaction to check.
* @returns {Promise<boolean>} - A promise that resolves to `true` if the transaction has failed, `false` otherwise.
*/
async isFailedTransaction(requestDetails: RequestDetails, transactionId?: string): Promise<boolean> {
const retryCount = 5;
try {
const transaction = await this.mirrorNodeClient.repeatedRequest(

Check warning on line 500 in packages/relay/src/lib/clients/sdkClient.ts

View check run for this annotation

Codecov / codecov/patch

packages/relay/src/lib/clients/sdkClient.ts#L498-L500

Added lines #L498 - L500 were not covered by tests
this.mirrorNodeClient.getTransactionById.name,
[transactionId, requestDetails],
retryCount,
requestDetails,
);
const isFailed = transaction !== null;
return isFailed;

Check warning on line 507 in packages/relay/src/lib/clients/sdkClient.ts

View check run for this annotation

Codecov / codecov/patch

packages/relay/src/lib/clients/sdkClient.ts#L506-L507

Added lines #L506 - L507 were not covered by tests
} catch (e: any) {
this.logger.error(

Check warning on line 509 in packages/relay/src/lib/clients/sdkClient.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L509 was not covered by tests
e,
`${requestDetails.formattedRequestId} Failed to check if transaction ${transactionId} is failed`,
);
return true;

Check warning on line 513 in packages/relay/src/lib/clients/sdkClient.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L513 was not covered by tests
}
}

/**
* Submits a contract call query to a smart contract on the Hedera network.
* @param {string} to - The address of the contract to call, in either Solidity or EVM format.
Expand Down Expand Up @@ -724,7 +784,7 @@
throw e;
}

const sdkClientError = new SDKClientError(e, e.message);
const sdkClientError = new SDKClientError(e, e.message, transaction.transactionId?.toString());

// Throw WRONG_NONCE error as more error handling logic for WRONG_NONCE is awaited in eth.sendRawTransactionErrorHandler().
if (sdkClientError.status && sdkClientError.status === Status.WrongNonce) {
Expand All @@ -737,9 +797,13 @@
);

if (!transactionResponse) {
throw predefined.INTERNAL_ERROR(
`${requestDetails.formattedRequestId} Transaction execution returns a null value: transactionId=${transaction.transactionId}, callerName=${callerName}, txConstructorName=${txConstructorName}`,
);
if (sdkClientError.isConnectionDropped() || sdkClientError.isTimeoutExceeded()) {
throw sdkClientError;

Check warning on line 801 in packages/relay/src/lib/clients/sdkClient.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L801 was not covered by tests
} else {
throw predefined.INTERNAL_ERROR(

Check warning on line 803 in packages/relay/src/lib/clients/sdkClient.ts

View check run for this annotation

Codecov / codecov/patch

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

Added line #L803 was not covered by tests
`${requestDetails.formattedRequestId} Transaction execution returns a null value: transactionId=${transaction.transactionId}, callerName=${callerName}, txConstructorName=${txConstructorName}`,
);
}
}
return transactionResponse;
} finally {
Expand Down
9 changes: 7 additions & 2 deletions packages/relay/src/lib/errors/SDKClientError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,27 @@ import { Status } from '@hashgraph/sdk';
export class SDKClientError extends Error {
public status: Status = Status.Unknown;
private validNetworkError: boolean = false;
private failedTransactionId: string | undefined;

constructor(e: any, message?: string) {
constructor(e: any, message?: string, transactionId?: string) {
super(e?.status?._code ? e.message : message);

if (e?.status?._code) {
this.validNetworkError = true;
this.status = e.status;
}

this.failedTransactionId = transactionId || '';
Object.setPrototypeOf(this, SDKClientError.prototype);
}

get statusCode(): number {
return this.status._code;
}

get transactionId(): string | undefined {
return this.failedTransactionId;
}

public isValidNetworkError(): boolean {
return this.validNetworkError;
}
Expand Down
7 changes: 4 additions & 3 deletions packages/relay/src/lib/services/hapiService/hapiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ export default class HAPIService {
this.clientMain = this.initClient(logger, this.hederaNetwork);

this.cacheService = cacheService;
this.client = this.initSDKClient(logger);
this.client = this.initSDKClient(logger, register);

const currentDateNow = Date.now();
// @ts-ignore
Expand Down Expand Up @@ -287,7 +287,7 @@ export default class HAPIService {
.inc(1);

this.clientMain = this.initClient(this.logger, this.hederaNetwork);
this.client = this.initSDKClient(this.logger);
this.client = this.initSDKClient(this.logger, this.register);
this.resetCounters();
}

Expand All @@ -306,13 +306,14 @@ export default class HAPIService {
* @param {Logger} logger
* @returns SDK Client
*/
private initSDKClient(logger: Logger): SDKClient {
private initSDKClient(logger: Logger, register: Registry): SDKClient {
return new SDKClient(
this.clientMain,
logger.child({ name: `consensus-node` }),
this.cacheService,
this.eventEmitter,
this.hbarLimitService,
register,
);
}

Expand Down
76 changes: 73 additions & 3 deletions packages/relay/tests/lib/eth/eth_sendRawTransaction.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,35 @@ import {
import { HbarLimitService } from '../../../src/lib/services/hbarLimitService';
import { EventEmitter } from 'events';
import pino from 'pino';
import { SDKClient } from '../../../src/lib/clients';
import { MirrorNodeClient, SDKClient } from '../../../src/lib/clients';
import { ACCOUNT_ADDRESS_1, DEFAULT_NETWORK_FEES, MAX_GAS_LIMIT_HEX, NO_TRANSACTIONS } from './eth-config';
import { JsonRpcError, predefined } from '../../../src';
import { Eth, JsonRpcError, predefined } from '../../../src';
import RelayAssertions from '../../assertions';
import { getRequestId, mockData, overrideEnvsInMochaDescribe, signTransaction } from '../../helpers';
import { generateEthTestEnv } from './eth-helpers';
import { SDKClientError } from '../../../src/lib/errors/SDKClientError';
import { RequestDetails } from '../../../src/lib/types';
import MockAdapter from 'axios-mock-adapter';
import HAPIService from '../../../src/lib/services/hapiService/hapiService';
import { CacheService } from '../../../src/lib/services/cacheService/cacheService';
import * as utils from '../../../src/formatters';

use(chaiAsPromised);

let sdkClientStub: sinon.SinonStubbedInstance<SDKClient>;
let mirrorNodeStub: sinon.SinonStubbedInstance<MirrorNodeClient>;
let getSdkClientStub: sinon.SinonStub;
let formatTransactionIdWithoutQueryParamsStub: sinon.SinonStub;

describe('@ethSendRawTransaction eth_sendRawTransaction spec', async function () {
this.timeout(10000);
let { restMock, hapiServiceInstance, ethImpl, cacheService } = generateEthTestEnv();
const {
restMock,
hapiServiceInstance,
ethImpl,
cacheService,
}: { restMock: MockAdapter; hapiServiceInstance: HAPIService; ethImpl: Eth; cacheService: CacheService } =
generateEthTestEnv();

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

Expand All @@ -62,6 +74,7 @@ describe('@ethSendRawTransaction eth_sendRawTransaction spec', async function ()
await cacheService.clear(requestDetails);
restMock.reset();
sdkClientStub = sinon.createStubInstance(SDKClient);
mirrorNodeStub = sinon.createStubInstance(MirrorNodeClient);
getSdkClientStub = sinon.stub(hapiServiceInstance, 'getSDKClient').returns(sdkClientStub);
restMock.onGet('network/fees').reply(200, DEFAULT_NETWORK_FEES);
});
Expand Down Expand Up @@ -281,5 +294,62 @@ describe('@ethSendRawTransaction eth_sendRawTransaction spec', async function ()
[signed, getRequestId()],
);
});

it('should call mirror node upon time out and return successful if found', async function () {
const transactionId = '0.0.902';
const contractResultEndpoint = `contracts/results/${transactionId}`;
formatTransactionIdWithoutQueryParamsStub = sinon.stub(utils, 'formatTransactionIdWithoutQueryParams');
formatTransactionIdWithoutQueryParamsStub.returns(transactionId);

restMock.onGet(contractResultEndpoint).reply(200, { hash: ethereumHash });

sdkClientStub.submitEthereumTransaction.restore();
mirrorNodeStub.repeatedRequest = sinon.stub();
mirrorNodeStub.getTransactionById = sinon.stub();
sdkClientStub.deleteFile.resolves();
sdkClientStub.createFile.resolves(new FileId(0, 0, 5644));
sdkClientStub.executeTransaction
.onCall(0)
.throws(new SDKClientError({ status: 21 }, 'timeout exceeded', transactionId));
sdkClientStub.isFailedTransaction.resolves(false);
const signed = await signTransaction(transaction);

const resultingHash = await ethImpl.sendRawTransaction(signed, requestDetails);
expect(resultingHash).to.equal(ethereumHash);
});

it('should call mirror node upon time out and throw error if not found', async function () {
sdkClientStub.submitEthereumTransaction.restore();
mirrorNodeStub.repeatedRequest = sinon.stub();
mirrorNodeStub.getTransactionById = sinon.stub();

sdkClientStub.createFile.resolves(new FileId(0, 0, 5644));
sdkClientStub.executeTransaction.onCall(0).throws(new SDKClientError({ status: 21 }, 'timeout exceeded'));
sdkClientStub.isFailedTransaction.resolves(true);
const signed = await signTransaction(transaction);

const response = (await ethImpl.sendRawTransaction(signed, requestDetails)) as JsonRpcError;
console.log(response);
expect(response).to.be.instanceOf(JsonRpcError);
expect(response.message).to.include('timeout exceeded');
sinon.assert.called(sdkClientStub.isFailedTransaction);
});

it('should call mirror node upon connection dropped and throw error if not found', async function () {
sdkClientStub.submitEthereumTransaction.restore();
mirrorNodeStub.repeatedRequest = sinon.stub();
mirrorNodeStub.getTransactionById = sinon.stub();

sdkClientStub.createFile.resolves(new FileId(0, 0, 5644));
sdkClientStub.executeTransaction.onCall(0).throws(new SDKClientError({ status: 21 }, 'Connection dropped'));
sdkClientStub.isFailedTransaction.resolves(true);
const signed = await signTransaction(transaction);

const response = (await ethImpl.sendRawTransaction(signed, requestDetails)) as JsonRpcError;
console.log(response);
expect(response).to.be.instanceOf(JsonRpcError);
expect(response.message).to.include('Connection dropped');
sinon.assert.called(sdkClientStub.isFailedTransaction);
});
});
});
1 change: 1 addition & 0 deletions packages/relay/tests/lib/sdkClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ describe('SdkClient', async function () {
new CacheService(logger.child({ name: `cache` }), registry),
eventEmitter,
hbarLimitService,
register,
);

instance = axios.create({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ describe('Metric Service', function () {
new CacheService(logger.child({ name: `cache` }), registry),
eventEmitter,
hbarLimitService,
register,
);
// Init new MetricService instance
metricService = new MetricService(logger, sdkClient, mirrorNodeClient, registry, eventEmitter, hbarLimitService);
Expand Down
Loading