UNPKG

hardhat

Version:

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

622 lines (545 loc) 17.9 kB
import type { HardhatConfig as HardhatConfigT } from "../../../types"; import type { Context, ValidationError, getFunctionName as getFunctionNameT, } from "io-ts/lib"; import type { Reporter } from "io-ts/lib/Reporter"; import * as t from "io-ts"; import { HARDHAT_MEMPOOL_SUPPORTED_ORDERS, HARDHAT_NETWORK_NAME, HARDHAT_NETWORK_SUPPORTED_HARDFORKS, } from "../../constants"; import { optional } from "../../util/io-ts"; import { fromEntries } from "../../util/lang"; import { HardhatError } from "../errors"; import { ERRORS } from "../errors-list"; import { hardforkGte, HardforkName } from "../../util/hardforks"; import { HardhatNetworkChainUserConfig } from "../../../types/config"; import { defaultHardhatNetworkParams } from "./default-config"; function stringify(v: any): string { if (typeof v === "function") { const { getFunctionName } = require("io-ts/lib") as { getFunctionName: typeof getFunctionNameT; }; return getFunctionName(v); } if (typeof v === "number" && !isFinite(v)) { if (isNaN(v)) { return "NaN"; } return v > 0 ? "Infinity" : "-Infinity"; } return JSON.stringify(v); } function getContextPath(context: Context): string { const keysPath = context .slice(1) .map((c) => c.key) .join("."); return `${context[0].type.name}.${keysPath}`; } function getMessage(e: ValidationError): string { const lastContext = e.context[e.context.length - 1]; return e.message !== undefined ? e.message : getErrorMessage( getContextPath(e.context), e.value, lastContext.type.name ); } function getErrorMessage(path: string, value: any, expectedType: string) { return `Invalid value ${stringify( value )} for ${path} - Expected a value of type ${expectedType}.`; } function getPrivateKeyError(index: number, network: string, message: string) { return `Invalid account: #${index} for network: ${network} - ${message}`; } function validatePrivateKey( privateKey: unknown, index: number, network: string, errors: string[] ) { if (typeof privateKey !== "string") { errors.push( getPrivateKeyError( index, network, `Expected string, received ${typeof privateKey}` ) ); } else { // private key validation const pkWithPrefix = /^0x/.test(privateKey) ? privateKey : `0x${privateKey}`; // 32 bytes = 64 characters + 2 char prefix = 66 if (pkWithPrefix.length < 66) { errors.push( getPrivateKeyError( index, network, "private key too short, expected 32 bytes" ) ); } else if (pkWithPrefix.length > 66) { errors.push( getPrivateKeyError( index, network, "private key too long, expected 32 bytes" ) ); } else if (hexString.decode(pkWithPrefix).isLeft()) { errors.push( getPrivateKeyError( index, network, "invalid hex character(s) found in string" ) ); } } } export function failure(es: ValidationError[]): string[] { return es.map(getMessage); } export function success(): string[] { return []; } export const DotPathReporter: Reporter<string[]> = { report: (validation) => validation.fold(failure, success), }; const HEX_STRING_REGEX = /^(0x)?([0-9a-f]{2})+$/gi; const DEC_STRING_REGEX = /^(0|[1-9][0-9]*)$/g; function isHexString(v: unknown): v is string { if (typeof v !== "string") { return false; } return v.trim().match(HEX_STRING_REGEX) !== null; } function isDecimalString(v: unknown): v is string { if (typeof v !== "string") { return false; } return v.match(DEC_STRING_REGEX) !== null; } export const hexString = new t.Type<string>( "hex string", isHexString, (u, c) => (isHexString(u) ? t.success(u) : t.failure(u, c)), t.identity ); function isAddress(v: unknown): v is string { if (typeof v !== "string") { return false; } const trimmed = v.trim(); return ( trimmed.match(HEX_STRING_REGEX) !== null && trimmed.startsWith("0x") && trimmed.length === 42 ); } export const address = new t.Type<string>( "address", isAddress, (u, c) => (isAddress(u) ? t.success(u) : t.failure(u, c)), t.identity ); export const decimalString = new t.Type<string>( "decimal string", isDecimalString, (u, c) => (isDecimalString(u) ? t.success(u) : t.failure(u, c)), t.identity ); // TODO: These types have outdated name. They should match the UserConfig types. // IMPORTANT: This t.types MUST be kept in sync with the actual types. const HardhatNetworkAccount = t.type({ privateKey: hexString, balance: decimalString, }); const commonHDAccountsFields = { initialIndex: optional(t.number), count: optional(t.number), path: optional(t.string), }; const HardhatNetworkHDAccountsConfig = t.type({ mnemonic: optional(t.string), accountsBalance: optional(decimalString), passphrase: optional(t.string), ...commonHDAccountsFields, }); const Integer = new t.Type<number>( "Integer", (num: unknown): num is number => typeof num === "number", (u, c) => { try { return typeof u === "string" ? t.success(parseInt(u, 10)) : t.failure(u, c); } catch { return t.failure(u, c); } }, t.identity ); const HardhatNetworkForkingConfig = t.type({ enabled: optional(t.boolean), url: t.string, blockNumber: optional(t.number), }); const HardhatNetworkMempoolConfig = t.type({ order: optional( t.keyof( fromEntries( HARDHAT_MEMPOOL_SUPPORTED_ORDERS.map((order) => [order, null]) ) ) ), }); const HardhatNetworkMiningConfig = t.type({ auto: optional(t.boolean), interval: optional(t.union([t.number, t.tuple([t.number, t.number])])), mempool: optional(HardhatNetworkMempoolConfig), }); function isValidHardforkName(name: string) { return Object.values(HardforkName).includes(name as HardforkName); } const HardforkNameType = new t.Type<HardforkName>( Object.values(HardforkName) .map((v) => `"${v}"`) .join(" | "), (name: unknown): name is HardforkName => typeof name === "string" && isValidHardforkName(name), (u, c) => { return typeof u === "string" && isValidHardforkName(u) ? t.success(u as HardforkName) : t.failure(u, c); }, t.identity ); const HardhatNetworkHardforkHistory = t.record( HardforkNameType, t.number, "HardhatNetworkHardforkHistory" ); const HardhatNetworkChainConfig = t.type({ hardforkHistory: HardhatNetworkHardforkHistory, }); const HardhatNetworkChainsConfig = t.record(Integer, HardhatNetworkChainConfig); const commonNetworkConfigFields = { chainId: optional(t.number), from: optional(t.string), gas: optional(t.union([t.literal("auto"), t.number])), gasPrice: optional(t.union([t.literal("auto"), t.number])), gasMultiplier: optional(t.number), }; const HardhatNetworkConfig = t.type({ ...commonNetworkConfigFields, hardfork: optional( t.keyof( fromEntries(HARDHAT_NETWORK_SUPPORTED_HARDFORKS.map((hf) => [hf, null])) ) ), accounts: optional( t.union([t.array(HardhatNetworkAccount), HardhatNetworkHDAccountsConfig]) ), blockGasLimit: optional(t.number), minGasPrice: optional(t.union([t.number, t.string])), throwOnTransactionFailures: optional(t.boolean), throwOnCallFailures: optional(t.boolean), allowUnlimitedContractSize: optional(t.boolean), initialDate: optional(t.string), loggingEnabled: optional(t.boolean), forking: optional(HardhatNetworkForkingConfig), mining: optional(HardhatNetworkMiningConfig), coinbase: optional(address), chains: optional(HardhatNetworkChainsConfig), }); const HDAccountsConfig = t.type({ mnemonic: t.string, passphrase: optional(t.string), ...commonHDAccountsFields, }); const NetworkConfigAccounts = t.union([ t.literal("remote"), t.array(hexString), HDAccountsConfig, ]); const HttpHeaders = t.record(t.string, t.string, "httpHeaders"); const HttpNetworkConfig = t.type({ ...commonNetworkConfigFields, url: optional(t.string), accounts: optional(NetworkConfigAccounts), httpHeaders: optional(HttpHeaders), timeout: optional(t.number), }); const NetworkConfig = t.union([HardhatNetworkConfig, HttpNetworkConfig]); const Networks = t.record(t.string, NetworkConfig); const ProjectPaths = t.type({ root: optional(t.string), cache: optional(t.string), artifacts: optional(t.string), sources: optional(t.string), tests: optional(t.string), }); const SingleSolcConfig = t.type({ version: t.string, settings: optional(t.any), }); const MultiSolcConfig = t.type({ compilers: t.array(SingleSolcConfig), overrides: optional(t.record(t.string, SingleSolcConfig)), }); const SolidityConfig = t.union([t.string, SingleSolcConfig, MultiSolcConfig]); const HardhatConfig = t.type( { defaultNetwork: optional(t.string), networks: optional(Networks), paths: optional(ProjectPaths), solidity: optional(SolidityConfig), }, "HardhatConfig" ); /** * Validates the config, throwing a HardhatError if invalid. * @param config */ export function validateConfig(config: any) { const errors = getValidationErrors(config); if (errors.length === 0) { return; } let errorList = errors.join("\n * "); errorList = ` * ${errorList}`; throw new HardhatError(ERRORS.GENERAL.INVALID_CONFIG, { errors: errorList }); } export function getValidationErrors(config: any): string[] { const errors: string[] = []; // These can't be validated with io-ts if (config !== undefined && typeof config.networks === "object") { const hardhatNetwork = config.networks[HARDHAT_NETWORK_NAME]; if (hardhatNetwork !== undefined && typeof hardhatNetwork === "object") { if ("url" in hardhatNetwork) { errors.push( `HardhatConfig.networks.${HARDHAT_NETWORK_NAME} can't have an url` ); } // Validating the accounts with io-ts leads to very confusing errors messages const { accounts, ...configExceptAccounts } = hardhatNetwork; const netConfigResult = HardhatNetworkConfig.decode(configExceptAccounts); if (netConfigResult.isLeft()) { errors.push( getErrorMessage( `HardhatConfig.networks.${HARDHAT_NETWORK_NAME}`, hardhatNetwork, "HardhatNetworkConfig" ) ); } // manual validation of accounts if (Array.isArray(accounts)) { for (const [index, account] of accounts.entries()) { if (typeof account !== "object") { errors.push( getPrivateKeyError( index, HARDHAT_NETWORK_NAME, `Expected object, received ${typeof account}` ) ); continue; } const { privateKey, balance } = account; validatePrivateKey(privateKey, index, HARDHAT_NETWORK_NAME, errors); if (typeof balance !== "string") { errors.push( getErrorMessage( `HardhatConfig.networks.${HARDHAT_NETWORK_NAME}.accounts[].balance`, balance, "string" ) ); } else if (decimalString.decode(balance).isLeft()) { errors.push( getErrorMessage( `HardhatConfig.networks.${HARDHAT_NETWORK_NAME}.accounts[].balance`, balance, "decimal(wei)" ) ); } } } else if (typeof hardhatNetwork.accounts === "object") { const hdConfigResult = HardhatNetworkHDAccountsConfig.decode( hardhatNetwork.accounts ); if (hdConfigResult.isLeft()) { errors.push( getErrorMessage( `HardhatConfig.networks.${HARDHAT_NETWORK_NAME}.accounts`, hardhatNetwork.accounts, "[{privateKey: string, balance: string}] | HardhatNetworkHDAccountsConfig | undefined" ) ); } } else if (hardhatNetwork.accounts !== undefined) { errors.push( getErrorMessage( `HardhatConfig.networks.${HARDHAT_NETWORK_NAME}.accounts`, hardhatNetwork.accounts, "[{privateKey: string, balance: string}] | HardhatNetworkHDAccountsConfig | undefined" ) ); } const hardfork = hardhatNetwork.hardfork ?? defaultHardhatNetworkParams.hardfork; if (hardforkGte(hardfork, HardforkName.LONDON)) { if (hardhatNetwork.minGasPrice !== undefined) { errors.push( `Unexpected config HardhatConfig.networks.${HARDHAT_NETWORK_NAME}.minGasPrice found - This field is not valid for networks with EIP-1559. Try an older hardfork or remove it.` ); } } else { if (hardhatNetwork.initialBaseFeePerGas !== undefined) { errors.push( `Unexpected config HardhatConfig.networks.${HARDHAT_NETWORK_NAME}.initialBaseFeePerGas found - This field is only valid for networks with EIP-1559. Try a newer hardfork or remove it.` ); } } if (hardhatNetwork.chains !== undefined) { Object.entries(hardhatNetwork.chains).forEach((chainEntry) => { const [chainId, chainConfig] = chainEntry as [ string, HardhatNetworkChainUserConfig ]; const { hardforkHistory } = chainConfig; if (hardforkHistory !== undefined) { Object.keys(hardforkHistory).forEach((hardforkName) => { if (!HARDHAT_NETWORK_SUPPORTED_HARDFORKS.includes(hardforkName)) { errors.push( getErrorMessage( `HardhatConfig.networks.${HARDHAT_NETWORK_NAME}.chains[${chainId}].hardforkHistory`, hardforkName, `"${HARDHAT_NETWORK_SUPPORTED_HARDFORKS.join('" | "')}"` ) ); } }); } }); } if (hardhatNetwork.hardfork !== undefined) { if ( !hardforkGte(hardhatNetwork.hardfork, HardforkName.CANCUN) && hardhatNetwork.enableTransientStorage === true ) { errors.push( `'enableTransientStorage' cannot be enabled if the hardfork is explicitly set to a pre-cancun value. If you want to use transient storage, use 'cancun' as the hardfork.` ); } if ( hardforkGte(hardhatNetwork.hardfork, HardforkName.CANCUN) && hardhatNetwork.enableTransientStorage === false ) { errors.push( `'enableTransientStorage' cannot be disabled if the hardfork is explicitly set to cancun or later. If you want to disable transient storage, use a hardfork before 'cancun'.` ); } } } for (const [networkName, netConfig] of Object.entries<any>( config.networks )) { if (networkName === HARDHAT_NETWORK_NAME) { continue; } if (networkName !== "localhost" || netConfig.url !== undefined) { if (typeof netConfig.url !== "string") { errors.push( getErrorMessage( `HardhatConfig.networks.${networkName}.url`, netConfig.url, "string" ) ); } } const { accounts, ...configExceptAccounts } = netConfig; const netConfigResult = HttpNetworkConfig.decode(configExceptAccounts); if (netConfigResult.isLeft()) { errors.push( getErrorMessage( `HardhatConfig.networks.${networkName}`, netConfig, "HttpNetworkConfig" ) ); } // manual validation of accounts if (Array.isArray(accounts)) { accounts.forEach((privateKey, index) => validatePrivateKey(privateKey, index, networkName, errors) ); } else if (typeof accounts === "object") { const hdConfigResult = HDAccountsConfig.decode(accounts); if (hdConfigResult.isLeft()) { errors.push( getErrorMessage( `HardhatConfig.networks.${networkName}`, accounts, "HttpNetworkHDAccountsConfig" ) ); } } else if (typeof accounts === "string") { if (accounts !== "remote") { errors.push( `Invalid 'accounts' entry for network '${networkName}': expected an array of accounts or the string 'remote', but got the string '${accounts}'` ); } } else if (accounts !== undefined) { errors.push( getErrorMessage( `HardhatConfig.networks.${networkName}.accounts`, accounts, '"remote" | string[] | HttpNetworkHDAccountsConfig | undefined' ) ); } } } // io-ts can get confused if there are errors that it can't understand. // Especially around Hardhat Network's config. It will treat it as an HTTPConfig, // and may give a loot of errors. if (errors.length > 0) { return errors; } const result = HardhatConfig.decode(config); if (result.isRight()) { return errors; } const ioTsErrors = DotPathReporter.report(result); return [...errors, ...ioTsErrors]; } export function validateResolvedConfig(resolvedConfig: HardhatConfigT) { const solcConfigs = [ ...resolvedConfig.solidity.compilers, ...Object.values(resolvedConfig.solidity.overrides), ]; const runs = solcConfigs .filter(({ settings }) => settings?.optimizer?.runs !== undefined) .map(({ settings }) => settings?.optimizer?.runs); for (const run of runs) { if (run >= 2 ** 32) { throw new HardhatError(ERRORS.GENERAL.INVALID_CONFIG, { errors: "The number of optimizer runs exceeds the maximum of 2**32 - 1", }); } } }