diff --git a/.changeset/young-baboons-sniff.md b/.changeset/young-baboons-sniff.md new file mode 100644 index 00000000000..75bcc5e012f --- /dev/null +++ b/.changeset/young-baboons-sniff.md @@ -0,0 +1,5 @@ +--- +"hardhat": patch +--- + +Move the chains config to the top level of the network config, rename it to chainDescriptors, and refactor it to include blockExplorers and name. diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/chain-descriptors.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/chain-descriptors.ts new file mode 100644 index 00000000000..3b03f0d638b --- /dev/null +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/chain-descriptors.ts @@ -0,0 +1,556 @@ +import type { ChainDescriptorsConfig } from "../../../types/config.js"; + +import { + GENERIC_CHAIN_TYPE, + L1_CHAIN_TYPE, + OPTIMISM_CHAIN_TYPE, +} from "../../constants.js"; + +export const DEFAULT_CHAIN_DESCRIPTORS: ChainDescriptorsConfig = new Map([ + // ethereum mainnet + [ + 1n, + { + name: "Ethereum", + chainType: L1_CHAIN_TYPE, + blockExplorers: { + etherscan: { + url: "https://etherscan.io", + apiUrl: "https://api.etherscan.io/api", + }, + blockscout: { + url: "https://eth.blockscout.com", + apiUrl: "https://eth.blockscout.com/api", + }, + }, + }, + ], + // holesky testnet + [ + 17_000n, + { + name: "Holesky", + chainType: L1_CHAIN_TYPE, + blockExplorers: { + etherscan: { + url: "https://holesky.etherscan.io", + apiUrl: "https://api-holesky.etherscan.io/api", + }, + blockscout: { + url: "https://eth-holesky.blockscout.com", + apiUrl: "https://eth-holesky.blockscout.com/api", + }, + }, + }, + ], + // hoodi testnet + [ + 560_048n, + { + name: "Hoodi", + chainType: L1_CHAIN_TYPE, + blockExplorers: { + etherscan: { + url: "https://hoodi.etherscan.io", + apiUrl: "https://api-hoodi.etherscan.io/api", + }, + blockscout: { + url: "https://eth-hoodi.blockscout.com", + apiUrl: "https://eth-hoodi.blockscout.com/api", + }, + }, + }, + ], + // sepolia testnet + [ + 11_155_111n, + { + name: "Sepolia", + chainType: L1_CHAIN_TYPE, + blockExplorers: { + etherscan: { + url: "https://sepolia.etherscan.io", + apiUrl: "https://api-sepolia.etherscan.io/api", + }, + blockscout: { + url: "https://eth-sepolia.blockscout.com", + apiUrl: "https://eth-sepolia.blockscout.com/api", + }, + }, + }, + ], + // optimism mainnet + [ + 10n, + { + name: "OP Mainnet", + chainType: OPTIMISM_CHAIN_TYPE, + blockExplorers: { + etherscan: { + url: "https://optimistic.etherscan.io", + apiUrl: "https://api-optimistic.etherscan.io/api", + }, + blockscout: { + url: "https://optimism.blockscout.com", + apiUrl: "https://optimism.blockscout.com/api", + }, + }, + }, + ], + // optimism sepolia testnet + [ + 11_155_420n, + { + name: "OP Sepolia", + chainType: OPTIMISM_CHAIN_TYPE, + blockExplorers: { + etherscan: { + url: "https://sepolia-optimism.etherscan.io", + apiUrl: "https://api-sepolia-optimism.etherscan.io/api", + }, + blockscout: { + url: "https://optimism-sepolia.blockscout.com", + apiUrl: "https://optimism-sepolia.blockscout.com/api", + }, + }, + }, + ], + // arbitrum one mainnet + [ + 42_161n, + { + name: "Arbitrum One", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + etherscan: { + name: "Arbiscan", + url: "https://arbiscan.io", + apiUrl: "https://api.arbiscan.io/api", + }, + blockscout: { + url: "https://arbitrum.blockscout.com", + apiUrl: "https://arbitrum.blockscout.com/api", + }, + }, + }, + ], + // arbitrum nova mainnet + [ + 42_170n, + { + name: "Arbitrum Nova", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + etherscan: { + name: "Arbiscan", + url: "https://nova.arbiscan.io", + apiUrl: "https://api-nova.arbiscan.io/api", + }, + blockscout: { + url: "https://arbitrum-nova.blockscout.com", + apiUrl: "https://arbitrum-nova.blockscout.com/api", + }, + }, + }, + ], + // arbitrum sepolia testnet + [ + 42_170n, + { + name: "Arbitrum Sepolia", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + etherscan: { + name: "Arbiscan", + url: "https://sepolia.arbiscan.io", + apiUrl: "https://api-sepolia.arbiscan.io/api", + }, + blockscout: { + url: "https://arbitrum-sepolia.blockscout.com", + apiUrl: "https://arbitrum-sepolia.blockscout.com/api", + }, + }, + }, + ], + // base mainnet + [ + 8_453n, + { + name: "Base", + chainType: OPTIMISM_CHAIN_TYPE, + blockExplorers: { + etherscan: { + name: "Basescan", + url: "https://basescan.org", + apiUrl: "https://api.basescan.org/api", + }, + blockscout: { + url: "https://base.blockscout.com", + apiUrl: "https://base.blockscout.com/api", + }, + }, + }, + ], + // base sepolia testnet + [ + 84_532n, + { + name: "Base Sepolia", + chainType: OPTIMISM_CHAIN_TYPE, + blockExplorers: { + etherscan: { + name: "Basescan", + url: "https://sepolia.basescan.org", + apiUrl: "https://api-sepolia.basescan.org/api", + }, + blockscout: { + url: "https://base-sepolia.blockscout.com", + apiUrl: "https://base-sepolia.blockscout.com/api", + }, + }, + }, + ], + // avalanche mainnet + [ + 43_114n, + { + name: "Avalanche", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + etherscan: { + name: "SnowTrace", + url: "https://snowtrace.io", + apiUrl: "https://api.snowtrace.io/api", + }, + }, + }, + ], + // avalanche fuji testnet + [ + 43_113n, + { + name: "Avalanche Fuji", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + etherscan: { + name: "SnowTrace", + url: "https://testnet.snowtrace.io", + apiUrl: "https://api-testnet.snowtrace.io/api", + }, + }, + }, + ], + // polygon mainnet + [ + 137n, + { + name: "Polygon", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + etherscan: { + name: "PolygonScan", + url: "https://polygonscan.com", + apiUrl: "https://api.polygonscan.com/api", + }, + blockscout: { + url: "https://polygon.blockscout.com", + apiUrl: "https://polygon.blockscout.com/api", + }, + }, + }, + ], + // polygon amoy testnet + [ + 80_002n, + { + name: "Polygon Amoy", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + etherscan: { + name: "PolygonScan", + url: "https://amoy.polygonscan.com", + apiUrl: "https://api-amoy.polygonscan.com/api", + }, + }, + }, + ], + // polygon zkevm mainnet + [ + 1_101n, + { + name: "Polygon zkEVM", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + etherscan: { + name: "PolygonScan", + url: "https://zkevm.polygonscan.com", + apiUrl: "https://api-zkevm.polygonscan.com/api", + }, + blockscout: { + url: "https://zkevm.blockscout.com", + apiUrl: "https://zkevm.blockscout.com/api", + }, + }, + }, + ], + // polygon zkevm cardona testnet + [ + 2_442n, + { + name: "Polygon zkEVM Cardona", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + etherscan: { + name: "PolygonScan", + url: "https://cardona-zkevm.polygonscan.com", + apiUrl: "https://api-cardona-zkevm.polygonscan.com/api", + }, + }, + }, + ], + // zksync era mainnet + [ + 324n, + { + name: "ZKsync Era", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + etherscan: { + name: "zkSync Era Explorer", + url: "https://era.zksync.network", + apiUrl: "https://api-era.zksync.network/api", + }, + blockscout: { + url: "https://zksync.blockscout.com", + apiUrl: "https://zksync.blockscout.com/api", + }, + }, + }, + ], + // zksync sepolia testnet + [ + 300n, + { + name: "ZKsync Sepolia Testnet", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + etherscan: { + name: "zkSync Era Explorer", + url: "https://sepolia-era.zksync.network", + apiUrl: "https://sepolia-era.zksync.network/api", + }, + blockscout: { + url: "https://zksync-sepolia.blockscout.com", + apiUrl: "https://zksync-sepolia.blockscout.com/api", + }, + }, + }, + ], + // binance smart chain mainnet + [ + 56n, + { + name: "Binance Smart Chain", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + etherscan: { + name: "BscScan", + url: "https://bscscan.com", + apiUrl: "https://api.bscscan.com/api", + }, + }, + }, + ], + // binance smart chain testnet + [ + 97n, + { + name: "Binance Smart Chain Testnet", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + etherscan: { + name: "BscScan", + url: "https://testnet.bscscan.com", + apiUrl: "https://api-testnet.bscscan.com/api", + }, + }, + }, + ], + // gnosis mainnet + [ + 100n, + { + name: "Gnosis", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + etherscan: { + name: "Gnosisscan", + url: "https://gnosisscan.io", + apiUrl: "https://api.gnosisscan.com/api", + }, + blockscout: { + url: "https://gnosis.blockscout.com", + apiUrl: "https://gnosis.blockscout.com/api", + }, + }, + }, + ], + // gnosis chiado testnet + [ + 10_200n, + { + name: "Gnosis Chiado", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + blockscout: { + url: "https://gnosis-chiado.blockscout.com", + apiUrl: "https://gnosis-chiado.blockscout.com/api", + }, + }, + }, + ], + // fantom mainnet + [ + 250n, + { + name: "Fantom", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + blockscout: { + name: "FTMScout", + url: "https://ftmscout.com", + apiUrl: "https://ftmscout.com/api", + }, + }, + }, + ], + // moonbeam mainnet + [ + 1_284n, + { + name: "Moonbeam", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + etherscan: { + name: "Moonscan", + url: "https://moonbeam.moonscan.io", + apiUrl: "https://api-moonbeam.moonscan.io/api", + }, + }, + }, + ], + // moonbeam moonbase alpha testnet + [ + 1_287n, + { + name: "Moonbase Alpha", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + etherscan: { + name: "Moonscan", + url: "https://moonbase.moonscan.io", + apiUrl: "https://api-moonbase.moonscan.io/api", + }, + }, + }, + ], + // moonriver mainnet + [ + 1_285n, + { + name: "Moonriver", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + etherscan: { + name: "Moonscan", + url: "https://moonriver.moonscan.io", + apiUrl: "https://api-moonriver.moonscan.io/api", + }, + }, + }, + ], + // ink mainnet + [ + 57_073n, + { + name: "Ink", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + blockscout: { + url: "https://explorer.inkonchain.com", + apiUrl: "https://explorer.inkonchain.com/api", + }, + }, + }, + ], + // ink sepolia testnet + [ + 763_373n, + { + name: "Ink Sepolia", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + blockscout: { + url: "https://explorer-sepolia.inkonchain.com", + apiUrl: "https://explorer-sepolia.inkonchain.com/api", + }, + }, + }, + ], + // aurora mainnet + [ + 1_313_161_554n, + { + name: "Aurora", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + blockscout: { + url: "https://explorer.mainnet.aurora.dev", + apiUrl: "https://explorer.mainnet.aurora.dev/api", + }, + }, + }, + ], + // aurora testnet + [ + 1_313_161_555n, + { + name: "Aurora Testnet", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + blockscout: { + url: "https://explorer.testnet.aurora.dev", + apiUrl: "https://explorer.testnet.aurora.dev/api", + }, + }, + }, + ], + // harmony one mainnet + [ + 1_666_600_000n, + { + name: "Harmony One", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + blockscout: { + url: "https://explorer.harmony.one", + apiUrl: "https://explorer.harmony.one/api", + }, + }, + }, + ], + // harmony testnet + [ + 1_666_700_000n, + { + name: "Harmony Testnet", + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: { + blockscout: { + url: "https://explorer.testnet.harmony.one", + apiUrl: "https://explorer.testnet.harmony.one/api", + }, + }, + }, + ], +]); diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/config-resolution.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/config-resolution.ts index 172c9acc948..94fac69de7c 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/config-resolution.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/config-resolution.ts @@ -2,9 +2,9 @@ import type { ConfigurationVariableResolver, EdrNetworkAccountsConfig, EdrNetworkAccountsUserConfig, - EdrNetworkChainConfig, - EdrNetworkChainsConfig, - EdrNetworkChainsUserConfig, + ChainDescriptorConfig, + ChainDescriptorsConfig, + ChainDescriptorsUserConfig, EdrNetworkConfig, EdrNetworkForkingConfig, EdrNetworkForkingUserConfig, @@ -22,27 +22,22 @@ import type { ChainType } from "../../../types/network.js"; import path from "node:path"; +import { toBigInt } from "@nomicfoundation/hardhat-utils/bigint"; import { hexStringToBytes, normalizeHexString, } from "@nomicfoundation/hardhat-utils/hex"; +import { deepClone } from "@nomicfoundation/hardhat-utils/lang"; -import { - L1_CHAIN_TYPE, - OPTIMISM_CHAIN_TYPE, - GENERIC_CHAIN_TYPE, -} from "../../constants.js"; +import { GENERIC_CHAIN_TYPE } from "../../constants.js"; import { DEFAULT_HD_ACCOUNTS_CONFIG_PARAMS } from "./accounts/constants.js"; +import { DEFAULT_CHAIN_DESCRIPTORS } from "./chain-descriptors.js"; import { DEFAULT_EDR_NETWORK_HD_ACCOUNTS_CONFIG_PARAMS, EDR_NETWORK_DEFAULT_COINBASE, } from "./edr/edr-provider.js"; -import { - getCurrentHardfork, - L1HardforkName, - OpHardforkName, -} from "./edr/types/hardfork.js"; +import { getCurrentHardfork } from "./edr/types/hardfork.js"; import { isHttpNetworkHdAccountsUserConfig } from "./type-validation.js"; export function resolveHttpNetwork( @@ -90,7 +85,6 @@ export function resolveEdrNetwork( allowUnlimitedContractSize: networkConfig.allowUnlimitedContractSize ?? false, blockGasLimit: BigInt(networkConfig.blockGasLimit ?? 30_000_000n), - chains: resolveChains(networkConfig.chains), coinbase: resolveCoinbase(networkConfig.coinbase), enableRip7212: networkConfig.enableRip7212 ?? false, enableTransientStorage: networkConfig.enableTransientStorage ?? false, @@ -228,141 +222,55 @@ export function resolveCoinbase( return hexStringToBytes(coinbase); } -export function resolveChains( - chains: EdrNetworkChainsUserConfig | undefined, -): EdrNetworkChainsConfig { - /** - * Block numbers / timestamps were taken from: - * - * L1 / Generic: - * https://github.com/ethereumjs/ethereumjs-monorepo/tree/master/packages/common/src/chains.ts - * Op: - * https://github.com/ethereum-optimism/superchain-registry/tree/main/superchain/configs/mainnet - * - * To find hardfork activation blocks by timestamp, use: - * https://api-TESTNET.etherscan.io/api?module=block&action=getblocknobytime×tamp=TIMESTAMP&closest=before&apikey=APIKEY - */ - const resolvedChains: EdrNetworkChainsConfig = new Map([ - [ - 1, // mainnet - { - chainType: L1_CHAIN_TYPE, - hardforkHistory: new Map([ - [L1HardforkName.FRONTIER, 0], - [L1HardforkName.HOMESTEAD, 1_150_000], - [L1HardforkName.DAO, 1_920_000], - [L1HardforkName.TANGERINE_WHISTLE, 2_463_000], - [L1HardforkName.SPURIOUS_DRAGON, 2_675_000], - [L1HardforkName.BYZANTIUM, 4_370_000], - [L1HardforkName.CONSTANTINOPLE, 7_280_000], - [L1HardforkName.PETERSBURG, 7_280_000], - [L1HardforkName.ISTANBUL, 9_069_000], - [L1HardforkName.MUIR_GLACIER, 9_200_000], - [L1HardforkName.BERLIN, 1_2244_000], - [L1HardforkName.LONDON, 12_965_000], - [L1HardforkName.ARROW_GLACIER, 13_773_000], - [L1HardforkName.GRAY_GLACIER, 15_050_000], - [L1HardforkName.MERGE, 15_537_394], - [L1HardforkName.SHANGHAI, 17_034_870], - [L1HardforkName.CANCUN, 19_426_589], - ]), - }, - ], - [ - 5, // goerli - { - chainType: L1_CHAIN_TYPE, - hardforkHistory: new Map([ - [L1HardforkName.ISTANBUL, 1_561_651], - [L1HardforkName.BERLIN, 4_460_644], - [L1HardforkName.LONDON, 5_062_605], - ]), - }, - ], - [ - 17000, // holesky - { - chainType: L1_CHAIN_TYPE, - hardforkHistory: new Map([ - [L1HardforkName.MERGE, 0], - [L1HardforkName.SHANGHAI, 6_698], - [L1HardforkName.CANCUN, 894_732], - ]), - }, - ], - [ - 560048, // hoodi - { - chainType: L1_CHAIN_TYPE, - hardforkHistory: new Map([ - [L1HardforkName.MERGE, 0], - [L1HardforkName.SHANGHAI, 0], - [L1HardforkName.CANCUN, 0], - ]), - }, - ], - [ - 11155111, // sepolia - { - chainType: L1_CHAIN_TYPE, - hardforkHistory: new Map([ - [L1HardforkName.GRAY_GLACIER, 0], - [L1HardforkName.MERGE, 1_450_409], - [L1HardforkName.SHANGHAI, 2_990_908], - [L1HardforkName.CANCUN, 5_187_023], - ]), - }, - ], - [ - 10, // op mainnet - { - chainType: OPTIMISM_CHAIN_TYPE, - hardforkHistory: new Map([ - [OpHardforkName.BEDROCK, 105_235_063], - [OpHardforkName.REGOLITH, 105_235_063], - [OpHardforkName.CANYON, 114_696_812], - [OpHardforkName.ECOTONE, 117_387_812], - [OpHardforkName.FJORD, 122_514_212], - [OpHardforkName.GRANITE, 125_235_812], - [OpHardforkName.HOLOCENE, 130_423_412], - ]), - }, - ], - [ - 11155420, // op sepolia - { - chainType: OPTIMISM_CHAIN_TYPE, - hardforkHistory: new Map([ - [OpHardforkName.BEDROCK, 0], - [OpHardforkName.REGOLITH, 0], - [OpHardforkName.CANYON, 4_089_330], - [OpHardforkName.ECOTONE, 8_366_130], - [OpHardforkName.FJORD, 12_597_930], - [OpHardforkName.GRANITE, 15_837_930], - [OpHardforkName.HOLOCENE, 20_415_330], - ]), - }, - ], - ]); - - if (chains === undefined) { - return resolvedChains; +export async function resolveChainDescriptors( + chainDescriptors: ChainDescriptorsUserConfig | undefined, +): Promise { + const resolvedChainDescriptors: ChainDescriptorsConfig = await deepClone( + DEFAULT_CHAIN_DESCRIPTORS, + ); + + if (chainDescriptors === undefined) { + return resolvedChainDescriptors; } - chains.forEach((chainConfig, chainId) => { - const resolvedChainConfig: EdrNetworkChainConfig = { - chainType: chainConfig.chainType ?? GENERIC_CHAIN_TYPE, - hardforkHistory: new Map(), - }; - if (chainConfig.hardforkHistory !== undefined) { - chainConfig.hardforkHistory.forEach((block, name) => { - resolvedChainConfig.hardforkHistory.set(name, block); - }); + // Loop over the user-provided chain descriptors + // and merge them with the default ones + for (const [chainId, userDescriptor] of Object.entries(chainDescriptors)) { + const existingDescriptor: ChainDescriptorConfig = + resolvedChainDescriptors.get(toBigInt(chainId)) ?? { + name: userDescriptor.name, + chainType: GENERIC_CHAIN_TYPE, + blockExplorers: {}, + }; + + existingDescriptor.name = userDescriptor.name; + + if (userDescriptor.chainType !== undefined) { + existingDescriptor.chainType = userDescriptor.chainType; } - resolvedChains.set(chainId, resolvedChainConfig); - }); - return resolvedChains; + if (userDescriptor.hardforkHistory !== undefined) { + existingDescriptor.hardforkHistory = new Map( + Object.entries(userDescriptor.hardforkHistory), + ); + } + + if (userDescriptor.blockExplorers?.etherscan !== undefined) { + existingDescriptor.blockExplorers.etherscan = await deepClone( + userDescriptor.blockExplorers.etherscan, + ); + } + + if (userDescriptor.blockExplorers?.blockscout !== undefined) { + existingDescriptor.blockExplorers.blockscout = await deepClone( + userDescriptor.blockExplorers.blockscout, + ); + } + + resolvedChainDescriptors.set(toBigInt(chainId), existingDescriptor); + } + + return resolvedChainDescriptors; } export function resolveHardfork( diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/edr-provider.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/edr-provider.ts index 3c6be4bd91a..009e7ec8931 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/edr-provider.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/edr-provider.ts @@ -1,6 +1,7 @@ import type { SolidityStackTrace } from "./stack-traces/solidity-stack-trace.js"; import type { LoggerConfig } from "./types/logger.js"; import type { + ChainDescriptorsConfig, EdrNetworkConfig, EdrNetworkHDAccountsConfig, } from "../../../../types/config.js"; @@ -62,7 +63,7 @@ import { hardhatMempoolOrderToEdrMineOrdering, hardhatHardforkToEdrSpecId, hardhatAccountsToEdrOwnedAccounts, - hardhatChainsToEdrChains, + hardhatChainDescriptorsToEdrChains, hardhatForkingConfigToEdrForkConfig, hardhatChainTypeToEdrChainType, } from "./utils/convert-to-edr.js"; @@ -126,6 +127,7 @@ export const EDR_NETWORK_DEFAULT_PRIVATE_KEYS: string[] = [ ]; interface EdrProviderConfig { + chainDescriptors: ChainDescriptorsConfig; networkConfig: RequireField; loggerConfig?: LoggerConfig; tracingConfig?: TracingConfigWithBuffers; @@ -142,6 +144,7 @@ export class EdrProvider extends BaseProvider { * Creates a new instance of `EdrProvider`. */ public static async create({ + chainDescriptors, networkConfig, loggerConfig = { enabled: false }, tracingConfig = {}, @@ -150,7 +153,10 @@ export class EdrProvider extends BaseProvider { const printLineFn = loggerConfig.printLineFn ?? printLine; const replaceLastLineFn = loggerConfig.replaceLastLineFn ?? replaceLastLine; - const providerConfig = await getProviderConfig(networkConfig); + const providerConfig = await getProviderConfig( + networkConfig, + chainDescriptors, + ); let edrProvider: EdrProvider; @@ -384,6 +390,7 @@ export class EdrProvider extends BaseProvider { async function getProviderConfig( networkConfig: RequireField, + chainDescriptors: ChainDescriptorsConfig, ): Promise { const specId = hardhatHardforkToEdrSpecId( networkConfig.hardfork, @@ -405,8 +412,8 @@ async function getProviderConfig( blockGasLimit: networkConfig.blockGasLimit, cacheDir: networkConfig.forking?.cacheDir, chainId: BigInt(networkConfig.chainId), - chains: hardhatChainsToEdrChains( - networkConfig.chains, + chains: hardhatChainDescriptorsToEdrChains( + chainDescriptors, networkConfig.chainType, ), // TODO: remove this cast when EDR updates the interface to accept Uint8Array diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/utils/convert-to-edr.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/utils/convert-to-edr.ts index fb3e7a15588..e7839293eed 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/utils/convert-to-edr.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/edr/utils/convert-to-edr.ts @@ -2,7 +2,7 @@ import type { EdrNetworkAccountConfig, EdrNetworkAccountsConfig, - EdrNetworkChainsConfig, + ChainDescriptorsConfig, EdrNetworkForkingConfig, EdrNetworkMempoolConfig, EdrNetworkMiningConfig, @@ -277,30 +277,34 @@ export async function normalizeEdrNetworkAccountsConfig( })); } -export function hardhatChainsToEdrChains( - chains: EdrNetworkChainsConfig, +export function hardhatChainDescriptorsToEdrChains( + chainDescriptors: ChainDescriptorsConfig, chainType: ChainType, ): ChainConfig[] { return ( - Array.from(chains) - // Skip chains that don't match the expected chain type - .filter(([_, config]) => { + Array.from(chainDescriptors) + // Skip chain descriptors that don't match the expected chain type + .filter(([_, descriptor]) => { if (chainType === GENERIC_CHAIN_TYPE) { // When "generic" is requested, include both "generic" and "l1" chains return ( - config.chainType === GENERIC_CHAIN_TYPE || - config.chainType === L1_CHAIN_TYPE + descriptor.chainType === GENERIC_CHAIN_TYPE || + descriptor.chainType === L1_CHAIN_TYPE ); } - return config.chainType === chainType; + return descriptor.chainType === chainType; }) - .map(([chainId, config]) => ({ - chainId: BigInt(chainId), - hardforks: Array.from(config.hardforkHistory).map( - ([hardfork, blockNumber]) => ({ - blockNumber: BigInt(blockNumber), - specId: hardhatHardforkToEdrSpecId(hardfork, config.chainType), + .map(([chainId, descriptor]) => ({ + chainId, + hardforks: Array.from(descriptor.hardforkHistory ?? new Map()).map( + ([hardfork, { blockNumber, timestamp }]) => ({ + specId: hardhatHardforkToEdrSpecId(hardfork, descriptor.chainType), + ...(blockNumber !== undefined + ? { blockNumber: BigInt(blockNumber) } + : // { timestamp: BigInt(timestamp) }), + // TODO: remplace this line with the one above when EDR supports it + { blockNumber: BigInt(timestamp) }), }), ), })) diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/config.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/config.ts index 77f50c4294d..f195fc21dec 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/config.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/config.ts @@ -12,7 +12,11 @@ import type { ConfigHooks } from "../../../../types/hooks.js"; import { HardhatError } from "@nomicfoundation/hardhat-errors"; import { GENERIC_CHAIN_TYPE } from "../../../constants.js"; -import { resolveEdrNetwork, resolveHttpNetwork } from "../config-resolution.js"; +import { + resolveChainDescriptors, + resolveEdrNetwork, + resolveHttpNetwork, +} from "../config-resolution.js"; import { validateNetworkUserConfig } from "../type-validation.js"; export default async (): Promise> => ({ @@ -99,6 +103,9 @@ export async function resolveUserConfig( return { ...resolvedConfig, + chainDescriptors: await resolveChainDescriptors( + userConfig.chainDescriptors, + ), defaultChainType: resolvedConfig.defaultChainType ?? GENERIC_CHAIN_TYPE, defaultNetwork: resolvedConfig.defaultNetwork ?? "hardhat", networks: resolvedNetworks, diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/hre.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/hre.ts index 9ceaa7d6cdf..5a05ba79d6a 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/hre.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/hook-handlers/hre.ts @@ -23,6 +23,7 @@ export default async (): Promise> => ({ context.hooks, context.artifacts, userConfigNetworks, + hre.config.chainDescriptors, ); } diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/network-manager.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/network-manager.ts index 0db793e2761..275444dd4a5 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/network-manager.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/network-manager.ts @@ -1,5 +1,6 @@ import type { ArtifactManager } from "../../../types/artifacts.js"; import type { + ChainDescriptorsConfig, NetworkConfig, NetworkConfigOverride, NetworkUserConfig, @@ -42,6 +43,7 @@ export class NetworkManagerImplementation implements NetworkManager { readonly #hookManager: Readonly; readonly #artifactsManager: Readonly; readonly #userConfigNetworks: Readonly>; + readonly #chainDescriptors: Readonly; #nextConnectionId = 0; @@ -52,6 +54,7 @@ export class NetworkManagerImplementation implements NetworkManager { hookManager: HookManager, artifactsManager: ArtifactManager, userConfigNetworks: Record | undefined, + chainDescriptors: ChainDescriptorsConfig, ) { this.#defaultNetwork = defaultNetwork; this.#defaultChainType = defaultChainType; @@ -59,6 +62,7 @@ export class NetworkManagerImplementation implements NetworkManager { this.#hookManager = hookManager; this.#artifactsManager = artifactsManager; this.#userConfigNetworks = userConfigNetworks ?? {}; + this.#chainDescriptors = chainDescriptors; } public async connect< @@ -206,6 +210,7 @@ export class NetworkManagerImplementation implements NetworkManager { } return EdrProvider.create({ + chainDescriptors: this.#chainDescriptors, // The resolvedNetworkConfig can have its chainType set to `undefined` // so we default to the default chain type here. networkConfig: { diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/type-extensions/config.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/type-extensions/config.ts index d59c4582891..395262621e7 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/type-extensions/config.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/type-extensions/config.ts @@ -3,11 +3,50 @@ import type { ChainType, DefaultChainType } from "../../../../types/network.js"; import "../../../../types/config.js"; declare module "../../../../types/config.js" { export interface HardhatUserConfig { + chainDescriptors?: ChainDescriptorsUserConfig; defaultChainType?: DefaultChainType; defaultNetwork?: string; networks?: Record; } + export interface ChainDescriptorsUserConfig { + [chainId: number | string]: ChainDescriptorUserConfig; + } + + export interface ChainDescriptorUserConfig { + name: string; + chainType?: ChainType; + hardforkHistory?: HardforkHistoryUserConfig; + blockExplorers?: BlockExplorersUserConfig; + } + + export interface HardforkHistoryUserConfig { + [hardforkName: string]: + | ActivationBlockNumberUserConfig + | ActivationTimestampUserConfig; + } + + export interface ActivationBlockNumberUserConfig { + blockNumber: number; + timestamp?: never; + } + + export interface ActivationTimestampUserConfig { + timestamp: number; + blockNumber?: never; + } + + export interface BlockExplorersUserConfig { + etherscan?: BlockExplorerUserConfig; + blockscout?: BlockExplorerUserConfig; + } + + export interface BlockExplorerUserConfig { + name?: string; + url: string; + apiUrl: string; + } + export type NetworkUserConfig = HttpNetworkUserConfig | EdrNetworkUserConfig; export type HttpNetworkConfigOverride = Partial< @@ -69,7 +108,6 @@ declare module "../../../../types/config.js" { allowBlocksWithSameTimestamp?: boolean; allowUnlimitedContractSize?: boolean; blockGasLimit?: number | bigint; - chains?: EdrNetworkChainsUserConfig; coinbase?: string; enableRip7212?: boolean; enableTransientStorage?: boolean; @@ -103,21 +141,6 @@ declare module "../../../../types/config.js" { path?: string; } - export type EdrNetworkChainsUserConfig = Map< - number /* chainId */, - EdrNetworkChainUserConfig - >; - - export interface EdrNetworkChainUserConfig { - chainType?: ChainType; - hardforkHistory?: HardforkHistoryUserConfig; - } - - export type HardforkHistoryUserConfig = Map< - string /* hardforkName */, - number /* blockNumber */ - >; - export interface EdrNetworkForkingUserConfig { enabled?: boolean; url: SensitiveString; @@ -136,11 +159,50 @@ declare module "../../../../types/config.js" { } export interface HardhatConfig { + chainDescriptors: ChainDescriptorsConfig; defaultChainType: DefaultChainType; defaultNetwork: string; networks: Record; } + export type ChainDescriptorsConfig = Map< + bigint /* chainId */, + ChainDescriptorConfig + >; + + export interface ChainDescriptorConfig { + name: string; + chainType: ChainType; + hardforkHistory?: HardforkHistoryConfig; + blockExplorers: BlockExplorersConfig; + } + + export type HardforkHistoryConfig = Map< + string /* hardforkName */, + ActivationBlockNumberConfig | ActivationTimestampConfig + >; + + export interface ActivationBlockNumberConfig { + blockNumber: number; + timestamp?: never; + } + + export interface ActivationTimestampConfig { + timestamp: number; + blockNumber?: never; + } + + export interface BlockExplorersConfig { + etherscan?: BlockExplorerConfig; + blockscout?: BlockExplorerConfig; + } + + export interface BlockExplorerConfig { + name?: string; + url: string; + apiUrl: string; + } + export type NetworkConfig = HttpNetworkConfig | EdrNetworkConfig; export interface HttpNetworkConfig { @@ -188,7 +250,6 @@ declare module "../../../../types/config.js" { allowBlocksWithSameTimestamp: boolean; allowUnlimitedContractSize: boolean; blockGasLimit: bigint; - chains: EdrNetworkChainsConfig; coinbase: Uint8Array; enableRip7212: boolean; enableTransientStorage: boolean; @@ -222,21 +283,6 @@ declare module "../../../../types/config.js" { path: string; } - export type EdrNetworkChainsConfig = Map< - number /* chainId */, - EdrNetworkChainConfig - >; - - export interface EdrNetworkChainConfig { - chainType: ChainType; - hardforkHistory: HardforkHistoryConfig; - } - - export type HardforkHistoryConfig = Map< - string /* hardforkName */, - number /* blockNumber */ - >; - export interface EdrNetworkForkingConfig { enabled: boolean; url: ResolvedConfigurationVariable; diff --git a/v-next/hardhat/src/internal/builtin-plugins/network-manager/type-validation.ts b/v-next/hardhat/src/internal/builtin-plugins/network-manager/type-validation.ts index 7f8664a9a5a..afcf178eea4 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/network-manager/type-validation.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/network-manager/type-validation.ts @@ -1,4 +1,6 @@ import type { + ActivationBlockNumberUserConfig, + ActivationTimestampUserConfig, EdrNetworkForkingConfig, EdrNetworkHDAccountsConfig, EdrNetworkMiningConfig, @@ -46,6 +48,132 @@ 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 + | z.ZodType +> = 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 blockExplorerUserConfigSchema = z.object({ + name: z.optional(z.string()), + url: z.string(), + apiUrl: z.string(), +}); + +const blockExplorersUserConfigSchema = z.object({ + etherscan: z.optional(blockExplorerUserConfigSchema), + blockscout: z.optional(blockExplorerUserConfigSchema), +}); + +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, @@ -78,15 +206,6 @@ const httpNetworkAccountsUserConfigSchema = conditionalUnionType( `Expected 'remote', an array with private keys or Configuration Variables, or an object with HD account details`, ); -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 gasUnitUserConfigSchema = unionType( [nonnegativeIntSchema.safe(), nonnegativeBigIntSchema], "Expected a positive safe int or a positive bigint", @@ -140,18 +259,6 @@ const edrNetworkAccountsUserConfigSchema = conditionalUnionType( `Expected an array with with objects with private key and balance or Configuration Variables, or an object with HD account details`, ); -const hardforkHistoryUserConfigSchema = z.map(z.string(), blockNumberSchema); - -const edrNetworkChainUserConfigSchema = z.object({ - chainType: z.optional(chainTypeUserConfigSchema), - hardforkHistory: z.optional(hardforkHistoryUserConfigSchema), -}); - -const edrNetworkChainsUserConfigSchema = z.map( - chainIdSchema, - edrNetworkChainUserConfigSchema, -); - const edrNetworkForkingUserConfigSchema = z.object({ enabled: z.optional(z.boolean()), url: sensitiveUrlSchema, @@ -196,7 +303,6 @@ const edrNetworkUserConfigSchema = z.object({ allowBlocksWithSameTimestamp: z.optional(z.boolean()), allowUnlimitedContractSize: z.optional(z.boolean()), blockGasLimit: z.optional(gasUnitUserConfigSchema), - chains: z.optional(edrNetworkChainsUserConfigSchema), coinbase: z.optional(z.string()), enableRip7212: z.optional(z.boolean()), enableTransientStorage: z.optional(z.boolean()), @@ -227,7 +333,6 @@ function refineEdrNetworkUserConfig( const { chainType = GENERIC_CHAIN_TYPE, hardfork, - chains, minGasPrice, initialBaseFeePerGas, enableTransientStorage, @@ -243,34 +348,6 @@ function refineEdrNetworkUserConfig( }); } - if (chains !== undefined) { - Array.from(chains).forEach(([chainId, chainConfig], chainIdx) => { - if (chainConfig.hardforkHistory === undefined) { - return; - } - - const type = chainConfig.chainType ?? GENERIC_CHAIN_TYPE; - Array.from(chainConfig.hardforkHistory).forEach( - ([name], hardforkIdx) => { - if (!isValidHardforkName(name, type)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: [ - "chains", - chainIdx, - "value", - "hardforkHistory", - hardforkIdx, - "value", - ], - message: `Invalid hardfork name ${name} found in chain ${chainId}. Expected ${getHardforks(type).join(" | ")}.`, - }); - } - }, - ); - }); - } - const resolvedHardfork = hardfork ?? getCurrentHardfork(chainType); if (chainType === L1_CHAIN_TYPE || chainType === GENERIC_CHAIN_TYPE) { if (hardforkGte(resolvedHardfork, L1HardforkName.LONDON, chainType)) { @@ -342,6 +419,7 @@ const networkUserConfigSchema = baseNetworkUserConfigSchema.superRefine( ); const userConfigSchema = z.object({ + chainDescriptors: z.optional(chainDescriptorsUserConfigSchema), defaultChainType: z.optional(chainTypeUserConfigSchema), defaultNetwork: z.optional(z.string()), networks: z.optional(z.record(networkUserConfigSchema)), diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/config-resolution.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/config-resolution.ts index ebbb4bbd5d1..a33e94db148 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/config-resolution.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/config-resolution.ts @@ -1,6 +1,6 @@ import type { ConfigurationVariableResolver, - EdrNetworkChainsUserConfig, + ChainDescriptorsUserConfig, EdrNetworkMiningUserConfig, EdrNetworkUserConfig, HttpNetworkUserConfig, @@ -11,13 +11,15 @@ import assert from "node:assert/strict"; import path from "node:path"; import { before, describe, it } from "node:test"; +import { toBigInt } from "@nomicfoundation/hardhat-utils/bigint"; import { bytesToHexString } from "@nomicfoundation/hardhat-utils/hex"; import { configVariable } from "../../../../src/config.js"; import { createHardhatRuntimeEnvironment } from "../../../../src/hre.js"; import { DEFAULT_HD_ACCOUNTS_CONFIG_PARAMS } from "../../../../src/internal/builtin-plugins/network-manager/accounts/constants.js"; +import { DEFAULT_CHAIN_DESCRIPTORS } from "../../../../src/internal/builtin-plugins/network-manager/chain-descriptors.js"; import { - resolveChains, + resolveChainDescriptors, resolveCoinbase, resolveEdrNetwork, resolveEdrNetworkAccounts, @@ -578,75 +580,194 @@ describe("config-resolution", () => { }); }); - describe("resolveChains", () => { - it("should return the resolved chains with the provided chains overriding the defaults", () => { - const chainsUserConfig: EdrNetworkChainsUserConfig = new Map([ - [ - 1, - { - chainType: L1_CHAIN_TYPE, - hardforkHistory: new Map([ - [L1HardforkName.BYZANTIUM, 1], - [L1HardforkName.CONSTANTINOPLE, 2], - ["newHardfork", 3], - ]), + describe("resolveChainDescriptors", () => { + it("should return the resolved chain descriptors with the provided chainDescriptors overriding the defaults", async () => { + const mainnetChainId = 1; + const myNetworkChainId = 31_337; + const chainDescriptorsUserConfig: ChainDescriptorsUserConfig = { + [mainnetChainId]: { + name: "Ethereum", + chainType: GENERIC_CHAIN_TYPE, + hardforkHistory: { + [L1HardforkName.BYZANTIUM]: { blockNumber: 1 }, + [L1HardforkName.CONSTANTINOPLE]: { blockNumber: 2 }, + newHardfork: { blockNumber: 3 }, }, - ], - [ - 31337, - { - hardforkHistory: new Map([[L1HardforkName.BYZANTIUM, 1]]), + }, + [myNetworkChainId]: { + name: "My Network", + hardforkHistory: { + [L1HardforkName.BYZANTIUM]: { blockNumber: 1 }, }, - ], - ]); - const chainsConfig = resolveChains(chainsUserConfig); + blockExplorers: { + etherscan: { + url: "http://localhost:8545", + apiUrl: "http://localhost:8545/api", + }, + }, + }, + }; + const chainDescriptorsConfig = await resolveChainDescriptors( + chainDescriptorsUserConfig, + ); - const mainnet = chainsConfig.get(1); - assert.ok(mainnet !== undefined, "chain 1 is not in the resolved chains"); - assert.equal(mainnet.chainType, L1_CHAIN_TYPE); - assert.equal(mainnet.hardforkHistory.get(L1HardforkName.BYZANTIUM), 1); - assert.equal( - mainnet.hardforkHistory.get(L1HardforkName.CONSTANTINOPLE), - 2, + const mainnetUserConfig = chainDescriptorsUserConfig[mainnetChainId]; + const mainnetConfig = chainDescriptorsConfig.get( + toBigInt(mainnetChainId), + ); + assert.equal(mainnetConfig?.chainType, mainnetUserConfig?.chainType); + assert.deepEqual( + mainnetConfig?.hardforkHistory, + new Map(Object.entries(mainnetUserConfig?.hardforkHistory ?? {})), ); - assert.equal(mainnet.hardforkHistory.get("newHardfork"), 3); - const myNetwork = chainsConfig.get(31337); - assert.ok( - myNetwork !== undefined, - "chain 31337 is not in the resolved chains", + const myNetworkUserConfig = chainDescriptorsUserConfig[myNetworkChainId]; + const myNetworkConfig = chainDescriptorsConfig.get( + toBigInt(myNetworkChainId), + ); + assert.equal(myNetworkConfig?.name, myNetworkUserConfig?.name); + assert.equal(myNetworkConfig?.chainType, GENERIC_CHAIN_TYPE); + assert.deepEqual( + myNetworkConfig?.hardforkHistory, + new Map(Object.entries(myNetworkUserConfig?.hardforkHistory ?? {})), + ); + assert.deepEqual( + myNetworkConfig?.blockExplorers, + myNetworkUserConfig?.blockExplorers, ); - assert.equal(myNetwork.chainType, GENERIC_CHAIN_TYPE); - assert.equal(myNetwork.hardforkHistory.get(L1HardforkName.BYZANTIUM), 1); }); - it("should return the default chains if no chains are provided", () => { - const chainsConfig = resolveChains(undefined); + it("should only override the provided fields", async () => { + const mainnetChainId = 1; + const sepoliaChainId = 11_155_111; + const holeskyChainId = 17_000; + const hoodiChainId = 560_048; + + const chainDescriptorsUserConfig: ChainDescriptorsUserConfig = { + [mainnetChainId]: { + name: "Ethereum Mainnet", + hardforkHistory: { + [L1HardforkName.BYZANTIUM]: { blockNumber: 1 }, + [L1HardforkName.CONSTANTINOPLE]: { blockNumber: 2 }, + newHardfork: { blockNumber: 3 }, + }, + }, + [sepoliaChainId]: { + name: "Sepolia Testnet", + blockExplorers: { + etherscan: { + url: "http://localhost:8545", + apiUrl: "http://localhost:8545/api", + }, + }, + }, + [holeskyChainId]: { + name: "Holesky Testnet", + chainType: GENERIC_CHAIN_TYPE, + }, + [hoodiChainId]: { + name: "Hoodi Testnet", + }, + }; + const chainDescriptorsConfig = await resolveChainDescriptors( + chainDescriptorsUserConfig, + ); - // Check some of the default values - const mainnet = chainsConfig.get(1); - assert.ok(mainnet !== undefined, "chain 1 is not in the resolved chains"); - assert.equal( - mainnet.hardforkHistory.get(L1HardforkName.BYZANTIUM), - 4_370_000, + const mainnetUserConfig = chainDescriptorsUserConfig[mainnetChainId]; + const mainnetConfig = chainDescriptorsConfig.get( + toBigInt(mainnetChainId), ); - assert.equal( - mainnet.hardforkHistory.get(L1HardforkName.CONSTANTINOPLE), - 7_280_000, + const mainnetDefault = DEFAULT_CHAIN_DESCRIPTORS.get( + toBigInt(mainnetChainId), ); - assert.equal( - mainnet.hardforkHistory.get(L1HardforkName.SHANGHAI), - 17_034_870, + assert.equal(mainnetConfig?.name, mainnetUserConfig.name); + assert.equal(mainnetConfig?.chainType, L1_CHAIN_TYPE); + assert.deepEqual( + mainnetConfig?.hardforkHistory, + new Map(Object.entries(mainnetUserConfig.hardforkHistory ?? {})), ); - assert.equal( - mainnet.hardforkHistory.get(L1HardforkName.CANCUN), - 19_426_589, + assert.deepEqual( + mainnetConfig?.blockExplorers, + mainnetDefault?.blockExplorers, ); - const myNetwork = chainsConfig.get(31337); - assert.ok( - myNetwork === undefined, - "chain 31337 is in the resolved chains", + + const sepoliaUserConfig = chainDescriptorsUserConfig[sepoliaChainId]; + const sepoliaConfig = chainDescriptorsConfig.get( + toBigInt(sepoliaChainId), + ); + const sepoliaDefault = DEFAULT_CHAIN_DESCRIPTORS.get( + toBigInt(sepoliaChainId), + ); + assert.equal(sepoliaConfig?.name, sepoliaUserConfig.name); + assert.equal(sepoliaConfig?.chainType, L1_CHAIN_TYPE); + assert.deepEqual( + sepoliaConfig?.hardforkHistory, + sepoliaDefault?.hardforkHistory, + ); + assert.deepEqual( + sepoliaConfig?.blockExplorers.etherscan, + sepoliaUserConfig.blockExplorers?.etherscan, + ); + assert.deepEqual( + sepoliaConfig?.blockExplorers.blockscout, + sepoliaDefault?.blockExplorers.blockscout, + ); + + const holeskyUserConfig = chainDescriptorsUserConfig[holeskyChainId]; + const holeskyConfig = chainDescriptorsConfig.get( + toBigInt(holeskyChainId), + ); + const holeskyDefault = DEFAULT_CHAIN_DESCRIPTORS.get( + toBigInt(holeskyChainId), + ); + assert.equal(holeskyConfig?.name, holeskyUserConfig.name); + assert.equal(holeskyConfig?.chainType, holeskyUserConfig.chainType); + assert.deepEqual( + holeskyConfig?.hardforkHistory, + holeskyDefault?.hardforkHistory, + ); + assert.deepEqual( + holeskyConfig?.blockExplorers, + holeskyDefault?.blockExplorers, ); + + const hoodiUserConfig = chainDescriptorsUserConfig[hoodiChainId]; + const hoodiConfig = chainDescriptorsConfig.get(toBigInt(hoodiChainId)); + const hoodiDefault = DEFAULT_CHAIN_DESCRIPTORS.get( + toBigInt(hoodiChainId), + ); + assert.equal(hoodiConfig?.name, hoodiUserConfig.name); + assert.equal(hoodiConfig?.chainType, hoodiDefault?.chainType); + assert.deepEqual( + hoodiConfig?.hardforkHistory, + hoodiDefault?.hardforkHistory, + ); + assert.deepEqual( + hoodiConfig?.blockExplorers, + hoodiDefault?.blockExplorers, + ); + }); + + it("should return the default chain descriptors if no value is provided", async () => { + const chainDescriptors = await resolveChainDescriptors(undefined); + assert.deepEqual(chainDescriptors, DEFAULT_CHAIN_DESCRIPTORS); + }); + + it("should assign default values to the fields that are not provided", async () => { + const chainDescriptorsUserConfig: ChainDescriptorsUserConfig = { + 31_337: { + name: "My Network", + }, + }; + const chainDescriptorsConfig = await resolveChainDescriptors( + chainDescriptorsUserConfig, + ); + + const myNetworkConfig = chainDescriptorsConfig.get(31_337n); + assert.equal(myNetworkConfig?.name, "My Network"); + assert.equal(myNetworkConfig?.chainType, GENERIC_CHAIN_TYPE); + assert.deepEqual(myNetworkConfig?.hardforkHistory, undefined); + assert.deepEqual(myNetworkConfig?.blockExplorers, {}); }); }); diff --git a/v-next/hardhat/test/internal/builtin-plugins/network-manager/network-manager.ts b/v-next/hardhat/test/internal/builtin-plugins/network-manager/network-manager.ts index 73b9f5e2a8e..d26b4f7d096 100644 --- a/v-next/hardhat/test/internal/builtin-plugins/network-manager/network-manager.ts +++ b/v-next/hardhat/test/internal/builtin-plugins/network-manager/network-manager.ts @@ -1,4 +1,5 @@ import type { + ChainDescriptorsConfig, EdrNetworkConfigOverride, EdrNetworkUserConfig, HardhatUserConfig, @@ -28,6 +29,7 @@ import { expectTypeOf } from "expect-type"; import { createHardhatRuntimeEnvironment } from "../../../../src/hre.js"; import { + resolveChainDescriptors, resolveEdrNetwork, resolveHttpNetwork, } from "../../../../src/internal/builtin-plugins/network-manager/config-resolution.js"; @@ -52,6 +54,7 @@ describe("NetworkManagerImplementation", () => { let networkManager: NetworkManager; let userNetworks: Record; let networks: Record; + let chainDescriptors: ChainDescriptorsConfig; before(async () => { const initialDate = new Date(); @@ -125,6 +128,8 @@ describe("NetworkManagerImplementation", () => { ), }; + chainDescriptors = await resolveChainDescriptors(undefined); + networkManager = new NetworkManagerImplementation( "localhost", GENERIC_CHAIN_TYPE, @@ -132,6 +137,7 @@ describe("NetworkManagerImplementation", () => { hre.hooks, hre.artifacts, userNetworks, + chainDescriptors, ); }); @@ -431,6 +437,236 @@ describe("NetworkManagerImplementation", () => { }; } + describe("chainDescriptors", () => { + it("should validate a valid network config", async () => { + const validationErrors = await validateNetworkUserConfig({ + chainDescriptors: { + 1: { + name: "Ethereum", + hardforkHistory: { + london: { blockNumber: 456 }, + }, + }, + 2: { + name: "My Optimism Chain", + chainType: OPTIMISM_CHAIN_TYPE, + hardforkHistory: { + bedrock: { blockNumber: 123 }, + regolith: { blockNumber: 456 }, + canyon: { timestamp: 1 }, + }, + }, + }, + }); + + assertValidationErrors(validationErrors, []); + }); + + it("should not validate an invalid network config", async () => { + let validationErrors = await validateNetworkUserConfig({ + chainDescriptors: { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Cast to test validation error */ + 123: true as any, + }, + }); + + assertValidationErrors(validationErrors, [ + { + path: ["chainDescriptors", "123"], + message: "Expected object, received boolean", + }, + ]); + + validationErrors = await validateNetworkUserConfig({ + chainDescriptors: { + 123: { + name: "My Network", + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Cast to test validation error */ + hardforkHistory: true as any, + }, + }, + }); + + assertValidationErrors(validationErrors, [ + { + path: ["chainDescriptors", "123", "hardforkHistory"], + message: "Expected object, received boolean", + }, + ]); + + validationErrors = await validateNetworkUserConfig({ + chainDescriptors: { + 1: { + name: "Ethereum", + hardforkHistory: { + london: { blockNumber: 123 }, + }, + }, + 2: { + name: "My Chain", + hardforkHistory: { + shanghai: { blockNumber: 456 }, + "random string": { blockNumber: 789 }, + }, + }, + }, + }); + + assertValidationErrors(validationErrors, [ + { + path: ["chainDescriptors", "2", "hardforkHistory", "random string"], + message: + "Invalid hardfork name random string found in chain descriptor for chain 2. Expected chainstart | homestead | dao | tangerineWhistle | spuriousDragon | byzantium | constantinople | petersburg | istanbul | muirGlacier | berlin | london | arrowGlacier | grayGlacier | merge | shanghai | cancun.", + }, + ]); + + validationErrors = await validateNetworkUserConfig({ + chainDescriptors: { + 1: { + name: "My Optimism Chain", + chainType: OPTIMISM_CHAIN_TYPE, + hardforkHistory: { + "random string": { blockNumber: 456 }, + bedrock: { blockNumber: 789 }, + }, + }, + 2: { + name: "My Chain", + hardforkHistory: { + london: { blockNumber: 123 }, + }, + }, + }, + }); + + assertValidationErrors(validationErrors, [ + { + path: ["chainDescriptors", "1", "hardforkHistory", "random string"], + message: + "Invalid hardfork name random string found in chain descriptor for chain 1. Expected bedrock | regolith | canyon | ecotone | fjord | granite | holocene.", + }, + ]); + + validationErrors = await validateNetworkUserConfig({ + chainDescriptors: { + 123: { + name: "My Network", + hardforkHistory: { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Cast to test validation error */ + london: true as any, + }, + }, + }, + }); + + assertValidationErrors(validationErrors, [ + { + path: ["chainDescriptors", "123", "hardforkHistory", "london"], + message: + "Expected an object with either a blockNumber or a timestamp", + }, + ]); + + validationErrors = await validateNetworkUserConfig({ + chainDescriptors: { + 123: { + name: "My Network", + hardforkHistory: { + /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions + -- Cast to test validation error */ + london: { + blockNumber: 123, + timestamp: 123, + } as any, + }, + }, + }, + }); + + assertValidationErrors(validationErrors, [ + { + path: ["chainDescriptors", "123", "hardforkHistory", "london"], + message: "Unrecognized key(s) in object: 'timestamp'", + }, + ]); + + validationErrors = await validateNetworkUserConfig({ + chainDescriptors: { + 123: { + name: "My Network", + hardforkHistory: { + london: { + blockNumber: 456, + }, + cancun: { + blockNumber: 123, + }, + }, + }, + }, + }); + + assertValidationErrors(validationErrors, [ + { + path: ["chainDescriptors", "123", "hardforkHistory", "cancun"], + message: + "Invalid block number 123 found in chain descriptor for chain 123. Block numbers must be in ascending order.", + }, + ]); + + validationErrors = await validateNetworkUserConfig({ + chainDescriptors: { + 123: { + name: "My Network", + hardforkHistory: { + london: { + timestamp: 456, + }, + cancun: { + timestamp: 123, + }, + }, + }, + }, + }); + + assertValidationErrors(validationErrors, [ + { + path: ["chainDescriptors", "123", "hardforkHistory", "cancun"], + message: + "Invalid timestamp 123 found in chain descriptor for chain 123. Timestamps must be in ascending order.", + }, + ]); + + validationErrors = await validateNetworkUserConfig({ + chainDescriptors: { + 123: { + name: "My Network", + hardforkHistory: { + london: { + timestamp: 456, + }, + cancun: { + blockNumber: 123, + }, + }, + }, + }, + }); + + assertValidationErrors(validationErrors, [ + { + path: ["chainDescriptors", "123", "hardforkHistory", "cancun"], + message: + "Invalid block number 123 found in chain descriptor for chain 123. Block number cannot be defined after a timestamp.", + }, + ]); + }); + }); + describe("accounts", () => { describe("http config", async () => { describe("allowed values", () => { @@ -1593,185 +1829,6 @@ describe("NetworkManagerImplementation", () => { }); }); - describe("chains", () => { - describe("edr config", () => { - it("should validate a valid network config", async () => { - const validationErrors = await validateNetworkUserConfig( - edrConfig({ - chains: new Map() - .set(1, { - hardforkHistory: new Map().set("london", 456), - }) - .set(2, { - chainType: OPTIMISM_CHAIN_TYPE, - hardforkHistory: new Map().set("bedrock", 456), - }), - }), - ); - - assertValidationErrors(validationErrors, []); - }); - - it("should not validate an invalid network config", async () => { - let validationErrors = await validateNetworkUserConfig( - edrConfig({ - chains: new Map().set("123", { - hardforkHistory: new Map().set("london", 456), - }), - }), - ); - - assertValidationErrors(validationErrors, [ - { - path: ["networks", "hardhat", "chains", 0, "key"], - message: "Expected number, received string", - }, - ]); - - validationErrors = await validateNetworkUserConfig( - edrConfig({ - chains: new Map().set(123, true), - }), - ); - - assertValidationErrors(validationErrors, [ - { - path: ["networks", "hardhat", "chains", 0, "value"], - message: "Expected object, received boolean", - }, - ]); - - validationErrors = await validateNetworkUserConfig( - edrConfig({ - chains: new Map().set(123, { - hardforkHistory: true, - }), - }), - ); - - assertValidationErrors(validationErrors, [ - { - path: [ - "networks", - "hardhat", - "chains", - 0, - "value", - "hardforkHistory", - ], - message: "Expected map, received boolean", - }, - ]); - - validationErrors = await validateNetworkUserConfig( - edrConfig({ - chains: new Map() - .set(1, { - hardforkHistory: new Map().set("london", 123), - }) - .set(2, { - hardforkHistory: new Map() - .set("shanghai", 456) - .set("random string", 789), - }), - }), - ); - - assertValidationErrors(validationErrors, [ - { - path: [ - "networks", - "hardhat", - "chains", - 1, - "value", - "hardforkHistory", - 1, - "value", - ], - message: - "Invalid hardfork name random string found in chain 2. Expected chainstart | homestead | dao | tangerineWhistle | spuriousDragon | byzantium | constantinople | petersburg | istanbul | muirGlacier | berlin | london | arrowGlacier | grayGlacier | merge | shanghai | cancun.", - }, - ]); - - validationErrors = await validateNetworkUserConfig( - edrConfig({ - chains: new Map() - .set(1, { - chainType: OPTIMISM_CHAIN_TYPE, - hardforkHistory: new Map() - .set("random string", 456) - .set("bedrock", 789), - }) - .set(2, { - hardforkHistory: new Map().set("london", 123), - }), - }), - ); - - assertValidationErrors(validationErrors, [ - { - path: [ - "networks", - "hardhat", - "chains", - 0, - "value", - "hardforkHistory", - 0, - "value", - ], - message: - "Invalid hardfork name random string found in chain 1. Expected bedrock | regolith | canyon | ecotone | fjord | granite | holocene.", - }, - ]); - - validationErrors = await validateNetworkUserConfig( - edrConfig({ - chains: new Map().set(123, { - hardforkHistory: new Map().set("london", true), - }), - }), - ); - - assertValidationErrors(validationErrors, [ - { - path: [ - "networks", - "hardhat", - "chains", - 0, - "value", - "hardforkHistory", - 0, - "value", - ], - message: "Expected number, received boolean", - }, - ]); - - validationErrors = await validateNetworkUserConfig( - edrConfig({ - chains: { - 123: { - hardforkHistory: { - london: 456, - }, - }, - }, - }), - ); - - assertValidationErrors(validationErrors, [ - { - path: ["networks", "hardhat", "chains"], - message: "Expected map, received object", - }, - ]); - }); - }); - }); - describe("coinbase", () => { describe("edr config", () => { it("should validate a valid network config", async () => {