Skip to content

Commit 745a2e7

Browse files
chore: cherry pick feat: transaction with CONTRACT_NEGATIVE_VALUE breaks some routes (#3387) to release/0.64 (#3436)
* feat: transaction with `CONTRACT_NEGATIVE_VALUE` breaks some routes (#3387) * chore: handle contract negative value calls Signed-off-by: nikolay <[email protected]> * chore: remove .only Signed-off-by: nikolay <[email protected]> * chore: fix comments Signed-off-by: nikolay <[email protected]> * chore: edit imports Signed-off-by: nikolay <[email protected]> * chore: resolving comments Signed-off-by: nikolay <[email protected]> * chore: fix json parsing Signed-off-by: nikolay <[email protected]> * chore: add e2e test Signed-off-by: nikolay <[email protected]> * chore: fix test Signed-off-by: nikolay <[email protected]> * chore: fix test Signed-off-by: nikolay <[email protected]> * chore: fix flaky test Signed-off-by: nikolay <[email protected]> * chore: tests Signed-off-by: nikolay <[email protected]> * chore: try to disable the test Signed-off-by: nikolay <[email protected]> * chore: add test Signed-off-by: nikolay <[email protected]> * chore: resolve comments Signed-off-by: nikolay <[email protected]> * chore: simplify the tests Signed-off-by: nikolay <[email protected]> --------- Signed-off-by: nikolay <[email protected]> Revert "feat: transaction with `CONTRACT_NEGATIVE_VALUE` breaks some routes (#3387)" This reverts commit 8bc3991. Reapply "feat: transaction with `CONTRACT_NEGATIVE_VALUE` breaks some routes (#3387)" This reverts commit 8d52b9a07a43cecd6241eaa9a7f2e65c4df8d4e5. Signed-off-by: Logan Nguyen <[email protected]> * fix: fixed ci Signed-off-by: Logan Nguyen <[email protected]> * fix: fixed CI Signed-off-by: Logan Nguyen <[email protected]> --------- Signed-off-by: Logan Nguyen <[email protected]> Co-authored-by: Logan Nguyen <[email protected]>
1 parent 5f9c2a4 commit 745a2e7

File tree

9 files changed

+198
-37
lines changed

9 files changed

+198
-37
lines changed

.github/workflows/acceptance-workflow.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ on:
3838
description: 'Codecov upload token'
3939
required: true
4040

41-
4241
env:
4342
OPERATOR_ID_MAIN: ${{ inputs.operator_id }}
4443

@@ -108,15 +107,15 @@ jobs:
108107

109108
- name: Upload Heap Snapshots
110109
if: ${{ !cancelled() }}
111-
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
110+
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
112111
with:
113112
name: Heap Snapshots
114-
path: "**/*.heapsnapshot"
113+
path: '**/*.heapsnapshot'
115114
if-no-files-found: ignore
116115

117116
- name: Upload Test Results
118117
if: always()
119-
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
118+
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
120119
with:
121120
name: Test Results (${{ inputs.testfilter }})
122121
path: test-*.xml

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ jobs:
5050

5151
- name: Upload Heap Snapshots
5252
if: ${{ !cancelled() }}
53-
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
53+
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
5454
with:
5555
name: Heap Snapshots
56-
path: "**/*.heapsnapshot"
56+
path: '**/*.heapsnapshot'
5757
if-no-files-found: ignore
5858

5959
- name: Upload coverage report

package-lock.json

Lines changed: 5 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/relay/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@
5050
},
5151
"dependencies": {
5252
"@ethersproject/asm": "^5.7.0",
53-
"@hashgraph/sdk": "^2.54.0-beta.1",
5453
"@hashgraph/json-rpc-config-service": "file:../config-service",
54+
"@hashgraph/sdk": "^2.54.0-beta.1",
5555
"@keyvhq/core": "^1.6.9",
5656
"axios": "^1.4.0",
5757
"axios-retry": "^3.5.1",
@@ -60,6 +60,7 @@
6060
"dotenv": "^16.0.0",
6161
"ethers": "^6.7.0",
6262
"find-config": "^1.0.0",
63+
"json-bigint": "^1.0.0",
6364
"keccak": "^3.0.2",
6465
"keyv": "^4.2.2",
6566
"keyv-file": "^0.3.0",

packages/relay/src/formatters.ts

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818
*
1919
*/
2020

21+
import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services';
22+
import { BigNumber } from '@hashgraph/sdk/lib/Transfer';
23+
import { BigNumber as BN } from 'bignumber.js';
2124
import crypto from 'crypto';
25+
2226
import constants from './lib/constants';
23-
import { BigNumber as BN } from 'bignumber.js';
24-
import { BigNumber } from '@hashgraph/sdk/lib/Transfer';
25-
import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services';
2627
import { Transaction, Transaction1559, Transaction2930 } from './lib/model';
2728

2829
const EMPTY_HEX = '0x';
@@ -178,7 +179,7 @@ const formatContractResult = (cr: any) => {
178179
transactionIndex: nullableNumberTo0x(cr.transaction_index),
179180
type: cr.type === null ? '0x0' : nanOrNumberTo0x(cr.type),
180181
v: cr.v === null ? '0x0' : nanOrNumberTo0x(cr.v),
181-
value: nanOrNumberTo0x(tinybarsToWeibars(cr.amount)),
182+
value: nanOrNumberInt64To0x(tinybarsToWeibars(cr.amount, true)),
182183
// for legacy EIP155 with tx.chainId=0x0, mirror-node will return a '0x' (EMPTY_HEX) value for contract result's chain_id
183184
// which is incompatibile with certain tools (i.e. foundry). By setting this field, chainId, to undefined, the end jsonrpc
184185
// object will leave out this field, which is the proper behavior for other tools to be compatible with.
@@ -265,6 +266,40 @@ const nanOrNumberTo0x = (input: number | BigNumber | bigint | null): string => {
265266
return input == null || Number.isNaN(input) ? numberTo0x(0) : numberTo0x(input);
266267
};
267268

269+
const nanOrNumberInt64To0x = (input: number | BigNumber | bigint | null): string => {
270+
// converting to string and then back to int is fixing a typescript warning
271+
if (input && Number(input) < 0) {
272+
// the hex of a negative number can be obtained from the binary value of that number positive value
273+
// the binary value needs to be negated and then to be incremented by 1
274+
275+
// how the transformation works (using 16 bits)
276+
// a 16 bits integer variables have values from -32768 to +32767, so:
277+
// 0 - 0x0000 - 0000 0000 0000 0000
278+
// 32767 - 0x7fff - 0111 1111 1111 1111
279+
// -32768 - 0x8000 - 1000 0000 0000 0000
280+
// -1 - 0xffff - 1111 1111 1111 1111
281+
282+
// converting int16 -10 will be done as following:
283+
// - make it positive = 10
284+
// - 16 bits binary value of 10 = 0000 0000 0000 1010
285+
// - inverse the bits = 1111 1111 1111 0101
286+
// - adding +1 = 1111 1111 1111 0110
287+
// - 1111 1111 1111 0110 bits = 0xfff6
288+
289+
// we're using 64 bits integer because that's the type returned by the mirror node - int64
290+
const bits = 64;
291+
// this mathematical expression serves as a shortcut for performing the two’s complement conversion
292+
// e.g. input = -10
293+
// we have: (BigInt(1) << BigInt(bits)) = 1 << 64 = 2^64 = 18446744073709551616
294+
// then: (BigInt(input.toString()) + (BigInt(1) << BigInt(bits))) = -10 + 2^64 = 18446744073709551606
295+
// this effectively represents -10 in an unsigned 64-bit representation:18446744073709551606 = 0xFFFFFFFFFFFFFFF6
296+
// finally, the modulo operation: % (1 << 64)
297+
return numberTo0x((BigInt(input.toString()) + (BigInt(1) << BigInt(bits))) % (BigInt(1) << BigInt(bits)));
298+
}
299+
300+
return nanOrNumberTo0x(input);
301+
};
302+
268303
const toHash32 = (value: string): string => {
269304
return value.substring(0, 66);
270305
};
@@ -303,8 +338,18 @@ const getFunctionSelector = (data?: string): string => {
303338
return data.replace(/^0x/, '').substring(0, 8);
304339
};
305340

306-
const tinybarsToWeibars = (value: number | null) => {
307-
if (value && value < 0) throw new Error('Invalid value - cannot pass negative number');
341+
const tinybarsToWeibars = (value: number | null, allowNegativeValues: boolean = false) => {
342+
if (value && value < 0) {
343+
// negative amount can be received only by CONTRACT_NEGATIVE_VALUE revert
344+
// e.g. tx https://hashscan.io/mainnet/transaction/1735241436.856862230
345+
// that's not a valid revert in the Ethereum world so we must NOT multiply
346+
// the amount sent via CONTRACT_CALL SDK call by TINYBAR_TO_WEIBAR_COEF
347+
// also, keep in mind that the mirror node returned amount is typed with int64
348+
if (allowNegativeValues) return value;
349+
350+
throw new Error('Invalid value - cannot pass negative number');
351+
}
352+
308353
if (value && value > constants.TOTAL_SUPPLY_TINYBARS)
309354
throw new Error('Value cannot be more than the total supply of tinybars in the blockchain');
310355

@@ -324,6 +369,7 @@ export {
324369
numberTo0x,
325370
nullableNumberTo0x,
326371
nanOrNumberTo0x,
372+
nanOrNumberInt64To0x,
327373
toHash32,
328374
toNullableBigNumber,
329375
toNullIfEmptyHex,

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { install as betterLookupInstall } from 'better-lookup';
2525
import { ethers } from 'ethers';
2626
import http from 'http';
2727
import https from 'https';
28+
import JSONBigInt from 'json-bigint';
2829
import { Logger } from 'pino';
2930
import { Histogram, Registry } from 'prom-client';
3031

@@ -361,6 +362,30 @@ export class MirrorNodeClient {
361362
if (pathLabel == MirrorNodeClient.GET_CONTRACTS_RESULTS_OPCODES) {
362363
response = await this.web3Client.get<T>(path, axiosRequestConfig);
363364
} else {
365+
// JavaScript supports integers only up to 53 bits. When a number exceeding this limit
366+
// is converted to a JS Number type, precision is lost due to rounding.
367+
// To prevent this, `transformResponse` is used to intercept
368+
// and process the response before Axios’s default JSON.parse conversion.
369+
// JSONBigInt reads the string representation from the received JSON
370+
// and converts large numbers into BigNumber objects to maintain accuracy.
371+
axiosRequestConfig['transformResponse'] = [
372+
(data) => {
373+
// if the data is not valid, just return it to stick to the current behaviour
374+
if (data) {
375+
try {
376+
// try to parse it, if the json is valid, numbers within it will be converted
377+
// this case will happen on almost every GET mirror node call
378+
return JSONBigInt.parse(data);
379+
} catch (e) {
380+
// in some unit tests, the mocked returned json is not property formatted
381+
// so we have to preprocess it here with JSON.stringify()
382+
return JSONBigInt.parse(JSON.stringify(data));
383+
}
384+
}
385+
386+
return data;
387+
},
388+
];
364389
response = await this.restClient.get<T>(path, axiosRequestConfig);
365390
}
366391
} else {

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

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818
*
1919
*/
2020

21+
import { BigNumber as BN } from 'bignumber.js';
2122
import { expect } from 'chai';
23+
import { AbiCoder, keccak256 } from 'ethers';
24+
2225
import {
2326
ASCIIToHex,
2427
decodeErrorMessage,
@@ -31,23 +34,22 @@ import {
3134
isHex,
3235
isValidEthereumAddress,
3336
mapKeysAndValues,
37+
nanOrNumberInt64To0x,
3438
nanOrNumberTo0x,
3539
nullableNumberTo0x,
3640
numberTo0x,
3741
parseNumericEnvVar,
3842
prepend0x,
3943
strip0x,
44+
tinybarsToWeibars,
4045
toHash32,
4146
toHexString,
4247
toNullableBigNumber,
4348
toNullIfEmptyHex,
4449
trimPrecedingZeros,
4550
weibarHexToTinyBarInt,
46-
tinybarsToWeibars,
4751
} from '../../src/formatters';
4852
import constants from '../../src/lib/constants';
49-
import { BigNumber as BN } from 'bignumber.js';
50-
import { AbiCoder, keccak256 } from 'ethers';
5153
import { overrideEnvsInMochaDescribe } from '../helpers';
5254

5355
describe('Formatters', () => {
@@ -399,6 +401,48 @@ describe('Formatters', () => {
399401
});
400402
});
401403

404+
describe('nanOrNumberInt64To0x', () => {
405+
it('should return 0x0 for nullable input', () => {
406+
expect(nanOrNumberInt64To0x(null)).to.equal('0x0');
407+
});
408+
it('should return 0x0 for NaN input', () => {
409+
expect(nanOrNumberInt64To0x(NaN)).to.equal('0x0');
410+
});
411+
412+
for (const [testName, testValues] of Object.entries({
413+
'2 digits': ['-10', '0xfffffffffffffff6'],
414+
'6 digits': ['-851969', '0xfffffffffff2ffff'],
415+
'19 digits -6917529027641081857': ['-6917529027641081857', '0x9fffffffffffffff'],
416+
'19 digits -9223372036586340353': ['-9223372036586340353', '0x800000000fffffff'],
417+
})) {
418+
it(`should convert negative int64 number (${testName})`, () => {
419+
expect(nanOrNumberInt64To0x(BigInt(testValues[0]))).to.equal(testValues[1]);
420+
});
421+
}
422+
423+
for (const [bits, testValues] of Object.entries({
424+
10: ['593', '0x251'],
425+
50: ['844424930131967', '0x2ffffffffffff'],
426+
51: ['1970324836974591', '0x6ffffffffffff'],
427+
52: ['3096224743817215', '0xaffffffffffff'],
428+
53: ['9007199254740991', '0x1fffffffffffff'],
429+
54: ['13510798882111487', '0x2fffffffffffff'],
430+
55: ['31525197391593471', '0x6fffffffffffff'],
431+
56: ['49539595901075455', '0xafffffffffffff'],
432+
57: ['144115188075855871', '0x1ffffffffffffff'],
433+
58: ['216172782113783807', '0x2ffffffffffffff'],
434+
59: ['504403158265495551', '0x6ffffffffffffff'],
435+
60: ['792633534417207295', '0xaffffffffffffff'],
436+
61: ['2305843009213693951', '0x1fffffffffffffff'],
437+
62: ['3458764513820540927', '0x2fffffffffffffff'],
438+
63: ['8070450532247928831', '0x6fffffffffffffff'],
439+
})) {
440+
it(`should convert positive ${bits} bits number`, () => {
441+
expect(nanOrNumberInt64To0x(BigInt(testValues[0]))).to.equal(testValues[1]);
442+
});
443+
}
444+
});
445+
402446
describe('toHash32', () => {
403447
it('should format more than 32 bytes hash to 32 bytes', () => {
404448
expect(
@@ -735,27 +779,33 @@ describe('Formatters', () => {
735779
});
736780

737781
describe('tinybarsToWeibars', () => {
738-
it('should convert tinybars to weibars', () => {
739-
expect(tinybarsToWeibars(10)).to.eql(100000000000);
740-
});
782+
for (const allowNegativeValues of [true, false]) {
783+
it(`should convert tinybars to weibars allowNegativeValues = ${allowNegativeValues}`, () => {
784+
expect(tinybarsToWeibars(10, allowNegativeValues)).to.eql(100000000000);
785+
});
741786

742-
it('should return null if null is passed', () => {
743-
expect(tinybarsToWeibars(null)).to.eql(null);
744-
});
787+
it(`should return null if null is passed allowNegativeValues = ${allowNegativeValues}`, () => {
788+
expect(tinybarsToWeibars(null, allowNegativeValues)).to.eql(null);
789+
});
745790

746-
it('should return 0 for 0 input', () => {
747-
expect(tinybarsToWeibars(0)).to.eql(0);
748-
});
791+
it(`should return 0 for 0 input allowNegativeValues = ${allowNegativeValues}`, () => {
792+
expect(tinybarsToWeibars(0, allowNegativeValues)).to.eql(0);
793+
});
794+
795+
it(`should throw an error when value is larger than the total supply of tinybars allowNegativeValues = ${allowNegativeValues}`, () => {
796+
expect(() => tinybarsToWeibars(constants.TOTAL_SUPPLY_TINYBARS * 10, allowNegativeValues)).to.throw(
797+
Error,
798+
'Value cannot be more than the total supply of tinybars in the blockchain',
799+
);
800+
});
801+
}
749802

750803
it('should throw an error when value is smaller than 0', () => {
751-
expect(() => tinybarsToWeibars(-10)).to.throw(Error, 'Invalid value - cannot pass negative number');
804+
expect(() => tinybarsToWeibars(-10, false)).to.throw(Error, 'Invalid value - cannot pass negative number');
752805
});
753806

754-
it('should throw an error when value is larger than the total supply of tinybars', () => {
755-
expect(() => tinybarsToWeibars(constants.TOTAL_SUPPLY_TINYBARS * 10)).to.throw(
756-
Error,
757-
'Value cannot be more than the total supply of tinybars in the blockchain',
758-
);
807+
it('should return the negative number if allowNegativeValues flag is set to true', () => {
808+
expect(tinybarsToWeibars(-10, true)).to.eql(-10);
759809
});
760810
});
761811
});

0 commit comments

Comments
 (0)