UNPKG

hardhat

Version:

Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.

496 lines (434 loc) 15 kB
import type { Artifacts, EIP1193Provider, EthSubscription, HardhatNetworkChainsConfig, RequestArguments, } from "../../../types"; import type { EdrContext, Provider as EdrProviderT, Response, SubscriptionEvent, HttpHeader, TracingConfigWithBuffers, } from "@nomicfoundation/edr"; import picocolors from "picocolors"; import debug from "debug"; import { EventEmitter } from "events"; import fsExtra from "fs-extra"; import { requireNapiRsModule } from "../../../common/napi-rs"; import { HARDHAT_NETWORK_RESET_EVENT, HARDHAT_NETWORK_REVERT_SNAPSHOT_EVENT, } from "../../constants"; import { InvalidArgumentsError, InvalidInputError, ProviderError, } from "../../core/providers/errors"; import { isErrorResponse } from "../../core/providers/http"; import { getHardforkName } from "../../util/hardforks"; import { ConsoleLogger } from "../stack-traces/consoleLogger"; import { encodeSolidityStackTrace } from "../stack-traces/solidity-errors"; import { SolidityStackTrace } from "../stack-traces/solidity-stack-trace"; import { getPackageJson } from "../../util/packageInfo"; import { ForkConfig, GenesisAccount, IntervalMiningConfig, MempoolOrder, } from "./node-types"; import { edrRpcDebugTraceToHardhat, edrTracingMessageResultToMinimalEVMResult, edrTracingMessageToMinimalMessage, edrTracingStepToMinimalInterpreterStep, ethereumjsIntervalMiningConfigToEdr, ethereumjsMempoolOrderToEdrMineOrdering, ethereumsjsHardforkToEdrSpecId, } from "./utils/convertToEdr"; import { LoggerConfig, printLine, replaceLastLine } from "./modules/logger"; import { MinimalEthereumJsVm, getMinimalEthereumJsVm } from "./vm/minimal-vm"; const log = debug("hardhat:core:hardhat-network:provider"); /* eslint-disable @nomicfoundation/hardhat-internal-rules/only-hardhat-error */ export const DEFAULT_COINBASE = "0xc014ba5ec014ba5ec014ba5ec014ba5ec014ba5e"; let _globalEdrContext: EdrContext | undefined; // Lazy initialize the global EDR context. export function getGlobalEdrContext(): EdrContext { const { EdrContext } = requireNapiRsModule( "@nomicfoundation/edr" ) as typeof import("@nomicfoundation/edr"); if (_globalEdrContext === undefined) { // Only one is allowed to exist _globalEdrContext = new EdrContext(); } return _globalEdrContext; } interface HardhatNetworkProviderConfig { hardfork: string; chainId: number; networkId: number; blockGasLimit: number; minGasPrice: bigint; automine: boolean; intervalMining: IntervalMiningConfig; mempoolOrder: MempoolOrder; chains: HardhatNetworkChainsConfig; genesisAccounts: GenesisAccount[]; allowUnlimitedContractSize: boolean; throwOnTransactionFailures: boolean; throwOnCallFailures: boolean; allowBlocksWithSameTimestamp: boolean; initialBaseFeePerGas?: number; initialDate?: Date; coinbase?: string; forkConfig?: ForkConfig; forkCachePath?: string; enableTransientStorage: boolean; enableRip7212: boolean; } class EdrProviderEventAdapter extends EventEmitter {} type CallOverrideCallback = ( address: Buffer, data: Buffer ) => Promise< { result: Buffer; shouldRevert: boolean; gas: bigint } | undefined >; export class EdrProviderWrapper extends EventEmitter implements EIP1193Provider { private _failedStackTraces = 0; // temporarily added to make smock work with HH+EDR private _callOverrideCallback?: CallOverrideCallback; private constructor( private readonly _provider: EdrProviderT, // we add this for backwards-compatibility with plugins like solidity-coverage private readonly _node: { _vm: MinimalEthereumJsVm; } ) { super(); } public static async create( config: HardhatNetworkProviderConfig, loggerConfig: LoggerConfig, tracingConfig?: TracingConfigWithBuffers ): Promise<EdrProviderWrapper> { const { Provider } = requireNapiRsModule( "@nomicfoundation/edr" ) as typeof import("@nomicfoundation/edr"); const coinbase = config.coinbase ?? DEFAULT_COINBASE; let fork; if (config.forkConfig !== undefined) { let httpHeaders: HttpHeader[] | undefined; if (config.forkConfig.httpHeaders !== undefined) { httpHeaders = []; for (const [name, value] of Object.entries( config.forkConfig.httpHeaders )) { httpHeaders.push({ name, value, }); } } fork = { jsonRpcUrl: config.forkConfig.jsonRpcUrl, blockNumber: config.forkConfig.blockNumber !== undefined ? BigInt(config.forkConfig.blockNumber) : undefined, httpHeaders, }; } const initialDate = config.initialDate !== undefined ? BigInt(Math.floor(config.initialDate.getTime() / 1000)) : undefined; // To accommodate construction ordering, we need an adapter to forward events // from the EdrProvider callback to the wrapper's listener const eventAdapter = new EdrProviderEventAdapter(); const printLineFn = loggerConfig.printLineFn ?? printLine; const replaceLastLineFn = loggerConfig.replaceLastLineFn ?? replaceLastLine; const hardforkName = getHardforkName(config.hardfork); const provider = await Provider.withConfig( getGlobalEdrContext(), { allowBlocksWithSameTimestamp: config.allowBlocksWithSameTimestamp ?? false, allowUnlimitedContractSize: config.allowUnlimitedContractSize, bailOnCallFailure: config.throwOnCallFailures, bailOnTransactionFailure: config.throwOnTransactionFailures, blockGasLimit: BigInt(config.blockGasLimit), chainId: BigInt(config.chainId), chains: Array.from(config.chains, ([chainId, hardforkConfig]) => { return { chainId: BigInt(chainId), hardforks: Array.from( hardforkConfig.hardforkHistory, ([hardfork, blockNumber]) => { return { blockNumber: BigInt(blockNumber), specId: ethereumsjsHardforkToEdrSpecId( getHardforkName(hardfork) ), }; } ), }; }), cacheDir: config.forkCachePath, coinbase: Buffer.from(coinbase.slice(2), "hex"), enableRip7212: config.enableRip7212, fork, hardfork: ethereumsjsHardforkToEdrSpecId(hardforkName), genesisAccounts: config.genesisAccounts.map((account) => { return { secretKey: account.privateKey, balance: BigInt(account.balance), }; }), initialDate, initialBaseFeePerGas: config.initialBaseFeePerGas !== undefined ? BigInt(config.initialBaseFeePerGas!) : undefined, minGasPrice: config.minGasPrice, mining: { autoMine: config.automine, interval: ethereumjsIntervalMiningConfigToEdr(config.intervalMining), memPool: { order: ethereumjsMempoolOrderToEdrMineOrdering(config.mempoolOrder), }, }, networkId: BigInt(config.networkId), }, { enable: loggerConfig.enabled, decodeConsoleLogInputsCallback: ConsoleLogger.getDecodedLogs, printLineCallback: (message: string, replace: boolean) => { if (replace) { replaceLastLineFn(message); } else { printLineFn(message); } }, }, tracingConfig ?? {}, (event: SubscriptionEvent) => { eventAdapter.emit("ethEvent", event); } ); const minimalEthereumJsNode = { _vm: getMinimalEthereumJsVm(provider), }; const wrapper = new EdrProviderWrapper(provider, minimalEthereumJsNode); // Pass through all events from the provider eventAdapter.addListener( "ethEvent", wrapper._ethEventListener.bind(wrapper) ); return wrapper; } public async request(args: RequestArguments): Promise<unknown> { if (args.params !== undefined && !Array.isArray(args.params)) { throw new InvalidInputError( "Hardhat Network doesn't support JSON-RPC params sent as an object" ); } const params = args.params ?? []; if (args.method === "hardhat_getStackTraceFailuresCount") { // stubbed for backwards compatibility return 0; } const stringifiedArgs = JSON.stringify({ method: args.method, params, }); const responseObject: Response = await this._provider.handleRequest( stringifiedArgs ); let response; if (typeof responseObject.data === "string") { response = JSON.parse(responseObject.data); } else { response = responseObject.data; } const needsTraces = this._node._vm.evm.events.eventNames().length > 0 || this._node._vm.events.eventNames().length > 0; if (needsTraces) { const rawTraces = responseObject.traces; for (const rawTrace of rawTraces) { // For other consumers in JS we need to marshall the entire trace over FFI const trace = rawTrace.trace(); // beforeTx event if (this._node._vm.events.listenerCount("beforeTx") > 0) { this._node._vm.events.emit("beforeTx"); } for (const traceItem of trace) { // step event if ("pc" in traceItem) { if (this._node._vm.evm.events.listenerCount("step") > 0) { this._node._vm.evm.events.emit( "step", edrTracingStepToMinimalInterpreterStep(traceItem) ); } } // afterMessage event else if ("executionResult" in traceItem) { if (this._node._vm.evm.events.listenerCount("afterMessage") > 0) { this._node._vm.evm.events.emit( "afterMessage", edrTracingMessageResultToMinimalEVMResult(traceItem) ); } } // beforeMessage event else { if (this._node._vm.evm.events.listenerCount("beforeMessage") > 0) { this._node._vm.evm.events.emit( "beforeMessage", edrTracingMessageToMinimalMessage(traceItem) ); } } } // afterTx event if (this._node._vm.events.listenerCount("afterTx") > 0) { this._node._vm.events.emit("afterTx"); } } } if (isErrorResponse(response)) { let error; let stackTrace: SolidityStackTrace | null = null; try { stackTrace = responseObject.stackTrace(); } catch (e) { log("Failed to get stack trace: %O", e); } if (stackTrace !== null) { error = encodeSolidityStackTrace(response.error.message, stackTrace); // Pass data and transaction hash from the original error (error as any).data = response.error.data?.data ?? undefined; (error as any).transactionHash = response.error.data?.transactionHash ?? undefined; } else { if (response.error.code === InvalidArgumentsError.CODE) { error = new InvalidArgumentsError(response.error.message); } else { error = new ProviderError( response.error.message, response.error.code ); } error.data = response.error.data; } // eslint-disable-next-line @nomicfoundation/hardhat-internal-rules/only-hardhat-error throw error; } if (args.method === "hardhat_reset") { this.emit(HARDHAT_NETWORK_RESET_EVENT); } else if (args.method === "evm_revert") { this.emit(HARDHAT_NETWORK_REVERT_SNAPSHOT_EVENT); } // Override EDR version string with Hardhat version string with EDR backend, // e.g. `HardhatNetwork/2.19.0/@nomicfoundation/edr/0.2.0-dev` if (args.method === "web3_clientVersion") { return clientVersion(response.result); } else if ( args.method === "debug_traceTransaction" || args.method === "debug_traceCall" ) { return edrRpcDebugTraceToHardhat(response.result); } else { return response.result; } } // temporarily added to make smock work with HH+EDR private _setCallOverrideCallback(callback: CallOverrideCallback) { this._callOverrideCallback = callback; this._provider.setCallOverrideCallback( async (address: Buffer, data: Buffer) => { return this._callOverrideCallback?.(address, data); } ); } private _setVerboseTracing(enabled: boolean) { this._provider.setVerboseTracing(enabled); } private _ethEventListener(event: SubscriptionEvent) { const subscription = `0x${event.filterId.toString(16)}`; const results = Array.isArray(event.result) ? event.result : [event.result]; for (const result of results) { this._emitLegacySubscriptionEvent(subscription, result); this._emitEip1193SubscriptionEvent(subscription, result); } } private _emitLegacySubscriptionEvent(subscription: string, result: any) { this.emit("notification", { subscription, result, }); } private _emitEip1193SubscriptionEvent(subscription: string, result: unknown) { const message: EthSubscription = { type: "eth_subscription", data: { subscription, result, }, }; this.emit("message", message); } } async function clientVersion(edrClientVersion: string): Promise<string> { const hardhatPackage = await getPackageJson(); const edrVersion = edrClientVersion.split("/")[1]; return `HardhatNetwork/${hardhatPackage.version}/@nomicfoundation/edr/${edrVersion}`; } export async function createHardhatNetworkProvider( hardhatNetworkProviderConfig: HardhatNetworkProviderConfig, loggerConfig: LoggerConfig, artifacts?: Artifacts ): Promise<EIP1193Provider> { log("Making tracing config"); const tracingConfig = await makeTracingConfig(artifacts); log("Creating EDR provider"); const provider = await EdrProviderWrapper.create( hardhatNetworkProviderConfig, loggerConfig, tracingConfig ); log("EDR provider created"); return provider; } async function makeTracingConfig( artifacts: Artifacts | undefined ): Promise<TracingConfigWithBuffers | undefined> { if (artifacts !== undefined) { const buildInfoFiles = await artifacts.getBuildInfoPaths(); try { const buildInfos = await Promise.all( buildInfoFiles.map((filePath) => fsExtra.readFile(filePath)) ); return { buildInfos, }; } catch (error) { console.warn( picocolors.yellow( "Stack traces engine could not be initialized. Run Hardhat with --verbose to learn more." ) ); log( "Solidity stack traces disabled: Failed to read solc's input and output files. Please report this to help us improve Hardhat.\n", error ); } } }