UNPKG

eth-block-tracker

Version:

A block tracker for the Ethereum blockchain. Keeps track of the latest block.

221 lines 7.54 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SubscribeBlockTracker = void 0; const safe_event_emitter_1 = __importDefault(require("@metamask/safe-event-emitter")); const json_rpc_random_id_1 = __importDefault(require("json-rpc-random-id")); const createRandomId = (0, json_rpc_random_id_1.default)(); const sec = 1000; const calculateSum = (accumulator, currentValue) => accumulator + currentValue; const blockTrackerEvents = ['sync', 'latest']; class SubscribeBlockTracker extends safe_event_emitter_1.default { constructor(opts = {}) { // parse + validate args if (!opts.provider) { throw new Error('SubscribeBlockTracker - no provider specified.'); } super(); // config this._blockResetDuration = opts.blockResetDuration || 20 * sec; this._usePastBlocks = opts.usePastBlocks || false; // state this._currentBlock = null; this._isRunning = false; // bind functions for internal use this._onNewListener = this._onNewListener.bind(this); this._onRemoveListener = this._onRemoveListener.bind(this); this._resetCurrentBlock = this._resetCurrentBlock.bind(this); // listen for handler changes this._setupInternalEvents(); // config this._provider = opts.provider; this._subscriptionId = null; } async destroy() { this._cancelBlockResetTimeout(); await this._maybeEnd(); super.removeAllListeners(); } isRunning() { return this._isRunning; } getCurrentBlock() { return this._currentBlock; } async getLatestBlock() { // return if available if (this._currentBlock) { return this._currentBlock; } // wait for a new latest block const latestBlock = await new Promise((resolve) => this.once('latest', resolve)); // return newly set current block return latestBlock; } // dont allow module consumer to remove our internal event listeners removeAllListeners(eventName) { // perform default behavior, preserve fn arity if (eventName) { super.removeAllListeners(eventName); } else { super.removeAllListeners(); } // re-add internal events this._setupInternalEvents(); // trigger stop check just in case this._onRemoveListener(); return this; } _setupInternalEvents() { // first remove listeners for idempotence this.removeListener('newListener', this._onNewListener); this.removeListener('removeListener', this._onRemoveListener); // then add them this.on('newListener', this._onNewListener); this.on('removeListener', this._onRemoveListener); } _onNewListener(eventName) { // `newListener` is called *before* the listener is added if (blockTrackerEvents.includes(eventName)) { // TODO: Handle dangling promise this._maybeStart(); } } _onRemoveListener() { // `removeListener` is called *after* the listener is removed if (this._getBlockTrackerEventCount() > 0) { return; } this._maybeEnd(); } async _maybeStart() { if (this._isRunning) { return; } this._isRunning = true; // cancel setting latest block to stale this._cancelBlockResetTimeout(); await this._start(); this.emit('_started'); } async _maybeEnd() { if (!this._isRunning) { return; } this._isRunning = false; this._setupBlockResetTimeout(); await this._end(); this.emit('_ended'); } _getBlockTrackerEventCount() { return blockTrackerEvents .map((eventName) => this.listenerCount(eventName)) .reduce(calculateSum); } _shouldUseNewBlock(newBlock) { const currentBlock = this._currentBlock; if (!currentBlock) { return true; } const newBlockInt = hexToInt(newBlock); const currentBlockInt = hexToInt(currentBlock); return ((this._usePastBlocks && newBlockInt < currentBlockInt) || newBlockInt > currentBlockInt); } _newPotentialLatest(newBlock) { if (!this._shouldUseNewBlock(newBlock)) { return; } this._setCurrentBlock(newBlock); } _setCurrentBlock(newBlock) { const oldBlock = this._currentBlock; this._currentBlock = newBlock; this.emit('latest', newBlock); this.emit('sync', { oldBlock, newBlock }); } _setupBlockResetTimeout() { // clear any existing timeout this._cancelBlockResetTimeout(); // clear latest block when stale this._blockResetTimeout = setTimeout(this._resetCurrentBlock, this._blockResetDuration); // nodejs - dont hold process open if (this._blockResetTimeout.unref) { this._blockResetTimeout.unref(); } } _cancelBlockResetTimeout() { if (this._blockResetTimeout) { clearTimeout(this._blockResetTimeout); } } _resetCurrentBlock() { this._currentBlock = null; } async checkForLatestBlock() { return await this.getLatestBlock(); } async _start() { if (this._subscriptionId === undefined || this._subscriptionId === null) { try { const blockNumber = (await this._call('eth_blockNumber')); this._subscriptionId = (await this._call('eth_subscribe', 'newHeads')); this._provider.on('data', this._handleSubData.bind(this)); this._newPotentialLatest(blockNumber); } catch (e) { this.emit('error', e); } } } async _end() { if (this._subscriptionId !== null && this._subscriptionId !== undefined) { try { await this._call('eth_unsubscribe', this._subscriptionId); this._subscriptionId = null; } catch (e) { this.emit('error', e); } } } async _call(method, ...params) { return new Promise((resolve, reject) => { this._provider.sendAsync({ id: createRandomId(), method, params, jsonrpc: '2.0', }, (err, res) => { if (err) { reject(err); } else { resolve(res.result); } }); }); } _handleSubData(_, response) { var _a; if (response.method === 'eth_subscription' && ((_a = response.params) === null || _a === void 0 ? void 0 : _a.subscription) === this._subscriptionId) { this._newPotentialLatest(response.params.result.number); } } } exports.SubscribeBlockTracker = SubscribeBlockTracker; /** * Converts a number represented as a string in hexadecimal format into a native * number. * * @param hexInt - The hex string. * @returns The number. */ function hexToInt(hexInt) { return Number.parseInt(hexInt, 16); } //# sourceMappingURL=SubscribeBlockTracker.js.map