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