From bde9a91470bb70b5542ff65ea7566b4667eb30d9 Mon Sep 17 00:00:00 2001 From: nikolay Date: Tue, 14 Jan 2025 17:08:06 +0200 Subject: [PATCH 01/15] chore: handle contract negative value calls Signed-off-by: nikolay --- packages/relay/src/formatters.ts | 51 ++++++++++++++++++--- packages/relay/tests/lib/formatters.spec.ts | 28 +++++++++-- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/packages/relay/src/formatters.ts b/packages/relay/src/formatters.ts index 4142d0db80..d2a122b31c 100644 --- a/packages/relay/src/formatters.ts +++ b/packages/relay/src/formatters.ts @@ -18,11 +18,12 @@ * */ +import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services'; +import { BigNumber } from '@hashgraph/sdk/lib/Transfer'; +import { BigNumber as BN } from 'bignumber.js'; import crypto from 'crypto'; + import constants from './lib/constants'; -import { BigNumber as BN } from 'bignumber.js'; -import { BigNumber } from '@hashgraph/sdk/lib/Transfer'; -import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services'; import { Transaction, Transaction1559, Transaction2930 } from './lib/model'; const EMPTY_HEX = '0x'; @@ -178,7 +179,7 @@ const formatContractResult = (cr: any) => { transactionIndex: nullableNumberTo0x(cr.transaction_index), type: cr.type === null ? '0x0' : nanOrNumberTo0x(cr.type), v: cr.v === null ? '0x0' : nanOrNumberTo0x(cr.v), - value: nanOrNumberTo0x(tinybarsToWeibars(cr.amount)), + value: nanOrNumberInt64To0x(tinybarsToWeibars(cr.amount, true)), // for legacy EIP155 with tx.chainId=0x0, mirror-node will return a '0x' (EMPTY_HEX) value for contract result's chain_id // which is incompatibile with certain tools (i.e. foundry). By setting this field, chainId, to undefined, the end jsonrpc // object will leave out this field, which is the proper behavior for other tools to be compatible with. @@ -265,6 +266,33 @@ const nanOrNumberTo0x = (input: number | BigNumber | bigint | null): string => { return input == null || Number.isNaN(input) ? numberTo0x(0) : numberTo0x(input); }; +const nanOrNumberInt64To0x = (input: number | BigNumber | bigint | null): string => { + if (input && input < 0) { + // the hex of a negative number can be obtained from the binary value of that number positive value, + // the binary value needs to be negated and then, to add 1, + + // how the transformation works (using 16 bits) + // a 16 bits integer variables has values from -32768 to +32767, so: + // 0 - 0x0000 - 0000 0000 0000 0000 + // 32767 - 0x7fff - 0111 1111 1111 1111 + // -32768 - 0x8000 - 1000 0000 0000 0000 + // -1 - 0xffff - 1111 1111 1111 1111 + + // converting int16 -10 will be done as following: + // - make it positive = 10 + // - 16 bits binary value of 10 = 0000 0000 0000 1010 + // - inverse the bytes = 1111 1111 1111 0101 + // - adding +1 = 1111 1111 1111 0110 + // - 1111 1111 1111 0110 bits = 0xfff6 + + // we're using 64 bits integer because that the type returned by the mirror node - int64 + const bits = 64; + return numberTo0x((BigInt(input.toString()) + (BigInt(1) << BigInt(bits))) % (BigInt(1) << BigInt(bits))); + } + + return nanOrNumberTo0x(input); +}; + const toHash32 = (value: string): string => { return value.substring(0, 66); }; @@ -303,8 +331,18 @@ const getFunctionSelector = (data?: string): string => { return data.replace(/^0x/, '').substring(0, 8); }; -const tinybarsToWeibars = (value: number | null) => { - if (value && value < 0) throw new Error('Invalid value - cannot pass negative number'); +const tinybarsToWeibars = (value: number | null, allowNegativeValues: boolean = false) => { + if (value && value < 0) { + // negative amount can be received only by CONTRACT_NEGATIVE_VALUE revert + // e.g. tx https://hashscan.io/mainnet/transaction/1735241436.856862230 + // that's not a valid revert in the Ethereum world so we must NOT multiply + // the amount sent via CONTRACT_CALL SDK call by TINYBAR_TO_WEIBAR_COEF + // also, keep in mind that the mirror node returned amount is typed with int64 + if (allowNegativeValues) return value; + + throw new Error('Invalid value - cannot pass negative number'); + } + if (value && value > constants.TOTAL_SUPPLY_TINYBARS) throw new Error('Value cannot be more than the total supply of tinybars in the blockchain'); @@ -324,6 +362,7 @@ export { numberTo0x, nullableNumberTo0x, nanOrNumberTo0x, + nanOrNumberInt64To0x, toHash32, toNullableBigNumber, toNullIfEmptyHex, diff --git a/packages/relay/tests/lib/formatters.spec.ts b/packages/relay/tests/lib/formatters.spec.ts index 5aed252afe..d479d908cc 100644 --- a/packages/relay/tests/lib/formatters.spec.ts +++ b/packages/relay/tests/lib/formatters.spec.ts @@ -18,7 +18,10 @@ * */ +import { BigNumber as BN } from 'bignumber.js'; import { expect } from 'chai'; +import { AbiCoder, keccak256 } from 'ethers'; + import { ASCIIToHex, decodeErrorMessage, @@ -31,26 +34,25 @@ import { isHex, isValidEthereumAddress, mapKeysAndValues, + nanOrNumberInt64To0x, nanOrNumberTo0x, nullableNumberTo0x, numberTo0x, parseNumericEnvVar, prepend0x, strip0x, + tinybarsToWeibars, toHash32, toHexString, toNullableBigNumber, toNullIfEmptyHex, trimPrecedingZeros, weibarHexToTinyBarInt, - tinybarsToWeibars, } from '../../src/formatters'; import constants from '../../src/lib/constants'; -import { BigNumber as BN } from 'bignumber.js'; -import { AbiCoder, keccak256 } from 'ethers'; import { overrideEnvsInMochaDescribe } from '../helpers'; -describe('Formatters', () => { +describe.only('Formatters', () => { describe('formatRequestIdMessage', () => { const exampleRequestId = '46530e63-e33a-4f42-8e44-b125f99f1a9b'; const expectedFormattedId = '[Request ID: 46530e63-e33a-4f42-8e44-b125f99f1a9b]'; @@ -399,6 +401,21 @@ describe('Formatters', () => { }); }); + describe('nanOrNumberInt64To0x', () => { + it('should return 0x0 for nullable input', () => { + expect(nanOrNumberInt64To0x(null)).to.equal('0x0'); + }); + it('should return 0x0 for NaN input', () => { + expect(nanOrNumberInt64To0x(NaN)).to.equal('0x0'); + }); + it('should convert a valid number', () => { + expect(nanOrNumberInt64To0x(593)).to.equal('0x251'); + }); + it('should covert a valid negative int64 number', () => { + expect(nanOrNumberInt64To0x(-10)).to.equal('0xfffffffffffffff6'); + }); + }); + describe('toHash32', () => { it('should format more than 32 bytes hash to 32 bytes', () => { expect( @@ -757,5 +774,8 @@ describe('Formatters', () => { 'Value cannot be more than the total supply of tinybars in the blockchain', ); }); + it('should return the negative number if allowNegativeValues flag is set to true', () => { + expect(tinybarsToWeibars(-10, true)).to.eql(-10); + }); }); }); From ec6dc59800f94a93786910ad4d4eb4821b95ad01 Mon Sep 17 00:00:00 2001 From: nikolay Date: Tue, 14 Jan 2025 17:44:19 +0200 Subject: [PATCH 02/15] chore: remove .only Signed-off-by: nikolay --- packages/relay/tests/lib/formatters.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/relay/tests/lib/formatters.spec.ts b/packages/relay/tests/lib/formatters.spec.ts index d479d908cc..dfaea812cc 100644 --- a/packages/relay/tests/lib/formatters.spec.ts +++ b/packages/relay/tests/lib/formatters.spec.ts @@ -52,7 +52,7 @@ import { import constants from '../../src/lib/constants'; import { overrideEnvsInMochaDescribe } from '../helpers'; -describe.only('Formatters', () => { +describe('Formatters', () => { describe('formatRequestIdMessage', () => { const exampleRequestId = '46530e63-e33a-4f42-8e44-b125f99f1a9b'; const expectedFormattedId = '[Request ID: 46530e63-e33a-4f42-8e44-b125f99f1a9b]'; From 48ce0a36ae333ade7aed534487013adca30b7e4f Mon Sep 17 00:00:00 2001 From: nikolay Date: Tue, 14 Jan 2025 17:47:32 +0200 Subject: [PATCH 03/15] chore: fix comments Signed-off-by: nikolay --- packages/relay/src/formatters.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/relay/src/formatters.ts b/packages/relay/src/formatters.ts index d2a122b31c..ee05a89ce6 100644 --- a/packages/relay/src/formatters.ts +++ b/packages/relay/src/formatters.ts @@ -268,11 +268,11 @@ const nanOrNumberTo0x = (input: number | BigNumber | bigint | null): string => { const nanOrNumberInt64To0x = (input: number | BigNumber | bigint | null): string => { if (input && input < 0) { - // the hex of a negative number can be obtained from the binary value of that number positive value, - // the binary value needs to be negated and then, to add 1, + // the hex of a negative number can be obtained from the binary value of that number positive value + // the binary value needs to be negated and then to be incremented by 1 // how the transformation works (using 16 bits) - // a 16 bits integer variables has values from -32768 to +32767, so: + // a 16 bits integer variables have values from -32768 to +32767, so: // 0 - 0x0000 - 0000 0000 0000 0000 // 32767 - 0x7fff - 0111 1111 1111 1111 // -32768 - 0x8000 - 1000 0000 0000 0000 From c17f02ab8bc0c12ac8c60954f5e2fea3cf5917a9 Mon Sep 17 00:00:00 2001 From: nikolay Date: Wed, 15 Jan 2025 12:05:33 +0200 Subject: [PATCH 04/15] chore: edit imports Signed-off-by: nikolay --- packages/relay/tests/lib/formatters.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/relay/tests/lib/formatters.spec.ts b/packages/relay/tests/lib/formatters.spec.ts index dfaea812cc..2a0200b27e 100644 --- a/packages/relay/tests/lib/formatters.spec.ts +++ b/packages/relay/tests/lib/formatters.spec.ts @@ -411,7 +411,7 @@ describe('Formatters', () => { it('should convert a valid number', () => { expect(nanOrNumberInt64To0x(593)).to.equal('0x251'); }); - it('should covert a valid negative int64 number', () => { + it('should convert a valid negative int64 number', () => { expect(nanOrNumberInt64To0x(-10)).to.equal('0xfffffffffffffff6'); }); }); From 5e9e1b10772cf6bd5d8668d4d41a62c37658c626 Mon Sep 17 00:00:00 2001 From: nikolay Date: Thu, 16 Jan 2025 11:42:55 +0200 Subject: [PATCH 05/15] chore: resolving comments Signed-off-by: nikolay --- packages/relay/src/formatters.ts | 7 +++-- packages/relay/tests/lib/formatters.spec.ts | 35 +++++++++++---------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/relay/src/formatters.ts b/packages/relay/src/formatters.ts index ee05a89ce6..bf6483dd2f 100644 --- a/packages/relay/src/formatters.ts +++ b/packages/relay/src/formatters.ts @@ -267,7 +267,8 @@ const nanOrNumberTo0x = (input: number | BigNumber | bigint | null): string => { }; const nanOrNumberInt64To0x = (input: number | BigNumber | bigint | null): string => { - if (input && input < 0) { + // converting to string and then back to int is fixing a typescript warning + if (input && parseInt(input.toString()) < 0) { // the hex of a negative number can be obtained from the binary value of that number positive value // the binary value needs to be negated and then to be incremented by 1 @@ -281,11 +282,11 @@ const nanOrNumberInt64To0x = (input: number | BigNumber | bigint | null): string // converting int16 -10 will be done as following: // - make it positive = 10 // - 16 bits binary value of 10 = 0000 0000 0000 1010 - // - inverse the bytes = 1111 1111 1111 0101 + // - inverse the bits = 1111 1111 1111 0101 // - adding +1 = 1111 1111 1111 0110 // - 1111 1111 1111 0110 bits = 0xfff6 - // we're using 64 bits integer because that the type returned by the mirror node - int64 + // we're using 64 bits integer because that's the type returned by the mirror node - int64 const bits = 64; return numberTo0x((BigInt(input.toString()) + (BigInt(1) << BigInt(bits))) % (BigInt(1) << BigInt(bits))); } diff --git a/packages/relay/tests/lib/formatters.spec.ts b/packages/relay/tests/lib/formatters.spec.ts index 2a0200b27e..97b4c59eef 100644 --- a/packages/relay/tests/lib/formatters.spec.ts +++ b/packages/relay/tests/lib/formatters.spec.ts @@ -752,28 +752,31 @@ describe('Formatters', () => { }); describe('tinybarsToWeibars', () => { - it('should convert tinybars to weibars', () => { - expect(tinybarsToWeibars(10)).to.eql(100000000000); - }); + for (const allowNegativeValues of [true, false]) { + it(`should convert tinybars to weibars allowNegativeValues = ${allowNegativeValues}`, () => { + expect(tinybarsToWeibars(10, allowNegativeValues)).to.eql(100000000000); + }); - it('should return null if null is passed', () => { - expect(tinybarsToWeibars(null)).to.eql(null); - }); + it(`should return null if null is passed allowNegativeValues = ${allowNegativeValues}`, () => { + expect(tinybarsToWeibars(null, allowNegativeValues)).to.eql(null); + }); - it('should return 0 for 0 input', () => { - expect(tinybarsToWeibars(0)).to.eql(0); - }); + it(`should return 0 for 0 input allowNegativeValues = ${allowNegativeValues}`, () => { + expect(tinybarsToWeibars(0, allowNegativeValues)).to.eql(0); + }); + + it(`should throw an error when value is larger than the total supply of tinybars allowNegativeValues = ${allowNegativeValues}`, () => { + expect(() => tinybarsToWeibars(constants.TOTAL_SUPPLY_TINYBARS * 10, allowNegativeValues)).to.throw( + Error, + 'Value cannot be more than the total supply of tinybars in the blockchain', + ); + }); + } it('should throw an error when value is smaller than 0', () => { - expect(() => tinybarsToWeibars(-10)).to.throw(Error, 'Invalid value - cannot pass negative number'); + expect(() => tinybarsToWeibars(-10, false)).to.throw(Error, 'Invalid value - cannot pass negative number'); }); - it('should throw an error when value is larger than the total supply of tinybars', () => { - expect(() => tinybarsToWeibars(constants.TOTAL_SUPPLY_TINYBARS * 10)).to.throw( - Error, - 'Value cannot be more than the total supply of tinybars in the blockchain', - ); - }); it('should return the negative number if allowNegativeValues flag is set to true', () => { expect(tinybarsToWeibars(-10, true)).to.eql(-10); }); From fa9f894e7ea4fa418f048bc95626a07dcde4b541 Mon Sep 17 00:00:00 2001 From: nikolay Date: Fri, 17 Jan 2025 14:06:34 +0200 Subject: [PATCH 06/15] chore: fix json parsing Signed-off-by: nikolay --- package-lock.json | 7 ++- packages/relay/package.json | 3 +- packages/relay/src/formatters.ts | 2 +- .../relay/src/lib/clients/mirrorNodeClient.ts | 23 ++++++++ packages/relay/tests/lib/formatters.spec.ts | 59 +++++++++++++++++-- 5 files changed, 86 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 97fce20103..1842f2ab2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10179,6 +10179,7 @@ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.5.tgz", "integrity": "sha512-HTm14iMQKK2FjFLRTM5lAVcyaUzOnqbPtesFIvREgXpJHdQm8bWS+GkQgIkfaBYRHuCnea7w8UVNfwiAQhlr9A==", "dev": true, + "hasInstallScript": true, "optional": true, "dependencies": { "node-gyp-build": "^4.3.0" @@ -10541,6 +10542,7 @@ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.7.tgz", "integrity": "sha512-vLt1O5Pp+flcArHGIyKEQq883nBt8nN8tVBcoL0qUXj2XT1n7p70yGIq2VK98I5FdZ1YHc0wk/koOnHjnXWk1Q==", "dev": true, + "hasInstallScript": true, "optional": true, "dependencies": { "node-gyp-build": "^4.3.0" @@ -12496,7 +12498,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "dev": true, + "license": "MIT", "dependencies": { "bignumber.js": "^9.0.0" } @@ -20708,6 +20710,7 @@ "dotenv": "^16.0.0", "ethers": "^6.7.0", "find-config": "^1.0.0", + "json-bigint": "^1.0.0", "keccak": "^3.0.2", "keyv": "^4.2.2", "keyv-file": "^0.3.0", @@ -24112,6 +24115,7 @@ "dotenv": "^16.0.0", "ethers": "^6.7.0", "find-config": "^1.0.0", + "json-bigint": "^1.0.0", "keccak": "^3.0.2", "keyv": "^4.2.2", "keyv-file": "^0.3.0", @@ -32535,7 +32539,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "dev": true, "requires": { "bignumber.js": "^9.0.0" } diff --git a/packages/relay/package.json b/packages/relay/package.json index 72bf189eb8..755e6ebbf0 100644 --- a/packages/relay/package.json +++ b/packages/relay/package.json @@ -50,8 +50,8 @@ }, "dependencies": { "@ethersproject/asm": "^5.7.0", - "@hashgraph/sdk": "^2.54.0-beta.1", "@hashgraph/json-rpc-config-service": "file:../config-service", + "@hashgraph/sdk": "^2.54.0-beta.1", "@keyvhq/core": "^1.6.9", "axios": "^1.4.0", "axios-retry": "^3.5.1", @@ -60,6 +60,7 @@ "dotenv": "^16.0.0", "ethers": "^6.7.0", "find-config": "^1.0.0", + "json-bigint": "^1.0.0", "keccak": "^3.0.2", "keyv": "^4.2.2", "keyv-file": "^0.3.0", diff --git a/packages/relay/src/formatters.ts b/packages/relay/src/formatters.ts index bf6483dd2f..9f407fa0cb 100644 --- a/packages/relay/src/formatters.ts +++ b/packages/relay/src/formatters.ts @@ -268,7 +268,7 @@ const nanOrNumberTo0x = (input: number | BigNumber | bigint | null): string => { const nanOrNumberInt64To0x = (input: number | BigNumber | bigint | null): string => { // converting to string and then back to int is fixing a typescript warning - if (input && parseInt(input.toString()) < 0) { + if (input && Number(input) < 0) { // the hex of a negative number can be obtained from the binary value of that number positive value // the binary value needs to be negated and then to be incremented by 1 diff --git a/packages/relay/src/lib/clients/mirrorNodeClient.ts b/packages/relay/src/lib/clients/mirrorNodeClient.ts index 0ed72afd9d..df3c3c6e96 100644 --- a/packages/relay/src/lib/clients/mirrorNodeClient.ts +++ b/packages/relay/src/lib/clients/mirrorNodeClient.ts @@ -25,6 +25,7 @@ import { install as betterLookupInstall } from 'better-lookup'; import { ethers } from 'ethers'; import http from 'http'; import https from 'https'; +import JSONBigInt from 'json-bigint'; import { Logger } from 'pino'; import { Histogram, Registry } from 'prom-client'; @@ -361,6 +362,28 @@ export class MirrorNodeClient { if (pathLabel == MirrorNodeClient.GET_CONTRACTS_RESULTS_OPCODES) { response = await this.web3Client.get(path, axiosRequestConfig); } else { + // JS supports up to 53 bits integers, once a number with more bits is converted to a js Number type, the initial value is already lost + // in order to fix that, we have to use a custom JSON parser that hooks up before + // the default axios JSON.parse conversion where the value will be rounded and lost + // JSONBigInt reads the string representation of received JSON and formats each number to BigNumber + axiosRequestConfig['transformResponse'] = [ + (data) => { + // if the data is not valid, just return it to stick to the current behaviour + if (data) { + try { + // try to parse it, if the json is valid, numbers within it will be converted + // this case will happen on almost every GET mirror node call + return JSONBigInt.parse(data); + } catch (e) { + // in some unit tests, the mocked returned json is not property formatted + // so we have to preprocess it here with JSON.stringify() + return JSONBigInt.parse(JSON.stringify(data)); + } + } + + return data; + }, + ]; response = await this.restClient.get(path, axiosRequestConfig); } } else { diff --git a/packages/relay/tests/lib/formatters.spec.ts b/packages/relay/tests/lib/formatters.spec.ts index 97b4c59eef..6377f10932 100644 --- a/packages/relay/tests/lib/formatters.spec.ts +++ b/packages/relay/tests/lib/formatters.spec.ts @@ -408,11 +408,62 @@ describe('Formatters', () => { it('should return 0x0 for NaN input', () => { expect(nanOrNumberInt64To0x(NaN)).to.equal('0x0'); }); - it('should convert a valid number', () => { - expect(nanOrNumberInt64To0x(593)).to.equal('0x251'); + it('should convert negative int64 number (2 digits)', () => { + expect(nanOrNumberInt64To0x(BigInt('-10'))).to.equal('0xfffffffffffffff6'); }); - it('should convert a valid negative int64 number', () => { - expect(nanOrNumberInt64To0x(-10)).to.equal('0xfffffffffffffff6'); + it('should convert negative int64 number (6 digits)', () => { + expect(nanOrNumberInt64To0x(BigInt('-851969'))).to.equal('0xfffffffffff2ffff'); + }); + it('should convert negative int64 number (19 digits -6917529027641081857)', () => { + expect(nanOrNumberInt64To0x(BigInt('-6917529027641081857'))).to.equal('0x9fffffffffffffff'); + }); + it('should convert negative int64 number (19 digits -9223372036586340353)', () => { + expect(nanOrNumberInt64To0x(BigInt('-9223372036586340353'))).to.equal('0x800000000fffffff'); + }); + it('should convert positive 10 bits number', () => { + expect(nanOrNumberInt64To0x(BigInt('593'))).to.equal('0x251'); + }); + it('should convert positive 50 bits number', () => { + expect(nanOrNumberInt64To0x(BigInt('844424930131967'))).to.equal('0x2ffffffffffff'); + }); + it('should convert positive 51 bits number', () => { + expect(nanOrNumberInt64To0x(BigInt('1970324836974591'))).to.equal('0x6ffffffffffff'); + }); + it('should convert positive 52 bits number', () => { + expect(nanOrNumberInt64To0x(BigInt('3096224743817215'))).to.equal('0xaffffffffffff'); + }); + it('should convert positive 53 bits number', () => { + expect(nanOrNumberInt64To0x(BigInt('9007199254740991'))).to.equal('0x1fffffffffffff'); + }); + it('should convert positive 54 bits number', () => { + expect(nanOrNumberInt64To0x(BigInt('13510798882111487'))).to.equal('0x2fffffffffffff'); + }); + it('should convert positive 55 bits number', () => { + expect(nanOrNumberInt64To0x(BigInt('31525197391593471'))).to.equal('0x6fffffffffffff'); + }); + it('should convert positive 56 bits number', () => { + expect(nanOrNumberInt64To0x(BigInt('49539595901075455'))).to.equal('0xafffffffffffff'); + }); + it('should convert positive 57 bits number', () => { + expect(nanOrNumberInt64To0x(BigInt('144115188075855871'))).to.equal('0x1ffffffffffffff'); + }); + it('should convert positive 58 bits number', () => { + expect(nanOrNumberInt64To0x(BigInt('216172782113783807'))).to.equal('0x2ffffffffffffff'); + }); + it('should convert positive 59 bits number', () => { + expect(nanOrNumberInt64To0x(BigInt('504403158265495551'))).to.equal('0x6ffffffffffffff'); + }); + it('should convert positive 60 bits number', () => { + expect(nanOrNumberInt64To0x(BigInt('792633534417207295'))).to.equal('0xaffffffffffffff'); + }); + it('should convert positive 61 bits number', () => { + expect(nanOrNumberInt64To0x(BigInt('2305843009213693951'))).to.equal('0x1fffffffffffffff'); + }); + it('should convert positive 62 bits number', () => { + expect(nanOrNumberInt64To0x(BigInt('3458764513820540927'))).to.equal('0x2fffffffffffffff'); + }); + it('should convert positive 63 bits number', () => { + expect(nanOrNumberInt64To0x(BigInt('8070450532247928831'))).to.equal('0x6fffffffffffffff'); }); }); From e3d5067fa3b54b5f123fb4fd2f7f05eac453392b Mon Sep 17 00:00:00 2001 From: nikolay Date: Tue, 28 Jan 2025 13:28:19 +0200 Subject: [PATCH 07/15] chore: add e2e test Signed-off-by: nikolay --- .../tests/acceptance/rpc_batch1.spec.ts | 33 ++++++++++++++++++- .../server/tests/clients/servicesClient.ts | 4 +-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/packages/server/tests/acceptance/rpc_batch1.spec.ts b/packages/server/tests/acceptance/rpc_batch1.spec.ts index ec8b8c6760..f761ffaa09 100644 --- a/packages/server/tests/acceptance/rpc_batch1.spec.ts +++ b/packages/server/tests/acceptance/rpc_batch1.spec.ts @@ -21,13 +21,14 @@ // External resources import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services'; // Other imports -import { numberTo0x, prepend0x } from '@hashgraph/json-rpc-relay/dist/formatters'; +import { formatTransactionId, numberTo0x, prepend0x } from '@hashgraph/json-rpc-relay/dist/formatters'; import Constants from '@hashgraph/json-rpc-relay/dist/lib/constants'; // Errors and constants from local resources import { predefined } from '@hashgraph/json-rpc-relay/dist/lib/errors/JsonRpcError'; import { RequestDetails } from '@hashgraph/json-rpc-relay/dist/lib/types'; import { AccountCreateTransaction, + ContractFunctionParameters, FileInfo, FileInfoQuery, Hbar, @@ -581,6 +582,36 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { Assertions.block(blockResult, mirrorBlock, mirrorTransactions, expectedGasPrice, false); }); + it('should execute "eth_getBlockByNumber", hydrated transactions = true for a block that contains a call with CONTRACT_NEGATIVE_VALUE status', async function() { + let transactionId; + let hasContractNegativeValueError = false; + try { + await servicesNode.executeContractCallWithAmount( + mirrorContractDetails.contract_id, + '', + new ContractFunctionParameters(), + 500_000, + -100 + ); + } catch (e: any) { + // regarding the docs and HederaResponseCodes.sol the CONTRACT_NEGATIVE_VALUE code equals 96; + expect(e.status._code).to.equal(96); + hasContractNegativeValueError = true; + transactionId = e.transactionId; + } + expect(hasContractNegativeValueError).to.be.true; + + const mirrorResult = await mirrorNode.get(`/contracts/results/${formatTransactionId(transactionId.toString())}`, requestId); + const txHash = mirrorResult.hash; + const blockResult = await relay.call( + RelayCalls.ETH_ENDPOINTS.ETH_GET_BLOCK_BY_NUMBER, + [numberTo0x(mirrorResult.block_number), true], + requestIdPrefix + ); + expect(blockResult.transactions).to.not.be.empty; + expect(blockResult.transactions).to.contain(txHash); + }); + it('should not cache "latest" block in "eth_getBlockByNumber" ', async function () { const blockResult = await relay.call( RelayCalls.ETH_ENDPOINTS.ETH_GET_BLOCK_BY_NUMBER, diff --git a/packages/server/tests/clients/servicesClient.ts b/packages/server/tests/clients/servicesClient.ts index e494c8a626..c9b5ed4a1d 100644 --- a/packages/server/tests/clients/servicesClient.ts +++ b/packages/server/tests/clients/servicesClient.ts @@ -308,9 +308,7 @@ export default class ServicesClient { .setFunction(functionName, params) .setTransactionMemo('Relay test contract execution'); - if (amount > 0) { - tx.setPayableAmount(Hbar.fromTinybars(amount)); - } + tx.setPayableAmount(Hbar.fromTinybars(amount)); let contractExecTransactionResponse: TransactionResponse; try { From 5f005ec4920dee67143c0cdae7168d18a3804bcb Mon Sep 17 00:00:00 2001 From: nikolay Date: Tue, 28 Jan 2025 16:38:30 +0200 Subject: [PATCH 08/15] chore: fix test Signed-off-by: nikolay --- .../server/tests/acceptance/rpc_batch1.spec.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/server/tests/acceptance/rpc_batch1.spec.ts b/packages/server/tests/acceptance/rpc_batch1.spec.ts index 1390a73162..87e1948463 100644 --- a/packages/server/tests/acceptance/rpc_batch1.spec.ts +++ b/packages/server/tests/acceptance/rpc_batch1.spec.ts @@ -582,7 +582,7 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { Assertions.block(blockResult, mirrorBlock, mirrorTransactions, expectedGasPrice, false); }); - it('should execute "eth_getBlockByNumber", hydrated transactions = true for a block that contains a call with CONTRACT_NEGATIVE_VALUE status', async function() { + it('should execute "eth_getBlockByNumber", hydrated transactions = true for a block that contains a call with CONTRACT_NEGATIVE_VALUE status', async function () { let transactionId; let hasContractNegativeValueError = false; try { @@ -591,7 +591,7 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { '', new ContractFunctionParameters(), 500_000, - -100 + -100, ); } catch (e: any) { // regarding the docs and HederaResponseCodes.sol the CONTRACT_NEGATIVE_VALUE code equals 96; @@ -601,15 +601,21 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { } expect(hasContractNegativeValueError).to.be.true; - const mirrorResult = await mirrorNode.get(`/contracts/results/${formatTransactionId(transactionId.toString())}`, requestId); + // waiting for at least one block time for data to be populated in the mirror node + // because on the step above we sent a sdk call + await new Promise((r) => setTimeout(r, 2100)); + const mirrorResult = await mirrorNode.get( + `/contracts/results/${formatTransactionId(transactionId.toString())}`, + requestId, + ); const txHash = mirrorResult.hash; const blockResult = await relay.call( RelayCalls.ETH_ENDPOINTS.ETH_GET_BLOCK_BY_NUMBER, [numberTo0x(mirrorResult.block_number), true], - requestIdPrefix + requestIdPrefix, ); expect(blockResult.transactions).to.not.be.empty; - expect(blockResult.transactions).to.contain(txHash); + expect(blockResult.transactions.map((tx) => tx.hash)).to.contain(txHash); }); it('should not cache "latest" block in "eth_getBlockByNumber" ', async function () { From e6ae8cd603535a89eafdcd79d66f585507a567cd Mon Sep 17 00:00:00 2001 From: nikolay Date: Tue, 28 Jan 2025 20:15:23 +0200 Subject: [PATCH 09/15] chore: fix test Signed-off-by: nikolay --- packages/server/tests/clients/servicesClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/tests/clients/servicesClient.ts b/packages/server/tests/clients/servicesClient.ts index c9b5ed4a1d..110cad2561 100644 --- a/packages/server/tests/clients/servicesClient.ts +++ b/packages/server/tests/clients/servicesClient.ts @@ -168,7 +168,7 @@ export default class ServicesClient { async executeTransaction(transaction: Transaction, requestId?: string) { const requestIdPrefix = Utils.formatRequestIdMessage(requestId); try { - const resp = await transaction.execute(this.client); + const resp = await (await transaction.freezeWith(this.client)).execute(this.client); this.logger.info( `${requestIdPrefix} Executed transaction of type ${ transaction.constructor.name From 94e77f6f7b8d2aad29e01fa8677a5b3c6d747dd8 Mon Sep 17 00:00:00 2001 From: nikolay Date: Tue, 28 Jan 2025 20:23:32 +0200 Subject: [PATCH 10/15] chore: fix flaky test Signed-off-by: nikolay --- packages/server/tests/acceptance/rpc_batch1.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/tests/acceptance/rpc_batch1.spec.ts b/packages/server/tests/acceptance/rpc_batch1.spec.ts index 87e1948463..f2a992d73c 100644 --- a/packages/server/tests/acceptance/rpc_batch1.spec.ts +++ b/packages/server/tests/acceptance/rpc_batch1.spec.ts @@ -960,7 +960,7 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { .addTokenTransfer(tokenId, servicesNode._thisAccountId(), -10) .addTokenTransfer(tokenId, accounts[2].accountId, 10) .setTransactionMemo('Relay test token transfer'); - const resp = await transaction.execute(servicesNode.client); + const resp = await (await transaction.freezeWith(servicesNode.client)).execute(servicesNode.client); await resp.getRecord(servicesNode.client); await Utils.wait(1000); const logsRes = await mirrorNode.get(`/contracts/results/logs?limit=1`, requestId); From 8e5c22a6631b2d7385b270a7e8293bfc1ef367bc Mon Sep 17 00:00:00 2001 From: nikolay Date: Wed, 29 Jan 2025 09:44:10 +0200 Subject: [PATCH 11/15] chore: tests Signed-off-by: nikolay --- packages/server/tests/acceptance/rpc_batch1.spec.ts | 2 +- packages/server/tests/clients/servicesClient.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/tests/acceptance/rpc_batch1.spec.ts b/packages/server/tests/acceptance/rpc_batch1.spec.ts index f2a992d73c..87e1948463 100644 --- a/packages/server/tests/acceptance/rpc_batch1.spec.ts +++ b/packages/server/tests/acceptance/rpc_batch1.spec.ts @@ -960,7 +960,7 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { .addTokenTransfer(tokenId, servicesNode._thisAccountId(), -10) .addTokenTransfer(tokenId, accounts[2].accountId, 10) .setTransactionMemo('Relay test token transfer'); - const resp = await (await transaction.freezeWith(servicesNode.client)).execute(servicesNode.client); + const resp = await transaction.execute(servicesNode.client); await resp.getRecord(servicesNode.client); await Utils.wait(1000); const logsRes = await mirrorNode.get(`/contracts/results/logs?limit=1`, requestId); diff --git a/packages/server/tests/clients/servicesClient.ts b/packages/server/tests/clients/servicesClient.ts index 110cad2561..c9b5ed4a1d 100644 --- a/packages/server/tests/clients/servicesClient.ts +++ b/packages/server/tests/clients/servicesClient.ts @@ -168,7 +168,7 @@ export default class ServicesClient { async executeTransaction(transaction: Transaction, requestId?: string) { const requestIdPrefix = Utils.formatRequestIdMessage(requestId); try { - const resp = await (await transaction.freezeWith(this.client)).execute(this.client); + const resp = await transaction.execute(this.client); this.logger.info( `${requestIdPrefix} Executed transaction of type ${ transaction.constructor.name From f31c86b77c57ac17a27cd9bafeac035eff5cc461 Mon Sep 17 00:00:00 2001 From: nikolay Date: Wed, 29 Jan 2025 16:53:20 +0200 Subject: [PATCH 12/15] chore: try to disable the test Signed-off-by: nikolay --- packages/server/tests/acceptance/rpc_batch1.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/tests/acceptance/rpc_batch1.spec.ts b/packages/server/tests/acceptance/rpc_batch1.spec.ts index 87e1948463..2e7db3fb2c 100644 --- a/packages/server/tests/acceptance/rpc_batch1.spec.ts +++ b/packages/server/tests/acceptance/rpc_batch1.spec.ts @@ -582,7 +582,7 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { Assertions.block(blockResult, mirrorBlock, mirrorTransactions, expectedGasPrice, false); }); - it('should execute "eth_getBlockByNumber", hydrated transactions = true for a block that contains a call with CONTRACT_NEGATIVE_VALUE status', async function () { + xit('should execute "eth_getBlockByNumber", hydrated transactions = true for a block that contains a call with CONTRACT_NEGATIVE_VALUE status', async function () { let transactionId; let hasContractNegativeValueError = false; try { From 38d8d04ded6e50f336a68eaa604b10f3828e0b7b Mon Sep 17 00:00:00 2001 From: nikolay Date: Wed, 29 Jan 2025 17:37:59 +0200 Subject: [PATCH 13/15] chore: add test Signed-off-by: nikolay --- .../tests/acceptance/rpc_batch1.spec.ts | 73 ++++++++++--------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/packages/server/tests/acceptance/rpc_batch1.spec.ts b/packages/server/tests/acceptance/rpc_batch1.spec.ts index 2e7db3fb2c..02eacb417d 100644 --- a/packages/server/tests/acceptance/rpc_batch1.spec.ts +++ b/packages/server/tests/acceptance/rpc_batch1.spec.ts @@ -582,42 +582,6 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { Assertions.block(blockResult, mirrorBlock, mirrorTransactions, expectedGasPrice, false); }); - xit('should execute "eth_getBlockByNumber", hydrated transactions = true for a block that contains a call with CONTRACT_NEGATIVE_VALUE status', async function () { - let transactionId; - let hasContractNegativeValueError = false; - try { - await servicesNode.executeContractCallWithAmount( - mirrorContractDetails.contract_id, - '', - new ContractFunctionParameters(), - 500_000, - -100, - ); - } catch (e: any) { - // regarding the docs and HederaResponseCodes.sol the CONTRACT_NEGATIVE_VALUE code equals 96; - expect(e.status._code).to.equal(96); - hasContractNegativeValueError = true; - transactionId = e.transactionId; - } - expect(hasContractNegativeValueError).to.be.true; - - // waiting for at least one block time for data to be populated in the mirror node - // because on the step above we sent a sdk call - await new Promise((r) => setTimeout(r, 2100)); - const mirrorResult = await mirrorNode.get( - `/contracts/results/${formatTransactionId(transactionId.toString())}`, - requestId, - ); - const txHash = mirrorResult.hash; - const blockResult = await relay.call( - RelayCalls.ETH_ENDPOINTS.ETH_GET_BLOCK_BY_NUMBER, - [numberTo0x(mirrorResult.block_number), true], - requestIdPrefix, - ); - expect(blockResult.transactions).to.not.be.empty; - expect(blockResult.transactions.map((tx) => tx.hash)).to.contain(txHash); - }); - it('should not cache "latest" block in "eth_getBlockByNumber" ', async function () { const blockResult = await relay.call( RelayCalls.ETH_ENDPOINTS.ETH_GET_BLOCK_BY_NUMBER, @@ -763,6 +727,43 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { expect(blockNumber).to.be.oneOf([mirrorBlockNumber, mirrorBlockNumber + 1]); }); }); + + it('should execute "eth_getBlockByNumber", hydrated transactions = true for a block that contains a call with CONTRACT_NEGATIVE_VALUE status', async function () { + let transactionId; + let hasContractNegativeValueError = false; + try { + await servicesNode.executeContractCallWithAmount( + mirrorContractDetails.contract_id, + '', + new ContractFunctionParameters(), + 500_000, + -100, + requestId, + ); + } catch (e: any) { + // regarding the docs and HederaResponseCodes.sol the CONTRACT_NEGATIVE_VALUE code equals 96; + expect(e.status._code).to.equal(96); + hasContractNegativeValueError = true; + transactionId = e.transactionId; + } + expect(hasContractNegativeValueError).to.be.true; + + // waiting for at least one block time for data to be populated in the mirror node + // because on the step above we sent a sdk call + await new Promise((r) => setTimeout(r, 2100)); + const mirrorResult = await mirrorNode.get( + `/contracts/results/${formatTransactionId(transactionId.toString())}`, + requestId, + ); + const txHash = mirrorResult.hash; + const blockResult = await relay.call( + RelayCalls.ETH_ENDPOINTS.ETH_GET_BLOCK_BY_NUMBER, + [numberTo0x(mirrorResult.block_number), true], + requestIdPrefix, + ); + expect(blockResult.transactions).to.not.be.empty; + expect(blockResult.transactions.map((tx) => tx.hash)).to.contain(txHash); + }); }); describe('Transaction related RPC Calls', () => { From d2ddb95c385575951d2366744d738ee37716ddde Mon Sep 17 00:00:00 2001 From: nikolay Date: Thu, 30 Jan 2025 10:10:32 +0200 Subject: [PATCH 14/15] chore: resolve comments Signed-off-by: nikolay --- packages/relay/src/formatters.ts | 6 ++++++ packages/relay/src/lib/clients/mirrorNodeClient.ts | 10 ++++++---- packages/server/tests/acceptance/rpc_batch1.spec.ts | 1 + 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/relay/src/formatters.ts b/packages/relay/src/formatters.ts index 9f407fa0cb..5c5081ca45 100644 --- a/packages/relay/src/formatters.ts +++ b/packages/relay/src/formatters.ts @@ -288,6 +288,12 @@ const nanOrNumberInt64To0x = (input: number | BigNumber | bigint | null): string // we're using 64 bits integer because that's the type returned by the mirror node - int64 const bits = 64; + // this mathematical expression serves as a shortcut for performing the two’s complement conversion + // e.g. input = -10 + // we have: (BigInt(1) << BigInt(bits)) = 1 << 64 = 2^64 = 18446744073709551616 + // then: (BigInt(input.toString()) + (BigInt(1) << BigInt(bits))) = -10 + 2^64 = 18446744073709551606 + // this effectively represents -10 in an unsigned 64-bit representation:18446744073709551606 = 0xFFFFFFFFFFFFFFF6 + // finally, the modulo operation: % (1 << 64) return numberTo0x((BigInt(input.toString()) + (BigInt(1) << BigInt(bits))) % (BigInt(1) << BigInt(bits))); } diff --git a/packages/relay/src/lib/clients/mirrorNodeClient.ts b/packages/relay/src/lib/clients/mirrorNodeClient.ts index 6a0e052fd8..83ef4edc7e 100644 --- a/packages/relay/src/lib/clients/mirrorNodeClient.ts +++ b/packages/relay/src/lib/clients/mirrorNodeClient.ts @@ -362,10 +362,12 @@ export class MirrorNodeClient { if (pathLabel == MirrorNodeClient.GET_CONTRACTS_RESULTS_OPCODES) { response = await this.web3Client.get(path, axiosRequestConfig); } else { - // JS supports up to 53 bits integers, once a number with more bits is converted to a js Number type, the initial value is already lost - // in order to fix that, we have to use a custom JSON parser that hooks up before - // the default axios JSON.parse conversion where the value will be rounded and lost - // JSONBigInt reads the string representation of received JSON and formats each number to BigNumber + // JavaScript supports integers only up to 53 bits. When a number exceeding this limit + // is converted to a JS Number type, precision is lost due to rounding. + // To prevent this, `transformResponse` is used to intercept + // and process the response before Axios’s default JSON.parse conversion. + // JSONBigInt reads the string representation from the received JSON + // and converts large numbers into BigNumber objects to maintain accuracy. axiosRequestConfig['transformResponse'] = [ (data) => { // if the data is not valid, just return it to stick to the current behaviour diff --git a/packages/server/tests/acceptance/rpc_batch1.spec.ts b/packages/server/tests/acceptance/rpc_batch1.spec.ts index 02eacb417d..abe420cae6 100644 --- a/packages/server/tests/acceptance/rpc_batch1.spec.ts +++ b/packages/server/tests/acceptance/rpc_batch1.spec.ts @@ -763,6 +763,7 @@ describe('@api-batch-1 RPC Server Acceptance Tests', function () { ); expect(blockResult.transactions).to.not.be.empty; expect(blockResult.transactions.map((tx) => tx.hash)).to.contain(txHash); + expect(blockResult.transactions.filter((tx) => tx.hash == txHash)[0].value).to.equal('0xffffffffffffff9c'); }); }); From 24e6f30e3352a5a84879a62aa3b012ece9463d5c Mon Sep 17 00:00:00 2001 From: nikolay Date: Thu, 30 Jan 2025 12:25:08 +0200 Subject: [PATCH 15/15] chore: simplify the tests Signed-off-by: nikolay --- packages/relay/tests/lib/formatters.spec.ts | 90 ++++++++------------- 1 file changed, 33 insertions(+), 57 deletions(-) diff --git a/packages/relay/tests/lib/formatters.spec.ts b/packages/relay/tests/lib/formatters.spec.ts index 6377f10932..508c3783ab 100644 --- a/packages/relay/tests/lib/formatters.spec.ts +++ b/packages/relay/tests/lib/formatters.spec.ts @@ -408,63 +408,39 @@ describe('Formatters', () => { it('should return 0x0 for NaN input', () => { expect(nanOrNumberInt64To0x(NaN)).to.equal('0x0'); }); - it('should convert negative int64 number (2 digits)', () => { - expect(nanOrNumberInt64To0x(BigInt('-10'))).to.equal('0xfffffffffffffff6'); - }); - it('should convert negative int64 number (6 digits)', () => { - expect(nanOrNumberInt64To0x(BigInt('-851969'))).to.equal('0xfffffffffff2ffff'); - }); - it('should convert negative int64 number (19 digits -6917529027641081857)', () => { - expect(nanOrNumberInt64To0x(BigInt('-6917529027641081857'))).to.equal('0x9fffffffffffffff'); - }); - it('should convert negative int64 number (19 digits -9223372036586340353)', () => { - expect(nanOrNumberInt64To0x(BigInt('-9223372036586340353'))).to.equal('0x800000000fffffff'); - }); - it('should convert positive 10 bits number', () => { - expect(nanOrNumberInt64To0x(BigInt('593'))).to.equal('0x251'); - }); - it('should convert positive 50 bits number', () => { - expect(nanOrNumberInt64To0x(BigInt('844424930131967'))).to.equal('0x2ffffffffffff'); - }); - it('should convert positive 51 bits number', () => { - expect(nanOrNumberInt64To0x(BigInt('1970324836974591'))).to.equal('0x6ffffffffffff'); - }); - it('should convert positive 52 bits number', () => { - expect(nanOrNumberInt64To0x(BigInt('3096224743817215'))).to.equal('0xaffffffffffff'); - }); - it('should convert positive 53 bits number', () => { - expect(nanOrNumberInt64To0x(BigInt('9007199254740991'))).to.equal('0x1fffffffffffff'); - }); - it('should convert positive 54 bits number', () => { - expect(nanOrNumberInt64To0x(BigInt('13510798882111487'))).to.equal('0x2fffffffffffff'); - }); - it('should convert positive 55 bits number', () => { - expect(nanOrNumberInt64To0x(BigInt('31525197391593471'))).to.equal('0x6fffffffffffff'); - }); - it('should convert positive 56 bits number', () => { - expect(nanOrNumberInt64To0x(BigInt('49539595901075455'))).to.equal('0xafffffffffffff'); - }); - it('should convert positive 57 bits number', () => { - expect(nanOrNumberInt64To0x(BigInt('144115188075855871'))).to.equal('0x1ffffffffffffff'); - }); - it('should convert positive 58 bits number', () => { - expect(nanOrNumberInt64To0x(BigInt('216172782113783807'))).to.equal('0x2ffffffffffffff'); - }); - it('should convert positive 59 bits number', () => { - expect(nanOrNumberInt64To0x(BigInt('504403158265495551'))).to.equal('0x6ffffffffffffff'); - }); - it('should convert positive 60 bits number', () => { - expect(nanOrNumberInt64To0x(BigInt('792633534417207295'))).to.equal('0xaffffffffffffff'); - }); - it('should convert positive 61 bits number', () => { - expect(nanOrNumberInt64To0x(BigInt('2305843009213693951'))).to.equal('0x1fffffffffffffff'); - }); - it('should convert positive 62 bits number', () => { - expect(nanOrNumberInt64To0x(BigInt('3458764513820540927'))).to.equal('0x2fffffffffffffff'); - }); - it('should convert positive 63 bits number', () => { - expect(nanOrNumberInt64To0x(BigInt('8070450532247928831'))).to.equal('0x6fffffffffffffff'); - }); + + for (const [testName, testValues] of Object.entries({ + '2 digits': ['-10', '0xfffffffffffffff6'], + '6 digits': ['-851969', '0xfffffffffff2ffff'], + '19 digits -6917529027641081857': ['-6917529027641081857', '0x9fffffffffffffff'], + '19 digits -9223372036586340353': ['-9223372036586340353', '0x800000000fffffff'], + })) { + it(`should convert negative int64 number (${testName})`, () => { + expect(nanOrNumberInt64To0x(BigInt(testValues[0]))).to.equal(testValues[1]); + }); + } + + for (const [bits, testValues] of Object.entries({ + 10: ['593', '0x251'], + 50: ['844424930131967', '0x2ffffffffffff'], + 51: ['1970324836974591', '0x6ffffffffffff'], + 52: ['3096224743817215', '0xaffffffffffff'], + 53: ['9007199254740991', '0x1fffffffffffff'], + 54: ['13510798882111487', '0x2fffffffffffff'], + 55: ['31525197391593471', '0x6fffffffffffff'], + 56: ['49539595901075455', '0xafffffffffffff'], + 57: ['144115188075855871', '0x1ffffffffffffff'], + 58: ['216172782113783807', '0x2ffffffffffffff'], + 59: ['504403158265495551', '0x6ffffffffffffff'], + 60: ['792633534417207295', '0xaffffffffffffff'], + 61: ['2305843009213693951', '0x1fffffffffffffff'], + 62: ['3458764513820540927', '0x2fffffffffffffff'], + 63: ['8070450532247928831', '0x6fffffffffffffff'], + })) { + it(`should convert positive ${bits} bits number`, () => { + expect(nanOrNumberInt64To0x(BigInt(testValues[0]))).to.equal(testValues[1]); + }); + } }); describe('toHash32', () => {