hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
319 lines (286 loc) • 11.3 kB
text/typescript
import type { CoverageConfig } from "./edr/types/coverage.js";
import type { ArtifactManager } from "../../../types/artifacts.js";
import type {
ChainDescriptorsConfig,
NetworkConfig,
NetworkConfigOverride,
NetworkUserConfig,
} from "../../../types/config.js";
import type { HookManager } from "../../../types/hooks.js";
import type {
ChainType,
DefaultChainType,
NetworkConnection,
NetworkConnectionParams,
NetworkManager,
} from "../../../types/network.js";
import type {
EthereumProvider,
JsonRpcRequest,
JsonRpcResponse,
} from "../../../types/providers.js";
import { HardhatError } from "@nomicfoundation/hardhat-errors";
import { readBinaryFile } from "@nomicfoundation/hardhat-utils/fs";
import { deepMerge } from "@nomicfoundation/hardhat-utils/lang";
import { resolveConfigurationVariable } from "../../core/configuration-variables.js";
import { isSupportedChainType } from "../../edr/chain-type.js";
import { resolveEdrNetwork, resolveHttpNetwork } from "./config-resolution.js";
import { EdrProvider } from "./edr/edr-provider.js";
import { HttpProvider } from "./http-provider.js";
import { NetworkConnectionImplementation } from "./network-connection.js";
import { validateNetworkConfigOverride } from "./type-validation.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 #userConfigNetworks: Readonly<Record<string, NetworkUserConfig>>;
readonly #chainDescriptors: Readonly<ChainDescriptorsConfig>;
#nextConnectionId = 0;
constructor(
defaultNetwork: string,
defaultChainType: DefaultChainType,
networkConfigs: Record<string, NetworkConfig>,
hookManager: HookManager,
artifactsManager: ArtifactManager,
userConfigNetworks: Record<string, NetworkUserConfig> | undefined,
chainDescriptors: ChainDescriptorsConfig,
) {
this.#defaultNetwork = defaultNetwork;
this.#defaultChainType = defaultChainType;
this.#networkConfigs = networkConfigs;
this.#hookManager = hookManager;
this.#artifactsManager = artifactsManager;
this.#userConfigNetworks = userConfigNetworks ?? {};
this.#chainDescriptors = chainDescriptors;
}
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>;
}
async #initializeNetworkConnection<ChainTypeT extends ChainType | string>(
networkName?: string,
chainType?: ChainTypeT,
networkConfigOverride?: NetworkConfigOverride,
): Promise<NetworkConnection<ChainTypeT>> {
const resolvedNetworkName = networkName ?? this.#defaultNetwork;
if (this.#networkConfigs[resolvedNetworkName] === undefined) {
throw new HardhatError(
HardhatError.ERRORS.CORE.NETWORK.NETWORK_NOT_FOUND,
{
networkName: resolvedNetworkName,
},
);
}
let resolvedNetworkConfigOverride: NetworkConfig | undefined;
if (networkConfigOverride !== undefined) {
if (
"type" in networkConfigOverride &&
networkConfigOverride.type !==
this.#networkConfigs[resolvedNetworkName].type
) {
throw new HardhatError(
HardhatError.ERRORS.CORE.NETWORK.INVALID_CONFIG_OVERRIDE,
{
errors: `\t* The type of the network cannot be changed.`,
},
);
}
const newConfig = deepMerge(
this.#userConfigNetworks[resolvedNetworkName],
networkConfigOverride,
);
// As normalizeNetworkConfigOverride is not type-safe, we validate the
// normalized network config override immediately after normalizing it.
const validationErrors = await validateNetworkConfigOverride(newConfig);
if (validationErrors.length > 0) {
throw new HardhatError(
HardhatError.ERRORS.CORE.NETWORK.INVALID_CONFIG_OVERRIDE,
{
errors: `\t${validationErrors
.map((error) =>
error.path.length > 0
? `* Error in ${error.path.join(".")}: ${error.message}`
: `* ${error.message}`,
)
.join("\n\t")}`,
},
);
}
resolvedNetworkConfigOverride =
newConfig.type === "http"
? resolveHttpNetwork(newConfig, (strOrConfigVar) =>
resolveConfigurationVariable(this.#hookManager, strOrConfigVar),
)
: resolveEdrNetwork(newConfig, "", (strOrConfigVar) =>
resolveConfigurationVariable(this.#hookManager, strOrConfigVar),
);
}
const resolvedNetworkConfig =
resolvedNetworkConfigOverride ??
this.#networkConfigs[resolvedNetworkName];
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions
-- Cast to ChainTypeT because we know it's valid */
const resolvedChainType = (chainType ??
resolvedNetworkConfig.chainType ??
this.#defaultChainType) as ChainTypeT;
/**
* If resolvedNetworkConfig.chainType is defined, it must match the
* provided chainType.
* We use resolvedChainType as it will be either chainType or
* resolvedNetworkConfig.chainType in this context.
*/
if (
resolvedNetworkConfig.chainType !== undefined &&
resolvedChainType !== resolvedNetworkConfig.chainType
) {
throw new HardhatError(
HardhatError.ERRORS.CORE.NETWORK.INVALID_CHAIN_TYPE,
{
networkName: resolvedNetworkName,
chainType: resolvedChainType,
networkChainType: resolvedNetworkConfig.chainType,
},
);
}
/* 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],
);
},
};
}
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,
});
}
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,
);
}
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;
}
}