diff --git a/.gitignore b/.gitignore index c22337c0c9a..72f88324900 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,5 @@ packages/web3/.in3/ benchmark-data.txt .eslintcache + +.history \ No newline at end of file diff --git a/packages/web3-utils/src/converters.ts b/packages/web3-utils/src/converters.ts index 6649bef1481..9c94a4332b0 100644 --- a/packages/web3-utils/src/converters.ts +++ b/packages/web3-utils/src/converters.ts @@ -79,7 +79,8 @@ export const ethUnitMap = { tether: BigInt('1000000000000000000000000000000'), }; -const PrecisionLossWarning = 'Warning: Using type `number` with values that are large or contain many decimals may cause loss of precision, it is recommended to use type `string` or `BigInt` when using conversion methods'; +const PrecisionLossWarning = + 'Warning: Using type `number` with values that are large or contain many decimals may cause loss of precision, it is recommended to use type `string` or `BigInt` when using conversion methods'; export type EtherUnits = keyof typeof ethUnitMap; /** @@ -366,7 +367,7 @@ export const toHex = ( return returnType ? 'bigint' : numberToHex(value); } - if(isUint8Array(value)) { + if (isUint8Array(value)) { return returnType ? 'bytes' : bytesToHex(value); } @@ -386,6 +387,15 @@ export const toHex = ( return returnType ? 'bytes' : `0x${value}`; } if (isHex(value) && !isInt(value) && isUInt(value)) { + // This condition seems problematic because meeting + // both conditions `!isInt(value) && isUInt(value)` should be impossible. + // But a value pass for those conditions: "101611154195520776335741463917853444671577865378275924493376429267637792638729" + // Note that according to the docs: it is supposed to be treated as a string (https://docs.web3js.org/guides/web3_upgrade_guide/x/web3_utils_migration_guide#conversion-to-hex) + // In short, the strange is that isInt(value) is false but isUInt(value) is true for the value above. + // TODO: isUInt(value) should be investigated. + + // However, if `toHex('101611154195520776335741463917853444671577865378275924493376429267637792638729', true)` is called, it will return `true`. + // But, if `toHex('101611154195520776335741463917853444671577865378275924493376429267637792638729')` is called, it will throw inside `numberToHex`. return returnType ? 'uint' : numberToHex(value); } @@ -419,14 +429,14 @@ export const toHex = ( */ export const toNumber = (value: Numbers): number | bigint => { if (typeof value === 'number') { - if (value > 1e+20) { - console.warn(PrecisionLossWarning) - // JavaScript converts numbers >= 10^21 to scientific notation when coerced to strings, - // leading to potential parsing errors and incorrect representations. - // For instance, String(10000000000000000000000) yields '1e+22'. - // Using BigInt prevents this - return BigInt(value); - } + if (value > 1e20) { + console.warn(PrecisionLossWarning); + // JavaScript converts numbers >= 10^21 to scientific notation when coerced to strings, + // leading to potential parsing errors and incorrect representations. + // For instance, String(10000000000000000000000) yields '1e+22'. + // Using BigInt prevents this + return BigInt(value); + } return value; } @@ -506,10 +516,9 @@ export const fromWei = (number: Numbers, unit: EtherUnits | number): string => { if (unit < 0 || !Number.isInteger(unit)) { throw new InvalidIntegerError(unit); } - denomination = bigintPower(BigInt(10),BigInt(unit)); + denomination = bigintPower(BigInt(10), BigInt(unit)); } - // value in wei would always be integer // 13456789, 1234 const value = String(toNumber(number)); @@ -575,8 +584,8 @@ export const toWei = (number: Numbers, unit: EtherUnits | number): string => { if (unit < 0 || !Number.isInteger(unit)) { throw new InvalidIntegerError(unit); } - - denomination = bigintPower(BigInt(10),BigInt(unit)); + + denomination = bigintPower(BigInt(10), BigInt(unit)); } let parsedNumber = number; diff --git a/packages/web3-utils/src/formatter.ts b/packages/web3-utils/src/formatter.ts index 42062add5c6..665965a8eed 100644 --- a/packages/web3-utils/src/formatter.ts +++ b/packages/web3-utils/src/formatter.ts @@ -57,10 +57,8 @@ const findSchemaByDataPath = ( for (const dataPart of dataPath) { if (result.oneOf && previousDataPath) { - const path = oneOfPath.find(function (element: [string, number]) { - return (this as unknown as string) === element[0]; - }, previousDataPath ?? ''); - + const currentDataPath = previousDataPath; + const path = oneOfPath.find(([key]) => key === currentDataPath); if (path && path[0] === previousDataPath) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access result = result.oneOf[path[1]]; @@ -75,10 +73,6 @@ const findSchemaByDataPath = ( } else if (result.items && (result.items as JsonSchema).properties) { const node = (result.items as JsonSchema).properties as Record; - if (!node) { - return undefined; - } - result = node[dataPart]; } else if (result.items && isObject(result.items)) { result = result.items; @@ -307,7 +301,7 @@ export const convert = ( // If value is an object, recurse into it if (isObject(value)) { - convert(value, schema, dataPath, format); + convert(value, schema, dataPath, format, oneOfPath); dataPath.pop(); continue; } diff --git a/packages/web3-utils/src/hash.ts b/packages/web3-utils/src/hash.ts index 6e498138c29..0289a429a52 100644 --- a/packages/web3-utils/src/hash.ts +++ b/packages/web3-utils/src/hash.ts @@ -17,25 +17,25 @@ along with web3.js. If not, see . /** * This package provides utility functions for Ethereum dapps and other web3.js packages. - * + * * For using Utils functions, first install Web3 package using `npm i web3` or `yarn add web3`. - * After that, Web3 Utils functions will be available as mentioned below. + * After that, Web3 Utils functions will be available as mentioned below. * ```ts * import { Web3 } from 'web3'; * const web3 = new Web3(); - * + * * const value = web3.utils.fromWei("1", "ether") - * + * * ``` - * + * * For using individual package install `web3-utils` package using `npm i web3-utils` or `yarn add web3-utils` and only import required functions. - * This is more efficient approach for building lightweight applications. + * This is more efficient approach for building lightweight applications. * ```ts * import { fromWei, soliditySha3Raw } from 'web3-utils'; - * + * * console.log(fromWei("1", "ether")); * console.log(soliditySha3Raw({ type: "string", value: "helloworld" })) - * + * * ``` * @module Utils */ @@ -172,7 +172,6 @@ const getType = (arg: Sha3Input): [string, EncodingTypes] => { if (Array.isArray(arg)) { throw new Error('Autodetection of array types is not supported.'); } - let type; let value; // if type is given diff --git a/packages/web3-utils/src/json_rpc.ts b/packages/web3-utils/src/json_rpc.ts index 573d39197e3..495a425611f 100644 --- a/packages/web3-utils/src/json_rpc.ts +++ b/packages/web3-utils/src/json_rpc.ts @@ -99,7 +99,7 @@ let requestIdSeed: number | undefined; /** * Optionally use to make the jsonrpc `id` start from a specific number. * Without calling this function, the `id` will be filled with a Uuid. - * But after this being called with a number, the `id` will be a number staring from the provided `start` variable. + * But after this being called with a number, the `id` will be a number starting from the provided `start` variable. * However, if `undefined` was passed to this function, the `id` will be a Uuid again. * @param start - a number to start incrementing from. * Or `undefined` to use a new Uuid (this is the default behavior) diff --git a/packages/web3-utils/src/promise_helpers.ts b/packages/web3-utils/src/promise_helpers.ts index c8e211fbbb7..a2140a2d1c5 100644 --- a/packages/web3-utils/src/promise_helpers.ts +++ b/packages/web3-utils/src/promise_helpers.ts @@ -20,7 +20,6 @@ import { isNullish } from 'web3-validator'; export type Timer = ReturnType; export type Timeout = ReturnType; - /** * An alternative to the node function `isPromise` that exists in `util/types` because it is not available on the browser. * @param object - to check if it is a `Promise` @@ -74,7 +73,6 @@ export async function waitWithTimeout( return result; } - /** * Repeatedly calls an async function with a given interval until the result of the function is defined (not undefined or null), * or until a timeout is reached. It returns promise and intervalId. @@ -85,25 +83,27 @@ export function pollTillDefinedAndReturnIntervalId( func: AsyncFunction, interval: number, ): [Promise>, Timer] { - let intervalId: Timer | undefined; const polledRes = new Promise>((resolve, reject) => { - intervalId = setInterval(function intervalCallbackFunc(){ - (async () => { - try { - const res = await waitWithTimeout(func, interval); - - if (!isNullish(res)) { + intervalId = setInterval( + (function intervalCallbackFunc() { + (async () => { + try { + const res = await waitWithTimeout(func, interval); + + if (!isNullish(res)) { + clearInterval(intervalId); + resolve(res as unknown as Exclude); + } + } catch (error) { clearInterval(intervalId); - resolve(res as unknown as Exclude); + reject(error); } - } catch (error) { - clearInterval(intervalId); - reject(error); - } - })() as unknown; - return intervalCallbackFunc;}() // this will immediate invoke first call - , interval); + })() as unknown; + return intervalCallbackFunc; + })(), // this will immediate invoke first call + interval, + ); }); return [polledRes as unknown as Promise>, intervalId!]; @@ -113,7 +113,7 @@ export function pollTillDefinedAndReturnIntervalId( * Repeatedly calls an async function with a given interval until the result of the function is defined (not undefined or null), * or until a timeout is reached. * pollTillDefinedAndReturnIntervalId() function should be used instead of pollTillDefined if you need IntervalId in result. - * This function will be deprecated in next major release so use pollTillDefinedAndReturnIntervalId(). + * This function will be deprecated in next major release so use pollTillDefinedAndReturnIntervalId(). * @param func - The function to call. * @param interval - The interval in milliseconds. */ @@ -146,7 +146,7 @@ export function rejectIfTimeout(timeout: number, error: Error): [Timer, Promise< /** * Sets an interval that repeatedly executes the given cond function with the specified interval between each call. * If the condition is met, the interval is cleared and a Promise that rejects with the returned value is returned. - * @param cond - The function/confition to call. + * @param cond - The function/condition to call. * @param interval - The interval in milliseconds. * @returns - an array with the interval ID and the Promise. */ @@ -168,4 +168,3 @@ export function rejectIfConditionAtInterval( }); return [intervalId!, rejectIfCondition]; } - diff --git a/packages/web3-utils/src/socket_provider.ts b/packages/web3-utils/src/socket_provider.ts index d847dcbccc9..0a73fb8378f 100644 --- a/packages/web3-utils/src/socket_provider.ts +++ b/packages/web3-utils/src/socket_provider.ts @@ -187,13 +187,13 @@ export abstract class SocketProvider< protected _validateProviderPath(path: string): boolean { return !!path; } - + /** * * @returns the pendingRequestQueue size */ // eslint-disable-next-line class-methods-use-this - public getPendingRequestQueueSize() { + public getPendingRequestQueueSize() { return this._pendingRequestsQueue.size; } @@ -350,32 +350,34 @@ export abstract class SocketProvider< /** * Safely disconnects the socket, async and waits for request size to be 0 before disconnecting - * @param forceDisconnect - If true, will clear queue after 5 attempts of waiting for both pending and sent queue to be 0 + * @param forceDisconnect - If true, will clear queue after 5 attempts of waiting for both pending and sent queue to be 0 * @param ms - Determines the ms of setInterval * @param code - The code to be sent to the server * @param data - The data to be sent to the server */ - public async safeDisconnect(code?: number, data?: string, forceDisconnect = false,ms = 1000) { + public async safeDisconnect(code?: number, data?: string, forceDisconnect = false, ms = 1000) { let retryAttempt = 0; - const checkQueue = async () => + const checkQueue = async () => new Promise(resolve => { const interval = setInterval(() => { - if (forceDisconnect && retryAttempt === 5) { + if (forceDisconnect && retryAttempt >= 5) { this.clearQueues(); } - if (this.getPendingRequestQueueSize() === 0 && this.getSentRequestsQueueSize() === 0) { + if ( + this.getPendingRequestQueueSize() === 0 && + this.getSentRequestsQueueSize() === 0 + ) { clearInterval(interval); resolve(true); } - retryAttempt+=1; - }, ms) - }) - + retryAttempt += 1; + }, ms); + }); + await checkQueue(); this.disconnect(code, data); } - /** * Removes all listeners for the specified event type. * @param type - The event type to remove the listeners for @@ -512,7 +514,7 @@ export abstract class SocketProvider< if (isNullish(responses) || responses.length === 0) { return; } - + for (const response of responses) { if ( jsonRpc.isResponseWithNotification(response as JsonRpcNotification) && @@ -544,7 +546,7 @@ export abstract class SocketProvider< this._sentRequestsQueue.delete(requestId); } } - + public clearQueues(event?: ConnectionEvent) { this._clearQueues(event); } diff --git a/packages/web3-utils/test/fixtures/converters.ts b/packages/web3-utils/test/fixtures/converters.ts index 9f0324b68ad..cecb0b3fafb 100644 --- a/packages/web3-utils/test/fixtures/converters.ts +++ b/packages/web3-utils/test/fixtures/converters.ts @@ -235,21 +235,20 @@ export const toHexValidData: [Numbers | Bytes | Address | boolean, [HexString, V ], ['-0x01', ['-0x1', 'int256']], ['123c', ['0x123c', 'bytes']], - [new Uint8Array([ - 221, 128, 128, 128, 148, 186, 248, - 242, 159, 130, 231, 84, 254, 199, - 252, 69, 21, 58, 104, 102, 201, - 137, 255, 3, 196, 10, 128, 128, - 128, 128 - ]), ['0xdd80808094baf8f29f82e754fec7fc45153a6866c989ff03c40a80808080', 'bytes']], - [Buffer.from([ - 221, 128, 128, 128, 148, 186, 248, - 242, 159, 130, 231, 84, 254, 199, - 252, 69, 21, 58, 104, 102, 201, - 137, 255, 3, 196, 10, 128, 128, - 128, 128 - ]), ['0xdd80808094baf8f29f82e754fec7fc45153a6866c989ff03c40a80808080', 'bytes']] - + [ + new Uint8Array([ + 221, 128, 128, 128, 148, 186, 248, 242, 159, 130, 231, 84, 254, 199, 252, 69, 21, 58, + 104, 102, 201, 137, 255, 3, 196, 10, 128, 128, 128, 128, + ]), + ['0xdd80808094baf8f29f82e754fec7fc45153a6866c989ff03c40a80808080', 'bytes'], + ], + [ + Buffer.from([ + 221, 128, 128, 128, 148, 186, 248, 242, 159, 130, 231, 84, 254, 199, 252, 69, 21, 58, + 104, 102, 201, 137, 255, 3, 196, 10, 128, 128, 128, 128, + ]), + ['0xdd80808094baf8f29f82e754fec7fc45153a6866c989ff03c40a80808080', 'bytes'], + ], ]; export const toHexInvalidData: [any, string][] = [ @@ -320,15 +319,15 @@ const conversionBaseData: [[Numbers, EtherUnits | number], string][] = [ export const fromWeiValidData: [[Numbers, EtherUnits | number], Numbers][] = [ ...conversionBaseData, - [['0xff', 'wei'], '255'], - [[1e+22, 'ether'], '10000'], - [[19999999999999991611392, 'ether'], '19999.999999999991611392'], - [[1.9999999999999991611392e+22, 'ether'], '19999.999999999991611392'], + [['0xff', 'wei'], '255'], + [[1e22, 'ether'], '10000'], + [[19999999999999991611392, 'ether'], '19999.999999999991611392'], + [[1.9999999999999991611392e22, 'ether'], '19999.999999999991611392'], [['1000000', 'ether'], '0.000000000001'], [['1123456789123456789', 'ether'], '1.123456789123456789'], [['1123', 'kwei'], '1.123'], - [['1234100' ,'kwei'], '1234.1'], - [['3308685546611893', 'ether'], '0.003308685546611893'] + [['1234100', 'kwei'], '1234.1'], + [['3308685546611893', 'ether'], '0.003308685546611893'], ]; export const toWeiValidData: [[Numbers, EtherUnits | number], Numbers][] = [ @@ -339,17 +338,24 @@ export const toWeiValidData: [[Numbers, EtherUnits | number], Numbers][] = [ [['1000000', 'ether'], 0.000000000001], [['1123456789123456789', 'ether'], '1.123456789123456789123'], [['1123', 'kwei'], '1.12345'], - [['1234100' ,'kwei'], '1234.1'], + [['1234100', 'kwei'], '1234.1'], [['3308685546611893', 'ether'], '0.0033086855466118933'], [['1123', 'kwei'], 1.12345], - ]; export const toWeiValidDataWarnings: [[Numbers, EtherUnits], string][] = [ - [[0.0000000000000000000001, 'ether'], 'Warning: Using type `number` with values that are large or contain many decimals may cause loss of precision, it is recommended to use type `string` or `BigInt` when using conversion methods'], - [[0.0000000000000000000001, 'ether'], 'Warning: Using type `number` with values that are large or contain many decimals may cause loss of precision, it is recommended to use type `string` or `BigInt` when using conversion methods'], - [[1999999000000009900000, 'kwei'], 'Warning: Using type `number` with values that are large or contain many decimals may cause loss of precision, it is recommended to use type `string` or `BigInt` when using conversion methods'], - + [ + [0.0000000000000000000001, 'ether'], + 'Warning: Using type `number` with values that are large or contain many decimals may cause loss of precision, it is recommended to use type `string` or `BigInt` when using conversion methods', + ], + [ + [0.0000000000000000000001, 'ether'], + 'Warning: Using type `number` with values that are large or contain many decimals may cause loss of precision, it is recommended to use type `string` or `BigInt` when using conversion methods', + ], + [ + [1999999000000009900000, 'kwei'], + 'Warning: Using type `number` with values that are large or contain many decimals may cause loss of precision, it is recommended to use type `string` or `BigInt` when using conversion methods', + ], ]; export const fromWeiInvalidData: [[any, any], string][] = [ @@ -362,8 +368,14 @@ export const fromWeiInvalidData: [[any, any], string][] = [ [[{}, 'kwei'], 'Invalid value given "{}". Error: can not parse as number data'], [['data', 'kwei'], 'Invalid value given "data". Error: can not parse as number data.'], [['1234', 'uwei'], 'Invalid value given "uwei". Error: invalid unit.'], - [['1234', -1], 'Invalid value given "-1". Error: not a valid unit. Must be a positive integer.'], - [['1234', 3.3], 'Invalid value given "3.3". Error: not a valid unit. Must be a positive integer.'] + [ + ['1234', -1], + 'Invalid value given "-1". Error: not a valid unit. Must be a positive integer.', + ], + [ + ['1234', 3.3], + 'Invalid value given "3.3". Error: not a valid unit. Must be a positive integer.', + ], ]; export const toWeiInvalidData: [[any, any], string][] = [ @@ -374,6 +386,7 @@ export const toWeiInvalidData: [[any, any], string][] = [ [[{}, 'kwei'], 'value "{}" at "/0" must pass "number" validation'], [['data', 'kwei'], 'value "data" at "/0" must pass "number" validation'], [['1234', 'uwei'], 'Invalid value given "uwei". Error: invalid unit.'], + [['123', -1], 'Invalid value given "-1". Error: not a valid unit. Must be a positive integer.'], ]; export const toCheckSumValidData: [string, string][] = [ ['0x0089d53f703f7e0843953d48133f74ce247184c2', '0x0089d53F703f7E0843953D48133f74cE247184c2'], @@ -397,6 +410,7 @@ export const bytesToUint8ArrayValidData: [Bytes, Uint8Array][] = [ ['0x1234', new Uint8Array([18, 52])], ['0x1234', new Uint8Array([18, 52])], [new Uint8Array(hexToBytes('0c12')), new Uint8Array(hexToBytes('0c12'))], + [[72, 12] as any, new Uint8Array([72, 12])], ]; export const toBigIntValidData: [any, bigint][] = [ diff --git a/packages/web3-utils/test/fixtures/formatter.ts b/packages/web3-utils/test/fixtures/formatter.ts index 7437373d708..5d44b05df2d 100644 --- a/packages/web3-utils/test/fixtures/formatter.ts +++ b/packages/web3-utils/test/fixtures/formatter.ts @@ -55,4 +55,5 @@ export const convertScalarValueValid: [[any, any, any], any][] = [ hexToBytes('0x00000000000000000000000000000000000000000000000000000000000000ff'), ), ], + [[255, 'bytes32', { bytes: 'invalidFormat' }], 255], // return original value when erroring ]; diff --git a/packages/web3-utils/test/fixtures/hash.ts b/packages/web3-utils/test/fixtures/hash.ts index 6bfe9f63f33..1869dd10fec 100644 --- a/packages/web3-utils/test/fixtures/hash.ts +++ b/packages/web3-utils/test/fixtures/hash.ts @@ -256,6 +256,7 @@ export const encodePackData: [TypedObject[] | TypedObjectAbbreviated[], any][] = '0x12480000000000000000000000000000000000000000000000000000000000003c69a194aaf415ba5d6afca734660d0a3d45acdc05d54cd1ca89a8988e7625b4', ], [[{ type: 'bytes4[]', value: ['0x11223344', '0x22334455'] }], '0x1122334422334455'], + [[{ type: '', value: '31323334' }], '0x'], ]; export const encodePackedInvalidData: [any, string][] = [ @@ -285,9 +286,13 @@ export const encodePackedInvalidData: [any, string][] = [ { type: 'bytes32', value: '0x1' }, 'Invalid value given "0x1". Error: can not parse as byte data.', ], + [ + [[{ type: 'string', value: '31323334' }], [{ type: '', value: '31323334' }]], + 'Autodetection of array types is not supported.', + ], ]; -export const keccak256ValidData: [string | Uint8Array | bigint, string][] = [ +export const keccak256ValidData: [string | Uint8Array | bigint | number[], string][] = [ ['my data', '0x8e0c48154711500d6fa119cc31df4dec339091e8b426cf4109a769fe89baad31'], [ new Uint8Array(Buffer.from('my data')), @@ -298,6 +303,8 @@ export const keccak256ValidData: [string | Uint8Array | bigint, string][] = [ '0x2d19cd91fbcc44e6412f92c11da7907cdedb1ace04c47447b42a61f1cd63b85a', ], [BigInt(3), '0x2a80e1ef1d7842f27f2e6be0972bb708b9a135c38860dbe73c27c3486c34f4de'], + [[0x3], '0x69c322e3248a5dfc29d73c5b0553b0185a35cd5bb6386747517ef7e53b15e287'], + [new Uint8Array([0x3]), '0x69c322e3248a5dfc29d73c5b0553b0185a35cd5bb6386747517ef7e53b15e287'], ]; export const elementaryNameValidData: [any, string][] = [ @@ -349,3 +356,8 @@ export const soliditySha3BigIntValidData: [Sha3Input[], string][] = [ return keccak256(abi.encodePacked(int(90071992547409))) ;} */ ]; + +export const getStorageSlotNumForLongStringValidData: [string | number, string | undefined][] = [ + [0, '0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563'], + ['0', '0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563'], +]; diff --git a/packages/web3-utils/test/fixtures/json_rpc.ts b/packages/web3-utils/test/fixtures/json_rpc.ts index 516711abceb..d801fa91587 100644 --- a/packages/web3-utils/test/fixtures/json_rpc.ts +++ b/packages/web3-utils/test/fixtures/json_rpc.ts @@ -18,6 +18,7 @@ import { JsonRpcNotification, SubscriptionParams } from 'web3-types'; const responseWithResult = { jsonrpc: '2.0', id: 1, result: '' }; const responseWithError = { jsonrpc: '2.0', id: 1, error: { code: 1, message: 'string' } }; +const responseWithRpcError = { jsonrpc: '2.0', id: 1, error: { code: -32000, message: 'string' } }; const responseWithSubscription = { id: 1, jsonrpc: '2.0', result: '' }; const responseWithNotfication = { jsonrpc: '2.0', @@ -27,11 +28,13 @@ const responseWithNotfication = { export const isResponseWithResultValidTest: [any, boolean][] = [ [responseWithResult, true], [responseWithError, false], + [{ ...responseWithResult, id: '1' }, true], ]; export const isResponseWithErrorValidTest: [any, boolean][] = [ [responseWithResult, false], [responseWithError, true], + [{ ...responseWithError, id: '1' }, true], ]; export const isResponseWithNotificationValidTest: [JsonRpcNotification, boolean][] = [ @@ -63,4 +66,41 @@ export const toPayloadValidTest: [any, any][] = [ params: undefined, }, ], + [ + { method: 'add', jsonrpc: '1.0', id: 1 }, + { + method: 'add', + id: 1, + jsonrpc: '1.0', + params: undefined, + }, + ], +]; + +export const isResponseRpcErrorValidData: [any, boolean][] = [ + [responseWithRpcError, true], + [responseWithError, false], +]; + +export const isBatchRequestValidData: [any, boolean][] = [ + [ + [ + { + method: 'add', + id: 1, + jsonrpc: '1.0', + params: undefined, + }, + ], + true, + ], + [ + { + method: 'add', + id: 1, + jsonrpc: '1.0', + params: undefined, + }, + false, + ], ]; diff --git a/packages/web3-utils/test/fixtures/string_manipulation.ts b/packages/web3-utils/test/fixtures/string_manipulation.ts index be5b298c803..dde49247677 100644 --- a/packages/web3-utils/test/fixtures/string_manipulation.ts +++ b/packages/web3-utils/test/fixtures/string_manipulation.ts @@ -58,7 +58,7 @@ export const padRightData: [[Numbers, number, string], HexString][] = [ [['15.5', 8, '0'], '15.50000'], ]; -export const toTwosComplementData: [[Numbers, number], HexString][] = [ +export const toTwosComplementData: [[Numbers, number | undefined], HexString][] = [ [[13, 32], '0x0000000000000000000000000000000d'], [[256, 30], '0x000000000000000000000000000100'], [[0, 32], '0x00000000000000000000000000000000'], @@ -69,9 +69,10 @@ export const toTwosComplementData: [[Numbers, number], HexString][] = [ [['13', 32], '0x0000000000000000000000000000000d'], [['-13', 32], '0xfffffffffffffffffffffffffffffff3'], [[-16, 2], '0xf0'], + [['0x1', undefined], '0x0000000000000000000000000000000000000000000000000000000000000001'], ]; -export const fromTwosComplementData: [[Numbers, number], number | bigint][] = [ +export const fromTwosComplementData: [[Numbers, number | undefined], number | bigint][] = [ [['0x0000000000000000000000000000000d', 32], 13], [['0x000000000000000000000000000100', 30], 256], [['0x00000000000000000020000000000000', 32], BigInt('9007199254740992')], @@ -81,6 +82,7 @@ export const fromTwosComplementData: [[Numbers, number], number | bigint][] = [ [[1000, 64], 1000], [[-1000, 64], -1000], [[BigInt(9), 1], -7], + [['0x0000000000000000000000000000000000000000000000000000000000000001', undefined], 1], ]; export const toTwosComplementInvalidData: [[Numbers, number], string][] = [ diff --git a/packages/web3-utils/test/unit/chunk_response_parser.test.ts b/packages/web3-utils/test/unit/chunk_response_parser.test.ts index 22100a3e6a8..bb4c70e8f1b 100644 --- a/packages/web3-utils/test/unit/chunk_response_parser.test.ts +++ b/packages/web3-utils/test/unit/chunk_response_parser.test.ts @@ -71,4 +71,18 @@ describe('chunk_response_parser', () => { }), ); }); + + it('lastChunkTimeout return empty when auto reconnect true', async () => { + const p = new ChunkResponseParser(eventEmiter, true); + // @ts-expect-error set private property + p.chunkTimeout = 10; + const result = p.parseResponse( + '{"jsonrpc":"2.0","id":"96aa3f13-077c-4c82-a64a-64b8626f8192","result":"0x141414141', + ); + const onError = jest.fn(); + eventEmiter.on('error', onError); + // eslint-disable-next-line no-promise-executor-return + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(result).toEqual([]); + }); }); diff --git a/packages/web3-utils/test/unit/converters.test.ts b/packages/web3-utils/test/unit/converters.test.ts index 940590f06e7..84073f4a4d7 100644 --- a/packages/web3-utils/test/unit/converters.test.ts +++ b/packages/web3-utils/test/unit/converters.test.ts @@ -15,6 +15,9 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ +import { InvalidBytesError } from 'web3-errors'; +import { validator, utils } from 'web3-validator'; + import { asciiToHex, bytesToHex, @@ -325,6 +328,16 @@ describe('converters', () => { it.each(toHexValidData)('%s', (input, output) => { expect(toHex(input, true)).toEqual(output[1]); }); + + it('an interesting case that needs investigation', () => { + // TODO: This case is to be investigated further + expect( + toHex( + '101611154195520776335741463917853444671577865378275924493376429267637792638729', + true, + ), + ).toBe('uint'); + }); }); describe('invalid cases', () => { @@ -341,6 +354,14 @@ describe('converters', () => { }); describe('fromWei', () => { + beforeEach(() => { + jest.spyOn(console, 'warn').mockImplementation(() => { + // do nothing + }); + }); + afterAll(() => { + jest.restoreAllMocks(); + }); describe('valid cases', () => { it.each(fromWeiValidData)('%s', (input, output) => { expect(fromWei(input[0], input[1])).toEqual(output); @@ -372,13 +393,14 @@ describe('converters', () => { // do nothing }); }); + afterAll(() => { + jest.restoreAllMocks(); + }); it.each(toWeiValidDataWarnings)('%s', (input, output) => { toWei(input[0], input[1]); - // expect(() => toWei(input[0], input[1])).toThrow(output); - expect(console.warn).toHaveBeenCalledWith(output) + expect(console.warn).toHaveBeenCalledWith(output); }); - - }) + }); }); describe('toChecksumAddress', () => { describe('valid cases', () => { @@ -391,6 +413,33 @@ describe('converters', () => { expect(() => toChecksumAddress(input)).toThrow(output); }); }); + it('should return an empty string if hash is nullish', () => { + const address = '0xc1912fee45d61c87cc5ea59dae31190fffff232d'; + + // mock utils.uint8ArrayToHexString to return an empty string + jest.mock('web3-validator'); + jest.spyOn(utils, 'uint8ArrayToHexString').mockReturnValue( + undefined as unknown as string, + ); + + const result = toChecksumAddress(address); + expect(result).toBe(''); + + jest.mock('web3-validator').restoreAllMocks(); + }); + + it('should return an empty string if hash is equal to "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"', () => { + const address = '0xc1912fee45d61c87cc5ea59dae31190fffff232d'; + + // mock utils.uint8ArrayToHexString to return '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470' + jest.mock('web3-validator'); + const hash = '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'; + jest.spyOn(utils, 'uint8ArrayToHexString').mockReturnValue(hash); + const result = toChecksumAddress(address); + expect(result).toBe(''); + + jest.mock('web3-validator').restoreAllMocks(); + }); }); describe('bytesToUint8Array', () => { describe('bytesToUint8Array', () => { @@ -404,6 +453,18 @@ describe('converters', () => { it.each(bytesToUint8ArrayInvalidData)('%s', (input, output) => { expect(() => bytesToUint8Array(input)).toThrow(output); }); + + it('should throw InvalidBytesError for invalid input even if it passed the validator', () => { + const invalidData = 8; + // the package 'web3-validator' contains `validator`. + // Mock mock the `validator.validate(...)` to not throw an error, but return `false` instead. + jest.mock('web3-validator'); + + jest.spyOn(validator, 'validate').mockReturnValue(undefined); + + expect(() => bytesToUint8Array(invalidData as any)).toThrow(InvalidBytesError); + jest.mock('web3-validator').restoreAllMocks(); + }); }); }); }); diff --git a/packages/web3-utils/test/unit/formatter.test.ts b/packages/web3-utils/test/unit/formatter.test.ts index bed8b55de8e..aa1a14fb6c9 100644 --- a/packages/web3-utils/test/unit/formatter.test.ts +++ b/packages/web3-utils/test/unit/formatter.test.ts @@ -24,9 +24,10 @@ import { HexString, Numbers, } from 'web3-types'; +import { FormatterError } from 'web3-errors'; import { expectTypeOf, typecheck } from '@humeris/espresso-shot'; import { isDataFormatValid, convertScalarValueValid } from '../fixtures/formatter'; -import { format, isDataFormat, convertScalarValue } from '../../src/formatter'; +import { format, isDataFormat, convertScalarValue, convert } from '../../src/formatter'; import { hexToBytes } from '../../src/converters'; type TestTransactionInfoType = { @@ -738,7 +739,6 @@ describe('formatter', () => { ).toEqual(result); }); }); - describe('object values', () => { it('should format simple object', () => { const schema = { @@ -776,6 +776,13 @@ describe('formatter', () => { expect(result).toEqual(expected); }); + it('should throw FormatterError when jsonSchema is invalid', () => { + const invalidSchema1 = {}; + const data = { key: 'value' }; + + expect(() => format(invalidSchema1, data)).toThrow(FormatterError); + }); + it('should format nested objects', () => { const schema = { type: 'object', @@ -846,4 +853,16 @@ describe('formatter', () => { }); }); }); + + describe('convert', () => { + it('should return empty when no properties or items', () => { + const data = { key: 'value' }; + const schema = { + type: 'object', + }; + const f = { number: FMT_NUMBER.NUMBER, bytes: FMT_BYTES.HEX }; + const result = convert(data, schema, [], f, []); + expect(result).toEqual({}); + }); + }); }); diff --git a/packages/web3-utils/test/unit/hash.test.ts b/packages/web3-utils/test/unit/hash.test.ts index d381e3c4240..814dacd45f2 100644 --- a/packages/web3-utils/test/unit/hash.test.ts +++ b/packages/web3-utils/test/unit/hash.test.ts @@ -23,6 +23,7 @@ import { soliditySha3Raw, encodePacked, keccak256 as web3keccak256, + getStorageSlotNumForLongString, } from '../../src/hash'; import { sha3Data, @@ -37,6 +38,7 @@ import { encodePackedInvalidData, keccak256ValidData, soliditySha3BigIntValidData, + getStorageSlotNumForLongStringValidData, } from '../fixtures/hash'; describe('hash', () => { @@ -149,4 +151,10 @@ describe('hash', () => { }); }); }); + + describe('getStorageSlotNumForLongString', () => { + it.each(getStorageSlotNumForLongStringValidData)('%s', (input, output) => { + expect(getStorageSlotNumForLongString(input)).toEqual(output); + }); + }); }); diff --git a/packages/web3-utils/test/unit/index.test.ts b/packages/web3-utils/test/unit/index.test.ts new file mode 100644 index 00000000000..8682408801a --- /dev/null +++ b/packages/web3-utils/test/unit/index.test.ts @@ -0,0 +1,64 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +import * as web3Utils from '../../src'; + +import * as converters from '../../src/converters.js'; +import * as eventEmitter from '../../src/event_emitter.js'; +import * as validation from '../../src/validation.js'; +import * as formatter from '../../src/formatter.js'; +import * as hash from '../../src/hash.js'; +import * as random from '../../src/random.js'; +import * as stringManipulation from '../../src/string_manipulation.js'; +import * as objects from '../../src/objects.js'; +import * as promiseHelpers from '../../src/promise_helpers.js'; +import * as jsonRpc from '../../src/json_rpc.js'; +import * as web3DeferredPromise from '../../src/web3_deferred_promise.js'; +import * as ChunkResponseParser from '../../src/chunk_response_parser.js'; +import * as uuid from '../../src/uuid.js'; +import * as web3Eip1193Provider from '../../src/web3_eip1193_provider.js'; +import * as socketProvider from '../../src/socket_provider.js'; +import * as uint8array from '../../src/uint8array.js'; + +describe('web3-utils exports', () => { + it('should export all modules', () => { + const modules = [ + converters, + eventEmitter, + validation, + formatter, + hash, + random, + stringManipulation, + objects, + promiseHelpers, + jsonRpc, + web3DeferredPromise, + ChunkResponseParser, + uuid, + web3Eip1193Provider, + socketProvider, + uint8array, + ]; + + modules.forEach(module => { + Object.keys(module).forEach((property: string | any[]) => { + expect(web3Utils).toHaveProperty(property); + }); + }); + }); +}); diff --git a/packages/web3-utils/test/unit/json_rpc.test.ts b/packages/web3-utils/test/unit/json_rpc.test.ts index 89261009916..ea16cd646d1 100644 --- a/packages/web3-utils/test/unit/json_rpc.test.ts +++ b/packages/web3-utils/test/unit/json_rpc.test.ts @@ -16,22 +16,28 @@ along with web3.js. If not, see . */ import { + isResponseRpcError, isResponseWithResult, isResponseWithError, isResponseWithNotification, isSubscriptionResult, isValidResponse, isBatchResponse, + setRequestIdStart, + toBatchPayload, + isBatchRequest, toPayload, } from '../../src/json_rpc'; import { isResponseWithResultValidTest, isResponseWithErrorValidTest, + isResponseRpcErrorValidData, isResponseWithNotificationValidTest, isSubscriptionResultValidTest, toPayloadValidTest, isValidResponseValidTest, isBatchResponseValidTest, + isBatchRequestValidData, } from '../fixtures/json_rpc'; describe('json rpc tests', () => { @@ -51,6 +57,14 @@ describe('json rpc tests', () => { }); }); }); + describe('isResponseRpcError', () => { + describe('valid cases', () => { + it.each(isResponseRpcErrorValidData)('%s', (input, output) => { + const result = isResponseRpcError(input); + expect(result).toBe(output); + }); + }); + }); describe('isResponseWithNotification', () => { describe('valid cases', () => { it.each(isResponseWithNotificationValidTest)('should have notify', (input, output) => { @@ -77,18 +91,57 @@ describe('json rpc tests', () => { }); describe('isBatchResponseValid', () => { describe('valid cases', () => { - it.each(isBatchResponseValidTest)('isValidresponse valid test', (input, output) => { - const result = isBatchResponse(input); - expect(result).toBe(output); + it.each(isBatchResponseValidTest)( + 'isBatchResponseValid valid test', + (input, output) => { + const result = isBatchResponse(input); + expect(result).toBe(output); + }, + ); + }); + }); + describe('isBatchRequest', () => { + describe('valid cases', () => { + it.each(isBatchRequestValidData)('isBatchRqeuest valid data', (input, output) => { + expect(isBatchRequest(input)).toBe(output); }); }); }); describe('toPayloadValid', () => { describe('valid cases', () => { - it.each(toPayloadValidTest)('isValidresponse valid test', (input, output) => { - const result = toPayload(input); + beforeEach(() => { + setRequestIdStart(undefined); + }); + it.each(toPayloadValidTest)('toPayload valid test', async (input, output) => { + const result = await new Promise(resolve => { + resolve(toPayload(input)); + }); expect(result).toStrictEqual(output); }); + it('should give payload that has requestid set', async () => { + setRequestIdStart(1); + const result = await new Promise(resolve => { + resolve(toPayload({ method: 'delete' })); + }); + expect(result).toStrictEqual({ + method: 'delete', + id: 2, + params: undefined, + jsonrpc: '2.0', + }); + }); + }); + }); + describe('toBatchPayload', () => { + it('should batch payload', async () => { + setRequestIdStart(0); + const result = await new Promise(resolve => { + resolve(toBatchPayload([{ method: 'delete' }, { method: 'add' }])); + }); + expect(result).toStrictEqual([ + { method: 'delete', id: 1, params: undefined, jsonrpc: '2.0' }, + { method: 'add', id: 2, params: undefined, jsonrpc: '2.0' }, + ]); }); }); }); diff --git a/packages/web3-utils/test/unit/objects.test.ts b/packages/web3-utils/test/unit/objects.test.ts index 1201b3b7345..cca9d46cba9 100644 --- a/packages/web3-utils/test/unit/objects.test.ts +++ b/packages/web3-utils/test/unit/objects.test.ts @@ -79,5 +79,14 @@ describe('objects', () => { expect(result.a).toStrictEqual(new Uint8Array([1, 2])); }); + + it('should return the destination object if it is not iterable', () => { + const destination = 123; // Replace with your desired destination object + const sources: Record[] = []; // Replace with your desired sources array + + const result = mergeDeep(destination as unknown as Record, ...sources); + + expect(result).toBe(destination); + }); }); }); diff --git a/packages/web3-utils/test/unit/promise_helpers.test.ts b/packages/web3-utils/test/unit/promise_helpers.test.ts index 7492c51fc24..14ccc00488d 100644 --- a/packages/web3-utils/test/unit/promise_helpers.test.ts +++ b/packages/web3-utils/test/unit/promise_helpers.test.ts @@ -60,6 +60,14 @@ describe('promise helpers', () => { new Error('time out'), ); }); + it('throws if result is an instance of Error', async () => { + const dummyError = new Error('dummy error'); + const asyncHelper = async () => { + return dummyError; + }; + + await expect(waitWithTimeout(asyncHelper(), 1000)).rejects.toThrow(dummyError); + }); }); describe('rejectIfTimeout', () => { it('%s', async () => { diff --git a/packages/web3-utils/test/unit/socket_provider.test.ts b/packages/web3-utils/test/unit/socket_provider.test.ts index a09f1467ce0..c2c3756c755 100644 --- a/packages/web3-utils/test/unit/socket_provider.test.ts +++ b/packages/web3-utils/test/unit/socket_provider.test.ts @@ -15,7 +15,14 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ -import { Web3APIPayload, EthExecutionAPI, JsonRpcResponse, Web3ProviderStatus } from 'web3-types'; +import { + Web3APIPayload, + EthExecutionAPI, + JsonRpcResponse, + Web3ProviderStatus, + JsonRpcIdentifier, +} from 'web3-types'; +import { MaxAttemptsReachedOnReconnectingError, InvalidClientError } from 'web3-errors'; import { EventEmitter } from '../../src/event_emitter'; // eslint-disable-next-line import/no-relative-packages import { sleep } from '../../../../fixtures/utils'; @@ -40,8 +47,27 @@ class TestProvider extends SocketProvider { // eslint-disable-next-line protected _sendToSocket(_payload: Web3APIPayload): void {} // eslint-disable-next-line - protected _parseResponses(_event: any): JsonRpcResponse[] { - return [] as JsonRpcResponse[]; + protected _parseResponses(_event: { data: string } | undefined): JsonRpcResponse[] { + if (!_event || !_event.data) { + return []; + } + const returnValues: JsonRpcResponse[] = []; + + // DE-CHUNKER + const dechunkedData = _event.data + .replace(/\}[\n\r]?\{/g, '}|--|{') // }{ + .replace(/\}\][\n\r]?\[\{/g, '}]|--|[{') // }][{ + .replace(/\}[\n\r]?\[\{/g, '}|--|[{') // }[{ + .replace(/\}\][\n\r]?\{/g, '}]|--|{') // }]{ + .split('|--|'); + + dechunkedData.forEach((chunkData: string) => { + const result = JSON.parse(chunkData) as unknown as JsonRpcResponse; + + if (result) returnValues.push(result); + }); + + return returnValues; } public message(_event: any): void { this._onMessage(_event); @@ -74,6 +100,32 @@ describe('SocketProvider', () => { expect(provider).toBeInstanceOf(SocketProvider); expect(provider.SocketConnection).toEqual(dummySocketConnection); }); + it('should call _clearQueues when chunkResponseParser emits an error', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + const clearQueuesSpy = jest.spyOn(provider as any, '_clearQueues'); + + try { + // @ts-expect-error access readonly method + provider['chunkResponseParser']['autoReconnect'] = false; + // @ts-expect-error access readonly method + provider['chunkResponseParser']['chunkTimeout'] = 0; + + provider['chunkResponseParser'].parseResponse('invalid-json'); + } catch (error) { + // nothing + } + + // wait 1 second for the timeout to trigger + await sleep(100); + + expect(clearQueuesSpy).toHaveBeenCalled(); + }); + it('should error when failing to _validateProviderPath', () => { + expect(() => { + // eslint-disable-next-line no-new + new TestProvider('', socketOption, { delay: 0 }); + }).toThrow(InvalidClientError); + }); }); describe('testing _reconnect() method', () => { it('should not be called when { autoReconnect: false }', () => { @@ -94,6 +146,111 @@ describe('SocketProvider', () => { // @ts-expect-error run protected method expect(provider._reconnect).not.toHaveBeenCalled(); }); + it('should call _reconnect when isReconnecting is true and an error happens', () => { + const provider = new TestProvider(socketPath, socketOption); + provider['_reconnect'] = jest.fn(); + provider['isReconnecting'] = true; + + provider['_onError']({}); + + expect(provider['_reconnect']).toHaveBeenCalled(); + }); + + it('should call _reconnect when isReconnecting is false and an error happens', () => { + const provider = new TestProvider(socketPath, socketOption); + provider['_reconnect'] = jest.fn(); + provider['isReconnecting'] = false; + + provider['_onError']({}); + expect(provider['_reconnect']).not.toHaveBeenCalled(); + }); + + it('should return if the provider is already isReconnecting', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + // just to run the test faster moke `connect` + jest.spyOn(provider, 'connect'); + + // @ts-expect-error access protected method + expect(provider._reconnectAttempts).toBe(0); + provider['_reconnect'](); + // @ts-expect-error access protected method + expect(provider._reconnectAttempts).toBe(1); + + // after the first call provider.isReconnecting will set to true and so the `_reconnectAttempts` will not be incremented + provider['_reconnect'](); + + // @ts-expect-error access protected method + expect(provider._reconnectAttempts).toBe(1); + }); + + it('should reconnect the socket when the number of reconnect attempts is less than the maximum attempts', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + // @ts-expect-error access protected method + const openSocketConnectionSpy = jest.spyOn(provider, '_openSocketConnection'); + // @ts-expect-error access protected method + const removeSocketListenersSpy = jest.spyOn(provider, '_removeSocketListeners'); + const connectSpy = jest.spyOn(provider, 'connect'); + + // Set the reconnect attempts to less than the maximum attempts + provider['_reconnectAttempts'] = 2; + + provider['_reconnect'](); + + // wait for the timeout to trigger + await sleep(100); + + expect(openSocketConnectionSpy).toHaveBeenCalled(); + expect(removeSocketListenersSpy).toHaveBeenCalled(); + expect(connectSpy).toHaveBeenCalled(); + }); + + it('should clear the queues and emit an error event when the number of reconnect attempts reaches the maximum attempts', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + const clearQueuesSpy = jest.spyOn(provider as any, '_clearQueues'); + // @ts-expect-error access protected method + const removeSocketListenersSpy = jest.spyOn(provider, '_removeSocketListeners'); + const errorEventSpy = jest.spyOn(provider['_eventEmitter'], 'emit'); + + // Set the reconnect attempts to the maximum attempts + provider['_reconnectAttempts'] = 5; + + provider['_reconnect'](); + + // wait for the timeout to trigger + await sleep(100); + + expect(clearQueuesSpy).toHaveBeenCalled(); + expect(removeSocketListenersSpy).toHaveBeenCalled(); + expect(errorEventSpy).toHaveBeenCalledWith( + 'error', + expect.any(MaxAttemptsReachedOnReconnectingError), + ); + }); + + it('should keep pending requests but clear the sent requests queue when reconnecting', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + + provider.setStatus('connected'); + // Add a sent request + provider.request({ id: 2, method: 'some_rpc_method' }).catch(() => { + // it will throw with "Connection not open" because no actual connection is used in the test. So ignore the error + }); + // @ts-expect-error run protected method + expect(provider._sentRequestsQueue.size).toBe(1); + + // @ts-expect-error access protected method + const rejectSpy = jest.spyOn(provider['_pendingRequestsQueue'], 'delete'); + const deleteSpy = jest.spyOn(provider['_sentRequestsQueue'], 'delete'); + + const pendingRequestsQueueSize = provider['_pendingRequestsQueue'].size; + const sentRequestsQueueSize = provider['_sentRequestsQueue'].size; + + provider['_reconnect'](); + + expect(provider['_pendingRequestsQueue'].size).toEqual(pendingRequestsQueueSize); + + expect(deleteSpy).toHaveBeenCalledTimes(sentRequestsQueueSize); + }); }); describe('testing connect() method', () => { @@ -298,6 +455,46 @@ describe('SocketProvider', () => { // @ts-expect-error run protected method expect(provider._sentRequestsQueue.get(payload.id).payload).toBe(payload); }); + + it('should add request to the `_sentRequestsQueue` when the status is `connected` for batch requests', () => { + const provider = new TestProvider(socketPath, socketOption); + const payload = [ + { id: 1, method: 'some_rpc_method', jsonrpc: '2.0' as JsonRpcIdentifier }, + { id: 2, method: 'some_rpc_method', jsonrpc: '2.0' as JsonRpcIdentifier }, + ]; + provider.setStatus('connected'); + const reqPromise = provider.request(payload as any); + expect(reqPromise).toBeInstanceOf(Promise); + + // the id of the first request in the batch is the one used to identify the batch request + // @ts-expect-error run protected method + expect(provider._sentRequestsQueue.get(payload[0].id).payload).toBe(payload); + }); + + it('should clear _sentRequestsQueue in case `_sendToSocket` had an error', async () => { + // Create a mock SocketProvider instance + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + + const deleteSpy = jest.spyOn(provider['_sentRequestsQueue'], 'delete'); + + provider.setStatus('connected'); + // Assert that the _sendToSocket method was called with the correct payload + // @ts-expect-error access protected method + provider._sendToSocket = () => { + throw new Error('any error'); + }; + // Call the request method + provider + .request({ id: 1, method: 'some_rpc_method' }) + .then(() => { + // nothing + }) + .catch(() => { + // nothing + }); + + expect(deleteSpy).toHaveBeenCalled(); + }); }); describe('testing _clearQueues() method', () => { @@ -339,4 +536,330 @@ describe('SocketProvider', () => { }); }); }); + + describe('safeDisconnect', () => { + it('should disconnect the socket when there are no pending or sent requests', async () => { + const provider = new TestProvider(socketPath, socketOption); + const disconnectSpy = jest.spyOn(provider, 'disconnect'); + await provider.safeDisconnect(); + expect(disconnectSpy).toHaveBeenCalled(); + }); + + it('should disconnect the socket after waiting for pending and sent requests to be empty', async () => { + const provider = new TestProvider(socketPath, socketOption); + const disconnectSpy = jest.spyOn(provider, 'disconnect'); + + // Add a pending request + provider.request({ id: 1, method: 'some_rpc_method' }).catch(() => { + // it will throw with "Connection not open" because no actual connection is used in the test. So ignore the error + }); + // Add a sent request + provider.request({ id: 2, method: 'some_rpc_method' }).catch(() => { + // it will throw with "Connection not open" because no actual connection is used in the test. So ignore the error + }); + expect(provider.getPendingRequestQueueSize()).toBe(2); + + provider.clearQueues(); + // Call safeDisconnect and wait for the queues to be empty + await provider.safeDisconnect(undefined, undefined, false, 100); + + expect(disconnectSpy).toHaveBeenCalled(); + expect(provider.getPendingRequestQueueSize()).toBe(0); + expect(provider.getSentRequestsQueueSize()).toBe(0); + }); + + it('should force disconnect the socket after waiting for 5 attempts', async () => { + const provider = new TestProvider(socketPath, socketOption); + const disconnectSpy = jest.spyOn(provider, 'disconnect'); + const clearQueuesSpy = jest.spyOn(provider as any, 'clearQueues'); + + // Add a pending request + provider.request({ id: 1, method: 'some_rpc_method' }).catch(() => { + // it will throw with "Connection not open" because no actual connection is used in the test. So ignore the error + }); + expect(provider.getPendingRequestQueueSize()).toBe(1); + + // Add a sent request + provider.request({ id: 2, method: 'some_rpc_method' }).catch(() => { + // it will throw with "Connection not open" because no actual connection is used in the test. So ignore the error + }); + // expect(provider.getSentRequestsQueueSize()).toBe(1); + + // Call safeDisconnect with forceDisconnect set to true and a small interval + await provider.safeDisconnect(undefined, undefined, true, 100); + + expect(disconnectSpy).toHaveBeenCalled(); + expect(clearQueuesSpy).toHaveBeenCalled(); + }); + }); + describe('removeAllListeners', () => { + it('should remove all listeners for the specified event type', () => { + const provider = new TestProvider(socketPath, socketOption); + const listener1 = jest.fn(); + const listener2 = jest.fn(); + const listener3 = jest.fn(); + provider.on('event', listener1); + provider.on('event', listener2); + provider.on('otherEvent', listener3); + + provider.removeAllListeners('event'); + + provider['_eventEmitter'].emit('event'); + provider['_eventEmitter'].emit('otherEvent'); + + expect(listener1).not.toHaveBeenCalled(); + expect(listener2).not.toHaveBeenCalled(); + expect(listener3).toHaveBeenCalled(); + }); + }); + + describe('_sendPendingRequests', () => { + it('should send pending requests to the socket', () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + + const payload1 = { id: 1, method: 'method1', params: [] }; + const payload2 = { id: 2, method: 'method2', params: [] }; + // Add a pending request + provider.request(payload1).catch(() => { + // it will throw with "Connection not open" because no actual connection is used in the test. So ignore the error + }); + // Add a sent request + provider.request(payload2).catch(() => { + // it will throw with "Connection not open" because no actual connection is used in the test. So ignore the error + }); + expect(provider.getPendingRequestQueueSize()).toBe(2); + + provider['_sendToSocket'] = jest.fn(); + + provider['_sendPendingRequests'](); + + expect(provider['_sendToSocket']).toHaveBeenCalledTimes(2); + expect(provider['_sendToSocket']).toHaveBeenCalledWith(payload1); + expect(provider['_sendToSocket']).toHaveBeenCalledWith(payload2); + expect(provider['_pendingRequestsQueue'].size).toBe(0); + expect(provider['_sentRequestsQueue'].size).toBe(2); + }); + + it('should not send any requests if the pending requests queue is empty', () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + provider['_sendToSocket'] = jest.fn(); + + provider['_sendPendingRequests'](); + + expect(provider['_sendToSocket']).not.toHaveBeenCalled(); + expect(provider['_pendingRequestsQueue'].size).toBe(0); + expect(provider['_sentRequestsQueue'].size).toBe(0); + }); + }); + + describe('_onConnect', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => { + // do nothing + }); // Spy on console.error to suppress and check calls + }); + + afterEach(() => { + jest.restoreAllMocks(); // Restore all mocks after each test + }); + + it('should set the connection status to "connected"', () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + + // Act + provider['_onConnect'](); + + expect(provider['_connectionStatus']).toBe('connected'); + }); + it('should set _accounts and _chainId when _getAccounts and _getChainId resolve', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + jest.spyOn(provider as any, '_getAccounts').mockResolvedValueOnce([123]); + jest.spyOn(provider as any, '_getChainId').mockResolvedValueOnce('1'); + + await new Promise(resolve => { + provider['_onConnect'](); + resolve(''); + }); + expect((provider as any)._chainId).toBe('1'); + expect((provider as any)._accounts).toEqual([123]); + }); + it('chainID should change when connecting twice', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + + await new Promise(resolve => { + jest.spyOn(provider as any, '_getAccounts').mockResolvedValueOnce([123]); + jest.spyOn(provider as any, '_getChainId').mockResolvedValueOnce('1'); + provider['_onConnect'](); + resolve(''); + }); + expect((provider as any)._chainId).toBe('1'); + expect((provider as any)._accounts).toEqual([123]); + + await new Promise(resolve => { + jest.spyOn(provider as any, '_getAccounts').mockResolvedValueOnce([123]); + jest.spyOn(provider as any, '_getChainId').mockResolvedValueOnce('2'); + provider['_onConnect'](); + resolve(''); + }); + expect((provider as any)._chainId).toBe('2'); + expect((provider as any)._accounts).toEqual([123]); + }); + it('should catch errors when _getAccounts and _getChainId throws', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + jest.spyOn(provider as any, '_getChainId').mockRejectedValueOnce(new Error('')); + jest.spyOn(provider as any, '_getAccounts').mockRejectedValueOnce(new Error('')); + jest.spyOn(provider, 'request').mockReturnValue(new Error() as unknown as Promise); + + await new Promise(resolve => { + provider['_onConnect'](); + resolve(''); + }); + expect((provider as any)._chainId).toBe(''); + expect((provider as any)._accounts).toEqual([]); + }); + it('should catch when connect emit fails', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + jest.spyOn(provider as any, '_getChainId').mockResolvedValueOnce(1); + jest.spyOn(provider as any, '_getAccounts').mockResolvedValueOnce([]); + (provider as any)._eventEmitter.emit = jest.fn(() => { + throw new Error('event emitter failed'); + }); + + await new Promise(resolve => { + provider['_onConnect'](); + resolve(''); + }); + // I would check if console.error is called, but facing a race condition + expect((provider as any)._eventEmitter.emit).toHaveBeenCalledTimes(1); + }); + }); + + describe('_getChainId', () => { + it('should return data from the chainId method', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + const chainId = 1; + jest.spyOn(provider as any, 'request').mockResolvedValueOnce({ result: chainId }); + const result = await provider['_getChainId'](); + expect(result).toBe(chainId); + }); + + it('should be returning undefined from the chainId method', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + jest.spyOn(provider as any, 'request').mockResolvedValueOnce({ result: undefined }); + const result = await provider['_getChainId'](); + expect(result).toBe(''); + }); + + it('should return empty from the chainId method', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + jest.spyOn(provider as any, 'request').mockResolvedValueOnce(undefined); + const result = await provider['_getChainId'](); + expect(result).toBe(''); + }); + }); + + describe('_getAccounts', () => { + it('should return data from the _getAccounts method', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + const accounts = [1]; + jest.spyOn(provider as any, 'request').mockResolvedValueOnce({ result: accounts }); + const result = await provider['_getAccounts'](); + expect(result).toBe(accounts); + }); + + it('should returning undefined from the _getAccounts method', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + jest.spyOn(provider as any, 'request').mockResolvedValueOnce({ result: undefined }); + const result = await provider['_getAccounts'](); + expect(result).toEqual([]); + }); + + it('should return empty from the _getAccounts method', async () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + jest.spyOn(provider as any, 'request').mockResolvedValueOnce(undefined); + const result = await provider['_getAccounts'](); + expect(result).toEqual([]); + }); + }); + + describe('_onMessage', () => { + it('should resolve the deferred promise for valid responses with errors', () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + + const payload1 = { + id: 1, + method: 'method1', + params: [], + jsonrpc: '2.0' as JsonRpcIdentifier, + error: { code: -32601, message: 'Method not found' }, + }; + const event = { + data: JSON.stringify(payload1), + }; + // Add a pending request + provider.request(payload1).catch(() => { + // it will throw with "Connection not open" because no actual connection is used in the test. So ignore the error + }); + expect(provider.getPendingRequestQueueSize()).toBe(1); + + // @ts-expect-error access protected method + provider['_sentRequestsQueue'] = provider['_pendingRequestsQueue']; + + const deferredPromiseResolveSpy = jest.spyOn( + provider['_sentRequestsQueue'].get(1)!.deferredPromise, + 'resolve', + ); + provider['_onMessage']({ + ...event, + }); + + expect(deferredPromiseResolveSpy).toHaveBeenCalledWith(payload1); + }); + + it('should not emit "message" event for invalid responses', () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + const event = { + data: JSON.stringify([ + { id: 1, jsonrpc: '2.0', error: { code: -32601, message: 'Method not found' } }, + { id: 2, jsonrpc: '2.0', error: { code: -32601, message: 'Method not found' } }, + ]), + }; + + const eventEmitterSpy = jest.spyOn(provider['_eventEmitter'], 'emit'); + + provider['_onMessage'](event); + + expect(eventEmitterSpy).not.toHaveBeenCalledWith('message', { + id: 1, + jsonrpc: '2.0', + error: { code: -32601, message: 'Method not found' }, + }); + expect(eventEmitterSpy).not.toHaveBeenCalledWith('message', { + id: 2, + jsonrpc: '2.0', + error: { code: -32601, message: 'Method not found' }, + }); + }); + + it('should emit "message" event for notifications', () => { + const provider = new TestProvider(socketPath, socketOption, { delay: 0 }); + const event = { + data: JSON.stringify({ + jsonrpc: '2.0', + method: 'notification_1_subscription', + params: {}, + }), + }; + + const eventEmitterSpy = jest.spyOn(provider['_eventEmitter'], 'emit'); + + provider['_onMessage'](event); + + expect(eventEmitterSpy).toHaveBeenCalledWith('message', { + jsonrpc: '2.0', + method: 'notification_1_subscription', + params: {}, + }); + }); + }); }); diff --git a/packages/web3-utils/test/unit/web3_deferred_promise.test.ts b/packages/web3-utils/test/unit/web3_deferred_promise.test.ts index b8180705969..6fe87129e73 100644 --- a/packages/web3-utils/test/unit/web3_deferred_promise.test.ts +++ b/packages/web3-utils/test/unit/web3_deferred_promise.test.ts @@ -53,4 +53,17 @@ describe('Web3DeferredPromise', () => { expect(promise.state).toBe('rejected'); }); }); + + describe('Web3DeferredPromise finally', () => { + it('should execute the callback when the promise is settled', async () => { + const promise = new Web3DeferredPromise(); + let callbackExecuted = false; + promise.resolve(1); + await promise.finally(() => { + callbackExecuted = true; + }); + + expect(callbackExecuted).toBe(true); + }); + }); });