UNPKG

@holographxyz/cli

Version:
609 lines (608 loc) 29.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const fs = tslib_1.__importStar(require("fs-extra")); const core_1 = require("@oclif/core"); const environment_1 = require("@holographxyz/environment"); const networks_1 = require("@holographxyz/networks"); const config_1 = require("../../utils/config"); const utils_1 = require("../../utils/utils"); const network_monitor_1 = require("../../utils/network-monitor"); const api_service_1 = tslib_1.__importDefault(require("../../services/api-service")); const api_1 = require("../../types/api"); var LogType; (function (LogType) { LogType["ContractDeployment"] = "ContractDeployment"; LogType["AvailableJob"] = "AvailableJob"; })(LogType || (LogType = {})); const getCorrectValue = (val1, val2) => (val1 && val1 !== val2 ? val1 : val2); const getTxStatus = (tx, currentStatus) => { let status; if (typeof currentStatus === 'string' && currentStatus === api_1.TransactionStatus.COMPLETED) { status = currentStatus; } else if (typeof tx === 'string') { status = api_1.TransactionStatus.COMPLETED; } else { status = api_1.TransactionStatus.PENDING; } return status; }; class Analyze extends core_1.Command { static hidden = true; static description = 'Extract all operator jobs and get their status'; static examples = [ `$ <%= config.bin %> <%= command.id %> --scope='{"network":"goerli","startBlock":10857626,"endBlock":11138178}' --scope='{"network":"mumbai","startBlock":26758573,"endBlock":27457918}' --scope='{"network":"fuji","startBlock":11406945,"endBlock":12192217}'`, ]; static flags = { scope: core_1.Flags.string({ description: 'JSON object of blocks to analyze "{ network: string, startBlock: number, endBlock: number }"', multiple: true, }), scopeFile: core_1.Flags.string({ description: 'JSON file path of blocks to analyze (ie "./scopeFile.json")', exclusive: ['scope'], required: false, }), output: core_1.Flags.string({ description: 'Specify a file to output the results to (ie "../../analyzeResults.json")', default: `./${(0, environment_1.getEnvironment)()}.analyzeResults.json`, multiple: false, }), updateApiOn: core_1.Flags.string({ description: 'Update DB cross-chain table with correct beam status', }), }; environment; outputFile; collectionMap = {}; operatorJobIndexMap = {}; operatorJobCounterMap = {}; transactionLogs = []; networkMonitor; blockJobs = {}; apiService; /** * Command Entry Point */ async run() { const { flags } = await this.parse(Analyze); const updateApiOn = flags.updateApiOn; this.log('Loading user configurations...'); const { environment, configFile } = await (0, config_1.ensureConfigFileIsValid)(this.config.configDir, undefined, false); this.log('User configurations loaded.'); this.environment = environment; if (updateApiOn) { try { const logger = { log: this.log, warn: this.warn, debug: this.debug, error: this.error, jsonEnabled: () => false, }; this.apiService = new api_service_1.default(updateApiOn, logger); await this.apiService.operatorLogin(); } catch (error) { this.error(error); } } const { networks, scopeJobs } = await this.scopeItOut(flags.scope, flags.scopeFile); this.log(`${JSON.stringify(scopeJobs, undefined, 2)}`); this.outputFile = flags.output; if (await fs.pathExists(this.outputFile)) { this.transactionLogs = (await fs.readJson(this.outputFile)); let i = 0; for (const logRaw of this.transactionLogs) { if (logRaw.logType === LogType.AvailableJob) { const log = logRaw; this.operatorJobIndexMap[log.jobHash] = i; this.operatorJobCounterMap[log.jobHash] = 0; if ('messageTx' in log && log.messageTx !== '') { this.operatorJobCounterMap[log.jobHash] += 1; } if ('bridgeTx' in log && log.bridgeTx !== '') { this.operatorJobCounterMap[log.jobHash] += 1; } if ('operatorTx' in log && log.operatorTx !== '') { this.operatorJobCounterMap[log.jobHash] += 1; } if (this.operatorJobIndexMap[log.jobHash] === 3) { delete this.operatorJobIndexMap[log.jobHash]; delete this.operatorJobCounterMap[log.jobHash]; } } i++; } } this.networkMonitor = new network_monitor_1.NetworkMonitor({ parent: this, configFile, networks, debug: this.debug, processTransactions: this.processTransactions, }); const blockJobs = {}; const injectBlocks = async () => { // Setup websocket subscriptions and start processing blocks for (let i = 0, l = networks.length; i < l; i++) { const network = networks[i]; blockJobs[network] = []; for (const scopeJob of scopeJobs) { if (scopeJob.network === network) { let endBlock = scopeJob.endBlock; // Allow syncing up to current block height if endBlock is set to 0 if (endBlock === 0) { endBlock = await this.networkMonitor.providers[network].getBlockNumber(); } for (let n = scopeJob.startBlock, nl = endBlock; n <= nl; n++) { blockJobs[network].push({ network: network, block: n, }); } } } } await this.filterBuilder(); }; this.networkMonitor.exitCallback = this.exitCallback.bind(this); await this.networkMonitor.run(false, blockJobs, injectBlocks.bind(this)); } /** * Keeps track of the operator jobs */ manageOperatorJobMaps(index, operatorJobHash, beam) { if (index >= 0) { this.transactionLogs[index] = beam; this.operatorJobCounterMap[operatorJobHash] = 1; } else { this.operatorJobIndexMap[operatorJobHash] = this.transactionLogs.push(beam) - 1; this.operatorJobCounterMap[operatorJobHash] += 1; } if (this.operatorJobCounterMap[operatorJobHash] === 3) { delete this.operatorJobIndexMap[operatorJobHash]; delete this.operatorJobCounterMap[operatorJobHash]; } } /** * Validates that the input scope is valid and using a supported network */ validateScope(scope, networks, scopeJobs) { if ('network' in scope && 'startBlock' in scope && 'endBlock' in scope) { if (networks_1.supportedShortNetworks.includes(scope.network)) { scope.network = (0, networks_1.getNetworkByShortKey)(scope.network).key; } if (networks_1.supportedNetworks.includes(scope.network)) { if (!networks.includes(scope.network)) { networks.push(scope.network); } scopeJobs.push(scope); } else { this.log(`${scope.network} is not a supported network`); } } else { this.log(`${scope} is an invalid Scope object`); } } /** * Checks all the input scopes and validates them */ async scopeItOut(scopeFlags, scopeFile) { const networks = []; const scopeJobs = []; if (scopeFlags === undefined && scopeFile === undefined) { this.error('scope or scopeFile should be informed'); } if (scopeFlags) { for (const scopeString of scopeFlags) { try { const scope = JSON.parse(scopeString); this.validateScope(scope, networks, scopeJobs); } catch { this.log(`${scopeString} is an invalid Scope JSON object`); } } } else if (scopeFile) { if (!(await fs.pathExists(scopeFile))) { this.error(`Problem reading ${scopeFile}`); } try { const scopes = (await fs.readJson(scopeFile)); for (const scope of scopes) this.validateScope(scope, networks, scopeJobs); } catch { this.error(`One or more lines are an invalid Scope JSON object`); } } else { this.error(`Invalid scope`); } return { networks, scopeJobs }; } exitCallback() { fs.writeFileSync(this.outputFile, JSON.stringify(this.transactionLogs, undefined, 2)); } /** * Build the filters to search for events via the network monitor */ async filterBuilder() { this.networkMonitor.filters = [ { type: network_monitor_1.FilterType.from, match: this.networkMonitor.LAYERZERO_RECEIVERS, networkDependant: true, }, { type: network_monitor_1.FilterType.to, match: this.networkMonitor.bridgeAddress, networkDependant: false, }, { type: network_monitor_1.FilterType.to, match: this.networkMonitor.operatorAddress, networkDependant: false, }, ]; return Promise.resolve(); } /** * Update cross chain transaction on DB */ async updateBeamStatusDB(beam, rawData) { if (this.apiService === undefined) { return; } let crossChainTx; let updatedTx; try { crossChainTx = await this.apiService.getCrossChainTransaction(beam.jobHash); } catch (error) { this.error(error); } const sourceChainId = networks_1.networks[beam.bridgeNetwork].chain; const messageChainId = networks_1.networks[beam.messageNetwork].chain; const operatorChainId = networks_1.networks[beam.operatorNetwork].chain; if (crossChainTx) { if (crossChainTx.sourceStatus === api_1.TransactionStatus.COMPLETED && crossChainTx.messageStatus === api_1.TransactionStatus.COMPLETED && crossChainTx.operatorStatus === api_1.TransactionStatus.COMPLETED && crossChainTx.sourceAddress !== undefined && crossChainTx.messageAddress !== undefined && crossChainTx.operatorAddress !== undefined && crossChainTx.data !== undefined) { this.log('Beaming is completed'); return; } if ((crossChainTx.sourceChainId && crossChainTx.sourceChainId !== sourceChainId) || (crossChainTx.messageChainId && crossChainTx.messageChainId !== messageChainId) || (crossChainTx.operatorChainId && crossChainTx.operatorChainId !== operatorChainId)) { this.log('Job hash collision'); return; } updatedTx = crossChainTx; updatedTx = { ...updatedTx, sourceTx: getCorrectValue(beam.bridgeTx, crossChainTx.sourceTx), sourceChainId: getCorrectValue(sourceChainId, crossChainTx.sourceChainId), sourceBlockNumber: getCorrectValue(beam.bridgeBlock, crossChainTx.sourceBlockNumber), sourceAddress: getCorrectValue(beam.operatorAddress, crossChainTx.sourceAddress), sourceStatus: getTxStatus(beam.bridgeTx, crossChainTx.sourceStatus), messageTx: getCorrectValue(beam.messageTx, crossChainTx.messageTx), messageChainId: getCorrectValue(messageChainId, crossChainTx.messageChainId), messageBlockNumber: getCorrectValue(beam.messageBlock, crossChainTx.messageBlockNumber), messageAddress: getCorrectValue(beam.messageAddress, crossChainTx.messageAddress), messageStatus: getTxStatus(beam.messageTx, crossChainTx.messageStatus), operatorTx: getCorrectValue(beam.operatorTx, crossChainTx.operatorTx), operatorChainId: getCorrectValue(operatorChainId, crossChainTx.operatorChainId), operatorBlockNumber: getCorrectValue(beam.operatorBlock, crossChainTx.operatorBlockNumber), operatorAddress: getCorrectValue(beam.operatorAddress, crossChainTx.operatorAddress), operatorStatus: getTxStatus(beam.operatorTx, crossChainTx.operatorStatus), }; if (rawData !== undefined && crossChainTx.data === undefined) { updatedTx.data = JSON.stringify(rawData); } else if (rawData === undefined && crossChainTx.data === undefined) { delete updatedTx.data; } delete updatedTx.id; } else { this.log('No source job found in DB'); if (!beam.jobType) return; this.log('Creating job instance in DB...'); updatedTx = { jobType: beam.jobType.toUpperCase(), jobHash: beam.jobHash, sourceTx: beam.bridgeTx, sourceChainId: sourceChainId, sourceBlockNumber: beam.bridgeBlock, sourceAddress: beam.bridgeAddress, sourceStatus: getTxStatus(beam.bridgeTx), messageTx: beam.messageTx, messageChainId: messageChainId, messageBlockNumber: beam.messageBlock, messageAddress: beam.messageAddress, messageStatus: getTxStatus(beam.messageTx), operatorTx: beam.operatorTx, operatorChainId: operatorChainId, operatorBlockNumber: beam.operatorBlock, operatorAddress: beam.operatorAddress, operatorStatus: getTxStatus(beam.operatorTx), }; if (rawData !== undefined) { updatedTx.data = JSON.stringify(rawData); } } try { const response = await this.apiService.updateCrossChainTransactionStatus(updatedTx); this.log(`Updated cross chain transaction ${response.id}`); } catch (error) { this.error(error); } } /** * Process the transactions in each block job */ async processTransactions(job, transactions) { if (transactions.length > 0) { for (const transaction of transactions) { const tags = []; tags.push(transaction.blockNumber, this.networkMonitor.randomTag()); this.networkMonitor.structuredLog(job.network, `Processing transaction ${transaction.hash}`, tags); const to = transaction.to?.toLowerCase(); const from = transaction.from?.toLowerCase(); if (to === this.networkMonitor.bridgeAddress) { // We have bridge job this.log('handleBridgeOutEvent'); await this.handleBridgeOutEvent(transaction, job.network, tags); } else if (to === this.networkMonitor.operatorAddress) { // We have a bridge job being executed // Check that it worked? this.log('handleBridgeInEvent'); await this.handleBridgeInEvent(transaction, job.network, tags); } else if (from === this.networkMonitor.LAYERZERO_RECEIVERS[job.network]) { // We have an available operator job event this.log('handleAvailableOperatorJobEvent'); await this.handleAvailableOperatorJobEvent(transaction, job.network, tags); } else { this.networkMonitor.structuredLog(job.network, `Function processTransactions stumbled on an unknown transaction ${transaction.hash}`, tags); } } } } /** * Finds bridge out events and keeps track of them */ async handleBridgeOutEvent(transaction, network, tags) { const receipt = await this.networkMonitor.getTransactionReceipt({ network, transactionHash: transaction.hash, attempts: 10, canFail: true, }); if (receipt === null) { throw new Error(`Could not get receipt for ${transaction.hash}`); } if (receipt.status === 1) { let operatorJobHash; let operatorJobPayload; let args; let rawData; switch (this.environment) { case environment_1.Environment.localhost: operatorJobHash = this.networkMonitor.decodeCrossChainMessageSentEvent(receipt, this.networkMonitor.operatorAddress); if (operatorJobHash !== undefined) { args = this.networkMonitor.decodeLzEvent(receipt, this.networkMonitor.lzEndpointAddress[network]); if (args !== undefined) { operatorJobPayload = args[2]; } } break; default: operatorJobHash = this.networkMonitor.decodeCrossChainMessageSentEvent(receipt, this.networkMonitor.operatorAddress); if (operatorJobHash !== undefined) { operatorJobPayload = this.networkMonitor.decodeLzPacketEvent(receipt); } break; } if (operatorJobHash === undefined) { this.networkMonitor.structuredLog(network, `Could not find a bridgeOutRequest for ${transaction.hash}`, tags); } else { // check that operatorJobPayload and operatorJobHash are the same if ((0, utils_1.sha3)(operatorJobPayload) !== operatorJobHash) { throw new Error('The hashed operatorJobPayload does not equal operatorJobHash!'); } const index = operatorJobHash in this.operatorJobIndexMap ? this.operatorJobIndexMap[operatorJobHash] : -1; const beam = index >= 0 ? this.transactionLogs[index] : { completed: false }; beam.logType = LogType.AvailableJob; beam.jobHash = operatorJobHash; beam.bridgeTx = transaction.hash; beam.bridgeNetwork = network; beam.bridgeBlock = transaction.blockNumber; beam.bridgeAddress = transaction.from; const parsedTransaction = this.networkMonitor.bridgeContract.interface.parseTransaction(transaction); if (parsedTransaction === null) { beam.jobType = network_monitor_1.TransactionType.unknown; } else { const toNetwork = (0, networks_1.getNetworkByHolographId)(parsedTransaction.args[0]).key; beam.messageNetwork = toNetwork; beam.operatorNetwork = toNetwork; const holographableContractAddress = parsedTransaction.args[1].toLowerCase(); if (holographableContractAddress === this.networkMonitor.factoryAddress) { beam.jobType = network_monitor_1.TransactionType.deploy; } else { const slot = await this.networkMonitor.providers[network].getStorageAt(holographableContractAddress, (0, utils_1.storageSlot)('eip1967.Holograph.contractType')); const contractType = (0, utils_1.toAscii)(slot); if (contractType === 'HolographERC20') { beam.jobType = network_monitor_1.TransactionType.erc20; } else if (contractType === 'HolographERC721') { beam.jobType = network_monitor_1.TransactionType.erc721; // creating json data field const erc721TransferEvent = this.networkMonitor.decodeErc721TransferEvent(receipt, holographableContractAddress); if (erc721TransferEvent === undefined) { this.warn("Couldn't create raw json data, since the tokenId is undefined"); this.exit(); } const from = erc721TransferEvent[0]; const to = erc721TransferEvent[1]; const tokenId = erc721TransferEvent[2]; rawData = { operatorJobPayload, from, to, tokenId, holographId: networks_1.networks[beam.bridgeNetwork].holographId, collection: holographableContractAddress, }; } } } this.networkMonitor.structuredLog(network, `Found a valid bridgeOutRequest for ${transaction.hash}`, tags); this.manageOperatorJobMaps(index, operatorJobHash, beam); await this.updateBeamStatusDB(beam, rawData); } } } /** * Handle the AvailableOperatorJob event from the Holograph Operator, when one is picked up while processing transactions */ async handleAvailableOperatorJobEvent(transaction, network, tags) { const receipt = await this.networkMonitor.getTransactionReceipt({ network, transactionHash: transaction.hash, attempts: 10, canFail: true, }); if (receipt === null) { throw new Error(`Could not get receipt for ${transaction.hash}`); } if (receipt.status === 1) { const args = this.networkMonitor.decodeAvailableOperatorJobEvent(receipt, this.networkMonitor.operatorAddress); const operatorJobHash = args === undefined ? undefined : args[0]; const operatorJobPayload = args === undefined ? undefined : args[1]; if (operatorJobHash === undefined) { this.networkMonitor.structuredLog(network, `Could not find an availableOperatorJob event for ${transaction.hash}`, tags); } else { // check that operatorJobPayload and operatorJobHash are the same if ((0, utils_1.sha3)(operatorJobPayload) !== operatorJobHash) { throw new Error('The hashed operatorJobPayload does not equal operatorJobHash!'); } const index = operatorJobHash in this.operatorJobIndexMap ? this.operatorJobIndexMap[operatorJobHash] : -1; const beam = index >= 0 ? this.transactionLogs[index] : {}; beam.logType = LogType.AvailableJob; beam.jobHash = operatorJobHash; beam.messageTx = transaction.hash; beam.messageNetwork = network; beam.messageBlock = transaction.blockNumber; beam.messageAddress = transaction.from; if (beam.completed !== true) { beam.completed = await this.validateOperatorJob(transaction.hash, network, operatorJobPayload, tags); } this.networkMonitor.structuredLog(network, `Found a valid availableOperatorJob for ${transaction.hash}`, tags); this.manageOperatorJobMaps(index, operatorJobHash, beam); await this.updateBeamStatusDB(beam); } } } /** * Finds bridge in events and keeps track of them */ async handleBridgeInEvent(transaction, network, tags) { const receipt = await this.networkMonitor.getTransactionReceipt({ network, transactionHash: transaction.hash, attempts: 10, canFail: true, }); if (receipt === null) { throw new Error(`Could not get receipt for ${transaction.hash}`); } if (receipt.status === 1) { const parsedTransaction = this.networkMonitor.operatorContract.interface.parseTransaction(transaction); if (parsedTransaction.name === 'executeJob') { const args = Object.values(parsedTransaction.args); const operatorJobPayload = args === undefined ? undefined : args[0]; const operatorJobHash = operatorJobPayload === undefined ? undefined : (0, utils_1.sha3)(operatorJobPayload); if (operatorJobHash === undefined) { this.networkMonitor.structuredLog(network, `Could not find a bridgeInRequest for ${transaction.hash}`, tags); } else { const index = operatorJobHash in this.operatorJobIndexMap ? this.operatorJobIndexMap[operatorJobHash] : -1; const beam = index >= 0 ? this.transactionLogs[index] : { completed: false }; beam.logType = LogType.AvailableJob; beam.operatorTx = transaction.hash; beam.operatorBlock = transaction.blockNumber; beam.operatorAddress = transaction.from; beam.completed = true; const bridgeTransaction = this.networkMonitor.bridgeContract.interface.parseTransaction({ data: operatorJobPayload }); if (parsedTransaction === null) { beam.jobType = network_monitor_1.TransactionType.unknown; } else { const fromNetwork = (0, networks_1.getNetworkByHolographId)(bridgeTransaction.args[1]).key; beam.bridgeNetwork = fromNetwork; const holographableContractAddress = bridgeTransaction.args[2].toLowerCase(); if (holographableContractAddress === this.networkMonitor.factoryAddress) { beam.jobType = network_monitor_1.TransactionType.deploy; } else { const slot = await this.networkMonitor.providers[network].getStorageAt(holographableContractAddress, (0, utils_1.storageSlot)('eip1967.Holograph.contractType')); const contractType = (0, utils_1.toAscii)(slot); if (contractType === 'HolographERC20') { beam.jobType = network_monitor_1.TransactionType.erc20; } else if (contractType === 'HolographERC721') { beam.jobType = network_monitor_1.TransactionType.erc721; } } } this.networkMonitor.structuredLog(network, `Found a valid bridgeOutRequest for ${transaction.hash}`, tags); this.manageOperatorJobMaps(index, operatorJobHash, beam); await this.updateBeamStatusDB(beam); } } else { this.networkMonitor.structuredLog(network, `Unknown bridge function executed for ${transaction.hash}`, tags); } } } /** * Checks if the operator job is valid and has not already been executed */ async validateOperatorJob(transactionHash, network, payload, tags) { const contract = this.networkMonitor.operatorContract.connect(this.networkMonitor.providers[network]); const gasLimit = await this.networkMonitor.getGasLimit({ network, contract, methodName: 'executeJob', args: [payload], }); if (gasLimit === null) { this.networkMonitor.structuredLog(network, `Transaction: ${transactionHash} has already been done`, tags); return true; } this.networkMonitor.structuredLog(network, `Transaction: ${transactionHash} job needs to be done`, tags); return false; } } exports.default = Analyze;