@holographxyz/cli
Version:
Holograph operator CLI
278 lines (277 loc) • 13.8 kB
JavaScript
"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;