From 80986bb998175ef367b3dc4fd7fd4f6aef8a9d18 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 4 Oct 2023 10:14:08 -0400 Subject: [PATCH 1/3] fix stale (#6473) --- .github/workflows/stale.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 80e37cfbbcd..d89cbdb6622 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -15,5 +15,5 @@ jobs: days-before-stale: 60 days-before-close: 14 operations-per-run: 100 - exempt-pr-labels: 'work-in-progress','4.x' - exempt-issue-labels: 'work-in-progress','4.x' + exempt-pr-labels: 'work-in-progress,4.x' + exempt-issue-labels: 'work-in-progress,4.x' From 0e782357ff3001de8aa7491bb95e1334d32dbba9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 15:56:55 +0200 Subject: [PATCH 2/3] Bump zod from 3.21.4 to 3.22.3 (#6477) Bumps [zod](https://github.com/colinhacks/zod) from 3.21.4 to 3.22.3. - [Release notes](https://github.com/colinhacks/zod/releases) - [Changelog](https://github.com/colinhacks/zod/blob/master/CHANGELOG.md) - [Commits](https://github.com/colinhacks/zod/compare/v3.21.4...v3.22.3) --- updated-dependencies: - dependency-name: zod dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index f937f1f8418..d54a0a8f3e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4021,13 +4021,20 @@ cross-fetch@^2.1.0: node-fetch "^2.6.7" whatwg-fetch "^2.0.4" -cross-fetch@^3.0.4, cross-fetch@^3.1.5: +cross-fetch@^3.0.4: version "3.1.5" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== dependencies: node-fetch "2.6.7" +cross-fetch@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" + integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== + dependencies: + node-fetch "^2.6.12" + cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -8593,6 +8600,13 @@ node-fetch@2.6.7, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.12: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-gyp-build@4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.4.0.tgz#42e99687ce87ddeaf3a10b99dc06abc11021f3f4" @@ -11986,6 +12000,6 @@ yocto-queue@^0.1.0: integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== zod@^3.21.4: - version "3.21.4" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" - integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== + version "3.22.3" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.3.tgz#2fbc96118b174290d94e8896371c95629e87a060" + integrity sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug== From 6d99cd02b8b118721f21c9504097e2757700bbdf Mon Sep 17 00:00:00 2001 From: Junaid <86780488+jdevcs@users.noreply.github.com> Date: Thu, 5 Oct 2023 16:07:52 +0200 Subject: [PATCH 3/3] waitForTransactionReceipt fix (#6464) * added pollTillDefinedAndReturnIntervalId * waitForTransactionReceipt updated interval calls * tests pollTillDefinedAndReturnIntervalId * reusing func for lib size optmz * lint * test fix * waitWithTimeout * unit test * formatting * lint fix * waitForTransactionReceipt unit tests --- .../src/utils/wait_for_transaction_receipt.ts | 10 +- .../defaults.transactionBlockTimeout.test.ts | 22 ++- .../wait_for_transaction_receipt.test.ts | 126 ++++++++++++++++++ packages/web3-utils/src/promise_helpers.ts | 39 +++--- .../test/unit/promise_helpers.test.ts | 63 +++++++++ 5 files changed, 229 insertions(+), 31 deletions(-) create mode 100644 packages/web3-eth/test/unit/utils/wait_for_transaction_receipt.test.ts diff --git a/packages/web3-eth/src/utils/wait_for_transaction_receipt.ts b/packages/web3-eth/src/utils/wait_for_transaction_receipt.ts index 54ff06f588b..d335683391e 100644 --- a/packages/web3-eth/src/utils/wait_for_transaction_receipt.ts +++ b/packages/web3-eth/src/utils/wait_for_transaction_receipt.ts @@ -20,7 +20,7 @@ import { TransactionPollingTimeoutError } from 'web3-errors'; import { EthExecutionAPI, Bytes, TransactionReceipt, DataFormat } from 'web3-types'; // eslint-disable-next-line import/no-cycle -import { pollTillDefined, rejectIfTimeout } from 'web3-utils'; +import { pollTillDefinedAndReturnIntervalId, rejectIfTimeout } from 'web3-utils'; // eslint-disable-next-line import/no-cycle import { rejectIfBlockTimeout } from './reject_if_block_timeout.js'; // eslint-disable-next-line import/no-cycle @@ -31,10 +31,11 @@ export async function waitForTransactionReceipt transactionHash: Bytes, returnFormat: ReturnFormat, ): Promise { + const pollingInterval = web3Context.transactionReceiptPollingInterval ?? web3Context.transactionPollingInterval; - const awaitableTransactionReceipt: Promise = pollTillDefined(async () => { + const [awaitableTransactionReceipt, IntervalId] = pollTillDefinedAndReturnIntervalId(async () => { try { return getTransactionReceipt(web3Context, transactionHash, returnFormat); } catch (error) { @@ -64,7 +65,10 @@ export async function waitForTransactionReceipt rejectOnBlockTimeout, // this will throw an error on Transaction Block Timeout ]); } finally { - clearTimeout(timeoutId); + if(timeoutId) + clearTimeout(timeoutId); + if(IntervalId) + clearInterval(IntervalId); blockTimeoutResourceCleaner.clean(); } } diff --git a/packages/web3-eth/test/integration/defaults.transactionBlockTimeout.test.ts b/packages/web3-eth/test/integration/defaults.transactionBlockTimeout.test.ts index b366b89d537..d547c2bc551 100644 --- a/packages/web3-eth/test/integration/defaults.transactionBlockTimeout.test.ts +++ b/packages/web3-eth/test/integration/defaults.transactionBlockTimeout.test.ts @@ -45,8 +45,6 @@ describe('defaults', () => { beforeEach(() => { clientUrl = getSystemTestProvider(); web3 = new Web3(clientUrl); - // Make the test run faster by casing the polling to start after 2 blocks - web3.eth.transactionBlockTimeout = 2; // Increase other timeouts so only `transactionBlockTimeout` would be reached web3.eth.transactionSendTimeout = MAX_32_SIGNED_INTEGER; @@ -64,6 +62,7 @@ describe('defaults', () => { account1 = await createLocalAccount(web3); account2 = await createLocalAccount(web3); // Setting a high `nonce` when sending a transaction, to cause the RPC call to stuck at the Node + const sentTx: Web3PromiEvent< TransactionReceipt, SendTransactionEvents @@ -81,18 +80,13 @@ describe('defaults', () => { // So, send 2 transactions, one after another, because in this test `transactionBlockTimeout = 2`. // eslint-disable-next-line no-void await sendFewSampleTxs(2); + + web3.eth.transactionBlockTimeout = 2; + + await expect(sentTx).rejects.toThrow(/was not mined within [0-9]+ blocks/); + + await expect(sentTx).rejects.toThrow(TransactionBlockTimeoutError); - try { - await sentTx; - throw new Error( - 'The test should fail if there is no exception when sending a transaction that could not be mined within transactionBlockTimeout', - ); - } catch (error) { - // eslint-disable-next-line jest/no-conditional-expect - expect(error).toBeInstanceOf(TransactionBlockTimeoutError); - // eslint-disable-next-line jest/no-conditional-expect - expect((error as Error).message).toMatch(/was not mined within [0-9]+ blocks/); - } await closeOpenConnection(web3.eth); }); @@ -128,6 +122,8 @@ describe('defaults', () => { // eslint-disable-next-line no-void, @typescript-eslint/no-unsafe-call void sendFewSampleTxs(2); + web3.eth.transactionBlockTimeout = 2; + await expect(sentTx).rejects.toThrow(/was not mined within [0-9]+ blocks/); await expect(sentTx).rejects.toThrow(TransactionBlockTimeoutError); diff --git a/packages/web3-eth/test/unit/utils/wait_for_transaction_receipt.test.ts b/packages/web3-eth/test/unit/utils/wait_for_transaction_receipt.test.ts new file mode 100644 index 00000000000..8b70dcdd3b2 --- /dev/null +++ b/packages/web3-eth/test/unit/utils/wait_for_transaction_receipt.test.ts @@ -0,0 +1,126 @@ +/* +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 { Web3Context } from 'web3-core'; +import { DEFAULT_RETURN_FORMAT, Web3EthExecutionAPI } from 'web3-types'; +import { TransactionBlockTimeoutError } from 'web3-errors'; +import { waitForTransactionReceipt } from '../../../src/utils/wait_for_transaction_receipt'; + +describe('waitForTransactionReceipt unit test', () => { + let web3Context: Web3Context; + + it(`waitForTransactionReceipt should throw error after block timeout`, async () => { + let blockNum = 1; + + web3Context = new Web3Context( + { + request: async (payload: any) => { + let response: { jsonrpc: string; id: any; result: string } | undefined; + + switch (payload.method) { + case 'eth_blockNumber': + blockNum += 50; + response = { + jsonrpc: '2.0', + id: payload.id, + result: `0x${blockNum.toString(16)}`, + }; + break; + + case 'eth_getTransactionReceipt': + response = undefined; + break; + + default: + throw new Error(`Unknown payload ${payload}`); + } + + return new Promise(resolve => { + resolve(response as any); + }); + }, + supportsSubscriptions: () => false, + }, + ); + + await expect(async () => + waitForTransactionReceipt( + web3Context, + '0x0430b701e657e634a9d5480eae0387a473913ef29af8e60c38a3cee24494ed54', + DEFAULT_RETURN_FORMAT + ) + ).rejects.toThrow(TransactionBlockTimeoutError); + + }); + + it(`waitForTransactionReceipt should resolve immediatly if receipt is avalible`, async () => { + let blockNum = 1; + const txHash = '0x85d995eba9763907fdf35cd2034144dd9d53ce32cbec21349d4b12823c6860c5'; + const blockHash = '0xa957d47df264a31badc3ae823e10ac1d444b098d9b73d204c40426e57f47e8c3'; + + web3Context = new Web3Context( + { + request: async (payload: any) => { + const response = { + jsonrpc: '2.0', + id: payload.id, + result: {}, + }; + + switch (payload.method) { + case 'eth_blockNumber': + blockNum += 10; + response.result = `0x${blockNum.toString(16)}`; + break; + + case 'eth_getTransactionReceipt': + response.result = { + blockHash, + blockNumber: `0x1`, + cumulativeGasUsed: '0xa12515', + from: payload.from, + gasUsed: payload.gasLimit, + status: '0x1', + to: payload.to, + transactionHash: txHash, + transactionIndex: '0x66', + + }; + break; + + default: + throw new Error(`Unknown payload ${payload}`); + } + + return new Promise(resolve => { + resolve(response as any); + }); + }, + supportsSubscriptions: () => false, + }, + ); + + const res = await waitForTransactionReceipt( + web3Context, + '0x0430b701e657e634a9d5480eae0387a473913ef29af8e60c38a3cee24494ed54', + DEFAULT_RETURN_FORMAT + ); + + expect(res).toBeDefined(); + expect(res.transactionHash).toStrictEqual(txHash); + expect(res.blockHash).toStrictEqual(blockHash); + }); +}) \ No newline at end of file diff --git a/packages/web3-utils/src/promise_helpers.ts b/packages/web3-utils/src/promise_helpers.ts index c968ce247e9..c8e211fbbb7 100644 --- a/packages/web3-utils/src/promise_helpers.ts +++ b/packages/web3-utils/src/promise_helpers.ts @@ -73,21 +73,22 @@ 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. + * or until a timeout is reached. It returns promise and intervalId. * @param func - The function to call. * @param interval - The interval in milliseconds. */ -export async function pollTillDefined( +export function pollTillDefinedAndReturnIntervalId( func: AsyncFunction, interval: number, -): Promise> { - const awaitableRes = waitWithTimeout(func, interval); +): [Promise>, Timer] { let intervalId: Timer | undefined; const polledRes = new Promise>((resolve, reject) => { - intervalId = setInterval(() => { + intervalId = setInterval(function intervalCallbackFunc(){ (async () => { try { const res = await waitWithTimeout(func, interval); @@ -101,19 +102,26 @@ export async function pollTillDefined( reject(error); } })() as unknown; - }, interval); + return intervalCallbackFunc;}() // this will immediate invoke first call + , interval); }); - // If the first call to awaitableRes succeeded, return the result - const res = await awaitableRes; - if (!isNullish(res)) { - if (intervalId) { - clearInterval(intervalId); - } - return res as unknown as Exclude; - } + return [polledRes as unknown as Promise>, intervalId!]; +} - return polledRes; +/** + * 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(). + * @param func - The function to call. + * @param interval - The interval in milliseconds. + */ +export async function pollTillDefined( + func: AsyncFunction, + interval: number, +): Promise> { + return pollTillDefinedAndReturnIntervalId(func, interval)[0]; } /** * Enforce a timeout on a promise, so that it can be rejected if it takes too long to complete @@ -160,3 +168,4 @@ export function rejectIfConditionAtInterval( }); return [intervalId!, rejectIfCondition]; } + diff --git a/packages/web3-utils/test/unit/promise_helpers.test.ts b/packages/web3-utils/test/unit/promise_helpers.test.ts index ce6eae9f481..b18f4f1a5b0 100644 --- a/packages/web3-utils/test/unit/promise_helpers.test.ts +++ b/packages/web3-utils/test/unit/promise_helpers.test.ts @@ -21,6 +21,7 @@ import { isPromise, pollTillDefined, rejectIfConditionAtInterval, + pollTillDefinedAndReturnIntervalId, } from '../../src/promise_helpers'; describe('promise helpers', () => { @@ -121,6 +122,68 @@ describe('promise helpers', () => { }); }); + describe('pollTillDefinedAndReturnIntervalId', () => { + it('returns when immediately resolved', async () => { + const asyncHelper = async () => + new Promise(resolve => { + resolve('resolved'); + }); + const [promise] = pollTillDefinedAndReturnIntervalId(asyncHelper, 100); + await expect(promise).resolves.toBe('resolved'); + }); + it('returns if later resolved', async () => { + let counter = 0; + const asyncHelper = async () => { + if (counter === 0) { + counter += 1; + return undefined; + } + return new Promise(resolve => { + resolve('resolved'); + }); + }; + const [promise] = pollTillDefinedAndReturnIntervalId(asyncHelper, 100); + await expect(promise).resolves.toBe('resolved'); + }); + + it('should return interval id if not resolved in specific time', async () => { + + let counter = 0; + const asyncHelper = async () => { + if (counter <= 3000000) { + counter += 1; + return undefined; + } + return "result"; + }; + + const testError = new Error('Test P2 Error'); + + const [neverResolvePromise, intervalId] = pollTillDefinedAndReturnIntervalId(asyncHelper, 100); + const promiCheck = Promise.race([neverResolvePromise, rejectIfTimeout(500,testError)[1]]); + + await expect(promiCheck).rejects.toThrow(testError); + expect(intervalId).toBeDefined(); + clearInterval(intervalId); + }); + + it('throws if later throws', async () => { + const dummyError = new Error('error'); + let counter = 0; + const asyncHelper = async () => { + if (counter === 0) { + counter += 1; + return undefined; + } + return new Promise((_, reject) => { + reject(dummyError); + }); + }; + const [promise] = pollTillDefinedAndReturnIntervalId(asyncHelper, 100); + await expect(promise).rejects.toThrow(dummyError); + }); + }); + describe('rejectIfConditionAtInterval', () => { it('reject if later throws', async () => { const dummyError = new Error('error');