eth-block-tracker
Version:
A block tracker for the Ethereum blockchain. Keeps track of the latest block.
221 lines • 7.54 kB
JavaScript
"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