import type { ActivationBlockNumberUserConfig, ActivationTimestampUserConfig, EdrNetworkForkingConfig, EdrNetworkHDAccountsConfig, EdrNetworkMiningConfig, HardhatUserConfig, HttpNetworkHDAccountsConfig, HttpNetworkHDAccountsUserConfig, NetworkUserConfig, } from "../../../types/config.js"; import type { HardhatUserConfigValidationError } from "../../../types/hooks.js"; import type { RefinementCtx } from "zod"; import { getUnprefixedHexString, isHexString, } from "@nomicfoundation/hardhat-utils/hex"; import { isObject } from "@nomicfoundation/hardhat-utils/lang"; import { conditionalUnionType, configurationVariableSchema, sensitiveStringSchema, sensitiveUrlSchema, unionType, validateUserConfigZodType, } from "@nomicfoundation/hardhat-zod-utils"; import { z } from "zod"; import { GENERIC_CHAIN_TYPE, L1_CHAIN_TYPE, OPTIMISM_CHAIN_TYPE, } from "../../constants.js"; import { hardforkGte, L1HardforkName, isValidHardforkName, getHardforks, getCurrentHardfork, } from "./edr/types/hardfork.js"; const nonnegativeNumberSchema = z.number().nonnegative(); const nonnegativeIntSchema = z.number().int().nonnegative(); const nonnegativeBigIntSchema = z.bigint().nonnegative(); const blockNumberSchema = nonnegativeIntSchema; const chainIdSchema = nonnegativeIntSchema; const chainTypeUserConfigSchema = unionType( [ z.literal(L1_CHAIN_TYPE), z.literal(OPTIMISM_CHAIN_TYPE), z.literal(GENERIC_CHAIN_TYPE), ], `Expected '${L1_CHAIN_TYPE}', '${OPTIMISM_CHAIN_TYPE}', or '${GENERIC_CHAIN_TYPE}'`, ); const hardforkHistoryUserConfigSchema: z.ZodRecord< z.ZodString, | z.ZodType | z.ZodType > = z.record( conditionalUnionType( [ [ (data) => isObject(data) && typeof data.blockNumber === "number", z.strictObject({ blockNumber: blockNumberSchema, }), ], [ (data) => isObject(data) && typeof data.timestamp === "number", z.strictObject({ timestamp: nonnegativeIntSchema, }), ], ], "Expected an object with either a blockNumber or a timestamp", ), ); const blockExplorerUserConfigSchema = z.object({ name: z.optional(z.string()), url: z.string(), apiUrl: z.string(), }); const blockExplorersUserConfigSchema = z.object({ etherscan: z.optional(blockExplorerUserConfigSchema), blockscout: z.optional(blockExplorerUserConfigSchema), }); const chainDescriptorUserConfigSchema = z.object({ name: z.string(), chainType: z.optional(chainTypeUserConfigSchema), hardforkHistory: z.optional(hardforkHistoryUserConfigSchema), blockExplorers: z.optional(blockExplorersUserConfigSchema), }); const chainDescriptorsUserConfigSchema = z .record( // Allow both numbers and strings for chainId to support larger chainIds unionType([chainIdSchema, z.string()], "Expected a number or a string"), chainDescriptorUserConfigSchema, ) .superRefine((chainDescriptors, ctx) => { if (chainDescriptors !== undefined) { Object.entries(chainDescriptors).forEach(([chainId, chainDescriptor]) => { if (chainDescriptor.hardforkHistory === undefined) { return; } const type = chainDescriptor.chainType ?? GENERIC_CHAIN_TYPE; let previousKind: "block" | "timestamp" = "block"; let previousValue = 0; Object.entries(chainDescriptor.hardforkHistory).forEach( ([name, activation]) => { const errorPath = [chainId, "hardforkHistory", name]; if (!isValidHardforkName(name, type)) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: errorPath, message: `Invalid hardfork name ${name} found in chain descriptor for chain ${chainId}. Expected ${getHardforks(type).join(" | ")}.`, }); } if (activation.blockNumber !== undefined) { // Block numbers must be in ascending order if ( previousKind === "block" && activation.blockNumber < previousValue ) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: errorPath, message: `Invalid block number ${activation.blockNumber} found in chain descriptor for chain ${chainId}. Block numbers must be in ascending order.`, }); } // Block numbers must be defined before timestamps if (previousKind === "timestamp") { ctx.addIssue({ code: z.ZodIssueCode.custom, path: errorPath, message: `Invalid block number ${activation.blockNumber} found in chain descriptor for chain ${chainId}. Block number cannot be defined after a timestamp.`, }); } previousKind = "block"; previousValue = activation.blockNumber; } // Timestamps must be in ascending order else if (activation.timestamp !== undefined) { if ( previousKind === "timestamp" && activation.timestamp < previousValue ) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: errorPath, message: `Invalid timestamp ${activation.timestamp} found in chain descriptor for chain ${chainId}. Timestamps must be in ascending order.`, }); } previousKind = "timestamp"; previousValue = activation.timestamp; } }, ); }); } }); const accountsPrivateKeyUserConfigSchema = unionType( [ configurationVariableSchema, z .string() .refine( (val) => isHexString(val) && getUnprefixedHexString(val).length === 64, ), ], `Expected a hex-encoded private key or a Configuration Variable`, ); const httpNetworkHDAccountsUserConfigSchema = z.object({ mnemonic: sensitiveStringSchema, count: z.optional(nonnegativeIntSchema), initialIndex: z.optional(nonnegativeIntSchema), passphrase: z.optional(sensitiveStringSchema), path: z.optional(z.string()), }); const httpNetworkAccountsUserConfigSchema = conditionalUnionType( [ [(data) => data === "remote", z.literal("remote")], [ (data) => Array.isArray(data), z.array(accountsPrivateKeyUserConfigSchema), ], [isObject, httpNetworkHDAccountsUserConfigSchema], ], `Expected 'remote', an array with private keys or Configuration Variables, or an object with HD account details`, ); const gasUnitUserConfigSchema = unionType( [nonnegativeIntSchema.safe(), nonnegativeBigIntSchema], "Expected a positive safe int or a positive bigint", ); const gasUserConfigSchema = unionType( [z.literal("auto"), gasUnitUserConfigSchema], "Expected 'auto', a positive safe int, or positive bigint", ); const httpNetworkUserConfigSchema = z.object({ type: z.literal("http"), accounts: z.optional(httpNetworkAccountsUserConfigSchema), chainId: z.optional(chainIdSchema), chainType: z.optional(chainTypeUserConfigSchema), from: z.optional(z.string()), gas: z.optional(gasUserConfigSchema), gasMultiplier: z.optional(nonnegativeNumberSchema), gasPrice: z.optional(gasUserConfigSchema), // HTTP network specific url: sensitiveUrlSchema, httpHeaders: z.optional(z.record(z.string())), timeout: z.optional(nonnegativeNumberSchema), }); const accountBalanceUserConfigSchema = unionType( [z.string(), nonnegativeBigIntSchema], "Expected a string or a positive bigint", ); const edrNetworkAccountUserConfigSchema = z.object({ balance: accountBalanceUserConfigSchema, privateKey: accountsPrivateKeyUserConfigSchema, }); const edrNetworkHDAccountsUserConfigSchema = z.object({ mnemonic: z.optional(sensitiveStringSchema), accountsBalance: z.optional(accountBalanceUserConfigSchema), count: z.optional(nonnegativeIntSchema), initialIndex: z.optional(nonnegativeIntSchema), passphrase: z.optional(sensitiveStringSchema), path: z.optional(z.string()), }); const edrNetworkAccountsUserConfigSchema = conditionalUnionType( [ [(data) => Array.isArray(data), z.array(edrNetworkAccountUserConfigSchema)], [isObject, edrNetworkHDAccountsUserConfigSchema], ], `Expected an array with objects with private key and balance or Configuration Variables, or an object with HD account details`, ); const edrNetworkForkingUserConfigSchema = z.object({ enabled: z.optional(z.boolean()), url: sensitiveUrlSchema, blockNumber: z.optional(blockNumberSchema), httpHeaders: z.optional(z.record(z.string())), }); const edrNetworkMempoolUserConfigSchema = z.object({ order: z.optional( unionType( [z.literal("fifo"), z.literal("priority")], "Expected 'fifo' or 'priority'", ), ), }); const edrNetworkMiningUserConfigSchema = z.object({ auto: z.optional(z.boolean()), interval: z.optional( unionType( [ nonnegativeIntSchema, z.tuple([nonnegativeIntSchema, nonnegativeIntSchema]), ], "Expected a number or an array of numbers", ), ), mempool: z.optional(edrNetworkMempoolUserConfigSchema), }); const edrNetworkUserConfigSchema = z.object({ type: z.literal("edr-simulated"), accounts: z.optional(edrNetworkAccountsUserConfigSchema), chainId: z.optional(chainIdSchema), chainType: z.optional(chainTypeUserConfigSchema), from: z.optional(z.string()), gas: z.optional(gasUserConfigSchema), gasMultiplier: z.optional(nonnegativeNumberSchema), gasPrice: z.optional(gasUserConfigSchema), // EDR network specific allowBlocksWithSameTimestamp: z.optional(z.boolean()), allowUnlimitedContractSize: z.optional(z.boolean()), blockGasLimit: z.optional(gasUnitUserConfigSchema), coinbase: z.optional(z.string()), forking: z.optional(edrNetworkForkingUserConfigSchema), hardfork: z.optional(z.string()), initialBaseFeePerGas: z.optional(gasUnitUserConfigSchema), initialDate: z.optional( unionType([z.string(), z.instanceof(Date)], "Expected a string or a Date"), ), loggingEnabled: z.optional(z.boolean()), minGasPrice: z.optional(gasUnitUserConfigSchema), mining: z.optional(edrNetworkMiningUserConfigSchema), networkId: z.optional(chainIdSchema), throwOnCallFailures: z.optional(z.boolean()), throwOnTransactionFailures: z.optional(z.boolean()), }); const baseNetworkUserConfigSchema = z.discriminatedUnion("type", [ httpNetworkUserConfigSchema, edrNetworkUserConfigSchema, ]); function refineEdrNetworkUserConfig( networkConfig: NetworkUserConfig, ctx: RefinementCtx, ): void { if (networkConfig.type === "edr-simulated") { const { chainType = GENERIC_CHAIN_TYPE, hardfork, minGasPrice, initialBaseFeePerGas, } = networkConfig; if (hardfork !== undefined && !isValidHardforkName(hardfork, chainType)) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["hardfork"], message: `Invalid hardfork name ${hardfork} for chainType ${chainType}. Expected ${getHardforks( chainType, ).join(" | ")}.`, }); } const resolvedHardfork = hardfork ?? getCurrentHardfork(chainType); if (chainType === L1_CHAIN_TYPE) { if (hardforkGte(resolvedHardfork, L1HardforkName.LONDON, chainType)) { if (minGasPrice !== undefined) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "minGasPrice is not valid for networks with EIP-1559. Try an older hardfork or remove it.", }); } } else { if (initialBaseFeePerGas !== undefined) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "initialBaseFeePerGas is only valid for networks with EIP-1559. Try a newer hardfork or remove it.", }); } } } if ( typeof networkConfig.mining?.interval === "number" || Array.isArray(networkConfig.mining?.interval) ) { const interval = networkConfig.mining.interval; const minInterval = typeof interval === "number" ? interval : Math.min(...interval); if ( minInterval < 1000 && networkConfig.allowBlocksWithSameTimestamp !== true ) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `mining.interval is set to less than 1000 ms. To avoid the block timestamp diverging from clock time, please set allowBlocksWithSameTimestamp: true on the network config`, }); } } } } // The superRefine is used to perform additional validation of correlated // fields of the edr network that are not possible to express with Zod's // built-in validation methods. // Ideally, it should be applied to the edrNetworkUserConfigSchema, but it // returns a ZodEffects, which is not compatible with the discriminatedUnion // method, so it is applied to the networkUserConfigSchema instead. const networkUserConfigSchema = baseNetworkUserConfigSchema.superRefine( refineEdrNetworkUserConfig, ); const userConfigSchema = z.object({ chainDescriptors: z.optional(chainDescriptorsUserConfigSchema), defaultChainType: z.optional(chainTypeUserConfigSchema), networks: z.optional(z.record(networkUserConfigSchema)), }); const networkConfigOverrideSchema = z .discriminatedUnion("type", [ httpNetworkUserConfigSchema.passthrough(), edrNetworkUserConfigSchema.passthrough(), ]) .superRefine(refineEdrNetworkUserConfig); export async function validateNetworkUserConfig( userConfig: HardhatUserConfig, ): Promise { return validateUserConfigZodType(userConfig, userConfigSchema); } export async function validateNetworkConfigOverride( networkConfigOverride: NetworkUserConfig, ): Promise { return validateUserConfigZodType( networkConfigOverride, networkConfigOverrideSchema, ); } // This type guard does not check beyond accounts being an object // because the actual structure of the object is validated by Zod. export function isHttpNetworkHdAccountsUserConfig( accounts: unknown, ): accounts is HttpNetworkHDAccountsUserConfig { return isObject(accounts); } export function isHttpNetworkHdAccountsConfig( accounts: unknown, ): accounts is HttpNetworkHDAccountsConfig { return ( isObject(accounts) && "mnemonic" in accounts && "count" in accounts && "initialIndex" in accounts && "passphrase" in accounts && "path" in accounts ); } export function isEdrNetworkHdAccountsConfig( accounts: unknown, ): accounts is EdrNetworkHDAccountsConfig { return ( isObject(accounts) && "mnemonic" in accounts && "accountsBalance" in accounts && "count" in accounts && "initialIndex" in accounts && "passphrase" in accounts && "path" in accounts ); } export function isEdrNetworkForkingConfig( forking: unknown, ): forking is EdrNetworkForkingConfig { return ( isObject(forking) && "enabled" in forking && "url" in forking && "cacheDir" in forking ); } export function isEdrNetworkMiningConfig( mining: unknown, ): mining is EdrNetworkMiningConfig { return ( isObject(mining) && "auto" in mining && "interval" in mining && "mempool" in mining ); }