UNPKG

hardhat

Version:

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

378 lines (339 loc) 10.9 kB
import type { HardhatUserConfig } from "../../../config.js"; import type { HardhatConfig, MultiVersionSolidityUserConfig, SingleVersionSolidityUserConfig, SolcConfig, SolcUserConfig, SolidityBuildProfileConfig, SolidityConfig, SolidityUserConfig, } from "../../../types/config.js"; import type { HardhatUserConfigValidationError } from "../../../types/hooks.js"; import os from "node:os"; 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"; 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 commonSolcUserConfigType = z.object({ isolated: z.boolean().optional(), }); const solcUserConfigType = z.object({ version: z.string(), settings: z.any().optional(), compilers: incompatibleFieldType("This field is incompatible with `version`"), overrides: incompatibleFieldType("This field is incompatible with `version`"), profiles: incompatibleFieldType("This field is incompatible with `version`"), }); // NOTE: This is only to match the setup present in ./type-extensions.ts const singleVersionSolcUserConfigType = solcUserConfigType.extend({ isolated: z.boolean().optional(), preferWasm: z.boolean().optional(), }); const multiVersionSolcUserConfigType = commonSolcUserConfigType.extend({ compilers: z.array(solcUserConfigType).nonempty(), overrides: z.record(z.string(), solcUserConfigType).optional(), isolated: z.boolean().optional(), preferWasm: z.boolean().optional(), version: incompatibleFieldType("This field is incompatible with `compilers`"), settings: incompatibleFieldType( "This field is incompatible with `compilers`", ), }); const commonSolidityUserConfigType = z.object({ npmFilesToBuild: z.array(z.string()).optional(), }); const singleVersionSolidityUserConfigType = singleVersionSolcUserConfigType .merge(commonSolidityUserConfigType) .extend({ compilers: incompatibleFieldType( "This field is incompatible with `version`", ), overrides: incompatibleFieldType( "This field is incompatible with `version`", ), profiles: incompatibleFieldType( "This field is incompatible with `version`", ), }); const multiVersionSolidityUserConfigType = multiVersionSolcUserConfigType .merge(commonSolidityUserConfigType) .extend({ version: incompatibleFieldType( "This field is incompatible with `compilers`", ), profiles: incompatibleFieldType( "This field is incompatible with `compilers`", ), }); const buildProfilesSolidityUserConfigType = commonSolidityUserConfigType.extend( { profiles: z.record( z.string(), conditionalUnionType( [ [ (data) => isObject(data) && "version" in data, singleVersionSolcUserConfigType, ], [ (data) => isObject(data) && "compilers" in data, multiVersionSolcUserConfigType, ], ], "Expected an object configuring one or more versions of Solidity", ), ), version: incompatibleFieldType( "This field is incompatible with `profiles`", ), compilers: incompatibleFieldType( "This field is incompatible with `profiles`", ), overrides: incompatibleFieldType( "This field is incompatible with `profiles`", ), }, ); const soldityUserConfigType = 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 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: soldityUserConfigType.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 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: [], }; } // 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 ?? [], }; } // 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 ?? [], }; } function resolveBuildProfileConfig( solidityConfig: | SingleVersionSolidityUserConfig | MultiVersionSolidityUserConfig, production: boolean = false, ): SolidityBuildProfileConfig { if ("version" in solidityConfig) { return { compilers: [resolveSolcConfig(solidityConfig, production)], overrides: {}, isolated: solidityConfig.isolated ?? production, preferWasm: solidityConfig.preferWasm ?? (production && shouldUseWasm()), }; } return { compilers: solidityConfig.compilers.map((compiler) => resolveSolcConfig(compiler, production), ), overrides: Object.fromEntries( Object.entries(solidityConfig.overrides ?? {}).map( ([userSourceName, override]) => [ userSourceName, resolveSolcConfig(override, production), ], ), ), isolated: solidityConfig.isolated ?? production, preferWasm: solidityConfig.preferWasm ?? (production && shouldUseWasm()), }; } function resolveSolcConfig( solcConfig: SolcUserConfig, production: boolean = false, ): SolcConfig { const defaultSolcConfigSettings: SolcConfig["settings"] = { outputSelection: { "*": { "": ["ast"], "*": [ "abi", "evm.bytecode", "evm.deployedBytecode", "evm.methodIdentifiers", "metadata", ], }, }, }; if (production) { defaultSolcConfigSettings.optimizer = { enabled: true, runs: 200, }; } return { version: solcConfig.version, settings: deepMerge(defaultSolcConfigSettings, solcConfig.settings ?? {}), path: solcConfig.path, }; } function copyFromDefault( defaultSolidityConfig: | SingleVersionSolidityUserConfig | MultiVersionSolidityUserConfig, ): SingleVersionSolidityUserConfig | MultiVersionSolidityUserConfig { if ("version" in defaultSolidityConfig) { return { version: defaultSolidityConfig.version, }; } return { compilers: defaultSolidityConfig.compilers.map((c) => ({ version: c.version, })), overrides: Object.fromEntries( Object.entries(defaultSolidityConfig.overrides ?? {}).map( ([userSourceName, override]) => [ userSourceName, { version: override.version }, ], ), ), }; } // We use wasm builds in production to avoid using unofficial builds for deployments // This should change once https://github.com/ethereum/solidity/issues/11351 gets resolved export function shouldUseWasm(): boolean { return os.platform() === "linux" && os.arch() === "arm64"; }