UNPKG

hardhat

Version:

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

621 lines (559 loc) 19 kB
import type { HardhatUserConfig } from "../../../config.js"; import type { SolidityCompilerConfig, SolidityCompilerUserConfig, HardhatConfig, MultiVersionSolidityUserConfig, SingleVersionSolidityUserConfig, SolidityBuildProfileConfig, SolidityConfig, SolidityUserConfig, CommonSolidityCompilerUserConfig, SolcSolidityCompilerConfig, SolcSolidityCompilerUserConfig, } from "../../../types/config.js"; import type { HardhatConfigValidationError, HardhatUserConfigValidationError, } from "../../../types/hooks.js"; import { deepMerge, isObject } from "@nomicfoundation/hardhat-utils/lang"; import { resolveFromRoot } from "@nomicfoundation/hardhat-utils/path"; import { conditionalUnionType, incompatibleFieldType, validateUserConfigZodType, } from "@nomicfoundation/hardhat-zod-utils"; import { z } from "zod"; import { DEFAULT_BUILD_PROFILES } from "./build-profiles.js"; import { hasArm64MirrorBuild, hasOfficialArm64Build, missesSomeOfficialNativeBuilds, } from "./build-system/solc-info.js"; /** * The top-level type SolidityUserConfig is a union type too complex for * TypeScript to handle properly. It accepts fields of different types of * configurations. For example, it accepts `compilers` inside of a * `SingleVersionSolidityUserConfig`. * * For this reason, we declare all the fields that shouldn't exist in the * presence of another one as incompatible. * * This object has all the fields that are incompatible with `version`. */ const incompatibleVersionFields = { compilers: incompatibleFieldType("This field is incompatible with `version`"), overrides: incompatibleFieldType("This field is incompatible with `version`"), profiles: incompatibleFieldType("This field is incompatible with `version`"), }; /** * This is the equivalent of `incompatibleVersionFields`, but for the * `profiles` field. */ const incompatibleProfileFields = { type: incompatibleFieldType("This field is incompatible with `profiles`"), version: incompatibleFieldType("This field is incompatible with `profiles`"), compilers: incompatibleFieldType( "This field is incompatible with `profiles`", ), overrides: incompatibleFieldType( "This field is incompatible with `profiles`", ), }; /** * This is the equivalent of `incompatibleVersionFields`, but for the * `compilers` field. */ const incompatibleCompilerFields = { type: incompatibleFieldType("This field is incompatible with `compilers`"), version: incompatibleFieldType("This field is incompatible with `compilers`"), profiles: incompatibleFieldType( "This field is incompatible with `compilers`", ), }; const commonSolidityUserConfigFields = { isolated: z.boolean().optional(), npmFilesToBuild: z.array(z.string()).optional(), }; const commonSolidityCompilerUserConfigFields = { type: z.string().optional(), version: z.string(), settings: z.any().optional(), path: z.string().optional(), }; const solcSolidityCompilerUserConfigType = z.object({ ...commonSolidityCompilerUserConfigFields, type: z.literal("solc").optional(), preferWasm: z.boolean().optional(), }); const otherSolidityCompilerUserConfigType = z.object( commonSolidityCompilerUserConfigFields, ); // Per-compiler config: preferWasm is only allowed for solc (type undefined or "solc") const solidityCompilerUserConfigType = conditionalUnionType( [ [ (data) => isObject(data) && (!("type" in data) || data.type === undefined || data.type === "solc"), solcSolidityCompilerUserConfigType, ], [ (data) => isObject(data) && "type" in data && data.type !== "solc", otherSolidityCompilerUserConfigType, ], ], "Expected a valid compiler configuration", ); const solcSingleVersionSolidityUserConfigType = solcSolidityCompilerUserConfigType.extend({ ...commonSolidityUserConfigFields, ...incompatibleVersionFields, }); const otherSingleVersionSolidityUserConfigType = otherSolidityCompilerUserConfigType.extend({ ...commonSolidityUserConfigFields, ...incompatibleVersionFields, }); const singleVersionSolidityUserConfigType = conditionalUnionType( [ [ (data) => isObject(data) && (!("type" in data) || data.type === undefined || data.type === "solc"), solcSingleVersionSolidityUserConfigType, ], [ (data) => isObject(data) && "type" in data && data.type !== "solc", otherSingleVersionSolidityUserConfigType, ], ], "Expected a valid single-version Solidity configuration", ); const multiVersionSolidityUserConfigType = z.object({ preferWasm: z.boolean().optional(), compilers: z.array(solidityCompilerUserConfigType).nonempty(), overrides: z.record(z.string(), solidityCompilerUserConfigType).optional(), ...commonSolidityUserConfigFields, ...incompatibleCompilerFields, }); // This definition needs to be aligned with solidityCompilerUserConfigType. // The reason to duplicate it is that we can't `.extend()` a conditional union // type. const singleVersionBuildProfileUserConfigType = conditionalUnionType( [ [ (data) => isObject(data) && (!("type" in data) || data.type === undefined || data.type === "solc"), solcSolidityCompilerUserConfigType.extend({ isolated: z.boolean().optional(), ...incompatibleVersionFields, }), ], [ (data) => isObject(data) && "type" in data && data.type !== "solc", otherSolidityCompilerUserConfigType.extend({ isolated: z.boolean().optional(), ...incompatibleVersionFields, }), ], ], "Expected a valid compiler configuration", ); const multiVersionBuildProfileUserConfigType = z.object({ preferWasm: z.boolean().optional(), compilers: z.array(solidityCompilerUserConfigType).nonempty(), overrides: z.record(z.string(), solidityCompilerUserConfigType).optional(), isolated: z.boolean().optional(), ...incompatibleCompilerFields, }); const buildProfilesSolidityUserConfigType = z.object({ profiles: z.record( z.string(), conditionalUnionType( [ [ (data) => isObject(data) && "version" in data, singleVersionBuildProfileUserConfigType, ], [ (data) => isObject(data) && "compilers" in data, multiVersionBuildProfileUserConfigType, ], ], "Expected an object configuring one or more versions of Solidity", ), ), ...incompatibleProfileFields, }); const solidityUserConfigType = conditionalUnionType( [ [(data) => typeof data === "string", z.string()], [(data) => Array.isArray(data), z.array(z.string()).nonempty()], [ (data) => isObject(data) && "version" in data, singleVersionSolidityUserConfigType, ], [ (data) => isObject(data) && "compilers" in data, multiVersionSolidityUserConfigType, ], [ (data) => isObject(data) && "profiles" in data, buildProfilesSolidityUserConfigType, ], ], "Expected a version string, an array of version strings, or an object configuring one or more versions of Solidity or multiple build profiles", ); const sourcePathsType = conditionalUnionType( [ [(data) => typeof data === "string", z.string()], [(data) => Array.isArray(data), z.array(z.string()).nonempty()], ], "Expected a string or an array of strings", ); const userConfigType = z.object({ paths: z .object({ sources: conditionalUnionType( [ [isObject, z.object({ solidity: sourcePathsType.optional() })], [ (data) => typeof data === "string" || Array.isArray(data), sourcePathsType, ], ], "Expected a string, an array of strings, or an object with an optional 'solidity' property", ).optional(), }) .optional(), solidity: solidityUserConfigType.optional(), }); export function validateSolidityUserConfig( userConfig: unknown, ): HardhatUserConfigValidationError[] { const result = validateUserConfigZodType(userConfig, userConfigType); if ( isObject(userConfig) && isObject(userConfig.solidity) && isObject(userConfig.solidity.profiles) && !("default" in userConfig.solidity.profiles) ) { result.push({ message: "The 'default' profile is required when using Solidity build profiles", path: ["solidity", "profiles"], }); } return result; } export function validateSolidityConfig( resolvedConfig: HardhatConfig, ): HardhatConfigValidationError[] { const errors: HardhatConfigValidationError[] = []; errors.push(...validateRegisteredCompilerTypes(resolvedConfig)); errors.push(...validatePreferWasmRequiresSolc(resolvedConfig)); return errors; } function validateRegisteredCompilerTypes( resolvedConfig: HardhatConfig, ): HardhatConfigValidationError[] { const errors: HardhatConfigValidationError[] = []; const registered = new Set(resolvedConfig.solidity.registeredCompilerTypes); for (const [profileName, profile] of Object.entries( resolvedConfig.solidity.profiles, )) { for (const [i, compiler] of profile.compilers.entries()) { const type = compiler.type ?? "solc"; if (!registered.has(type)) { errors.push({ path: ["solidity", "profiles", profileName, "compilers", i, "type"], message: `Unknown compiler type "${type}". Registered types: ${[...registered].join(", ")}`, }); } } for (const [sourceName, override] of Object.entries(profile.overrides)) { const type = override.type ?? "solc"; if (!registered.has(type)) { errors.push({ path: [ "solidity", "profiles", profileName, "overrides", sourceName, "type", ], message: `Unknown compiler type "${type}". Registered types: ${[...registered].join(", ")}`, }); } } } return errors; } function validatePreferWasmRequiresSolc( resolvedConfig: HardhatConfig, ): HardhatConfigValidationError[] { const errors: HardhatConfigValidationError[] = []; for (const [profileName, profile] of Object.entries( resolvedConfig.solidity.profiles, )) { if (!profile.preferWasm) { continue; } for (const [i, compiler] of profile.compilers.entries()) { const type = compiler.type; if (type !== undefined && type !== "solc") { /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We need to cast because within Hardhat core the type of `type` is `never`, as you can only get into this if with a plugin. */ const compilerType: string = (compiler as any).type; errors.push({ path: ["solidity", "profiles", profileName, "compilers", i, "type"], message: `Compiler type must be "solc" if \`preferWasm\` is \`true\` in the build profile, but found type "${compilerType}"`, }); } } for (const [sourceName, override] of Object.entries(profile.overrides)) { const type = override.type; if (type !== undefined && type !== "solc") { /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We need to cast because within Hardhat core the type of `type` is `never`, as you can only get into this if with a plugin. */ const overrideType: string = (override as any).type; errors.push({ path: [ "solidity", "profiles", profileName, "overrides", sourceName, "type", ], message: `Compiler type must be "solc" if \`preferWasm\` is \`true\` in the build profile, but found type "${overrideType}"`, }); } } } return errors; } export async function resolveSolidityUserConfig( userConfig: HardhatUserConfig, resolvedConfig: HardhatConfig, ): Promise<HardhatConfig> { let sourcesPaths = userConfig.paths?.sources; // TODO: use isObject when the type narrowing issue is fixed sourcesPaths = typeof sourcesPaths === "object" && !Array.isArray(sourcesPaths) ? sourcesPaths.solidity : sourcesPaths; sourcesPaths ??= "contracts"; sourcesPaths = Array.isArray(sourcesPaths) ? sourcesPaths : [sourcesPaths]; const resolvedPaths = sourcesPaths.map((p) => resolveFromRoot(resolvedConfig.paths.root, p), ); return { ...resolvedConfig, paths: { ...resolvedConfig.paths, sources: { ...resolvedConfig.paths.sources, solidity: resolvedPaths, }, }, solidity: resolveSolidityConfig(userConfig.solidity ?? "0.8.0"), }; } function resolveSolidityConfig( solidityConfig: SolidityUserConfig, ): SolidityConfig { if (typeof solidityConfig === "string") { solidityConfig = [solidityConfig]; } // user provided an array of versions or a single version if (Array.isArray(solidityConfig)) { const defaultSolidityConfig = { compilers: solidityConfig.map((version) => ({ version })), }; return { profiles: { default: resolveBuildProfileConfig(defaultSolidityConfig), production: resolveBuildProfileConfig( copyFromDefault(defaultSolidityConfig), true, ), }, npmFilesToBuild: [], registeredCompilerTypes: ["solc"], }; } // user provided a single version config or a multi version config if ("version" in solidityConfig || "compilers" in solidityConfig) { return { profiles: { default: resolveBuildProfileConfig(solidityConfig), production: resolveBuildProfileConfig( copyFromDefault(solidityConfig), true, ), }, npmFilesToBuild: solidityConfig.npmFilesToBuild ?? [], registeredCompilerTypes: ["solc"], }; } // user provided a build profiles config const profiles: Record<string, SolidityBuildProfileConfig> = {}; for (const [profileName, profile] of Object.entries( solidityConfig.profiles, )) { profiles[profileName] = resolveBuildProfileConfig( profile, profileName === "production", ); } // This will generate default build profiles (e.g. production) when they are // not specified in the config, cloning from 'default', which is always present for (const profile of DEFAULT_BUILD_PROFILES) { if (!(profile in profiles)) { profiles[profile] = resolveBuildProfileConfig( copyFromDefault(solidityConfig.profiles.default), profile === "production", ); } } return { profiles, npmFilesToBuild: solidityConfig.npmFilesToBuild ?? [], registeredCompilerTypes: ["solc"], }; } function resolveBuildProfileConfig( solidityConfig: | SingleVersionSolidityUserConfig | MultiVersionSolidityUserConfig, production: boolean = false, ): SolidityBuildProfileConfig { if ("version" in solidityConfig) { return { compilers: [resolveSolidityCompilerConfig(solidityConfig, production)], overrides: {}, isolated: solidityConfig.isolated ?? production, preferWasm: solidityConfig.preferWasm ?? false, }; } return { compilers: solidityConfig.compilers.map((compiler) => resolveSolidityCompilerConfig(compiler, production), ), overrides: Object.fromEntries( Object.entries(solidityConfig.overrides ?? {}).map( ([userSourceName, override]) => [ userSourceName, resolveSolidityCompilerConfig(override, production), ], ), ), isolated: solidityConfig.isolated ?? production, preferWasm: solidityConfig.preferWasm ?? false, }; } function resolveSolidityCompilerConfig( compilerConfig: SolidityCompilerUserConfig, production: boolean = false, ): SolidityCompilerConfig { const defaultSettings: SolidityCompilerConfig["settings"] = { outputSelection: { "*": { "": ["ast"], "*": [ "abi", "evm.bytecode", "evm.deployedBytecode", "evm.methodIdentifiers", "metadata", ], }, }, }; if (production && isSolcSolidityCompilerUserConfig(compilerConfig)) { defaultSettings.optimizer = { enabled: true, runs: 200, }; } const resolvedSettings = deepMerge( defaultSettings, compilerConfig.settings ?? {}, ); // Resolve solc-specific preferWasm if this is a SolcSolidityCompilerUserConfig if (isSolcSolidityCompilerUserConfig(compilerConfig)) { // Resolve per-compiler preferWasm: // If explicitly set, use that value. // Otherwise, for ARM64 Linux: // - Versions below the mirror threshold (< 0.5.0) always use WASM, // since no native ARM64 build exists anywhere. // - In production, versions without official ARM64 builds // also default to WASM. let resolvedPreferWasm: boolean | undefined = compilerConfig.preferWasm; if (resolvedPreferWasm === undefined && missesSomeOfficialNativeBuilds()) { const version = compilerConfig.version; if (!hasOfficialArm64Build(version) && !hasArm64MirrorBuild(version)) { resolvedPreferWasm = true; } else if (production && !hasOfficialArm64Build(version)) { resolvedPreferWasm = true; } } const solcResolved: SolcSolidityCompilerConfig = { type: compilerConfig.type, version: compilerConfig.version, settings: resolvedSettings, path: compilerConfig.path, preferWasm: resolvedPreferWasm, }; return solcResolved; } const unknownCompilerConfig = /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We need to cast here because compilerConfig has `never` type here, as this case is only accessible when there are other types of compilers registered through plugins. */ compilerConfig as unknown as CommonSolidityCompilerUserConfig; return { type: unknownCompilerConfig.type, version: unknownCompilerConfig.version, settings: resolvedSettings, path: unknownCompilerConfig.path, }; } export function isSolcSolidityCompilerUserConfig( config: SolidityCompilerUserConfig, ): config is SolcSolidityCompilerUserConfig { return config.type === undefined || config.type === "solc"; } function copyFromDefault( defaultSolidityConfig: | SingleVersionSolidityUserConfig | MultiVersionSolidityUserConfig, ): SingleVersionSolidityUserConfig | MultiVersionSolidityUserConfig { if ("version" in defaultSolidityConfig) { return { version: defaultSolidityConfig.version, type: defaultSolidityConfig.type, }; } return { compilers: defaultSolidityConfig.compilers.map((c) => ({ version: c.version, type: c.type, })), overrides: Object.fromEntries( Object.entries(defaultSolidityConfig.overrides ?? {}).map( ([userSourceName, override]) => [ userSourceName, { version: override.version, type: override.type }, ], ), ), }; }