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
text/typescript
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;
}
}