hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
477 lines (430 loc) • 15.6 kB
text/typescript
import type {
ActivationBlockNumberUserConfig,
ActivationTimestampUserConfig,
EdrNetworkForkingConfig,
EdrNetworkHDAccountsConfig,
EdrNetworkMiningConfig,
HardhatUserConfig,
HttpNetworkHDAccountsConfig,
HttpNetworkHDAccountsUserConfig,
} 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<ActivationBlockNumberUserConfig>
| z.ZodType<ActivationTimestampUserConfig>
> = 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 blockExplorerEtherscanUserConfigSchema = z.object({
name: z.optional(z.string()),
url: z.optional(z.string()),
apiUrl: z.optional(z.string()),
});
const blockExplorerBlockscoutUserConfigSchema = z.object({
name: z.optional(z.string()),
url: z.optional(z.string()),
apiUrl: z.optional(z.string()),
});
const blockExplorersUserConfigSchema = z.object({
etherscan: z.optional(blockExplorerEtherscanUserConfigSchema),
blockscout: z.optional(blockExplorerBlockscoutUserConfigSchema),
});
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 networkUserConfigSchema = z.discriminatedUnion("type", [
httpNetworkUserConfigSchema,
edrNetworkUserConfigSchema,
]);
const userConfigSchema = z
.object({
chainDescriptors: z.optional(chainDescriptorsUserConfigSchema),
defaultChainType: z.optional(chainTypeUserConfigSchema),
networks: z.optional(z.record(networkUserConfigSchema)),
})
// 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. It is applied to the whole user config object
// because we need access to the defaultChainType.
.superRefine(refineEdrNetworkUserConfig);
function refineEdrNetworkUserConfig(
{ defaultChainType = GENERIC_CHAIN_TYPE, networks = {} }: HardhatUserConfig,
ctx: RefinementCtx,
): void {
for (const [networkName, network] of Object.entries(networks)) {
if (network.type === "edr-simulated") {
const { chainType, hardfork, minGasPrice, initialBaseFeePerGas } =
network;
const resolvedChainType = chainType ?? defaultChainType;
if (
hardfork !== undefined &&
!isValidHardforkName(hardfork, resolvedChainType)
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["networks", networkName, "hardfork"],
message: `Invalid hardfork name ${hardfork} for chainType ${resolvedChainType}. Expected ${getHardforks(
resolvedChainType,
).join(" | ")}.`,
});
}
const resolvedHardfork =
hardfork ?? getCurrentHardfork(resolvedChainType);
if (resolvedChainType === L1_CHAIN_TYPE) {
if (
hardforkGte(
resolvedHardfork,
L1HardforkName.LONDON,
resolvedChainType,
)
) {
if (minGasPrice !== undefined) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["networks", networkName, "minGasPrice"],
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,
path: ["networks", networkName, "initialBaseFeePerGas"],
message:
"initialBaseFeePerGas is only valid for networks with EIP-1559. Try a newer hardfork or remove it.",
});
}
}
}
const interval = network.mining?.interval;
if (typeof interval === "number" || Array.isArray(interval)) {
const minInterval =
typeof interval === "number" ? interval : Math.min(...interval);
if (
minInterval < 1000 &&
network.allowBlocksWithSameTimestamp !== true
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["networks", networkName, "mining", "interval"],
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`,
});
}
}
}
}
}
export async function validateNetworkUserConfig(
userConfig: HardhatUserConfig,
): Promise<HardhatUserConfigValidationError[]> {
return validateUserConfigZodType(userConfig, userConfigSchema);
}
// 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
);
}