@holographxyz/cli
Version:
Holograph operator CLI
1,061 lines (1,060 loc) ⢠76 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NetworkMonitor = exports.keepAlive = exports.TransactionType = exports.FilterType = exports.ProviderStatus = exports.OperatorMode = exports.networkFlag = exports.networksFlag = exports.warpFlag = void 0;
const tslib_1 = require("tslib");
const fs = tslib_1.__importStar(require("fs-extra"));
const path = tslib_1.__importStar(require("node:path"));
const ws_1 = tslib_1.__importDefault(require("ws"));
const core_1 = require("@oclif/core");
const color_1 = tslib_1.__importDefault(require("@oclif/color"));
const wallet_1 = require("@ethersproject/wallet");
const contracts_1 = require("@ethersproject/contracts");
const bignumber_1 = require("@ethersproject/bignumber");
const units_1 = require("@ethersproject/units");
const keccak256_1 = require("@ethersproject/keccak256");
const abi_1 = require("@ethersproject/abi");
const providers_1 = require("@ethersproject/providers");
const environment_1 = require("@holographxyz/environment");
const networks_1 = require("@holographxyz/networks");
const gas_1 = require("./gas");
const utils_1 = require("./utils");
const contracts_2 = require("./contracts");
exports.warpFlag = {
warp: core_1.Flags.integer({
description: 'Start from the beginning of the chain',
default: 0,
char: 'w',
}),
};
exports.networksFlag = {
networks: core_1.Flags.string({
description: 'Space separated list of networks to use',
options: networks_1.supportedShortNetworks,
required: false,
multiple: true,
}),
};
exports.networkFlag = {
network: core_1.Flags.string({
description: 'Name of network to use',
options: networks_1.supportedShortNetworks,
multiple: false,
required: false,
}),
};
var OperatorMode;
(function (OperatorMode) {
OperatorMode["listen"] = "listen";
OperatorMode["manual"] = "manual";
OperatorMode["auto"] = "auto";
})(OperatorMode = exports.OperatorMode || (exports.OperatorMode = {}));
var ProviderStatus;
(function (ProviderStatus) {
ProviderStatus["NOT_CONFIGURED"] = "NOT_CONFIGURED";
ProviderStatus["CONNECTED"] = "CONNECTED";
ProviderStatus["DISCONNECTED"] = "DISCONNECTED";
})(ProviderStatus = exports.ProviderStatus || (exports.ProviderStatus = {}));
var FilterType;
(function (FilterType) {
FilterType[FilterType["to"] = 0] = "to";
FilterType[FilterType["from"] = 1] = "from";
FilterType[FilterType["functionSig"] = 2] = "functionSig";
})(FilterType = exports.FilterType || (exports.FilterType = {}));
var TransactionType;
(function (TransactionType) {
TransactionType["unknown"] = "unknown";
TransactionType["erc20"] = "erc20";
TransactionType["erc721"] = "erc721";
TransactionType["deploy"] = "deploy";
})(TransactionType = exports.TransactionType || (exports.TransactionType = {}));
const TIMEOUT_THRESHOLD = 20000;
const ZERO = bignumber_1.BigNumber.from('0');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const ONE = bignumber_1.BigNumber.from('1');
const TWO = bignumber_1.BigNumber.from('2');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const TEN = bignumber_1.BigNumber.from('10');
const webSocketErrorCodes = {
1000: 'Normal Closure',
1001: 'Going Away',
1002: 'Protocol Error',
1003: 'Unsupported Data',
1004: '(For future)',
1005: 'No Status Received',
1006: 'Abnormal Closure',
1007: 'Invalid frame payload data',
1008: 'Policy Violation',
1009: 'Message too big',
1010: 'Missing Extension',
1011: 'Internal Error',
1012: 'Service Restart',
1013: 'Try Again Later',
1014: 'Bad Gateway',
1015: 'TLS Handshake',
};
const keepAlive = ({ debug, websocket, onDisconnect, expectedPongBack = TIMEOUT_THRESHOLD, checkInterval = Math.round(TIMEOUT_THRESHOLD / 2), }) => {
let pingTimeout = null;
let keepAliveInterval = null;
let counter = 0;
let errorCounter = 0;
let terminator = null;
const errHandler = (err) => {
if (errorCounter === 0) {
errorCounter++;
debug(`websocket error event triggered ${err.code} ${JSON.stringify(err.reason)}`);
if (keepAliveInterval) {
clearInterval(keepAliveInterval);
}
if (pingTimeout) {
clearTimeout(pingTimeout);
}
terminator = setTimeout(() => {
websocket.terminate();
}, checkInterval);
}
};
websocket.on('ping', (data) => {
websocket.pong(data);
});
websocket.on('redirect', (url, request) => {
debug(JSON.stringify({
on: 'redirect',
url,
request,
}, undefined, 2));
});
websocket.on('unexpected-response', (request, response) => {
debug(JSON.stringify({
on: 'unexpected-response',
request,
response,
}, undefined, 2));
});
websocket.on('open', () => {
debug(`websocket open event triggered`);
websocket.off('error', errHandler);
if (terminator) {
clearTimeout(terminator);
}
keepAliveInterval = setInterval(() => {
websocket.ping();
pingTimeout = setTimeout(() => {
websocket.terminate();
}, expectedPongBack);
}, checkInterval);
});
websocket.on('close', (code, reason) => {
debug(`websocket close event triggered`);
if (counter === 0) {
debug(`websocket closed`);
counter++;
if (keepAliveInterval) {
clearInterval(keepAliveInterval);
}
if (pingTimeout) {
clearTimeout(pingTimeout);
}
setTimeout(() => {
onDisconnect(code, reason);
}, checkInterval);
}
});
websocket.on('error', errHandler);
websocket.on('pong', () => {
if (pingTimeout) {
clearInterval(pingTimeout);
}
});
};
exports.keepAlive = keepAlive;
const cleanTags = (tagIds) => {
if (tagIds === undefined) {
return '';
}
const tags = [];
if (typeof tagIds === 'string' || typeof tagIds === 'number') {
tags.push(tagIds.toString());
}
else {
if (tagIds.length === 0) {
return '';
}
for (const tag of tagIds) {
tags.push(tag.toString());
}
}
return ' [' + tags.join('] [') + ']';
};
class NetworkMonitor {
verbose = true;
environment;
parent;
configFile;
userWallet;
LAST_BLOCKS_FILE_NAME;
filters = [];
processTransactions;
log;
warn;
debug;
networks = [];
runningProcesses = 0;
bridgeAddress;
factoryAddress;
interfacesAddress;
operatorAddress;
registryAddress;
messagingModuleAddress;
wallets = {};
walletNonces = {};
providers = {};
ws = {};
activated = {};
abiCoder = abi_1.defaultAbiCoder;
networkColors = {};
latestBlockHeight = {};
currentBlockHeight = {};
blockJobs = {};
exited = false;
lastProcessBlockDone = {};
lastBlockJobDone = {};
blockJobMonitorProcess = {};
gasPrices = {};
holograph;
holographer;
bridgeContract;
factoryContract;
interfacesContract;
operatorContract;
registryContract;
messagingModuleContract;
HOLOGRAPH_ADDRESSES = contracts_2.HOLOGRAPH_ADDRESSES;
// this is specifically for handling localhost-based CLI usage with holograph-protocol package
localhostWallets = {};
static localhostPrivateKey = '0x13f46463f9079380515b26f04e42069760b34989cc23c5f082e7d3ed3757bb4a';
lzEndpointAddress = {};
lzEndpointContract = {};
LAYERZERO_RECEIVERS = {
localhost: '0x830e22aa238b6aeD78087FaCea8Bb95c6b7A7E2a'.toLowerCase(),
localhost2: '0x830e22aa238b6aeD78087FaCea8Bb95c6b7A7E2a'.toLowerCase(),
ethereumTestnetRinkeby: '0xF5E8A439C599205C1aB06b535DE46681Aed1007a'.toLowerCase(),
ethereumTestnetGoerli: '0xF5E8A439C599205C1aB06b535DE46681Aed1007a'.toLowerCase(),
polygonTestnet: '0xF5E8A439C599205C1aB06b535DE46681Aed1007a'.toLowerCase(),
avalancheTestnet: '0xF5E8A439C599205C1aB06b535DE46681Aed1007a'.toLowerCase(),
};
needToSubscribe = false;
warp = 0;
targetEvents = {
AvailableJob: '0x6114b34f1f941c01691c47744b4fbc0dd9d542be34241ba84fc4c0bd9bef9b11',
'0x6114b34f1f941c01691c47744b4fbc0dd9d542be34241ba84fc4c0bd9bef9b11': 'AvailableJob',
AvailableOperatorJob: '0x4422a85db963f113e500bc4ada8f9e9f1a7bcd57cbec6907fbb2bf6aaf5878ff',
'0x4422a85db963f113e500bc4ada8f9e9f1a7bcd57cbec6907fbb2bf6aaf5878ff': 'AvailableOperatorJob',
FinishedOperatorJob: '0xfc3963369d694e97f35e33cc03fcd382bfa4dbb688ae43d318fcf344f479425e',
'0xfc3963369d694e97f35e33cc03fcd382bfa4dbb688ae43d318fcf344f479425e': 'FinishedOperatorJob',
FailedOperatorJob: '0x26dc03e6c4feb5e9d33804dc1646860c976c3aeabb458f4719c53dcbadbf44b5',
'0x26dc03e6c4feb5e9d33804dc1646860c976c3aeabb458f4719c53dcbadbf44b5': 'FailedOperatorJob',
BridgeableContractDeployed: '0xa802207d4c618b40db3b25b7b90e6f483e16b2c1f8d3610b15b345a718c6b41b',
'0xa802207d4c618b40db3b25b7b90e6f483e16b2c1f8d3610b15b345a718c6b41b': 'BridgeableContractDeployed',
CrossChainMessageSent: '0x0f5759b4182507dcfc771071166f98d7ca331262e5134eaa74b676adce2138b7',
'0x0f5759b4182507dcfc771071166f98d7ca331262e5134eaa74b676adce2138b7': 'CrossChainMessageSent',
LzEvent: '0x138bae39f5887c9423d9c61fbf2cba537d68671ee69f2008423dbc28c8c41663',
'0x138bae39f5887c9423d9c61fbf2cba537d68671ee69f2008423dbc28c8c41663': 'LzEvent',
LzPacket: '0xe9bded5f24a4168e4f3bf44e00298c993b22376aad8c58c7dda9718a54cbea82',
'0xe9bded5f24a4168e4f3bf44e00298c993b22376aad8c58c7dda9718a54cbea82': 'LzPacket',
Packet: '0xe8d23d927749ec8e512eb885679c2977d57068839d8cca1a85685dbbea0648f6',
'0xe8d23d927749ec8e512eb885679c2977d57068839d8cca1a85685dbbea0648f6': 'Packet',
Transfer: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef': 'Transfer',
};
getProviderStatus() {
const output = {};
for (const network of networks_1.supportedNetworks) {
if (!(network in this.configFile.networks) ||
!('providerUrl' in this.configFile.networks[network]) ||
this.configFile.networks[network].providerUrl === undefined ||
this.configFile.networks[network].providerUrl === '') {
output[network] = ProviderStatus.NOT_CONFIGURED;
}
else {
output[network] = this.providers[network] === undefined ? ProviderStatus.DISCONNECTED : ProviderStatus.CONNECTED;
// check if using a WebSocketProvider connection
if (output[network] === ProviderStatus.CONNECTED && network in this.ws) {
// using WebSocketProvider, do a more thorough test
output[network] =
this.ws[network].readyState === ws_1.default.OPEN ? ProviderStatus.CONNECTED : ProviderStatus.DISCONNECTED;
}
}
}
return output;
}
checkConnectionStatus() {
for (const network of this.networks) {
if (!this.activated[network]) {
this.structuredLogError(network, 'Cannot start RPC provider');
// eslint-disable-next-line no-process-exit, unicorn/no-process-exit
process.exit();
}
}
}
constructor(options) {
this.environment = (0, environment_1.getEnvironment)();
this.parent = options.parent;
this.configFile = options.configFile;
this.LAST_BLOCKS_FILE_NAME = options.lastBlockFilename || 'blocks.json';
this.log = this.parent.log.bind(this.parent);
this.warn = this.parent.warn.bind(this.parent);
this.debug = options.debug.bind(this.parent);
if (options.filters !== undefined) {
this.filters = options.filters;
}
if (options.verbose !== undefined) {
this.verbose = options.verbose;
}
if (options.processTransactions !== undefined) {
this.processTransactions = options.processTransactions.bind(this.parent);
}
if (options.userWallet !== undefined) {
this.userWallet = options.userWallet;
}
if (options.warp !== undefined && options.warp > 0) {
this.warp = options.warp;
}
if (options.networks === undefined || '') {
options.networks = Object.keys(this.configFile.networks);
}
options.networks = options.networks.filter((network) => {
if (network === '') {
return false;
}
if (networks_1.supportedNetworks.includes(network)) {
return true;
}
if (networks_1.supportedShortNetworks.includes(network)) {
return true;
}
return false;
});
for (let i = 0, l = options.networks.length; i < l; i++) {
if (networks_1.supportedShortNetworks.includes(options.networks[i])) {
options.networks[i] = (0, networks_1.getNetworkByShortKey)(options.networks[i]).key;
}
const network = options.networks[i];
this.blockJobs[network] = [];
}
this.networks = [...new Set(options.networks)];
// Color the networks š
for (let i = 0, l = this.networks.length; i < l; i++) {
const network = this.networks[i];
this.networkColors[network] = color_1.default.hex(utils_1.NETWORK_COLORS[network]);
}
}
async run(continuous, blockJobs, ethersInitializedCallback) {
// check connections in 60 seconds, if something failed, kill the process
setTimeout(this.checkConnectionStatus.bind(this), 60000);
await this.initializeEthers();
if (ethersInitializedCallback !== undefined) {
await ethersInitializedCallback.bind(this.parent)();
}
if (this.verbose) {
this.log(``);
this.log(`š Holograph address: ${this.HOLOGRAPH_ADDRESSES[this.environment]}`);
this.log(`š Bridge address: ${this.bridgeAddress}`);
this.log(`š Factory address: ${this.factoryAddress}`);
this.log(`š Interfaces address: ${this.interfacesAddress}`);
this.log(`š Operator address: ${this.operatorAddress}`);
this.log(`š Registry address: ${this.registryAddress}`);
this.log(`š Messaging Module address: ${this.messagingModuleAddress}`);
this.log(``);
}
if (blockJobs !== undefined) {
this.blockJobs = blockJobs;
}
for (const network of this.networks) {
if (!(network in this.blockJobs)) {
this.blockJobs[network] = [];
}
this.lastBlockJobDone[network] = Date.now();
this.lastProcessBlockDone[network] = Date.now();
this.runningProcesses += 1;
if (continuous) {
this.needToSubscribe = true;
}
// Subscribe to events š§
this.networkSubscribe(network);
// Process blocks š§±
this.blockJobHandler(network);
// Activate Job Monitor for disconnect recovery after 10 seconds / Monitor every second
setTimeout(() => {
this.blockJobMonitorProcess[network] = setInterval(this.monitorBuilder.bind(this)(network), 1000);
}, 10000);
}
// Catch all exit events
for (const eventType of [`EEXIT`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `uncaughtException`, `SIGTERM`]) {
process.on(eventType, this.exitRouter.bind(this, { exit: true }));
}
process.on('exit', this.exitHandler);
}
async loadLastBlocks(configDir) {
const filePath = path.join(configDir, this.environment + '.' + this.LAST_BLOCKS_FILE_NAME);
let lastBlocks = {};
if (await fs.pathExists(filePath)) {
lastBlocks = await fs.readJson(filePath);
}
return lastBlocks;
}
saveLastBlocks(configDir, lastBlocks) {
const filePath = path.join(configDir, this.environment + '.' + this.LAST_BLOCKS_FILE_NAME);
fs.writeFileSync(filePath, JSON.stringify(lastBlocks), 'utf8');
}
disconnectBuilder(network, rpcEndpoint, subscribe) {
return (code, reason) => {
const restart = () => {
this.structuredLog(network, `Error in websocket connection, restarting... ${webSocketErrorCodes[code]} ${JSON.stringify(reason)}`);
this.lastBlockJobDone[network] = Date.now();
this.walletNonces[network] = -1;
this.failoverWebSocketProvider(network, rpcEndpoint, subscribe);
};
this.structuredLog(network, `Websocket is closed. Restarting connection for ${networks_1.networks[network].shortKey}`);
// terminate the existing websocket
this.ws[network].terminate();
restart();
};
}
failoverWebSocketProvider(network, rpcEndpoint, subscribe) {
this.log('this.providers', networks_1.networks[network].shortKey);
this.ws[network] = new ws_1.default(rpcEndpoint);
(0, exports.keepAlive)({
debug: this.debug,
websocket: this.ws[network],
onDisconnect: this.disconnectBuilder.bind(this)(network, rpcEndpoint, true),
});
this.providers[network] = new providers_1.WebSocketProvider(this.ws[network]);
if (this.userWallet !== undefined) {
this.wallets[network] = this.userWallet.connect(this.providers[network]);
}
if (subscribe && this.needToSubscribe) {
this.networkSubscribe(network);
}
}
async initializeEthers() {
for (const network of this.networks) {
const rpcEndpoint = this.configFile.networks[network].providerUrl;
const protocol = new URL(rpcEndpoint).protocol;
switch (protocol) {
case 'https:':
this.providers[network] = new providers_1.JsonRpcProvider(rpcEndpoint);
break;
case 'http:':
this.providers[network] = new providers_1.JsonRpcProvider(rpcEndpoint);
break;
case 'wss:':
this.failoverWebSocketProvider.bind(this)(network, rpcEndpoint, true);
break;
default:
throw new Error('Unsupported RPC provider protocol -> ' + protocol);
}
this.walletNonces[network] = -1;
if (this.userWallet !== undefined) {
this.wallets[network] = this.userWallet.connect(this.providers[network]);
this.walletNonces[network] = await this.getNonce({
network,
walletAddress: await this.wallets[network].getAddress(),
canFail: false,
});
}
if (this.warp > 0) {
if (this.verbose) {
this.structuredLog(network, `Starting Operator from ${this.warp} blocks back...`);
}
const currentBlock = await this.providers[network].getBlockNumber();
this.blockJobs[network] = [];
for (let n = currentBlock - this.warp, nl = currentBlock; n <= nl; n++) {
this.blockJobs[network].push({
network,
block: n,
});
}
}
else if (network in this.latestBlockHeight && this.latestBlockHeight[network] > 0) {
if (this.verbose) {
this.structuredLog(network, `Resuming Operator from block height ${this.latestBlockHeight[network]}`);
}
this.currentBlockHeight[network] = this.latestBlockHeight[network];
}
else {
if (this.verbose) {
this.structuredLog(network, `Starting Operator from latest block height`);
}
this.latestBlockHeight[network] = 0;
this.currentBlockHeight[network] = 0;
}
this.gasPrices[network] = await (0, gas_1.initializeGasPricing)(network, this.providers[network]);
this.activated[network] = true;
}
const holographABI = await fs.readJson(path.join(__dirname, `../abi/${this.environment}/Holograph.json`));
this.holograph = new contracts_1.Contract(this.HOLOGRAPH_ADDRESSES[this.environment], holographABI, this.providers[this.networks[0]]);
const holographerABI = await fs.readJson(path.join(__dirname, `../abi/${this.environment}/Holographer.json`));
this.holographer = new contracts_1.Contract(utils_1.zeroAddress, holographerABI, this.providers[this.networks[0]]);
this.bridgeAddress = (await this.holograph.getBridge()).toLowerCase();
this.factoryAddress = (await this.holograph.getFactory()).toLowerCase();
this.interfacesAddress = (await this.holograph.getInterfaces()).toLowerCase();
this.operatorAddress = (await this.holograph.getOperator()).toLowerCase();
this.registryAddress = (await this.holograph.getRegistry()).toLowerCase();
const holographBridgeABI = await fs.readJson(path.join(__dirname, `../abi/${this.environment}/HolographBridge.json`));
this.bridgeContract = new contracts_1.Contract(this.bridgeAddress, holographBridgeABI, this.providers[this.networks[0]]);
const holographFactoryABI = await fs.readJson(path.join(__dirname, `../abi/${this.environment}/HolographFactory.json`));
this.factoryContract = new contracts_1.Contract(this.factoryAddress, holographFactoryABI, this.providers[this.networks[0]]);
const holographInterfacesABI = await fs.readJson(path.join(__dirname, `../abi/${this.environment}/HolographInterfaces.json`));
this.interfacesContract = new contracts_1.Contract(this.interfacesAddress, holographInterfacesABI, this.providers[this.networks[0]]);
const holographOperatorABI = await fs.readJson(path.join(__dirname, `../abi/${this.environment}/HolographOperator.json`));
this.operatorContract = new contracts_1.Contract(this.operatorAddress, holographOperatorABI, this.providers[this.networks[0]]);
this.messagingModuleAddress = (await this.operatorContract.getMessagingModule()).toLowerCase();
const holographRegistryABI = await fs.readJson(path.join(__dirname, `../abi/${this.environment}/HolographRegistry.json`));
this.registryContract = new contracts_1.Contract(this.registryAddress, holographRegistryABI, this.providers[this.networks[0]]);
const holographMessagingModuleABI = await fs.readJson(path.join(__dirname, `../abi/${this.environment}/LayerZeroModule.json`));
this.messagingModuleContract = new contracts_1.Contract(this.messagingModuleAddress, holographMessagingModuleABI, this.providers[this.networks[0]]);
for (const network of this.networks) {
if (this.environment === environment_1.Environment.localhost) {
this.localhostWallets[network] = new wallet_1.Wallet(NetworkMonitor.localhostPrivateKey).connect(this.providers[network]);
// since sample localhost deployer key is used, nonce is out of sync
this.lzEndpointAddress[network] = (await this.messagingModuleContract.connect(this.providers[network]).getLZEndpoint()).toLowerCase();
const lzEndpointABI = await fs.readJson(path.join(__dirname, `../abi/${this.environment}/MockLZEndpoint.json`));
this.lzEndpointContract[network] = new contracts_1.Contract(this.lzEndpointAddress[network], lzEndpointABI, this.localhostWallets[network]);
}
}
}
exitCallback;
exitHandler = async (exitCode) => {
/**
* Before exit, save the block heights to the local db
*/
if (this.exited === false) {
this.log('');
if (this.needToSubscribe) {
this.log(`\nš¾ Saving current block heights:\n${JSON.stringify(this.latestBlockHeight, undefined, 2)}\n`);
this.saveLastBlocks(this.parent.config.configDir, this.latestBlockHeight);
}
this.log(`š Exiting ${this.parent.constructor.name} with code ${color_1.default.keyword('red')(exitCode)}`);
this.log(`\nš Thank you, come again\n`);
this.exited = true;
}
};
exitRouter = (options, exitCode) => {
/**
* Before exit, save the block heights to the local db
*/
if ((exitCode && exitCode === 0) || exitCode === 'SIGINT' || exitCode === 'SIGTERM') {
if (this.exited === false) {
this.log('');
if (this.needToSubscribe) {
this.log(`\nš¾ Saving current block heights:\n${JSON.stringify(this.latestBlockHeight, undefined, 2)}\n`);
this.saveLastBlocks(this.parent.config.configDir, this.latestBlockHeight);
}
this.log(`š Exiting ${this.parent.constructor.name} with code ${color_1.default.keyword('red')(exitCode)}`);
this.log(`\nš Thank you, come again\n`);
this.exited = true;
}
this.debug(`\nExit code ${exitCode}`);
if (options.exit) {
if (this.exitCallback !== undefined) {
this.exitCallback();
}
// eslint-disable-next-line no-process-exit, unicorn/no-process-exit
process.exit();
}
}
else {
this.debug('exitRouter triggered');
this.debug(`\nError: ${exitCode}`);
}
};
monitorBuilder = (network) => {
return () => {
this.blockJobMonitor.bind(this)(network);
};
};
restartProvider = async (network) => {
const rpcEndpoint = this.configFile.networks[network].providerUrl;
const protocol = new URL(rpcEndpoint).protocol;
switch (protocol) {
case 'http:':
this.providers[network] = new providers_1.JsonRpcProvider(rpcEndpoint);
break;
case 'https:':
this.providers[network] = new providers_1.JsonRpcProvider(rpcEndpoint);
break;
case 'wss:':
this.ws[network].close(1012, 'Block Job Handler has been inactive longer than threshold time.');
// this.failoverWebSocketProvider.bind(this)(network, rpcEndpoint, false)
break;
default:
throw new Error('Unsupported RPC provider protocol -> ' + protocol);
}
if (this.userWallet !== undefined) {
this.wallets[network] = this.userWallet.connect(this.providers[network]);
this.walletNonces[network] = await this.getNonce({
network,
walletAddress: await this.wallets[network].getAddress(),
canFail: false,
});
}
// apply this logic to catch a potential processBlock failing and being dropped during a provider restart cycle
// allow for up to 3 provider restarts to occur before triggering this
if (Date.now() - this.lastProcessBlockDone[network] > TIMEOUT_THRESHOLD * 3) {
this.blockJobHandler(network);
}
Promise.resolve();
};
blockJobMonitor = (network) => {
return new Promise(() => {
if (Date.now() - this.lastBlockJobDone[network] > TIMEOUT_THRESHOLD) {
this.structuredLogError(network, 'Block Job Handler has been inactive longer than threshold time. Restarting.', []);
this.lastBlockJobDone[network] = Date.now();
this.restartProvider(network);
}
});
};
jobHandlerBuilder = (network) => {
return () => {
this.blockJobHandler(network);
};
};
blockJobHandler = (network, job) => {
if (job !== undefined) {
this.latestBlockHeight[job.network] = job.block;
// we assume that this is latest
if (this.verbose) {
this.structuredLog(job.network, `Processed block`, job.block);
}
this.blockJobs[job.network].shift();
}
this.lastBlockJobDone[network] = Date.now();
this.lastProcessBlockDone[network] = Date.now();
if (this.blockJobs[network].length > 0) {
const blockJob = this.blockJobs[network][0];
this.processBlock(blockJob);
}
else if (this.needToSubscribe) {
setTimeout(this.jobHandlerBuilder.bind(this)(network), 1000);
}
else {
if (network in this.blockJobMonitorProcess) {
this.structuredLog(network, 'All jobs done for network');
clearInterval(this.blockJobMonitorProcess[network]);
delete this.blockJobMonitorProcess[network];
this.runningProcesses -= 1;
}
if (this.runningProcesses === 0) {
this.log('Finished the last job. Exiting...');
this.exitRouter({ exit: true }, 'SIGINT');
}
}
};
filterTransaction(job, transaction, interestingTransactions) {
const to = transaction.to?.toLowerCase() || '';
const from = transaction.from?.toLowerCase() || '';
let data;
for (const filter of this.filters) {
const match = filter.networkDependant
? filter.match[job.network]
: filter.match;
switch (filter.type) {
case FilterType.to:
if (to === match) {
interestingTransactions.push(transaction);
}
break;
case FilterType.from:
if (from === match) {
interestingTransactions.push(transaction);
}
break;
case FilterType.functionSig:
data = transaction.data?.slice(0, 10) || '';
if (data === match) {
interestingTransactions.push(transaction);
}
break;
default:
break;
}
}
}
extractGasData(network, block, tx) {
if (this.gasPrices[network].isEip1559) {
// set current tx priority fee
let priorityFee = ZERO;
let remainder;
if (tx.maxFeePerGas === undefined || tx.maxPriorityFeePerGas === undefined) {
// we have a legacy transaction here, so need to calculate priority fee out
priorityFee = tx.gasPrice.sub(block.baseFeePerGas);
}
else {
// we have EIP-1559 transaction here, get priority fee
// check first that base block fee is less than maxFeePerGas
remainder = tx.maxFeePerGas.sub(block.baseFeePerGas);
priorityFee = remainder.gt(tx.maxPriorityFeePerGas) ? tx.maxPriorityFeePerGas : remainder;
}
if (this.gasPrices[network].nextPriorityFee === null) {
this.gasPrices[network].nextPriorityFee = priorityFee;
}
else {
this.gasPrices[network].nextPriorityFee = this.gasPrices[network].nextPriorityFee.add(priorityFee).div(TWO);
}
}
// for legacy networks, get average gasPrice
else if (this.gasPrices[network].gasPrice === null) {
this.gasPrices[network].gasPrice = tx.gasPrice;
}
else {
this.gasPrices[network].gasPrice = this.gasPrices[network].gasPrice.add(tx.gasPrice).div(TWO);
}
}
async processBlock(job) {
this.activated[job.network] = true;
if (this.verbose) {
this.structuredLog(job.network, `Processing block`, job.block);
}
const block = await this.getBlockWithTransactions({
network: job.network,
blockNumber: job.block,
attempts: 10,
canFail: true,
});
if (block !== undefined && block !== null && 'transactions' in block) {
const recentBlock = this.currentBlockHeight[job.network] - job.block < 5;
if (this.verbose) {
this.structuredLog(job.network, `Block retrieved`, job.block);
/*
this.structuredLog(job.network, `Calculating block gas`, job.block)
if (this.gasPrices[job.network].isEip1559) {
this.structuredLog(
job.network,
`Calculated block gas price was ${formatUnits(
this.gasPrices[job.network].nextBlockFee!,
'gwei',
)} GWEI, and actual block gas price is ${formatUnits(block.baseFeePerGas!, 'gwei')} GWEI`,
job.block,
)
}
*/
}
if (recentBlock) {
this.gasPrices[job.network] = (0, gas_1.updateGasPricing)(job.network, block, this.gasPrices[job.network]);
}
// const priorityFees: BigNumber = this.gasPrices[job.network].nextPriorityFee!
if (this.verbose && block.transactions.length === 0) {
this.structuredLog(job.network, `Zero transactions in block`, job.block);
}
const interestingTransactions = [];
for (let i = 0, l = block.transactions.length; i < l; i++) {
if (recentBlock) {
this.extractGasData(job.network, block, block.transactions[i]);
}
this.filterTransaction(job, block.transactions[i], interestingTransactions);
}
if (recentBlock) {
this.gasPrices[job.network] = (0, gas_1.updateGasPricing)(job.network, block, this.gasPrices[job.network]);
}
/*
if (this.verbose && this.gasPrices[job.network].isEip1559 && priorityFees !== null) {
this.structuredLog(
job.network,
`Calculated block priority fees was ${formatUnits(
priorityFees,
'gwei',
)} GWEI, and actual block priority fees is ${formatUnits(
this.gasPrices[job.network].nextPriorityFee!,
'gwei',
)} GWEI`,
job.block,
)
}
*/
if (interestingTransactions.length > 0) {
if (this.verbose) {
this.structuredLog(job.network, `Found ${interestingTransactions.length} interesting transactions`, job.block);
}
if (this.processTransactions !== undefined) {
await this.processTransactions?.bind(this.parent)(job, interestingTransactions);
}
this.blockJobHandler(job.network, job);
}
else {
this.blockJobHandler(job.network, job);
}
}
else {
if (this.verbose) {
this.structuredLog(job.network, `${color_1.default.red('Dropped block')}`, job.block);
}
this.blockJobHandler(job.network);
}
}
networkSubscribe(network) {
this.providers[network].on('block', (blockNumber) => {
const block = Number.parseInt(blockNumber, 10);
if (this.currentBlockHeight[network] !== 0 && block - this.currentBlockHeight[network] > 1) {
if (this.verbose) {
this.structuredLog(network, `Resuming previously dropped connection, gotta do some catching up`);
}
let latest = this.currentBlockHeight[network];
while (block - latest > 0) {
if (this.verbose) {
this.structuredLog(network, `Block (Syncing)`, latest);
}
this.blockJobs[network].push({
network: network,
block: latest,
});
latest++;
}
}
this.currentBlockHeight[network] = block;
if (this.verbose) {
this.structuredLog(network, `New block mined`, block);
}
this.blockJobs[network].push({
network: network,
block: block,
});
});
}
structuredLog(network, msg, tagId) {
const timestamp = new Date(Date.now()).toISOString();
const timestampColor = color_1.default.keyword('green');
this.log(`[${timestampColor(timestamp)}] [${this.parent.constructor.name}] [${this.networkColors[network]((0, utils_1.capitalize)(networks_1.networks[network].shortKey))}]${cleanTags(tagId)} ${msg}`);
}
structuredLogError(network, error, tagId) {
let errorMessage = `unknown error message`;
if (typeof error === 'string') {
errorMessage = error;
}
else if ('message' in error) {
errorMessage = `${error.message}`;
}
else if ('reason' in error) {
errorMessage = `${error.reason}`;
}
else if ('error' in error && 'reason' in error.error) {
errorMessage = `${error.error.reason}`;
}
const timestamp = new Date(Date.now()).toISOString();
const timestampColor = color_1.default.keyword('green');
const errorColor = color_1.default.keyword('red');
this.warn(`[${timestampColor(timestamp)}] [${this.parent.constructor.name}] [${this.networkColors[network]((0, utils_1.capitalize)(networks_1.networks[network].shortKey))}] [${errorColor('error')}]${cleanTags(tagId)} ${errorMessage}`);
}
static iface = new abi_1.Interface([]);
static packetEventFragment = abi_1.EventFragment.from('Packet(uint16 chainId, bytes payload)');
static lzPacketEventFragment = abi_1.EventFragment.from('Packet(bytes payload)');
static lzEventFragment = abi_1.EventFragment.from('LzEvent(uint16 _dstChainId, bytes _destination, bytes _payload)');
static erc20TransferEventFragment = abi_1.EventFragment.from('Transfer(address indexed _from, address indexed _to, uint256 _value)');
static erc721TransferEventFragment = abi_1.EventFragment.from('Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId)');
static availableJobEventFragment = abi_1.EventFragment.from('AvailableJob(bytes payload)');
static bridgeableContractDeployedEventFragment = abi_1.EventFragment.from('BridgeableContractDeployed(address indexed contractAddress, bytes32 indexed hash)');
static availableOperatorJobEventFragment = abi_1.EventFragment.from('AvailableOperatorJob(bytes32 jobHash, bytes payload)');
static crossChainMessageSentEventFragment = abi_1.EventFragment.from('CrossChainMessageSent(bytes32 messageHash)');
static finishedOperatorJobEventFragment = abi_1.EventFragment.from('FinishedOperatorJob(bytes32 jobHash, address operator)');
static failedOperatorJobEventFragment = abi_1.EventFragment.from('FailedOperatorJob(bytes32 jobHash)');
decodePacketEvent(receipt, target) {
if (target !== undefined) {
target = target.toLowerCase().trim();
}
const toFind = this.operatorAddress.slice(2, 42);
if ('logs' in receipt && receipt.logs !== null && receipt.logs.length > 0) {
for (let i = 0, l = receipt.logs.length; i < l; i++) {
const log = receipt.logs[i];
if (log.topics[0] === this.targetEvents.Packet &&
(target === undefined || (target !== undefined && log.address.toLowerCase() === target))) {
const packetPayload = NetworkMonitor.iface.decodeEventLog(NetworkMonitor.packetEventFragment, log.data, log.topics)[1];
if (packetPayload.indexOf(toFind) > 0) {
let index = packetPayload.indexOf(toFind);
// address + address
index += 40 + 40;
return ('0x' + packetPayload.slice(Math.max(0, index))).toLowerCase();
}
}
}
}
return undefined;
}
decodeLzPacketEvent(receipt, target) {
if (target !== undefined) {
target = target.toLowerCase().trim();
}
const toFind = this.messagingModuleAddress.slice(2, 42);
if ('logs' in receipt && receipt.logs !== null && receipt.logs.length > 0) {
for (let i = 0, l = receipt.logs.length; i < l; i++) {
const log = receipt.logs[i];
if (log.topics[0] === this.targetEvents.LzPacket &&
(target === undefined || (target !== undefined && log.address.toLowerCase() === target))) {
const packetPayload = NetworkMonitor.iface.decodeEventLog(NetworkMonitor.lzPacketEventFragment, log.data, log.topics)[0];
if (packetPayload.indexOf(toFind) > 0) {
let index = packetPayload.indexOf(toFind);
// address + bytes2 + address
index += 40 + 4 + 40;
return ('0x' + packetPayload.slice(Math.max(0, index))).toLowerCase();
}
}
}
}
return undefined;
}
decodeLzEvent(receipt, target) {
if (target !== undefined) {
target = target.toLowerCase().trim();
}
if ('logs' in receipt && receipt.logs !== null && receipt.logs.length > 0) {
for (let i = 0, l = receipt.logs.length; i < l; i++) {
const log = receipt.logs[i];
if (log.topics[0] === this.targetEvents.LzEvent &&
(target === undefined || (target !== undefined && log.address.toLowerCase() === target))) {
const event = NetworkMonitor.iface.decodeEventLog(NetworkMonitor.lzEventFragment, log.data, log.topics);
return this.lowerCaseAllStrings(event);
}
}
}
return undefined;
}
decodeErc20TransferEvent(receipt, target) {
if (target !== undefined) {
target = target.toLowerCase().trim();
}
if ('logs' in receipt && receipt.logs !== null && receipt.logs.length > 0) {
for (let i = 0, l = receipt.logs.length; i < l; i++) {
const log = receipt.logs[i];
if (log.topics[0] === this.targetEvents.Transfer &&
(target === undefined || (target !== undefined && log.address.toLowerCase() === target))) {
const event = NetworkMonitor.iface.decodeEventLog(NetworkMonitor.erc20TransferEventFragment, log.data, log.topics);
return this.lowerCaseAllStrings(event, log.address);
}
}
}
return undefined;
}
decodeErc721TransferEvent(receipt, target) {
if (target !== undefined) {
target = target.toLowerCase().trim();
}
if ('logs' in receipt && receipt.logs !== null && receipt.logs.length > 0) {
for (let i = 0, l = receipt.logs.length; i < l; i++) {
const log = receipt.logs[i];
if (log.topics[0] === this.targetEvents.Transfer &&
(target === undefined || (target !== undefined && log.address.toLowerCase() === target))) {
const event = NetworkMonitor.iface.decodeEventLog(NetworkMonitor.erc721TransferEventFragment, log.data, log.topics);
return this.lowerCaseAllStrings(event, log.address);
}
}
}
return undefined;
}
decodeAvailableJobEvent(receipt, target) {
if (target !== undefined) {
target = target.toLowerCase().trim();
}
if ('logs' in receipt && receipt.logs !== null && receipt.logs.length > 0) {
for (let i = 0, l = receipt.logs.length; i < l; i++) {
const log = receipt.logs[i];
if (log.topics[0] === this.targetEvents.AvailableJob &&
(target === undefined || (target !== undefined && log.address.toLowerCase() === target))) {
return NetworkMonitor.iface.decodeEventLog(NetworkMonitor.availableJobEventFragment, log.data, log.topics)[0].toLowerCase();
}
}
}
return undefined;
}
decodeAvailableOperatorJobEvent(receipt, target) {
if (target !== undefined) {
target = target.toLowerCase().trim();
}
if ('logs' in receipt && receipt.logs !== null && receipt.logs.length > 0) {
for (let i = 0, l = receipt.logs.length; i < l; i++) {
const log = receipt.logs[i];
if (log.topics[0] === this.targetEvents.AvailableOperatorJob &&
(target === undefined || (target !== undefined && log.address.toLowerCase() === target))) {
const output = NetworkMonitor.iface.decodeEventLog(NetworkMonitor.availableOperatorJobEventFragment, log.data, log.topics);
return this.lowerCaseAllStrings(output);
}
}
}
return undefined;
}
decodeBridgeableContractDeployedEvent(receipt, target) {
if (target !== undefined) {
target = target.toLowerCase().trim();
}
if ('logs' in receipt && receipt.logs !== null && receipt.logs.length > 0) {
for (let i = 0, l = receipt.logs.length; i < l; i++) {
const log = receipt.logs[i];
if (log.topics[0] === this.targetEvents.BridgeableContractDeployed &&
(target === undefined || (target !== undefined && log.address.toLowerCase() === target))) {
return this.lowerCaseAllStrings(NetworkMonitor.iface.decodeEventLog(NetworkMonitor.bridgeableContractDeployedEventFragment, log.data, log.topics));
}
}
}
return undefined;
}
decodeCrossChainMessageSentEvent(receipt, target) {
if (target !== undefined) {
target = target.toLowerCase().trim();
}
if ('logs' in receipt && receipt.logs !== null && receipt.logs.length > 0) {
for (let i = 0, l = receipt.logs.length; i < l; i++) {
const log = receipt.logs[i];
if (log.topics[0] === this.targetEvents.CrossChainMessageSent &&
(target === undefined || (target !== undefined && log.address.toLowerCase() === target))) {
return NetworkMonitor.iface.decodeEventLog(NetworkMonitor.crossChainMessageSentEventFragment, log.data, log.topics)[0].toLowerCase();
}
}
}
return undefined;
}
decodeFinishedOperatorJobEvent(receipt, target) {
if (target !== undefined) {
target = target.toLowerCase().trim();
}
if ('logs' in receipt && receipt.logs !== null && receipt.logs.length > 0) {
for (let i = 0, l = receipt.logs.length; i < l; i++) {
const log = receipt.logs[i];
if (log.topics[0] === this.targetEvents.FinishedOperatorJob &&
(target === undefined || (target !== undefined && log.address.toLowerCase() === target))) {
const output = NetworkMonitor.iface.decodeEventLog(NetworkMonitor.finishedOperatorJobEventFragment, log.data, log.topics);
return this.lowerCaseAllStrings(output);
}
}
}
return undefined;
}
decodeFailedOperatorJobEvent(receipt, target) {
if (target !== undefined) {
target = target.toLowerCase().trim();
}
if ('logs' in receipt && receipt.logs !== null && receipt.logs.length > 0) {
for (let i = 0, l = receipt.logs.length; i < l; i++) {
const log = receipt.logs[i];
if (log.topics[0] === this.targetEvents.FailedOperatorJob &&
(target === undefined || (target !== undefined && log.address.toLowerCase() === target))) {
return NetworkMonitor.iface.decodeEventLog(NetworkMonitor.failedOperatorJobEventFragment, log.data, log.topics)[0].toLowerCase();
}
}
}
return undefined;
}
lowerCaseAllStrings(input, add) {
const output = [...input];
if (add !== undefined) {
output.push(add);
}
for (let i = 0, l = output.length; i < l; i++) {
if (typeof output[i] === 'string') {
output[i] = output[i].toLowerCase();
}
}
return output;
}
randomTag() {
// 4_294_967_295 is max value for 2^32 which is uint32
return Math.floor(Math.random() * 4294967295).toString(16);
}
async getBlock({ blockNumber, network, tags = [], attempts = 10, canFail = false, interval = 5000, }) {
return new Promise((topResolve, _topReject) => {