Skip to content

Commit 5ed708b

Browse files
authored
ContractCallQuery retry logic (#812)
* feat: retry query on INSUFFICIENT_TX_FEE Signed-off-by: Ivo Yankov <[email protected]> * feat: add unit tests Signed-off-by: Ivo Yankov <[email protected]> * fix: set cost when paymentTransaction is set Signed-off-by: Ivo Yankov <[email protected]> Signed-off-by: Ivo Yankov <[email protected]>
1 parent b3b7650 commit 5ed708b

File tree

5 files changed

+157
-9
lines changed

5 files changed

+157
-9
lines changed

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

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -257,10 +257,31 @@ export class SDKClient {
257257
.setPaymentTransactionId(TransactionId.generate(this.clientMain.operatorAccountId));
258258
}
259259

260-
const cost = await contractCallQuery
261-
.getCost(this.clientMain);
262-
return this.executeQuery(contractCallQuery
263-
.setQueryPayment(cost), this.clientMain, callerName, requestId);
260+
return this.executeQuery(contractCallQuery, this.clientMain, callerName, requestId);
261+
}
262+
263+
async increaseCostAndRetryExecution(query: Query<any>, baseCost: Hbar, client: Client, maxRetries: number, currentRetry: number, requestId?: string) {
264+
const baseMultiplier = constants.QUERY_COST_INCREMENTATION_STEP;
265+
const multiplier = Math.pow(baseMultiplier, currentRetry);
266+
267+
const cost = Hbar.fromTinybars(
268+
baseCost._valueInTinybar.multipliedBy(multiplier).toFixed(0)
269+
);
270+
271+
try {
272+
const resp = await query.setQueryPayment(cost).execute(client);
273+
return {resp, cost};
274+
}
275+
catch(e: any) {
276+
const sdkClientError = new SDKClientError(e);
277+
if (maxRetries > currentRetry && sdkClientError.isInsufficientTxFee()) {
278+
const newRetry = currentRetry + 1;
279+
this.logger.info(`${requestId} Retrying query execution with increased cost, retry number: ${newRetry}`);
280+
return await this.increaseCostAndRetryExecution(query, baseCost, client, maxRetries, newRetry, requestId);
281+
}
282+
283+
throw e;
284+
}
264285
}
265286

266287
private convertGasPriceToTinyBars = (feeComponents: FeeComponents | undefined, exchangeRates: ExchangeRates) => {
@@ -284,11 +305,18 @@ export class SDKClient {
284305
throw predefined.HBAR_RATE_LIMIT_EXCEEDED;
285306
}
286307

287-
const resp = await query.execute(client);
288-
const cost = query._queryPayment?.toTinybars().toNumber();
289-
if (cost) {
308+
let resp, cost;
309+
if (query.paymentTransactionId) {
310+
const baseCost = await query.getCost(this.clientMain);
311+
const res = await this.increaseCostAndRetryExecution(query, baseCost, client, 3, 0, requestId);
312+
resp = res.resp;
313+
cost = res.cost.toTinybars().toNumber();
290314
this.hbarLimiter.addExpense(cost, currentDateNow);
291315
}
316+
else {
317+
resp = await query.execute(client);
318+
cost = query._queryPayment?.toTinybars().toNumber();
319+
}
292320

293321
this.logger.info(`${requestIdPrefix} ${query.paymentTransactionId} ${callerName} ${query.constructor.name} status: ${Status.Success} (${Status.Success._code}), cost: ${query._queryPayment}`);
294322
this.captureMetrics(

packages/relay/src/lib/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,6 @@ export default {
6262
BALANCES_UPDATE_INTERVAL: 900, // 15 minutes
6363
MAX_MIRROR_NODE_PAGINATION: 20,
6464
MIRROR_NODE_QUERY_LIMIT: 100,
65-
NEXT_LINK_PREFIX: '/api/v1/'
65+
NEXT_LINK_PREFIX: '/api/v1/',
66+
QUERY_COST_INCREMENTATION_STEP: 1.1
6667
};

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,9 @@ export class SDKClientError extends Error {
5151
return this.isValidNetworkError() &&
5252
(this.statusCode === Status.InvalidContractId._code || this.message?.includes(Status.InvalidContractId.toString()));
5353
}
54+
55+
public isInsufficientTxFee(): boolean {
56+
return this.statusCode === Status.InsufficientTxFee._code;
57+
}
5458
}
5559

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*-
2+
*
3+
* Hedera JSON RPC Relay
4+
*
5+
* Copyright (C) 2022 Hedera Hashgraph, LLC
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*
19+
*/
20+
21+
import path from 'path';
22+
import dotenv from 'dotenv';
23+
import { expect } from 'chai';
24+
import sinon from 'sinon';
25+
import { Registry } from 'prom-client';
26+
dotenv.config({ path: path.resolve(__dirname, '../test.env') });
27+
import {SDKClient} from '../../src/lib/clients/sdkClient';
28+
const registry = new Registry();
29+
import pino from 'pino';
30+
import {AccountId, Client, ContractCallQuery, PrivateKey, TransactionId, Hbar, Status} from "@hashgraph/sdk";
31+
const logger = pino();
32+
import constants from '../../src/lib/constants';
33+
34+
describe('SdkClient', async function () {
35+
this.timeout(20000);
36+
let sdkClient, client;
37+
38+
before(() => {
39+
client = Client.forNetwork(JSON.parse(process.env.HEDERA_NETWORK));
40+
client = client.setOperator(
41+
AccountId.fromString(process.env.OPERATOR_ID_MAIN),
42+
PrivateKey.fromString(process.env.OPERATOR_KEY_MAIN)
43+
);
44+
45+
sdkClient = new SDKClient(client, logger.child({ name: `consensus-node` }), registry);
46+
})
47+
48+
describe('increaseCostAndRetryExecution', async () => {
49+
let queryStub, contractCallQuery;
50+
const successResponse = "0x00001";
51+
const costTinybars = 1000;
52+
const baseCost = Hbar.fromTinybars(costTinybars);
53+
54+
beforeEach(() => {
55+
contractCallQuery = new ContractCallQuery()
56+
.setContractId('0.0.1010')
57+
.setPaymentTransactionId(TransactionId.generate(client.operatorAccountId))
58+
queryStub = sinon.stub(contractCallQuery, 'execute');
59+
})
60+
61+
it('executes the query', async () => {
62+
queryStub.returns(successResponse);
63+
let {resp, cost} = await sdkClient.increaseCostAndRetryExecution(contractCallQuery, baseCost, client, 3, 0);
64+
expect(resp).to.eq(successResponse);
65+
expect(cost.toTinybars().toNumber()).to.eq(costTinybars);
66+
expect(queryStub.callCount).to.eq(1);
67+
});
68+
69+
it('increases the cost when INSUFFICIENT_TX_FEE is thrown', async () => {
70+
queryStub.onCall(0).throws({
71+
status: Status.InsufficientTxFee
72+
});
73+
74+
queryStub.onCall(1).returns(successResponse);
75+
let {resp, cost} = await sdkClient.increaseCostAndRetryExecution(contractCallQuery, baseCost, client, 3, 0);
76+
expect(resp).to.eq(successResponse);
77+
expect(cost.toTinybars().toNumber()).to.eq(costTinybars * constants.QUERY_COST_INCREMENTATION_STEP);
78+
expect(queryStub.callCount).to.eq(2);
79+
});
80+
81+
it('increases the cost when INSUFFICIENT_TX_FEE is thrown on every repeat', async () => {
82+
queryStub.onCall(0).throws({
83+
status: Status.InsufficientTxFee
84+
});
85+
86+
queryStub.onCall(1).throws({
87+
status: Status.InsufficientTxFee
88+
});
89+
90+
queryStub.onCall(2).returns(successResponse);
91+
92+
let {resp, cost} = await sdkClient.increaseCostAndRetryExecution(contractCallQuery, baseCost, client, 3, 0);
93+
expect(resp).to.eq(successResponse);
94+
expect(cost.toTinybars().toNumber()).to.eq(Math.floor(costTinybars * Math.pow(constants.QUERY_COST_INCREMENTATION_STEP, 2)));
95+
expect(queryStub.callCount).to.eq(3);
96+
});
97+
98+
it('is repeated at most 4 times', async () => {
99+
try {
100+
queryStub.throws({
101+
status: Status.InsufficientTxFee
102+
});
103+
104+
let {resp, cost} = await sdkClient.increaseCostAndRetryExecution(contractCallQuery, baseCost, client, 3, 0);
105+
}
106+
catch(e: any) {
107+
expect(queryStub.callCount).to.eq(4);
108+
expect(e.status).to.eq(Status.InsufficientTxFee);
109+
}
110+
});
111+
})
112+
});

packages/relay/tests/test.env

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
CHAIN_ID=5
22
MIRROR_NODE_URL=http://localhost:5551
3-
MIRROR_NODE_LIMIT_PARAM = 100
3+
MIRROR_NODE_LIMIT_PARAM = 100
4+
HEDERA_NETWORK={"127.0.0.1:50211":"0.0.3"}
5+
OPERATOR_ID_MAIN=0.0.2
6+
OPERATOR_KEY_MAIN=302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137

0 commit comments

Comments
 (0)