diff --git a/packages/web3-eth/src/utils/watch_transaction_by_polling.ts b/packages/web3-eth/src/utils/watch_transaction_by_polling.ts index 34e01581422..a429eabb818 100644 --- a/packages/web3-eth/src/utils/watch_transaction_by_polling.ts +++ b/packages/web3-eth/src/utils/watch_transaction_by_polling.ts @@ -56,8 +56,10 @@ export const watchTransactionByPolling = < let confirmations = 1; const intervalId = setInterval(() => { (async () => { - if (confirmations >= web3Context.transactionConfirmationBlocks) + if (confirmations >= web3Context.transactionConfirmationBlocks){ clearInterval(intervalId); + return; + } const nextBlock = await ethRpcMethods.getBlockByNumber( web3Context.requestManager, diff --git a/packages/web3-eth/test/unit/utils/watch_transaction_by_subscription.test.ts b/packages/web3-eth/test/unit/utils/watch_transaction_by_subscription.test.ts index d7f4169aeaf..23c92c793a6 100644 --- a/packages/web3-eth/test/unit/utils/watch_transaction_by_subscription.test.ts +++ b/packages/web3-eth/test/unit/utils/watch_transaction_by_subscription.test.ts @@ -14,117 +14,124 @@ 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, Web3RequestManager } from 'web3-core'; -import { format } from 'web3-utils'; -import { DEFAULT_RETURN_FORMAT, JsonRpcResponseWithResult, Web3EthExecutionAPI } from 'web3-types'; -import { ethRpcMethods } from 'web3-rpc-methods'; -import { WebSocketProvider } from 'web3-providers-ws'; + +import { Web3Context } from 'web3-core'; +import { DEFAULT_RETURN_FORMAT, Web3EthExecutionAPI } from 'web3-types'; +import WebSocketProvider from 'web3-providers-ws'; + import * as rpcMethodWrappers from '../../../src/rpc_method_wrappers'; -import * as WatchTransactionBySubscription from '../../../src/utils/watch_transaction_by_subscription'; import { expectedTransactionReceipt, expectedTransactionHash, testData, } from '../rpc_method_wrappers/fixtures/send_signed_transaction'; -import { transactionReceiptSchema } from '../../../src/schemas'; -import { registeredSubscriptions } from '../../../src'; +import { blockMockResult } from '../../fixtures/transactions_data'; + -jest.mock('web3-rpc-methods'); jest.mock('web3-providers-ws'); -jest.mock('../../../src/utils/watch_transaction_by_polling'); const testMessage = 'Title: %s\ninputSignedTransaction: %s\nexpectedTransactionHash: %s\nexpectedTransactionReceipt: %s\n'; -async function waitUntilCalled(mock: jest.Mock, timeout = 1000): Promise { - return new Promise((resolve, reject) => { - let timeoutId: NodeJS.Timeout | undefined; - const intervalId = setInterval(() => { - if (mock.mock.calls.length > 0) { - clearInterval(intervalId); - if (timeoutId) { - clearTimeout(timeoutId); - } - resolve(mock); - } - }, 100); - timeoutId = setTimeout(() => { - clearInterval(intervalId); - if (timeoutId) { - clearTimeout(timeoutId); - } - reject(new Error('timeout')); - }, timeout); - }); -} describe('watchTransactionBySubscription', () => { + const CONFIRMATION_BLOCKS = 5; describe('should revert to polling in cases where getting by subscription did not workout', () => { let web3Context: Web3Context; beforeEach(() => { - jest.spyOn(Web3RequestManager.prototype, 'send').mockImplementation(async () => { - return {} as Promise; - }); - jest.spyOn(WebSocketProvider.prototype, 'request').mockImplementation(async () => { - return {} as Promise>; - }); - - (ethRpcMethods.sendRawTransaction as jest.Mock).mockResolvedValue( - expectedTransactionHash, - ); - (ethRpcMethods.getTransactionReceipt as jest.Mock).mockResolvedValue( - expectedTransactionHash, - ); web3Context = new Web3Context({ - // dummy provider that does supports subscription - provider: new WebSocketProvider('ws://localhost:8546'), - registeredSubscriptions, - }); + provider: new WebSocketProvider('wss://localhost:8546'),} + ); + (web3Context.provider as any).supportsSubscriptions = () => true; + web3Context.transactionConfirmationBlocks = CONFIRMATION_BLOCKS; + web3Context.enableExperimentalFeatures.useSubscriptionWhenCheckingBlockTimeout = + true; + }); - afterEach(() => { - // to clear the interval inside the subscription function: - web3Context.transactionConfirmationBlocks = 0; - }); - let counter = 0; - it.each(testData)( - `should call getBlockNumber if blockHeaderTimeout reached\n ${testMessage}`, - async (_, inputTransaction) => { - if (counter > 0) { - return; - } - counter += 1; - const formattedTransactionReceipt = format( - transactionReceiptSchema, - expectedTransactionReceipt, - DEFAULT_RETURN_FORMAT, - ); - web3Context.enableExperimentalFeatures.useSubscriptionWhenCheckingBlockTimeout = - true; - // this will case the function to revert to polling: - web3Context.blockHeaderTimeout = 0; + it.each(testData)( + `should call getBlockByNumber if blockHeaderTimeout reached\n ${testMessage}`, + async (_, inputTransaction,) => { - web3Context.transactionSendTimeout = 2; + let blockNum = 100; + let ethGetBlockByNumberCount = 0; + web3Context.requestManager.send = jest.fn(async (request) => { + + if (request.method === 'eth_getBlockByNumber') { + ethGetBlockByNumberCount += 1; + return Promise.resolve( + { ...blockMockResult.result, + number: (request as any).params[0] + }); + } + if (request.method === 'eth_call') { + + return Promise.resolve("0x"); + } + if (request.method === 'eth_blockNumber') { + blockNum += 1; + return Promise.resolve(blockNum.toString(16)); + } + if (request.method === 'eth_sendRawTransaction') { + return Promise.resolve(expectedTransactionHash); + } + if (request.method === 'eth_getTransactionReceipt') { + return Promise.resolve(expectedTransactionReceipt); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return Promise.reject(new Error("Unknown Request")) as any; + }); const promiEvent = rpcMethodWrappers.sendSignedTransaction( web3Context, inputTransaction, DEFAULT_RETURN_FORMAT, ); - // await promiEvent; - WatchTransactionBySubscription.watchTransactionBySubscription({ - web3Context, - transactionReceipt: formattedTransactionReceipt, - transactionPromiEvent: promiEvent, - returnFormat: DEFAULT_RETURN_FORMAT, + + let confirmationsCount = 0; + const confirmationPromise = new Promise((resolve, reject) => { + + const handleConfirmation = (confirmation: { confirmations: bigint }) => { + confirmationsCount += 1; + + if (confirmation.confirmations >= CONFIRMATION_BLOCKS) { + resolve(); + } + }; + + const handleError = (_error: any) => { + reject(); + }; + + promiEvent + .on('confirmation', handleConfirmation) + .on('error', handleError) + .then((res) => { + // eslint-disable-next-line jest/no-conditional-expect + expect(res).toBeDefined(); + }) + .catch(reject); + }); + + // Wait for the confirmationPromise to resolve or timeout after 5 seconds + let timeoutId; + const timeout = new Promise((_res, reject) => { + timeoutId = setTimeout(() => reject(new Error('Timeout waiting for confirmations')), 500000); }); - await waitUntilCalled(ethRpcMethods.getBlockNumber as jest.Mock, 5000); - await promiEvent; - }, - 60000, + await Promise.race([confirmationPromise, timeout]); + + clearTimeout(timeoutId); + + expect(confirmationsCount).toBe(CONFIRMATION_BLOCKS); + expect(ethGetBlockByNumberCount).toBe(CONFIRMATION_BLOCKS - 1); // means polling called getblock 4 times as first confirmation is receipt it self + + } ); + + }); });