@solarity/hardhat-migrate
Version:
The simplest way to deploy smart contracts
540 lines (382 loc) • 17.2 kB
text/typescript
/* 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
*/
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;
}