UNPKG

hardhat

Version:

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

315 lines (285 loc) 9.94 kB
import type { HardhatUserConfig } from "../../../config.js"; import type { ConfigurationVariableResolver, HardhatConfig, ResolvedConfigurationVariable, } from "../../../types/config.js"; import type { HardhatUserConfigValidationError } from "../../../types/hooks.js"; import type { SolidityTestForkingConfig, SolidityTestProfileConfig, SolidityTestProfileUserConfig, } from "../../../types/test.js"; import path from "node:path"; import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors"; import { isObject } from "@nomicfoundation/hardhat-utils/lang"; import { resolveFromRoot } from "@nomicfoundation/hardhat-utils/path"; import { conditionalUnionType, incompatibleFieldType, sensitiveStringSchema, sensitiveUrlSchema, unionType, validateUserConfigZodType, } from "@nomicfoundation/hardhat-zod-utils"; import { z } from "zod"; import { DEFAULT_TEST_PROFILE } from "./test-profiles.js"; // the keccak256 of "built for ethereum" export const DEFAULT_FUZZ_SEED = "0x7727ea51af0441c20da14dcd68a15dac8c9ebd589c5be8fa8c87c1d3720450bc"; const nonnegativeSafeIntOrBigInt = unionType( [z.number().int().nonnegative().safe(), z.bigint().nonnegative()], "Expected a nonnegative safe int or a nonnegative bigint", ); const nonnegativeSafeIntOrSafeBigInt = unionType( [ z.number().int().nonnegative().safe(), z .bigint() .nonnegative() .refine((value) => value <= BigInt(Number.MAX_SAFE_INTEGER)), ], "Expected a nonnegative safe int or a nonnegative safe bigint", ); const solidityTestProfileUserConfigType = z.object({ fsPermissions: z .object({ readWriteFile: z.array(z.string()).optional(), readFile: z.array(z.string()).optional(), writeFile: z.array(z.string()).optional(), dangerouslyReadWriteDirectory: z.array(z.string()).optional(), readDirectory: z.array(z.string()).optional(), dangerouslyWriteDirectory: z.array(z.string()).optional(), }) .optional(), isolate: z.boolean().optional(), ffi: z.boolean().optional(), allowInternalExpectRevert: z.boolean().optional(), from: z.string().startsWith("0x").optional(), txOrigin: z.string().startsWith("0x").optional(), initialBalance: z.bigint().optional(), blockBaseFeePerGas: z.bigint().optional(), coinbase: z.string().startsWith("0x").optional(), blockTimestamp: z.bigint().optional(), prevRandao: z.bigint().optional(), gasLimit: z.bigint().optional(), memoryLimit: nonnegativeSafeIntOrBigInt.optional(), blockGasLimit: z.number().or(z.bigint()).or(z.literal(false)).optional(), transactionGasCap: z.number().or(z.bigint()).or(z.literal(false)).optional(), fuzz: z .object({ failurePersistDir: z.string().optional(), failurePersistFile: z.string().optional(), runs: z.number().optional(), maxTestRejects: z.number().optional(), seed: z.string().optional(), dictionaryWeight: z.number().optional(), includeStorage: z.boolean().optional(), includePushBytes: z.boolean().optional(), showLogs: z.boolean().optional(), timeout: nonnegativeSafeIntOrSafeBigInt.optional(), }) .optional(), forking: z .object({ url: z.optional(sensitiveUrlSchema), blockNumber: z.optional( unionType( [z.number().int().nonnegative().safe(), z.bigint().nonnegative()], "Expected a nonnegative safe int or a nonnegative bigint", ), ), rpcEndpoints: z.record(sensitiveStringSchema).optional(), }) .optional(), invariant: z .object({ failurePersistDir: z.string().optional(), runs: z.number().optional(), depth: z.number().optional(), failOnRevert: z.boolean().optional(), callOverride: z.boolean().optional(), dictionaryWeight: z.number().optional(), includeStorage: z.boolean().optional(), includePushBytes: z.boolean().optional(), shrinkRunLimit: z.number().optional(), timeout: nonnegativeSafeIntOrSafeBigInt.optional(), }) .optional(), eip712Types: z .object({ include: z.array(z.string()).optional(), exclude: z.array(z.string()).optional(), }) .optional(), }); const solidityTestFlatUserConfigType = solidityTestProfileUserConfigType.extend( { profiles: incompatibleFieldType( "This field is incompatible with the flat solidity test config", ), }, ); const solidityTestProfilesUserConfigType = z.object({ profiles: z .record(z.string(), solidityTestProfileUserConfigType) .refine( (profiles) => DEFAULT_TEST_PROFILE in profiles, "A `default` profile is required when using `profiles`", ) .refine( (profiles) => !(DEFAULT_TEST_PROFILE in profiles) || Object.keys(profiles).every((name) => name === DEFAULT_TEST_PROFILE), "Only the `default` profile is supported. Other profile names will be supported in a future release.", ), }); const solidityTestUserConfigType = conditionalUnionType( [ [ (data) => isObject(data) && "profiles" in data && Object.keys(data).length === 1, solidityTestProfilesUserConfigType, ], [isObject, solidityTestFlatUserConfigType], ], "Expected a Solidity test config or a `{ profiles: { ... } }` wrapper", ); const userConfigType = z.object({ paths: z .object({ test: conditionalUnionType( [ [isObject, z.object({ solidity: z.string().optional() })], [(data) => typeof data === "string", z.string()], ], "Expected a string or an object with an optional 'solidity' property", ).optional(), }) .optional(), test: z .object({ solidity: solidityTestUserConfigType.optional(), }) .optional(), }); export function resolveSolidityTestForkingConfig( forkingUserConfig: SolidityTestProfileUserConfig["forking"], resolveConfigurationVariable: ConfigurationVariableResolver, ): SolidityTestForkingConfig | undefined { if (forkingUserConfig === undefined) { return undefined; } const resolvedRpcEndpoints: Record<string, ResolvedConfigurationVariable> = {}; if (forkingUserConfig.rpcEndpoints !== undefined) { for (const [name, url] of Object.entries(forkingUserConfig.rpcEndpoints)) { resolvedRpcEndpoints[name] = resolveConfigurationVariable(url); } } return { ...forkingUserConfig, blockNumber: forkingUserConfig.blockNumber !== undefined ? BigInt(forkingUserConfig.blockNumber) : undefined, url: forkingUserConfig.url !== undefined ? resolveConfigurationVariable(forkingUserConfig.url) : undefined, rpcEndpoints: resolvedRpcEndpoints, }; } export function validateSolidityTestUserConfig( userConfig: unknown, ): HardhatUserConfigValidationError[] { return validateUserConfigZodType(userConfig, userConfigType); } export async function resolveSolidityTestUserConfig( userConfig: HardhatUserConfig, resolvedConfig: HardhatConfig, resolveConfigurationVariable: ConfigurationVariableResolver, ): Promise<HardhatConfig> { let testsPath = userConfig.paths?.tests; // TODO: use isObject when the type narrowing issue is fixed testsPath = typeof testsPath === "object" ? testsPath.solidity : testsPath; testsPath ??= "test"; const defaultRpcCachePath = path.join(resolvedConfig.paths.cache, "edr"); const solidityUserConfig = userConfig.test?.solidity; let profileUserConfig: SolidityTestProfileUserConfig | undefined; if (solidityUserConfig !== undefined && "profiles" in solidityUserConfig) { profileUserConfig = solidityUserConfig.profiles[DEFAULT_TEST_PROFILE]; assertHardhatInvariant( profileUserConfig !== undefined, "default profile must be present when the profiles wrapper user config is supplied", ); } else { profileUserConfig = solidityUserConfig; } const resolvedForking = resolveSolidityTestForkingConfig( profileUserConfig?.forking, resolveConfigurationVariable, ); const { memoryLimit, fuzz, invariant, eip712Types, ...otherProfileUserConfig } = profileUserConfig ?? {}; const resolvedDefaultProfile = { rpcCachePath: defaultRpcCachePath, ...otherProfileUserConfig, memoryLimit: memoryLimit !== undefined ? BigInt(memoryLimit) : undefined, fuzz: resolveFuzzConfig(fuzz), invariant: resolveInvariantConfig(invariant), forking: resolvedForking, eip712Types: resolveEip712TypesConfig(eip712Types), }; return { ...resolvedConfig, paths: { ...resolvedConfig.paths, tests: { ...resolvedConfig.paths.tests, solidity: resolveFromRoot(resolvedConfig.paths.root, testsPath), }, }, test: { ...resolvedConfig.test, solidity: { profiles: { [DEFAULT_TEST_PROFILE]: resolvedDefaultProfile }, }, }, }; } export function resolveFuzzConfig( fuzzUserConfig: SolidityTestProfileUserConfig["fuzz"] = {}, ): SolidityTestProfileConfig["fuzz"] { const { seed, timeout, ...otherFuzzUserConfig } = fuzzUserConfig; return { ...otherFuzzUserConfig, seed: seed ?? DEFAULT_FUZZ_SEED, ...(timeout !== undefined ? { timeout: Number(timeout) } : {}), }; } export function resolveInvariantConfig( invariantUserConfig: SolidityTestProfileUserConfig["invariant"], ): SolidityTestProfileConfig["invariant"] { if (invariantUserConfig === undefined) { return undefined; } const { timeout, ...otherInvariantUserConfig } = invariantUserConfig; return { ...otherInvariantUserConfig, ...(timeout !== undefined ? { timeout: Number(timeout) } : {}), }; } export function resolveEip712TypesConfig( eip712TypesUserConfig: SolidityTestProfileUserConfig["eip712Types"] = {}, ): SolidityTestProfileConfig["eip712Types"] { return { include: eip712TypesUserConfig.include ?? [], exclude: eip712TypesUserConfig.exclude ?? [], }; }