UNPKG

web3-eth

Version:

Web3 module to interact with the Ethereum blockchain and smart contracts.

179 lines (163 loc) 6.31 kB
/* 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 <http://www.gnu.org/licenses/>. */ import { EthExecutionAPI, Bytes, Web3BaseProvider, BlockHeaderOutput } from 'web3-types'; import { Web3Context } from 'web3-core'; import { rejectIfConditionAtInterval } from 'web3-utils'; import { TransactionBlockTimeoutError } from 'web3-errors'; import { NUMBER_DATA_FORMAT } from '../constants.js'; // eslint-disable-next-line import/no-cycle import { getBlockNumber } from '../rpc_method_wrappers.js'; import { NewHeadsSubscription } from '../web3_subscriptions.js'; export interface ResourceCleaner { clean: () => void; } function resolveByPolling( web3Context: Web3Context<EthExecutionAPI>, starterBlockNumber: number, transactionHash?: Bytes, ): [Promise<never>, ResourceCleaner] { const pollingInterval = web3Context.transactionPollingInterval; const [intervalId, promiseToError] = rejectIfConditionAtInterval(async () => { let lastBlockNumber; try { lastBlockNumber = await getBlockNumber(web3Context, NUMBER_DATA_FORMAT); } catch (error) { console.warn('An error happen while trying to get the block number', error); return undefined; } const numberOfBlocks = lastBlockNumber - starterBlockNumber; if (numberOfBlocks >= web3Context.transactionBlockTimeout) { return new TransactionBlockTimeoutError({ starterBlockNumber, numberOfBlocks, transactionHash, }); } return undefined; }, pollingInterval); const clean = () => { clearInterval(intervalId); }; return [promiseToError, { clean }]; } async function resolveBySubscription( web3Context: Web3Context<EthExecutionAPI>, starterBlockNumber: number, transactionHash?: Bytes, ): Promise<[Promise<never>, ResourceCleaner]> { // The following variable will stay true except if the data arrived, // or if watching started after an error had occurred. let needToWatchLater = true; let subscription: NewHeadsSubscription; let resourceCleaner: ResourceCleaner; // internal helper function function revertToPolling( reject: (value: Error | PromiseLike<Error>) => void, previousError?: Error, ) { if (previousError) { console.warn('error happened at subscription. So revert to polling...', previousError); } resourceCleaner.clean(); needToWatchLater = false; const [promiseToError, newResourceCleaner] = resolveByPolling( web3Context, starterBlockNumber, transactionHash, ); resourceCleaner.clean = newResourceCleaner.clean; promiseToError.catch(error => reject(error as Error)); } try { subscription = (await web3Context.subscriptionManager?.subscribe( 'newHeads', )) as unknown as NewHeadsSubscription; resourceCleaner = { clean: () => { // Remove the subscription, if it was not removed somewhere // else by calling, for example, subscriptionManager.clear() if (subscription.id) { web3Context.subscriptionManager ?.removeSubscription(subscription) .then(() => { // Subscription ended successfully }) .catch(() => { // An error happened while ending subscription. But no need to take any action. }); } }, }; } catch (error) { return resolveByPolling(web3Context, starterBlockNumber, transactionHash); } const promiseToError: Promise<never> = new Promise((_, reject) => { try { subscription.on('data', (lastBlockHeader: BlockHeaderOutput) => { needToWatchLater = false; if (!lastBlockHeader?.number) { return; } const numberOfBlocks = Number( BigInt(lastBlockHeader.number) - BigInt(starterBlockNumber), ); if (numberOfBlocks >= web3Context.transactionBlockTimeout) { // Transaction Block Timeout is known to be reached by subscribing to new heads reject( new TransactionBlockTimeoutError({ starterBlockNumber, numberOfBlocks, transactionHash, }), ); } }); subscription.on('error', error => { revertToPolling(reject, error); }); } catch (error) { revertToPolling(reject, error as Error); } // Fallback to polling if tx receipt didn't arrived in "blockHeaderTimeout" [10 seconds] setTimeout(() => { if (needToWatchLater) { revertToPolling(reject); } }, web3Context.blockHeaderTimeout * 1000); }); return [promiseToError, resourceCleaner]; } /* TODO: After merge, there will be constant block mining time (exactly 12 second each block, except slot missed that currently happens in <1% of slots. ) so we can optimize following function for POS NWs, we can skip checking getBlockNumber(); after interval and calculate only based on time that certain num of blocked are mined after that for internal double check, can do one getBlockNumber() call and timeout. */ export async function rejectIfBlockTimeout( web3Context: Web3Context<EthExecutionAPI>, transactionHash?: Bytes, ): Promise<[Promise<never>, ResourceCleaner]> { const { provider } = web3Context.requestManager; let callingRes: [Promise<never>, ResourceCleaner]; const starterBlockNumber = await getBlockNumber(web3Context, NUMBER_DATA_FORMAT); // TODO: once https://github.com/web3/web3.js/issues/5521 is implemented, remove checking for `enableExperimentalFeatures.useSubscriptionWhenCheckingBlockTimeout` if ( (provider as Web3BaseProvider).supportsSubscriptions?.() && web3Context.enableExperimentalFeatures.useSubscriptionWhenCheckingBlockTimeout ) { // eslint-disable-next-line @typescript-eslint/no-floating-promises callingRes = await resolveBySubscription(web3Context, starterBlockNumber, transactionHash); } else { // eslint-disable-next-line @typescript-eslint/no-floating-promises callingRes = resolveByPolling(web3Context, starterBlockNumber, transactionHash); } return callingRes; }