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
text/typescript
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";
}