UNPKG

@holographxyz/cli

Version:
278 lines (277 loc) 13.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const fs = tslib_1.__importStar(require("fs-extra")); const inquirer = tslib_1.__importStar(require("inquirer")); const core_1 = require("@oclif/core"); const config_1 = require("../../utils/config"); const networks_1 = require("@holographxyz/networks"); const utils_1 = require("../../utils/utils"); const contract_deployment_1 = require("../../utils/contract-deployment"); const network_monitor_1 = require("../../utils/network-monitor"); const healthcheck_1 = require("../../base-commands/healthcheck"); class Propagator extends healthcheck_1.HealthCheck { static hidden = true; static description = 'Listen for EVM events deploys collections to the supported networks'; static examples = [ '$ <%= config.bin %> <%= command.id %> --networks ethereumTestnetRinkeby polygonTestnet avalancheTestnet --mode=auto', ]; static flags = { mode: core_1.Flags.string({ description: 'The mode in which to run the propagator', options: ['listen', 'manual', 'auto'], char: 'm', }), sync: core_1.Flags.boolean({ description: 'Start from last saved block position instead of latest block position', default: false, }), unsafePassword: core_1.Flags.string({ description: 'Enter the plain text password for the wallet in the holograph cli config', }), ...network_monitor_1.warpFlag, ...network_monitor_1.networksFlag, recover: core_1.Flags.string({ description: 'Provide a JSON array of RecoveryData objects to manually ensure propagation', default: '[]', }), recoverFile: core_1.Flags.string({ description: 'Filename reference to JSON array of RecoveryData objects to manually ensure propagation', }), ...healthcheck_1.HealthCheck.flags, }; crossDeployments = []; operatorMode = network_monitor_1.OperatorMode.listen; networkMonitor; /** * Command Entry Point */ async run() { const { flags } = await this.parse(Propagator); const enableHealthCheckServer = flags.healthCheck; const healthCheckPort = flags.healthCheckPort; const syncFlag = flags.sync; const unsafePassword = flags.unsafePassword; // Have the user input the mode if it's not provided let mode = flags.mode; if (!mode) { const prompt = await inquirer.prompt([ { name: 'mode', message: 'Enter the mode in which to run the operator', type: 'list', choices: ['listen', 'manual', 'auto'], default: 'listen', }, ]); mode = prompt.mode; } this.operatorMode = network_monitor_1.OperatorMode[mode]; this.log(`Operator mode: ${this.operatorMode}`); this.log('Loading user configurations...'); const { userWallet, configFile } = await (0, config_1.ensureConfigFileIsValid)(this.config.configDir, unsafePassword, true); this.log('User configurations loaded.'); this.networkMonitor = new network_monitor_1.NetworkMonitor({ parent: this, configFile, networks: flags.networks, debug: this.debug, processTransactions: this.processTransactions, userWallet, lastBlockFilename: 'propagator-blocks.json', warp: flags.warp, }); this.networkMonitor.latestBlockHeight = await this.networkMonitor.loadLastBlocks(this.config.configDir); let canSync = false; const lastBlockKeys = Object.keys(this.networkMonitor.latestBlockHeight); for (let i = 0, l = lastBlockKeys.length; i < l; i++) { if (this.networkMonitor.latestBlockHeight[lastBlockKeys[i]] > 0) { canSync = true; break; } } if (canSync && !syncFlag) { const syncPrompt = await inquirer.prompt([ { name: 'shouldSync', message: 'Propagator has previous (missed) blocks that can be synced. Would you like to sync?', type: 'confirm', default: true, }, ]); if (syncPrompt.shouldSync === false) { this.networkMonitor.latestBlockHeight = {}; this.networkMonitor.currentBlockHeight = {}; } } core_1.CliUx.ux.action.start(`Starting propagator in mode: ${network_monitor_1.OperatorMode[this.operatorMode]}`); await this.networkMonitor.run(!(flags.warp > 0), undefined, this.filterBuilder); core_1.CliUx.ux.action.stop('🚀'); let recoveryData = JSON.parse(flags.recover); const recoverDataFileString = flags.recoverFile; if (recoverDataFileString !== undefined && recoverDataFileString !== '') { if (fs.existsSync(recoverDataFileString)) { recoveryData = (await fs.readJson(recoverDataFileString)); } else { throw new Error('The recoverFile does not exist'); } } if (recoveryData.length > 0) { this.log(`Manually running ${recoveryData.length} recovery jobs`); for (const data of recoveryData) { let network = (0, networks_1.getNetworkByChainId)(data.chain_id).key; const checkNetworks = networks_1.supportedNetworks; if (checkNetworks.includes(network)) { checkNetworks.splice(checkNetworks.indexOf(network), 1); } let tx = await this.networkMonitor.getTransaction({ transactionHash: data.tx, network, canFail: true, attempts: 10, interval: 500, }); for (const checkNetwork of checkNetworks) { if (tx === null) { this.networkMonitor.structuredLog(network, `Transaction ${data.tx} is on wrong network`); network = checkNetwork; tx = await this.networkMonitor.getTransaction({ transactionHash: data.tx, network, canFail: true, attempts: 10, interval: 500, }); } else { break; } } if (tx === null) { this.networkMonitor.structuredLog(network, `Could not find ${data.tx} on any network`); } else { await this.handleContractDeployedEvents(tx, network); } } this.log('Done running recovery jobs'); } // Start health check server on port 6000 or healthCheckPort // Can be used to monitor that the operator is online and running if (enableHealthCheckServer) { await this.config.runHook('healthCheck', { networkMonitor: this.networkMonitor, healthCheckPort }); } } async filterBuilder() { this.networkMonitor.filters = [ { type: network_monitor_1.FilterType.to, match: this.networkMonitor.factoryAddress, networkDependant: false, }, ]; return Promise.resolve(); } async processTransactions(job, transactions) { if (transactions.length > 0) { for (const transaction of transactions) { this.debug(`Processing transaction ${transaction.hash} on ${job.network} at block ${transaction.blockNumber}`); const to = transaction.to?.toLowerCase(); if (to === this.networkMonitor.factoryAddress) { await this.handleContractDeployedEvents(transaction, job.network); } else { this.networkMonitor.structuredLog(job.network, `Function processTransactions stumbled on an unknown transaction ${transaction.hash}`); } } } } async handleContractDeployedEvents(transaction, network) { 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) { this.networkMonitor.structuredLog(network, `Checking if a new Holograph contract was deployed at tx: ${transaction.hash}`); const deploymentInfo = this.networkMonitor.decodeBridgeableContractDeployedEvent(receipt, this.networkMonitor.factoryAddress); if (deploymentInfo === undefined) { this.networkMonitor.structuredLog(network, `BridgeableContractDeployed event not found in ${transaction.hash}`); } else { const deploymentAddress = deploymentInfo[0]; const config = (0, contract_deployment_1.decodeDeploymentConfigInput)(transaction.data); this.networkMonitor.structuredLog(network, `HolographFactory deployed a new collection on ${(0, utils_1.capitalize)(network)} at address ${deploymentAddress}. Wallet that deployed the collection is ${transaction.from}. The config used for deployHolographableContract was ${JSON.stringify(config, null, 2)}. The transaction hash is: ${transaction.hash}`); if (this.operatorMode !== network_monitor_1.OperatorMode.listen && !this.crossDeployments.includes(deploymentAddress.toLowerCase())) { await this.executePayload(network, config, deploymentAddress); } } } } async deployContract(network, deploymentConfig, deploymentAddress) { const contractCode = await this.networkMonitor.providers[network].getCode(deploymentAddress, 'latest'); const registry = this.networkMonitor.registryContract.connect(this.networkMonitor.providers[network]); if ((contractCode === '0x' || contractCode === '' || contractCode === undefined) && !(await registry.callStatic.isHolographedContract(deploymentAddress, { blockTag: 'latest' }))) { const deployReceipt = await this.networkMonitor.executeTransaction({ network, contract: this.networkMonitor.factoryContract, methodName: 'deployHolographableContract', args: [deploymentConfig.config, deploymentConfig.signature, deploymentConfig.signer], }); if (deployReceipt === null) { this.networkMonitor.structuredLog(network, `Submitting tx for collection ${deploymentAddress} failed`); } else { this.networkMonitor.structuredLog(network, `Transaction minted with hash ${deployReceipt.transactionHash} for collection ${deploymentAddress}`); const deploymentInfo = this.networkMonitor.decodeBridgeableContractDeployedEvent(deployReceipt, this.networkMonitor.factoryAddress); if (deploymentInfo === undefined) { this.networkMonitor.structuredLog(network, `Failed extracting BridgeableContractDeployedEvent for collection ${deploymentAddress}`); } else { const collectionAddress = deploymentInfo[0]; this.networkMonitor.structuredLog(network, `Successfully deployed collection ${collectionAddress} = ${deploymentAddress}`); } } } else { this.networkMonitor.structuredLog(network, `Collection ${deploymentAddress} already deployed`); } } async executePayload(network, config, deploymentAddress) { // If the propagator is in listen mode, contract deployments will not be executed // If the propagator is in manual mode, the contract deployments must be manually executed // If the propagator is in auto mode, the contract deployments will be executed automatically let operate = this.operatorMode === network_monitor_1.OperatorMode.auto; if (this.operatorMode === network_monitor_1.OperatorMode.manual) { const propagatorPrompt = await inquirer.prompt([ { name: 'shouldContinue', message: `A contract appeared on ${network} for cross-chain deployment, would you like to deploy?\n`, type: 'confirm', default: false, }, ]); operate = propagatorPrompt.shouldContinue; } if (operate) { this.crossDeployments.push(deploymentAddress.toLowerCase()); for (const selectedNetwork of this.networkMonitor.networks) { if (selectedNetwork !== network) { this.networkMonitor.structuredLog(network, `Trying to deploy contract from ${network} to ${selectedNetwork}`); await this.deployContract(selectedNetwork, config, deploymentAddress); } } } else { this.networkMonitor.structuredLog(network, 'Dropped potential contract deployment to execute'); } } } exports.default = Propagator;