hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
498 lines (433 loc) • 13.5 kB
text/typescript
import type { LoDashStatic } from "lodash";
import path from "path";
import semver from "semver";
import {
HardhatConfig,
HardhatNetworkAccountsConfig,
HardhatNetworkChainConfig,
HardhatNetworkChainsConfig,
HardhatNetworkConfig,
HardhatNetworkForkingConfig,
HardhatNetworkMiningConfig,
HardhatNetworkMiningUserConfig,
HardhatNetworkMempoolConfig,
HardhatNetworkMempoolUserConfig,
HardhatNetworkUserConfig,
HardhatUserConfig,
HDAccountsUserConfig,
HttpNetworkAccountsConfig,
HttpNetworkAccountsUserConfig,
HttpNetworkConfig,
HttpNetworkUserConfig,
MultiSolcUserConfig,
NetworksConfig,
NetworksUserConfig,
NetworkUserConfig,
ProjectPathsConfig,
ProjectPathsUserConfig,
SolcConfig,
SolcUserConfig,
SolidityConfig,
SolidityUserConfig,
} from "../../../types";
import { HARDHAT_NETWORK_NAME } from "../../constants";
import { HardforkName } from "../../util/hardforks";
import { fromEntries } from "../../util/lang";
import { assertHardhatInvariant } from "../errors";
import { getRealPathSync } from "../../util/fs-utils";
import {
DEFAULT_SOLC_VERSION,
defaultDefaultNetwork,
defaultHardhatNetworkHdAccountsConfigParams,
defaultHardhatNetworkParams,
defaultHdAccountsConfigParams,
defaultHttpNetworkParams,
defaultLocalhostNetworkParams,
defaultMochaOptions,
defaultSolcOutputSelection,
} from "./default-config";
/**
* This functions resolves the hardhat config, setting its defaults and
* normalizing its types if necessary.
*
* @param userConfigPath the user config filepath
* @param userConfig the user config object
*
* @returns the resolved config
*/
export function resolveConfig(
userConfigPath: string,
userConfig: HardhatUserConfig
): HardhatConfig {
const cloneDeep = require("lodash/cloneDeep") as LoDashStatic["cloneDeep"];
userConfig = cloneDeep(userConfig);
return {
...userConfig,
defaultNetwork: userConfig.defaultNetwork ?? defaultDefaultNetwork,
paths: resolveProjectPaths(userConfigPath, userConfig.paths),
networks: resolveNetworksConfig(userConfig.networks),
solidity: resolveSolidityConfig(userConfig),
mocha: resolveMochaConfig(userConfig),
};
}
function resolveNetworksConfig(
networksConfig: NetworksUserConfig = {}
): NetworksConfig {
const cloneDeep = require("lodash/cloneDeep") as LoDashStatic["cloneDeep"];
const hardhatNetworkConfig = networksConfig[HARDHAT_NETWORK_NAME];
const localhostNetworkConfig =
(networksConfig.localhost as HttpNetworkUserConfig) ?? undefined;
const hardhat = resolveHardhatNetworkConfig(hardhatNetworkConfig);
const localhost = resolveHttpNetworkConfig({
...cloneDeep(defaultLocalhostNetworkParams),
...localhostNetworkConfig,
});
const otherNetworks: { [name: string]: HttpNetworkConfig } = fromEntries(
Object.entries(networksConfig)
.filter(
([name, config]) =>
name !== "localhost" &&
name !== "hardhat" &&
config !== undefined &&
isHttpNetworkConfig(config)
)
.map(([name, config]) => [
name,
resolveHttpNetworkConfig(config as HttpNetworkUserConfig),
])
);
return {
hardhat,
localhost,
...otherNetworks,
};
}
function isHttpNetworkConfig(
config: NetworkUserConfig
): config is HttpNetworkUserConfig {
return "url" in config;
}
function normalizeHexString(str: string): string {
const normalized = str.trim().toLowerCase();
if (normalized.startsWith("0x")) {
return normalized;
}
return `0x${normalized}`;
}
function resolveHardhatNetworkConfig(
hardhatNetworkConfig: HardhatNetworkUserConfig = {}
): HardhatNetworkConfig {
const cloneDeep = require("lodash/cloneDeep") as LoDashStatic["cloneDeep"];
const clonedDefaultHardhatNetworkParams = cloneDeep(
defaultHardhatNetworkParams
);
const accounts: HardhatNetworkAccountsConfig =
hardhatNetworkConfig.accounts === undefined
? defaultHardhatNetworkHdAccountsConfigParams
: Array.isArray(hardhatNetworkConfig.accounts)
? hardhatNetworkConfig.accounts.map(({ privateKey, balance }) => ({
privateKey: normalizeHexString(privateKey),
balance,
}))
: {
...defaultHardhatNetworkHdAccountsConfigParams,
...hardhatNetworkConfig.accounts,
};
const forking: HardhatNetworkForkingConfig | undefined =
hardhatNetworkConfig.forking !== undefined
? {
url: hardhatNetworkConfig.forking.url,
enabled: hardhatNetworkConfig.forking.enabled ?? true,
httpHeaders: {},
}
: undefined;
if (forking !== undefined) {
const blockNumber = hardhatNetworkConfig?.forking?.blockNumber;
if (blockNumber !== undefined) {
forking.blockNumber = hardhatNetworkConfig?.forking?.blockNumber;
}
const httpHeaders = hardhatNetworkConfig.forking?.httpHeaders;
if (httpHeaders !== undefined) {
forking.httpHeaders = httpHeaders;
}
}
const mining = resolveMiningConfig(hardhatNetworkConfig.mining);
const minGasPrice = BigInt(
hardhatNetworkConfig.minGasPrice ??
clonedDefaultHardhatNetworkParams.minGasPrice
);
const blockGasLimit =
hardhatNetworkConfig.blockGasLimit ??
clonedDefaultHardhatNetworkParams.blockGasLimit;
const gas = hardhatNetworkConfig.gas ?? blockGasLimit;
const gasPrice =
hardhatNetworkConfig.gasPrice ?? clonedDefaultHardhatNetworkParams.gasPrice;
const initialBaseFeePerGas =
hardhatNetworkConfig.initialBaseFeePerGas ??
clonedDefaultHardhatNetworkParams.initialBaseFeePerGas;
const initialDate =
hardhatNetworkConfig.initialDate ?? new Date().toISOString();
const chains: HardhatNetworkChainsConfig = new Map(
defaultHardhatNetworkParams.chains
);
if (hardhatNetworkConfig.chains !== undefined) {
for (const [chainId, userChainConfig] of Object.entries(
hardhatNetworkConfig.chains
)) {
const chainConfig: HardhatNetworkChainConfig = {
hardforkHistory: new Map(),
};
if (userChainConfig.hardforkHistory !== undefined) {
for (const [name, block] of Object.entries(
userChainConfig.hardforkHistory
)) {
chainConfig.hardforkHistory.set(
name as HardforkName,
block as number
);
}
}
chains.set(parseInt(chainId, 10), chainConfig);
}
}
const config: HardhatNetworkConfig = {
...clonedDefaultHardhatNetworkParams,
...hardhatNetworkConfig,
accounts,
forking,
mining,
blockGasLimit,
gas,
gasPrice,
initialBaseFeePerGas,
initialDate,
minGasPrice,
chains,
};
// We do it this way because ts gets lost otherwise
if (config.forking === undefined) {
delete config.forking;
}
if (config.initialBaseFeePerGas === undefined) {
delete config.initialBaseFeePerGas;
}
if (
hardhatNetworkConfig.enableTransientStorage === true &&
hardhatNetworkConfig.hardfork === undefined
) {
config.hardfork = "cancun";
}
if (
hardhatNetworkConfig.enableTransientStorage === false &&
hardhatNetworkConfig.hardfork === undefined
) {
config.hardfork = "shanghai";
}
return config;
}
function isHdAccountsConfig(
accounts: HttpNetworkAccountsUserConfig
): accounts is HDAccountsUserConfig {
return typeof accounts === "object" && !Array.isArray(accounts);
}
function resolveHttpNetworkConfig(
networkConfig: HttpNetworkUserConfig
): HttpNetworkConfig {
const cloneDeep = require("lodash/cloneDeep") as LoDashStatic["cloneDeep"];
const accounts: HttpNetworkAccountsConfig =
networkConfig.accounts === undefined
? defaultHttpNetworkParams.accounts
: isHdAccountsConfig(networkConfig.accounts)
? {
...defaultHdAccountsConfigParams,
...networkConfig.accounts,
}
: Array.isArray(networkConfig.accounts)
? networkConfig.accounts.map(normalizeHexString)
: "remote";
const url = networkConfig.url;
assertHardhatInvariant(
url !== undefined,
"Invalid http network config provided. URL missing."
);
return {
...cloneDeep(defaultHttpNetworkParams),
...networkConfig,
accounts,
url,
gas: networkConfig.gas ?? defaultHttpNetworkParams.gas,
gasPrice: networkConfig.gasPrice ?? defaultHttpNetworkParams.gasPrice,
};
}
function resolveMiningConfig(
userConfig: HardhatNetworkMiningUserConfig | undefined
): HardhatNetworkMiningConfig {
const mempool = resolveMempoolConfig(userConfig?.mempool);
if (userConfig === undefined) {
return {
auto: true,
interval: 0,
mempool,
};
}
const { auto, interval } = userConfig;
if (auto === undefined && interval === undefined) {
return {
auto: true,
interval: 0,
mempool,
};
}
if (auto === undefined && interval !== undefined) {
return {
auto: false,
interval,
mempool,
};
}
if (auto !== undefined && interval === undefined) {
return {
auto,
interval: 0,
mempool,
};
}
// ts can't infer it, but both values are defined here
return {
auto: auto!,
interval: interval!,
mempool,
};
}
function resolveMempoolConfig(
userConfig: HardhatNetworkMempoolUserConfig | undefined
): HardhatNetworkMempoolConfig {
if (userConfig === undefined) {
return {
order: "priority",
};
}
if (userConfig.order === undefined) {
return {
order: "priority",
};
}
return {
order: userConfig.order,
} as HardhatNetworkMempoolConfig;
}
function resolveSolidityConfig(userConfig: HardhatUserConfig): SolidityConfig {
const userSolidityConfig = userConfig.solidity ?? DEFAULT_SOLC_VERSION;
const multiSolcConfig: MultiSolcUserConfig =
normalizeSolidityConfig(userSolidityConfig);
const overrides = multiSolcConfig.overrides ?? {};
return {
compilers: multiSolcConfig.compilers.map(resolveCompiler),
overrides: fromEntries(
Object.entries(overrides).map(([name, config]) => [
name,
resolveCompiler(config),
])
),
};
}
function normalizeSolidityConfig(
solidityConfig: SolidityUserConfig
): MultiSolcUserConfig {
if (typeof solidityConfig === "string") {
return {
compilers: [
{
version: solidityConfig,
},
],
};
}
if ("version" in solidityConfig) {
return { compilers: [solidityConfig] };
}
return solidityConfig;
}
function resolveCompiler(compiler: SolcUserConfig): SolcConfig {
const resolved: SolcConfig = {
version: compiler.version,
settings: compiler.settings ?? {},
};
if (semver.gte(resolved.version, "0.8.20")) {
resolved.settings.evmVersion = compiler.settings?.evmVersion ?? "paris";
}
resolved.settings.optimizer = {
enabled: false,
runs: 200,
...resolved.settings.optimizer,
};
if (resolved.settings.outputSelection === undefined) {
resolved.settings.outputSelection = {};
}
for (const [file, contractSelection] of Object.entries(
defaultSolcOutputSelection
)) {
if (resolved.settings.outputSelection[file] === undefined) {
resolved.settings.outputSelection[file] = {};
}
for (const [contract, outputs] of Object.entries(contractSelection)) {
if (resolved.settings.outputSelection[file][contract] === undefined) {
resolved.settings.outputSelection[file][contract] = [];
}
for (const output of outputs) {
const includesOutput: boolean =
resolved.settings.outputSelection[file][contract].includes(output);
if (!includesOutput) {
resolved.settings.outputSelection[file][contract].push(output);
}
}
}
}
return resolved;
}
function resolveMochaConfig(userConfig: HardhatUserConfig): Mocha.MochaOptions {
const cloneDeep = require("lodash/cloneDeep") as LoDashStatic["cloneDeep"];
return {
...cloneDeep(defaultMochaOptions),
...userConfig.mocha,
};
}
/**
* This function resolves the ProjectPathsConfig object from the user-provided config
* and its path. The logic of this is not obvious and should well be document.
* The good thing is that most users will never use this.
*
* Explanation:
* - paths.configFile is not overridable
* - If a path is absolute it is used "as is".
* - If the root path is relative, it's resolved from paths.configFile's dir.
* - If any other path is relative, it's resolved from paths.root.
* - Plugin-defined paths are not resolved, but encouraged to follow the same pattern.
*/
export function resolveProjectPaths(
userConfigPath: string,
userPaths: ProjectPathsUserConfig = {}
): ProjectPathsConfig {
const configFile = getRealPathSync(userConfigPath);
const configDir = path.dirname(configFile);
const root = resolvePathFrom(configDir, "", userPaths.root);
return {
...userPaths,
root,
configFile,
sources: resolvePathFrom(root, "contracts", userPaths.sources),
cache: resolvePathFrom(root, "cache", userPaths.cache),
artifacts: resolvePathFrom(root, "artifacts", userPaths.artifacts),
tests: resolvePathFrom(root, "test", userPaths.tests),
};
}
function resolvePathFrom(
from: string,
defaultPath: string,
relativeOrAbsolutePath: string = defaultPath
) {
if (path.isAbsolute(relativeOrAbsolutePath)) {
return relativeOrAbsolutePath;
}
return path.join(from, relativeOrAbsolutePath);
}