UNPKG

eth-block-tracker

Version:

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

161 lines (133 loc) 4.04 kB
const EthQuery = require('eth-query') const pify = require('pify') const SafeEventEmitter = require('safe-event-emitter') const sec = 1000 const calculateSum = (accumulator, currentValue) => accumulator + currentValue const blockTrackerEvents = ['sync', 'latest'] class BaseBlockTracker extends SafeEventEmitter { // // public // constructor (opts = {}) { super() // config this._blockResetDuration = opts.blockResetDuration || 20 * sec // state this._blockResetTimeout 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() } 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() } // // to be implemented in subclass // _start () { // default behavior is noop } _end () { // default behavior is noop } // // private // _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, handler) { // `newListener` is called *before* the listener is added if (!blockTrackerEvents.includes(eventName)) return this._maybeStart() } _onRemoveListener (eventName, handler) { // `removeListener` is called *after* the listener is removed if (this._getBlockTrackerEventCount() > 0) return this._maybeEnd() } _maybeStart () { if (this._isRunning) return this._isRunning = true // cancel setting latest block to stale this._cancelBlockResetTimeout() this._start() } _maybeEnd () { if (!this._isRunning) return this._isRunning = false this._setupBlockResetTimeout() this._end() } _getBlockTrackerEventCount () { return blockTrackerEvents .map(eventName => this.listenerCount(eventName)) .reduce(calculateSum) } _newPotentialLatest (newBlock) { const currentBlock = this._currentBlock // only update if blok number is higher if (currentBlock && (hexToInt(newBlock) <= hexToInt(currentBlock))) 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 () { clearTimeout(this._blockResetTimeout) } _resetCurrentBlock () { this._currentBlock = null } } module.exports = BaseBlockTracker function hexToInt(hexInt) { return Number.parseInt(hexInt, 16) }