UNPKG

@holographxyz/cli

Version:
1,061 lines (1,060 loc) • 76 kB
"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) => {