UNPKG

hardhat

Version:

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

438 lines (391 loc) 15.5 kB
import type { CoverageConfig } from "./edr/types/coverage.js"; import type { ArtifactManager } from "../../../types/artifacts.js"; import type { ChainDescriptorsConfig, HardhatUserConfig, NetworkConfig, NetworkConfigOverride, } from "../../../types/config.js"; import type { HookManager } from "../../../types/hooks.js"; import type { ChainType, DefaultChainType, JsonRpcServer, NetworkConnection, NetworkConnectionParams, NetworkManager, } from "../../../types/network.js"; import type { HardhatPlugin } from "../../../types/plugins.js"; import type { EthereumProvider, JsonRpcRequest, JsonRpcResponse, } from "../../../types/providers.js"; import type { GasReportConfig } from "@nomicfoundation/edr"; import { HardhatError, assertHardhatInvariant, } from "@nomicfoundation/hardhat-errors"; import { exists, readBinaryFile } from "@nomicfoundation/hardhat-utils/fs"; import { deepMerge } from "@nomicfoundation/hardhat-utils/lang"; import { resolveUserConfigToHardhatConfig } from "../../core/hre.js"; import { isSupportedChainType } from "../../edr/chain-type.js"; import { JsonRpcServerImplementation } from "../node/json-rpc/server.js"; import { EdrProvider } from "./edr/edr-provider.js"; import { getHardforks } from "./edr/types/hardfork.js"; import { edrGasReportToHardhatGasMeasurements } from "./edr/utils/convert-to-edr.js"; import { HttpProvider } from "./http-provider.js"; import { NetworkConnectionImplementation } from "./network-connection.js"; export type JsonRpcRequestWrapperFunction = ( request: JsonRpcRequest, defaultBehavior: (r: JsonRpcRequest) => Promise<JsonRpcResponse>, ) => Promise<JsonRpcResponse>; export class NetworkManagerImplementation implements NetworkManager { readonly #defaultNetwork: string; readonly #defaultChainType: DefaultChainType; readonly #networkConfigs: Readonly<Record<string, Readonly<NetworkConfig>>>; readonly #hookManager: Readonly<HookManager>; readonly #artifactsManager: Readonly<ArtifactManager>; readonly #userConfig: Readonly<HardhatUserConfig>; readonly #chainDescriptors: Readonly<ChainDescriptorsConfig>; readonly #userProvidedConfigPath: Readonly<string | undefined>; readonly #projectRoot: string; #nextConnectionId = 0; constructor( defaultNetwork: string, defaultChainType: DefaultChainType, networkConfigs: Record<string, NetworkConfig>, hookManager: HookManager, artifactsManager: ArtifactManager, userConfig: HardhatUserConfig, chainDescriptors: ChainDescriptorsConfig, userProvidedConfigPath: string | undefined, projectRoot: string, ) { this.#defaultNetwork = defaultNetwork; this.#defaultChainType = defaultChainType; this.#networkConfigs = networkConfigs; this.#hookManager = hookManager; this.#artifactsManager = artifactsManager; this.#userConfig = userConfig; this.#chainDescriptors = chainDescriptors; this.#userProvidedConfigPath = userProvidedConfigPath; this.#projectRoot = projectRoot; } public async connect< ChainTypeT extends ChainType | string = DefaultChainType, >( networkOrParams?: NetworkConnectionParams<ChainTypeT> | string, ): Promise<NetworkConnection<ChainTypeT>> { let networkName: string | undefined; let chainType: ChainTypeT | undefined; let override: NetworkConfigOverride | undefined; if (typeof networkOrParams === "string") { networkName = networkOrParams; } else if (networkOrParams !== undefined) { networkName = networkOrParams.network; chainType = networkOrParams.chainType; override = networkOrParams.override; } const networkConnection = await this.#hookManager.runHandlerChain( "network", "newConnection", [], async (_context) => this.#initializeNetworkConnection(networkName, chainType, override), ); /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Cast to NetworkConnection<ChainTypeT> because we know it's valid */ return networkConnection as NetworkConnection<ChainTypeT>; } public async createServer( networkOrParams: NetworkConnectionParams | string = "default", _hostname?: string, port?: number, ): Promise<JsonRpcServer> { const insideDocker = await exists("/.dockerenv"); const hostname = _hostname ?? (insideDocker ? "0.0.0.0" : "127.0.0.1"); const { provider } = await this.connect(networkOrParams); return new JsonRpcServerImplementation({ hostname, port, provider, }); } async #initializeNetworkConnection<ChainTypeT extends ChainType | string>( networkName?: string, chainType?: ChainTypeT, networkConfigOverride?: NetworkConfigOverride, ): Promise<NetworkConnection<ChainTypeT>> { const resolvedNetworkName = networkName ?? this.#defaultNetwork; const existingNetworkConfig = this.#networkConfigs[resolvedNetworkName]; if (existingNetworkConfig === undefined) { throw new HardhatError( HardhatError.ERRORS.CORE.NETWORK.NETWORK_NOT_FOUND, { networkName: resolvedNetworkName, }, ); } /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- * Type assertion is safe: defaultChainType ensures non-undefined, and the * resolved value will be ChainTypeT (if provided) or a fallback that * satisfies the ChainType | string constraint */ const resolvedChainType = (chainType ?? existingNetworkConfig.chainType ?? this.#defaultChainType) as ChainTypeT; const resolvedNetworkConfig = await this.#resolveNetworkConfig( resolvedNetworkName, networkConfigOverride, resolvedChainType, ); /* Capture the hook manager in a local variable to avoid retaining a reference to the NetworkManager instance, allowing the garbage collector to clean up the NetworkConnectionImplementation instances properly. */ const hookManager = this.#hookManager; const createProvider = async ( networkConnection: NetworkConnectionImplementation<ChainTypeT>, ): Promise<EthereumProvider> => { const jsonRpcRequestWrapper: JsonRpcRequestWrapperFunction = ( request, defaultBehavior, ) => hookManager.runHandlerChain( "network", "onRequest", [networkConnection, request], async (_context, _connection, req) => defaultBehavior(req), ); if (resolvedNetworkConfig.type === "edr-simulated") { if (!isSupportedChainType(resolvedChainType)) { throw new HardhatError( HardhatError.ERRORS.CORE.GENERAL.UNSUPPORTED_OPERATION, { operation: `Simulating chain type ${resolvedChainType}` }, ); } let coverageConfig: CoverageConfig | undefined; const shouldEnableCoverage = await hookManager.hasHandlers( "network", "onCoverageData", ); if (shouldEnableCoverage) { coverageConfig = { onCollectedCoverageCallback: async (coverageData: Uint8Array[]) => { // NOTE: We cast the tag we receive from EDR to a hex string to // make it easier to debug. const tags = coverageData.map((tag) => Buffer.from(tag).toString("hex"), ); await hookManager.runParallelHandlers( "network", "onCoverageData", [tags], ); }, }; } let gasReportConfig: GasReportConfig | undefined; const shouldEnableGasStats = await hookManager.hasHandlers( "network", "onGasMeasurement", ); if (shouldEnableGasStats) { gasReportConfig = { onCollectedGasReportCallback: async (gasReport) => { const gasMeasurements = edrGasReportToHardhatGasMeasurements(gasReport); for (const measurement of gasMeasurements) { await hookManager.runParallelHandlers( "network", "onGasMeasurement", [measurement], ); } }, }; } return EdrProvider.create({ chainDescriptors: this.#chainDescriptors, // The resolvedNetworkConfig can have its chainType set to `undefined` // so we default to the default chain type here. networkConfig: { ...resolvedNetworkConfig, // When coverage is enabled, we set allowUnlimitedContractSize to true // because the added coverage data can push the contract size over the limit. allowUnlimitedContractSize: shouldEnableCoverage ? true : resolvedNetworkConfig.allowUnlimitedContractSize, /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- This case is safe because we have a check above */ chainType: resolvedChainType as ChainType, }, jsonRpcRequestWrapper, tracingConfig: { buildInfos: await this.#getBuildInfosAndOutputsAsBuffers(), ignoreContracts: false, }, coverageConfig, gasReportConfig, }); } return HttpProvider.create({ url: await resolvedNetworkConfig.url.getUrl(), networkName: resolvedNetworkName, extraHeaders: resolvedNetworkConfig.httpHeaders, timeout: resolvedNetworkConfig.timeout, jsonRpcRequestWrapper, }); }; return NetworkConnectionImplementation.create( this.#nextConnectionId++, resolvedNetworkName, resolvedChainType, resolvedNetworkConfig, async (connection: NetworkConnectionImplementation<ChainTypeT>) => { await hookManager.runHandlerChain( "network", "closeConnection", [connection], async (_context, conn) => { await conn.provider.close(); }, ); }, createProvider, ); } /** * Resolve the network connection configuration settings for the network name * and taking into account any configuration overrides. * * @param resolvedNetworkName the network name for selecting the appropriate network config * @param networkConfigOverride any network config options to override the * defaults for the named network * @returns a valid network configuration including any config additions from * plugins */ async #resolveNetworkConfig<ChainTypeT extends ChainType | string>( resolvedNetworkName: string, networkConfigOverride: NetworkConfigOverride | undefined = {}, resolvedChainType: ChainTypeT, ): Promise<NetworkConfig> { const existingNetworkConfig = this.#networkConfigs[resolvedNetworkName]; if ( Object.keys(networkConfigOverride).length === 0 && resolvedChainType === existingNetworkConfig.chainType ) { return existingNetworkConfig; } if ( "type" in networkConfigOverride && networkConfigOverride.type !== existingNetworkConfig.type ) { throw new HardhatError( HardhatError.ERRORS.CORE.NETWORK.INVALID_CONFIG_OVERRIDE, { errors: `\t* The type of the network cannot be changed.`, }, ); } if ( "chainType" in networkConfigOverride && networkConfigOverride.chainType !== existingNetworkConfig.chainType ) { throw new HardhatError( HardhatError.ERRORS.CORE.NETWORK.INVALID_CONFIG_OVERRIDE, { errors: `\t* The chainType cannot be specified in config overrides. Pass it at the top level instead: hre.network.connect({ chainType: 'op' })`, }, ); } const userConfigWithOverrides = deepMerge(this.#userConfig, { networks: { [resolvedNetworkName]: { ...networkConfigOverride, chainType: resolvedChainType, }, }, }); // This is safe, the plugins used in resolution are registered // with the hook handler, this property is only used for // ensuring the original plugins are available at the end // of resolution. const resolvedPlugins: HardhatPlugin[] = []; const configResolutionResult = await resolveUserConfigToHardhatConfig( userConfigWithOverrides, this.#hookManager, this.#projectRoot, this.#userProvidedConfigPath, resolvedPlugins, ); if (!configResolutionResult.success) { throw new HardhatError( HardhatError.ERRORS.CORE.NETWORK.INVALID_CONFIG_OVERRIDE, { errors: `\t${configResolutionResult.userConfigValidationErrors .map((error) => { const path = this.#normaliseErrorPathToNetworkConfig( error.path, resolvedNetworkName, ); let errorMessage = error.message; // When chainType is changed but the network has a configured hardfork, // provide a specific message explaining the hardfork must also be updated if (path[0] === "hardfork") { errorMessage = `Your configured hardfork is incompatible with chainType ${resolvedChainType}. ` + `You need to update the hardfork in your network config or pass a valid hardfork ` + `in the overrides when connecting to the network. ` + `Valid hardforks for chainType ${resolvedChainType} are: ` + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We know resolvedChainType is a valid ChainType */ `${getHardforks(resolvedChainType as ChainType).join(", ")}.`; } return path.length > 0 ? `* Error in ${path.join(".")}: ${errorMessage}` : `* ${errorMessage}`; }) .join("\n\t")}`, }, ); } const resolvedNetworkConfigOverride = configResolutionResult.config.networks[resolvedNetworkName]; assertHardhatInvariant( resolvedNetworkConfigOverride !== undefined, "The overridden network config should translate through the hook resolution of user config", ); return resolvedNetworkConfigOverride; } async #getBuildInfosAndOutputsAsBuffers(): Promise< Array<{ buildInfo: Uint8Array; output: Uint8Array }> > { const results = []; for (const id of await this.#artifactsManager.getAllBuildInfoIds()) { const buildInfoPath = await this.#artifactsManager.getBuildInfoPath(id); const buildInfoOutputPath = await this.#artifactsManager.getBuildInfoOutputPath(id); if (buildInfoPath !== undefined && buildInfoOutputPath !== undefined) { const buildInfo = await readBinaryFile(buildInfoPath); const output = await readBinaryFile(buildInfoOutputPath); results.push({ buildInfo, output, }); } } return results; } #normaliseErrorPathToNetworkConfig( path: Array<string | number>, resolvedNetworkName: string, ): Array<string | number> { if (path[0] !== undefined && path[0] === "networks") { path = path.slice(1); } if (path[0] !== undefined && path[0] === resolvedNetworkName) { path = path.slice(1); } return path; } }