UNPKG

@solarity/hardhat-migrate

Version:
540 lines (382 loc) 17.2 kB
/* eslint-disable no-console */ import ora, { Ora } from "ora"; import { Network, TransactionResponse, TransactionReceipt, id } from "ethers"; import { HardhatRuntimeEnvironment } from "hardhat/types"; import { ReporterStorage } from "./ReporterStorage"; import { networkManager } from "../network/NetworkManager"; import { castAmount, catchError, underline } from "../../utils"; import { MigrateConfig } from "../../types/migrations"; import { ChainRecord, CustomChainRecord, predefinedChains } from "../../types/verifier"; import { ContractFieldsToSave, MigrationMetadata, TransactionFieldsToSave } from "../../types/tools"; /** * Global error handling for network-related issues is conducted within the NetworkManager class */ @catchError class BaseReporter { private _hre: HardhatRuntimeEnvironment = {} as any; private _config: MigrateConfig = {} as any; private _network: Network = {} as any; private _spinner: Ora | null = null; private _spinnerMessage: string | null = null; private _spinnerInterval: NodeJS.Timeout | null = null; private _spinnerState: string[] = []; private _nativeSymbol: string = ""; private _explorerUrl: string = ""; private _txExplorerUrl: string = ""; private _warningsToPrint: Map<string, string> = new Map(); private _storage: ReporterStorage | null = null; public async init(hre: HardhatRuntimeEnvironment) { this._hre = hre; this._config = hre.config.migrate; this._network = await this._getNetwork(); this._nativeSymbol = await this._getNativeSymbol(); this._explorerUrl = await this.getExplorerUrl(); try { this._txExplorerUrl = this._explorerUrl !== "" ? new URL("tx/", this._explorerUrl).toString() : ""; } catch { this._txExplorerUrl = ""; } this._storage = new ReporterStorage(hre); } public async completeReport() { await this._storage!.finishReport(); } public notifyStorageAboutContracts(contracts: [name: string, address: string][]) { this._storage!.storeReportedContracts(contracts); } public reportMigrationBegin(files: string[]) { this._reportMigrationFiles(files); this._reportChainInfo(); console.log("\nStarting migration...\n"); this._storage!.storeMigrationBegin(); } public reportMigrationFileBegin(file: string) { console.log(`\n${underline(`Running ${file}...`)}`); this._storage!.storeMigrationFileBegin(file); } public reportTransactionResponseHeader(tx: TransactionResponse, instanceName: string) { console.log("\n" + underline(this._parseTransactionTitle(tx, instanceName))); const txLink = this._getExplorerLink(tx.hash); console.log(`> explorer: ${txLink}`); this._storage!.storeTransactionResponseHeader(tx, instanceName, txLink); } public async startTxReporting(tx: TransactionResponse) { if (this._hre.config.migrate.execution.withoutCLIReporting) { return; } const timeStart = Date.now(); const blockStart = await networkManager!.provider.getBlockNumber(); const formatPendingTimeTask = async () => this._formatPendingTime(tx, timeStart, blockStart); return this.startSpinner("tx-report", formatPendingTimeTask); } public async startSpinner(id: string, getSpinnerText: (args?: any) => string | Promise<string>) { if (this._spinnerState.includes(id)) return; if (this._spinnerState.length === 0) { this._spinner = ora(await getSpinnerText()).start(); this._spinnerInterval = setInterval(async () => { if (!this._spinner) { clearInterval(this._spinnerInterval!); return; } this._spinner.text = await getSpinnerText(); }, this._config.execution.transactionStatusCheckInterval); } this._spinnerState.push(id); } public stopSpinner() { if (!this._spinner) return; this._spinnerMessage = null; this._spinnerState.pop(); if (this._spinnerState.length === 0) { clearInterval(this._spinnerInterval!); this._spinner.stop(); this._spinner = null; } } public async reportTransactionReceipt(receipt: TransactionReceipt) { let output = ""; if (receipt.contractAddress) { output += `> contractAddress: ${receipt.contractAddress}\n`; } const nativeSymbol = this._nativeSymbol; output += `> blockNumber: ${receipt.blockNumber}\n`; output += `> blockTimestamp: ${(await receipt.getBlock()).timestamp}\n`; output += `> account: ${receipt.from}\n`; const value = (await receipt.getTransaction()).value; output += `> value: ${castAmount(value, nativeSymbol)}\n`; output += `> balance: ${castAmount(await receipt.provider.getBalance(receipt.from), nativeSymbol)}\n`; output += `> gasUsed: ${receipt.gasUsed}\n`; output += `> gasPrice: ${castAmount(receipt.gasPrice, nativeSymbol)}\n`; output += `> fee: ${castAmount(receipt.fee, nativeSymbol)}\n`; console.log(output); this._storage!.storeTransactionReceipt(receipt, value); } public summary(totalTransactions: bigint, totalCost: bigint) { const output = `> ${"Total transactions:".padEnd(20)} ${totalTransactions}\n` + `> ${"Final cost:".padEnd(20)} ${castAmount(totalCost, this._nativeSymbol)}\n`; console.log(`\n${output}`); this.reportWarnings(); this._storage!.completeReport(); } public notifyDeploymentInsteadOfRecovery(contractName: string): void { const output = `\nCan't recover contract address for ${contractName}. Deploying instead...`; console.log(output); } public notifyDeploymentOfMissingLibrary(libraryName: string): void { const output = `\nDeploying missing library ${libraryName}...`; console.log(output); this._storage!.storeDeploymentOfMissingLibrary(libraryName); } public notifyTransactionSendingInsteadOfRecovery(contractMethod: string): void { const output = `\nCan't recover transaction for ${contractMethod}. Sending instead...`; console.log(output); } public notifyContractRecovery(contractName: string, contractAddress: string): void { const output = `\nContract address for ${contractName} has been recovered: ${contractAddress}`; console.log(output); this._storage!.storeContractRecovery(contractName, contractAddress); } public notifyTransactionRecovery(methodString: string, savedTx: TransactionFieldsToSave): void { const output = `\nTransaction ${methodString} has been recovered.`; console.log(output); this._storage!.storeTransactionRecovery(methodString, savedTx); } public reportVerificationBatchBegin() { console.log("\nStarting verification of all deployed contracts"); } public reportNothingToVerify() { console.log(`\nNothing to verify. Selected network is ${this._network.name}`); this._storage!.storeNothingToVerify(); } public reportSuccessfulVerification(contractAddress: string, contractName: string) { const output = `\nContract ${contractName} (${contractAddress}) verified successfully.`; console.log(output); this._storage!.storeVerificationSuccess(contractName, contractAddress); } public reportAlreadyVerified(contractAddress: string, contractName: string) { const output = `\nContract ${contractName} (${contractAddress}) already verified.`; console.log(output); this._storage!.storeAlreadyVerified(contractName, contractAddress); } public reportVerificationError(contractAddress: string, contractName: string, message: string) { const output = `\nContract ${contractName} (${contractAddress}) verification failed: ${message}`; console.log(output); this._storage!.storeVerificationFailure(contractName, contractAddress); } public reportVerificationFailedToSave(contractName: string) { const output = `\nFailed to save verification arguments for contract: ${contractName}`; console.log(output); this._storage!.storeVerificationSaveFailure(contractName); } public notifyContractCollisionByName(oldData: ContractFieldsToSave, dataToSave: ContractFieldsToSave) { const output = `\nContract collision by Contract Name detected!`; this._printContractCollision(output, oldData, dataToSave); } public notifyContractCollisionByKeyFields(oldData: ContractFieldsToSave, dataToSave: ContractFieldsToSave) { let output = `\nContract collision by key fields detected!`; output += `\nKey fields are bytecode, from, chainId, value and contract name`; this._printContractCollision(output, oldData, dataToSave); } public notifyTransactionCollision(oldData: TransactionFieldsToSave, dataToSave: TransactionFieldsToSave) { let output = `\nTransaction collision detected!`; output += `\n> Previous Collision Details: `; output += `\n\t- Migration Number: ${oldData.metadata.migrationNumber}`; output += `\n\t- Method Name: ${oldData.metadata.methodName || "N/A"}`; output += `\n> New Collision Details: `; output += `\n\t- Migration Number: ${dataToSave.metadata.migrationNumber}`; output += `\n\t- Method Name: ${dataToSave.metadata.methodName || "N/A"}`; const key = id(output); if (!this._warningsToPrint.has(key)) { console.log(output); } this._warningsToPrint.set(key, output); this._storage!.storeTransactionCollision(oldData, dataToSave); } public addWarning(warning: string) { this._warningsToPrint.set(id(warning), warning); } public notifyUnknownCollision( metadata: MigrationMetadata, dataToSave: TransactionFieldsToSave | ContractFieldsToSave, ) { let output = `\nUnknown collision detected!`; output += `\n> Previous Collision Details: `; output += `\n\t- Migration Number: ${metadata.migrationNumber}`; output += `\n\t- Method Name: ${metadata.methodName || "N/A"}`; output += `\n\t- Contract Name: ${metadata.contractName || "N/A"}`; output += `\n> New Collision Details: `; output += `\n\t- Migration Number: ${dataToSave.metadata.migrationNumber}`; output += `\n\t- Method Name: ${dataToSave.metadata.methodName || "N/A"}`; output += `\n\t- Contract Name: ${dataToSave.metadata.contractName || "N/A"}`; const key = id(output); if (!this._warningsToPrint.has(key)) { console.log(output); } this._warningsToPrint.set(key, output); this._storage!.storeUnknownCollision(metadata, dataToSave); } public reportWarnings() { if (this.getWarningsCount() === 0) { return; } console.log("\nWarnings:"); this._warningsToPrint.forEach((warning) => { console.log(warning); }); console.log( "\n\nDue to the detected collision(s), there's a high likelihood that migration recovery using '--continue' may not function as expected.\n" + "To mitigate this, consider specifying a unique name for the contract during deployment.\n", ); console.log(""); } public getWarningsCount(): number { return this._warningsToPrint.size; } private _printContractCollision(output: string, oldData: ContractFieldsToSave, dataToSave: ContractFieldsToSave) { output += `\n> Contract: ${oldData.contractKeyData?.name || dataToSave.contractKeyData?.name}`; output += `\n> Previous Collision Details: `; output += `\n\t- Migration Number: ${oldData.metadata.migrationNumber}`; output += `\n\t- Contract Address: ${oldData.contractAddress}`; output += `\n> New Collision Details: `; output += `\n\t- Migration Number: ${dataToSave.metadata.migrationNumber}`; output += `\n\t- Contract Address: ${dataToSave.contractAddress}`; const key = id(`${oldData.contractAddress}-${dataToSave.contractAddress}`); if (!this._warningsToPrint.has(key)) { console.log(output); } this._warningsToPrint.set(key, output); this._storage!.storeContractCollision(oldData, dataToSave); } public async getExplorerUrl(): Promise<string> { const chainId = Number(this._network.chainId); const customChain = this._getInfoFromHardhatConfig(chainId); if (customChain) { return customChain.urls.browserURL; } if ( predefinedChains[chainId] && predefinedChains[chainId].explorers !== undefined && predefinedChains[chainId].explorers.length > 0 ) { return predefinedChains[chainId].explorers[0].url; } const chain = await this._getChainMetadataById(chainId); return chain.explorers[0].url; } public reportSuccessfulProxyLinking(proxyAddress: string, implementationAddress: string) { console.log(`Proxy ${proxyAddress} linked to implementation ${implementationAddress}`); this._storage!.storeSuccessfulProxyLinking(proxyAddress, implementationAddress); } public reportFailedProxyLinking(proxyAddress: string, implementationAddress: string, result: string) { console.log(`Failed to link proxy ${proxyAddress} to implementation ${implementationAddress}: ${result}`); this._storage!.storeFailedProxyLinking(proxyAddress, implementationAddress); } public notifyOfProxyConstructorUsage(proxyFactoryName: string, constructorName: string) { console.log(`\nProxy constructor ${constructorName} used in ${proxyFactoryName} (proxy)\n`); } private _parseTransactionTitle(tx: TransactionResponse, instanceName: string): string { if (tx.to === null) { if (instanceName.split(":").length == 1) { return `Deploying ${instanceName}`; } return `Deploying${instanceName ? " " + instanceName.split(":")[1] : ""}`; } return `Transaction: ${instanceName}`; } private async _formatPendingTime(tx: TransactionResponse, startTime: number, blockStart: number): Promise<string> { if (this._spinnerMessage) { return this._spinnerMessage; } return `Confirmations: ${await tx.confirmations()}; Blocks: ${ (await networkManager!.provider.getBlockNumber()) - blockStart }; Seconds: ${((Date.now() - startTime) / 1000).toFixed(0)}`; } private _getExplorerLink(txHash: string): string { try { return this._txExplorerUrl !== "" ? new URL(txHash, this._txExplorerUrl).toString() : `tx/${txHash}`; } catch { return `tx/${txHash}`; } } private _reportMigrationFiles(files: string[]) { console.log("\nMigration files:"); files.forEach((file) => { console.log(`> ${file}`); }); console.log(""); this._storage!.storeMigrationFiles(files); } private _reportChainInfo() { console.log(`> ${"Network:".padEnd(20)} ${this._network.name}`); console.log(`> ${"Network id:".padEnd(20)} ${this._network.chainId}`); this._storage!.storeChainInfo({ network: this._network, explorer: this._explorerUrl }); } private async _getNetwork(): Promise<Network> { try { return networkManager!.provider.getNetwork(); } catch { return new Network("Local Ethereum", 1337); } } private async _getNativeSymbol(): Promise<string> { const chainId = Number(this._network.chainId); if (predefinedChains[chainId]) { return predefinedChains[chainId].nativeCurrency.symbol; } const chain = await this._getChainMetadataById(chainId); return chain.nativeCurrency.symbol; } private async _getChainMetadataById(chainId: number): Promise<ChainRecord> { let chain: ChainRecord; try { const chains = await this._tryGetAllRecords(); chain = chains.find((chain) => chain.chainId === chainId) ?? predefinedChains[1337]; } catch { chain = predefinedChains[1337]; } if (chain.explorers === undefined || chain.explorers.length === 0) { chain.explorers = [ { url: "", name: "", }, ]; } const hardhatChainInfo = this._getInfoFromHardhatConfig(chainId); if (hardhatChainInfo && chain.explorers.length > 0 && hardhatChainInfo.urls.browserURL !== chain.explorers[0].url) { chain.explorers[0].url = hardhatChainInfo.urls.browserURL; // Also we reset the Native Currency symbol as it may be different chain.nativeCurrency.symbol = predefinedChains[1337].nativeCurrency.symbol; } predefinedChains[chainId] = chain; return chain; } private _getInfoFromHardhatConfig(chainId: number): CustomChainRecord | undefined { let customChains: CustomChainRecord[] = []; if ((this._hre.config as any).etherscan && (this._hre.config as any).etherscan.customChains) { customChains = (this._hre.config as any).etherscan.customChains; } return customChains.find((chain) => chain.chainId === chainId); } private async _tryGetAllRecords(): Promise<ChainRecord[]> { const url = "https://chainid.network/chains.json"; const response = await networkManager!.axios.get(url); // Assuming the JSON response is an array of record objects return response.data as ChainRecord[]; } } export let Reporter: BaseReporter | null = null; export async function createAndInitReporter(hre: HardhatRuntimeEnvironment) { if (Reporter) { return; } Reporter = new BaseReporter(); await Reporter.init(hre); } /** * Used only in test environments to ensure test atomicity */ export function resetReporter() { Reporter = null; }