Skip to main content

foundry_evm_networks/
lib.rs

1//! # foundry-evm-networks
2//!
3//! Foundry EVM network configuration.
4
5use crate::celo::transfer::{
6    CELO_TRANSFER_ADDRESS, CELO_TRANSFER_LABEL, PRECOMPILE_ID_CELO_TRANSFER,
7};
8use alloy_chains::{
9    Chain, NamedChain,
10    NamedChain::{Chiado, Gnosis, Moonbase, Moonbeam, MoonbeamDev, Moonriver, Rsk, RskTestnet},
11};
12use alloy_eips::eip1559::BaseFeeParams;
13use alloy_evm::precompiles::{DynPrecompile, PrecompilesMap};
14use alloy_primitives::{Address, ChainId, address, map::AddressHashMap};
15use clap::Parser;
16#[cfg(feature = "monad")]
17type MonadHardfork = foundry_evm_hardforks::MonadHardfork;
18#[cfg(feature = "optimism")]
19use foundry_evm_hardforks::OpHardfork;
20use foundry_evm_hardforks::{
21    EthereumHardfork, FoundryHardfork, TempoHardfork, latest_active_tempo_hardfork,
22};
23#[cfg(not(feature = "monad"))]
24type MonadHardfork = ();
25use revm::precompile::{
26    Precompile as RevmPrecompile,
27    secp256r1::{P256VERIFY, P256VERIFY_OSAKA},
28};
29use serde::{Deserialize, Serialize};
30use std::collections::BTreeMap;
31use tempo_contracts::precompiles::{
32    ACCOUNT_KEYCHAIN_ADDRESS, ADDRESS_REGISTRY_ADDRESS, CURRENT_COMMITTEE_ADDRESS,
33    NONCE_PRECOMPILE_ADDRESS, RECEIVE_POLICY_GUARD_ADDRESS, SIGNATURE_VERIFIER_ADDRESS,
34    STABLECOIN_DEX_ADDRESS, STORAGE_CREDITS_ADDRESS, TIP_FEE_MANAGER_ADDRESS,
35    TIP20_CHANNEL_RESERVE_ADDRESS, TIP20_FACTORY_ADDRESS, TIP403_REGISTRY_ADDRESS,
36    VALIDATOR_CONFIG_ADDRESS, VALIDATOR_CONFIG_V2_ADDRESS,
37};
38
39/// The Monad cheatcode handler address.
40pub const MONAD_CHEATCODE_ADDRESS: Address = address!("0xc0FFeeCD43A10e1C2b0De63c6CDCFe5B7d0e0CEA");
41
42#[cfg(feature = "monad")]
43const MONAD_CHEATCODE_ADDRESSES: &[Address] = &[MONAD_CHEATCODE_ADDRESS];
44
45pub mod arbitrum;
46pub mod celo;
47
48#[cfg(feature = "optimism")]
49mod optimism;
50
51const TEMPO_PRECOMPILES: &[(&str, Address)] = &[
52    ("Nonce", NONCE_PRECOMPILE_ADDRESS),
53    ("StablecoinDex", STABLECOIN_DEX_ADDRESS),
54    ("TIP20Factory", TIP20_FACTORY_ADDRESS),
55    ("TIP403Registry", TIP403_REGISTRY_ADDRESS),
56    ("FeeManager", TIP_FEE_MANAGER_ADDRESS),
57    ("ValidatorConfig", VALIDATOR_CONFIG_ADDRESS),
58    ("ValidatorConfigV2", VALIDATOR_CONFIG_V2_ADDRESS),
59    ("AccountKeychain", ACCOUNT_KEYCHAIN_ADDRESS),
60    ("SignatureVerifier", SIGNATURE_VERIFIER_ADDRESS),
61    ("AddressRegistry", ADDRESS_REGISTRY_ADDRESS),
62    ("TIP20ChannelReserve", TIP20_CHANNEL_RESERVE_ADDRESS),
63    ("ReceivePolicyGuard", RECEIVE_POLICY_GUARD_ADDRESS),
64    ("StorageCredits", STORAGE_CREDITS_ADDRESS),
65    ("CurrentCommittee", CURRENT_COMMITTEE_ADDRESS),
66];
67
68#[cfg(feature = "monad")]
69const MONAD_PRECOMPILE_LABELS: &[(&str, Address)] = &[
70    ("Staking", monad_revm::staking::STAKING_ADDRESS),
71    ("ReserveBalance", monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS),
72];
73
74#[cfg(feature = "monad")]
75const MONAD_PRECOMPILES: &[(&str, Address)] = &[
76    ("MonadStaking", monad_revm::staking::STAKING_ADDRESS),
77    ("MonadReserveBalance", monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS),
78];
79
80/// BSC secp256r1 precompile address introduced by the Haber hardfork.
81const BSC_P256_ADDRESS: Address = address!("0000000000000000000000000000000000000100");
82
83const BSC_MAINNET_CHAIN_ID: u64 = 56;
84const BSC_TESTNET_CHAIN_ID: u64 = 97;
85const BSC_MAINNET_HABER_TIMESTAMP: u64 = 1_718_863_500;
86const BSC_TESTNET_HABER_TIMESTAMP: u64 = 1_716_962_820;
87const BSC_MAINNET_OSAKA_TIMESTAMP: u64 = 1_777_343_400;
88const BSC_TESTNET_OSAKA_TIMESTAMP: u64 = 1_774_319_400;
89
90/// Returns the BSC P256 precompile for the given timestamp. The outer option distinguishes BSC
91/// chains from unrelated chains, while the inner option disables P256 before Haber.
92const fn bsc_p256_precompile(chain_id: ChainId, timestamp: u64) -> Option<Option<RevmPrecompile>> {
93    let (haber_timestamp, osaka_timestamp) = match chain_id {
94        BSC_MAINNET_CHAIN_ID => (BSC_MAINNET_HABER_TIMESTAMP, BSC_MAINNET_OSAKA_TIMESTAMP),
95        BSC_TESTNET_CHAIN_ID => (BSC_TESTNET_HABER_TIMESTAMP, BSC_TESTNET_OSAKA_TIMESTAMP),
96        _ => return None,
97    };
98
99    if timestamp < haber_timestamp {
100        Some(None)
101    } else if timestamp < osaka_timestamp {
102        Some(Some(P256VERIFY))
103    } else {
104        Some(Some(P256VERIFY_OSAKA))
105    }
106}
107
108/// All well-known Tempo precompile addresses.
109pub const TEMPO_PRECOMPILE_ADDRESSES: &[Address] = &[
110    NONCE_PRECOMPILE_ADDRESS,
111    STABLECOIN_DEX_ADDRESS,
112    TIP20_FACTORY_ADDRESS,
113    TIP403_REGISTRY_ADDRESS,
114    TIP_FEE_MANAGER_ADDRESS,
115    VALIDATOR_CONFIG_ADDRESS,
116    VALIDATOR_CONFIG_V2_ADDRESS,
117    ACCOUNT_KEYCHAIN_ADDRESS,
118    SIGNATURE_VERIFIER_ADDRESS,
119    ADDRESS_REGISTRY_ADDRESS,
120    TIP20_CHANNEL_RESERVE_ADDRESS,
121    RECEIVE_POLICY_GUARD_ADDRESS,
122    STORAGE_CREDITS_ADDRESS,
123    CURRENT_COMMITTEE_ADDRESS,
124];
125
126/// Returns whether a well-known Tempo precompile address is active at `hardfork`.
127pub fn is_tempo_precompile_active_at(address: Address, hardfork: TempoHardfork) -> bool {
128    if address == CURRENT_COMMITTEE_ADDRESS {
129        hardfork.is_t8()
130    } else if address == TIP20_CHANNEL_RESERVE_ADDRESS {
131        hardfork.is_t5()
132    } else if address == RECEIVE_POLICY_GUARD_ADDRESS {
133        hardfork.is_t6()
134    } else if address == STORAGE_CREDITS_ADDRESS {
135        hardfork.is_t7()
136    } else if address == ADDRESS_REGISTRY_ADDRESS || address == SIGNATURE_VERIFIER_ADDRESS {
137        hardfork.is_t3()
138    } else {
139        true
140    }
141}
142
143/// Returns the well-known Tempo precompile addresses active at `hardfork`.
144pub fn active_tempo_precompile_addresses(hardfork: TempoHardfork) -> impl Iterator<Item = Address> {
145    TEMPO_PRECOMPILE_ADDRESSES
146        .iter()
147        .copied()
148        .filter(move |&address| is_tempo_precompile_active_at(address, hardfork))
149}
150
151/// Returns whether a well-known Monad precompile address is active at `hardfork`.
152#[cfg(feature = "monad")]
153pub fn is_monad_precompile_active_at(address: Address, hardfork: MonadHardfork) -> bool {
154    address == monad_revm::staking::STAKING_ADDRESS
155        || (address == monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS
156            && MonadHardfork::MonadNine.is_enabled_in(hardfork))
157}
158
159#[derive(
160    Clone,
161    Copy,
162    Debug,
163    Default,
164    PartialEq,
165    Eq,
166    PartialOrd,
167    Ord,
168    Hash,
169    Serialize,
170    Deserialize,
171    clap::ValueEnum,
172)]
173#[serde(rename_all = "lowercase")]
174#[clap(rename_all = "lowercase")]
175pub enum NetworkVariant {
176    #[default]
177    Ethereum,
178    #[cfg(feature = "optimism")]
179    Optimism,
180    Tempo,
181    #[cfg(feature = "monad")]
182    Monad,
183}
184
185/// Runtime and initcode byte-size limits for a configured network.
186#[derive(Clone, Copy, Debug, PartialEq, Eq)]
187pub struct NetworkContractSizeLimits {
188    /// Maximum deployed runtime bytecode size.
189    pub runtime: usize,
190    /// Maximum initcode bytecode size.
191    pub initcode: usize,
192}
193
194impl std::str::FromStr for NetworkVariant {
195    type Err = String;
196
197    fn from_str(s: &str) -> Result<Self, Self::Err> {
198        match s {
199            "ethereum" => Ok(Self::Ethereum),
200            #[cfg(feature = "optimism")]
201            "optimism" => Ok(Self::Optimism),
202            "tempo" => Ok(Self::Tempo),
203            #[cfg(feature = "monad")]
204            "monad" => Ok(Self::Monad),
205            _ => Err(format!("unknown network variant: {s}")),
206        }
207    }
208}
209
210impl NetworkVariant {
211    /// Returns the network family identified by a known chain ID.
212    ///
213    /// Unknown chain IDs return `None` so callers can consult endpoint metadata instead of
214    /// assuming Ethereum. If a known chain belongs to a feature-gated family that is not enabled,
215    /// this returns an error rather than silently selecting a different EVM.
216    pub fn from_known_chain_id(chain_id: ChainId) -> Result<Option<Self>, String> {
217        let chain = Chain::from_id(chain_id);
218        if chain.is_tempo() {
219            return Ok(Some(Self::Tempo));
220        }
221        if matches!(chain.named(), Some(NamedChain::Celo | NamedChain::CeloSepolia)) {
222            return Ok(Some(Self::Ethereum));
223        }
224        if matches!(chain.named(), Some(NamedChain::Monad | NamedChain::MonadTestnet)) {
225            #[cfg(feature = "monad")]
226            return Ok(Some(Self::Monad));
227            #[cfg(not(feature = "monad"))]
228            return Err("network family `monad` is not enabled in this build".to_string());
229        }
230        if chain.is_optimism() {
231            #[cfg(feature = "optimism")]
232            return Ok(Some(Self::Optimism));
233            #[cfg(not(feature = "optimism"))]
234            return Err("network family `optimism` is not enabled in this build".to_string());
235        }
236        Ok(chain.named().map(|_| Self::Ethereum))
237    }
238
239    /// Parses an explicit network family reported by `anvil_nodeInfo`.
240    pub fn from_node_info_name(network: &str) -> Result<Self, String> {
241        match network {
242            "ethereum" => Ok(Self::Ethereum),
243            #[cfg(feature = "optimism")]
244            "optimism" => Ok(Self::Optimism),
245            #[cfg(not(feature = "optimism"))]
246            "optimism" => Err("network family `optimism` is not enabled in this build".to_string()),
247            "tempo" => Ok(Self::Tempo),
248            #[cfg(feature = "monad")]
249            "monad" => Ok(Self::Monad),
250            #[cfg(not(feature = "monad"))]
251            "monad" => Err("network family `monad` is not enabled in this build".to_string()),
252            network => {
253                Err(format!("unsupported network family `{network}` reported by fork endpoint"))
254            }
255        }
256    }
257
258    /// Resolves an RPC endpoint's network family.
259    ///
260    /// The outer `node_info_network` option distinguishes an unavailable `anvil_nodeInfo` method
261    /// from a successful legacy response that omitted the network. An explicit name is
262    /// authoritative even when the endpoint exposes a well-known execution chain ID. A legacy
263    /// omission falls back to the known chain ID, or Ethereum for a custom chain.
264    pub fn from_rpc_identity(
265        chain_id: ChainId,
266        node_info_network: Option<Option<&str>>,
267    ) -> Result<Option<Self>, String> {
268        Self::from_rpc_identity_with_fallback(chain_id, node_info_network, None)
269    }
270
271    /// Resolves an RPC endpoint's network family with a caller-selected unknown-chain fallback.
272    ///
273    /// Explicit endpoint metadata is authoritative. Legacy `anvil_nodeInfo` responses that omit
274    /// the family first use a known chain ID, then the supplied fallback, and finally Ethereum to
275    /// preserve historical behavior. Endpoints without `anvil_nodeInfo` use only a known chain ID
276    /// or the supplied fallback.
277    pub fn from_rpc_identity_with_fallback(
278        chain_id: ChainId,
279        node_info_network: Option<Option<&str>>,
280        unknown_fallback: Option<Self>,
281    ) -> Result<Option<Self>, String> {
282        match node_info_network {
283            Some(Some(network)) => Self::from_node_info_name(network).map(Some),
284            Some(None) => Ok(Self::from_known_chain_id(chain_id)?
285                .or(unknown_fallback)
286                .or(Some(Self::Ethereum))),
287            None => Ok(Self::from_known_chain_id(chain_id)?.or(unknown_fallback)),
288        }
289    }
290
291    /// Parses a hardfork name reported by an RPC endpoint in this network's namespace.
292    pub fn parse_hardfork(self, hardfork: &str) -> Result<FoundryHardfork, String> {
293        format!("{}:{hardfork}", self.name()).parse()
294    }
295
296    /// Returns the active hardfork for this network at the given chain and timestamp.
297    ///
298    /// Unknown chain IDs fall back to the network's default hardfork. The selected network owns
299    /// the lookup so an explicit network choice is not overridden by the chain ID's family.
300    pub fn hardfork_at(self, chain_id: ChainId, timestamp: u64) -> FoundryHardfork {
301        match self {
302            Self::Ethereum => {
303                EthereumHardfork::from_chain_and_timestamp(Chain::from_id(chain_id), timestamp)
304                    .unwrap_or_default()
305                    .into()
306            }
307            Self::Tempo => TempoHardfork::from_chain_and_timestamp(chain_id, timestamp)
308                .unwrap_or_else(latest_active_tempo_hardfork)
309                .into(),
310            #[cfg(feature = "optimism")]
311            Self::Optimism => {
312                OpHardfork::from_chain_and_timestamp(Chain::from_id(chain_id), timestamp)
313                    .unwrap_or_default()
314                    .into()
315            }
316            #[cfg(feature = "monad")]
317            Self::Monad => MonadHardfork::from_chain_and_timestamp(chain_id, timestamp)
318                .unwrap_or_default()
319                .into(),
320        }
321    }
322
323    /// Returns `true` if this is the Ethereum network variant.
324    pub const fn is_ethereum(&self) -> bool {
325        matches!(self, Self::Ethereum)
326    }
327
328    /// Returns `true` if this is the Optimism network variant.
329    #[cfg(feature = "optimism")]
330    pub const fn is_optimism(&self) -> bool {
331        matches!(self, Self::Optimism)
332    }
333
334    /// Returns `true` if this is the Tempo network variant.
335    pub const fn is_tempo(&self) -> bool {
336        matches!(self, Self::Tempo)
337    }
338
339    /// Returns `true` if this is the Monad network variant.
340    #[cfg(feature = "monad")]
341    pub const fn is_monad(&self) -> bool {
342        matches!(self, Self::Monad)
343    }
344
345    /// Returns `false` when Monad support is not compiled in.
346    #[cfg(not(feature = "monad"))]
347    pub const fn is_monad(&self) -> bool {
348        false
349    }
350
351    /// Returns the network variant name.
352    pub const fn name(&self) -> &'static str {
353        match self {
354            Self::Ethereum => "ethereum",
355            #[cfg(feature = "optimism")]
356            Self::Optimism => "optimism",
357            Self::Tempo => "tempo",
358            #[cfg(feature = "monad")]
359            Self::Monad => "monad",
360        }
361    }
362
363    /// Returns the hardfork namespace used by this network family.
364    pub const fn hardfork_namespace(&self) -> Option<&'static str> {
365        match self {
366            Self::Ethereum => None,
367            #[cfg(feature = "optimism")]
368            Self::Optimism => Some("optimism"),
369            Self::Tempo => Some("tempo"),
370            #[cfg(feature = "monad")]
371            Self::Monad => Some("monad"),
372        }
373    }
374}
375
376impl std::fmt::Display for NetworkVariant {
377    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
378        f.write_str(self.name())
379    }
380}
381
382impl From<ChainId> for NetworkVariant {
383    fn from(chain_id: ChainId) -> Self {
384        Self::from_known_chain_id(chain_id).ok().flatten().unwrap_or(Self::Ethereum)
385    }
386}
387
388#[derive(Clone, Debug, Default, Parser, Deserialize, Copy, PartialEq, Eq, Hash)]
389pub struct NetworkConfigs {
390    /// Enable a specific network family.
391    #[arg(help_heading = "Networks", long, short, num_args = 1, value_name = "NETWORK", value_enum, conflicts_with_all = ["celo", "tempo"])]
392    #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
393    #[cfg_attr(feature = "monad", arg(conflicts_with = "monad"))]
394    #[serde(default)]
395    pub(crate) network: Option<NetworkVariant>,
396    /// Enable Celo network features.
397    #[arg(help_heading = "Networks", long, conflicts_with_all = ["network", "tempo"])]
398    #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
399    #[cfg_attr(feature = "monad", arg(conflicts_with = "monad"))]
400    celo: bool,
401    /// Enable Optimism network features (deprecated: use --network optimism).
402    #[cfg(feature = "optimism")]
403    #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "tempo"])]
404    #[cfg_attr(feature = "monad", arg(conflicts_with = "monad"))]
405    // Deserialize-only legacy alias: accepted in foundry.toml but never serialized — the
406    // canonical form is `network = "optimism"`.
407    #[serde(default)]
408    pub(crate) optimism: bool,
409    /// Enable Tempo network features (deprecated: use --network tempo).
410    #[arg(long, hide = true, conflicts_with_all = ["network", "celo"])]
411    #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
412    #[cfg_attr(feature = "monad", arg(conflicts_with = "monad"))]
413    // Deserialize-only legacy alias: accepted in foundry.toml but never serialized — the
414    // canonical form is `network = "tempo"`.
415    #[serde(default)]
416    tempo: bool,
417    /// Enable Monad network features (deprecated: use --network monad).
418    #[cfg(feature = "monad")]
419    #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "tempo"])]
420    #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
421    // Deserialize-only legacy alias: accepted in foundry.toml but never serialized - the
422    // canonical form is `network = "monad"`.
423    #[serde(default)]
424    monad: bool,
425    /// Whether to bypass prevrandao.
426    #[arg(skip)]
427    #[serde(default)]
428    bypass_prevrandao: bool,
429}
430
431// Custom `Serialize` impl: always emits the *resolved* network as the canonical
432// `network = "..."` field, and never emits the legacy `tempo` / `optimism` / `monad` aliases.
433// This avoids confusing output like `network = "monad"` next to `monad = false`, and ensures
434// legacy aliases in foundry.toml round-trip as canonical network values.
435impl Serialize for NetworkConfigs {
436    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
437        use serde::ser::SerializeStruct;
438        let mut s = serializer.serialize_struct("NetworkConfigs", 3)?;
439        s.serialize_field("network", &self.resolved_network())?;
440        s.serialize_field("celo", &self.celo)?;
441        s.serialize_field("bypass_prevrandao", &self.bypass_prevrandao)?;
442        s.end()
443    }
444}
445
446impl NetworkConfigs {
447    /// Validates that all configured network selectors resolve to the same execution profile.
448    ///
449    /// Canonical and legacy selectors for the same family remain compatible. Selectors for
450    /// different families are rejected instead of being silently resolved by field priority.
451    pub fn validate(&self) -> Result<(), String> {
452        let mut selectors = Vec::new();
453        if let Some(network) = self.network {
454            selectors.push((network.name(), format!("network = \"{}\"", network.name())));
455        }
456        if self.celo {
457            selectors.push(("celo", "celo = true".to_string()));
458        }
459        #[cfg(feature = "optimism")]
460        if self.optimism {
461            selectors.push(("optimism", "optimism = true".to_string()));
462        }
463        if self.tempo {
464            selectors.push(("tempo", "tempo = true".to_string()));
465        }
466        #[cfg(feature = "monad")]
467        if self.monad {
468            selectors.push(("monad", "monad = true".to_string()));
469        }
470
471        if let Some((family, selector)) = selectors.first()
472            && let Some((_, conflicting)) =
473                selectors.iter().find(|(candidate, _)| candidate != family)
474        {
475            return Err(format!(
476                "network selectors `{selector}` and `{conflicting}` conflict; select only one \
477                 network"
478            ));
479        }
480
481        Ok(())
482    }
483
484    pub fn with_ethereum() -> Self {
485        Self { network: Some(NetworkVariant::Ethereum), ..Default::default() }
486    }
487
488    pub fn with_celo() -> Self {
489        Self { celo: true, ..Default::default() }
490    }
491
492    pub fn with_tempo() -> Self {
493        Self { network: Some(NetworkVariant::Tempo), tempo: true, ..Default::default() }
494    }
495
496    #[cfg(feature = "monad")]
497    pub fn with_monad() -> Self {
498        Self { network: Some(NetworkVariant::Monad), monad: true, ..Default::default() }
499    }
500
501    pub const fn is_tempo(&self) -> bool {
502        if let Some(network) = self.resolved_network() { network.is_tempo() } else { false }
503    }
504
505    #[cfg(feature = "monad")]
506    pub const fn is_monad(&self) -> bool {
507        if let Some(network) = self.resolved_network() { network.is_monad() } else { false }
508    }
509
510    #[cfg(not(feature = "monad"))]
511    pub const fn is_monad(&self) -> bool {
512        false
513    }
514
515    /// Returns additional cheatcode contract addresses for the active network.
516    pub const fn extra_cheatcode_addresses(&self) -> &'static [Address] {
517        #[cfg(feature = "monad")]
518        if self.is_monad() {
519            return MONAD_CHEATCODE_ADDRESSES;
520        }
521        &[]
522    }
523
524    pub const fn is_celo(&self) -> bool {
525        self.celo
526    }
527
528    /// Returns the resolved network variant, folding legacy flags.
529    pub const fn resolved_network(&self) -> Option<NetworkVariant> {
530        if let Some(n) = self.network {
531            return Some(n);
532        }
533        #[cfg(feature = "optimism")]
534        if self.optimism {
535            return Some(NetworkVariant::Optimism);
536        }
537        if self.tempo {
538            return Some(NetworkVariant::Tempo);
539        }
540        #[cfg(feature = "monad")]
541        if self.monad {
542            return Some(NetworkVariant::Monad);
543        }
544        None
545    }
546
547    /// Returns whether a network family was selected in this configuration.
548    pub const fn has_network_selection(&self) -> bool {
549        self.celo || self.resolved_network().is_some()
550    }
551
552    /// Returns the execution family represented by this configuration.
553    pub const fn execution_family_name(&self) -> &'static str {
554        self.execution_network().name()
555    }
556
557    /// Returns a label for the complete execution configuration.
558    pub const fn execution_profile_name(&self) -> &'static str {
559        if self.celo { "celo" } else { self.execution_family_name() }
560    }
561
562    /// Returns the concrete execution network, treating an unresolved configuration as Ethereum.
563    pub const fn execution_network(&self) -> NetworkVariant {
564        if let Some(network) = self.resolved_network() { network } else { NetworkVariant::Ethereum }
565    }
566
567    /// Returns whether both configurations can use the same instantiated EVM backend.
568    pub fn has_same_execution_profile(&self, other: &Self) -> bool {
569        self.celo == other.celo
570            && match (self.resolved_network(), other.resolved_network()) {
571                (Some(left), Some(right)) => left == right,
572                (Some(left), None) => left.is_ethereum(),
573                (None, Some(right)) => right.is_ethereum(),
574                (None, None) => true,
575            }
576    }
577
578    /// Returns whether this execution configuration can use `source` as a fork state source
579    /// without rebuilding the instantiated EVM.
580    ///
581    /// Monad uses a distinct EVM factory and instruction provider, so forks cannot cross the
582    /// Monad boundary. Existing non-Monad fork-source compatibility remains unchanged.
583    pub const fn supports_fork_source(&self, source: &Self) -> bool {
584        self.is_monad() == source.is_monad()
585    }
586
587    /// Returns the name of the currently active non-Ethereum network, or `None` for plain Ethereum.
588    pub fn active_network_name(&self) -> Option<&'static str> {
589        self.resolved_network().and_then(|network| network.hardfork_namespace())
590    }
591
592    /// Returns the base fee parameters for the configured network.
593    ///
594    /// For Optimism networks, returns Canyon parameters if the Canyon hardfork is active
595    /// at the given timestamp, otherwise returns pre-Canyon parameters.
596    #[cfg(feature = "optimism")]
597    pub fn base_fee_params(&self, timestamp: u64) -> BaseFeeParams {
598        if self.is_optimism() {
599            return self.op_base_fee_params(timestamp);
600        }
601        BaseFeeParams::ethereum()
602    }
603
604    /// Returns the base fee parameters for the configured network.
605    #[cfg(not(feature = "optimism"))]
606    pub const fn base_fee_params(&self, timestamp: u64) -> BaseFeeParams {
607        let _ = timestamp;
608        BaseFeeParams::ethereum()
609    }
610
611    /// Returns contract size limits for networks that override Ethereum defaults.
612    #[cfg(feature = "monad")]
613    pub fn contract_size_limits(&self) -> Option<NetworkContractSizeLimits> {
614        self.is_monad().then_some(NetworkContractSizeLimits {
615            runtime: monad_revm::MONAD_MAX_CODE_SIZE,
616            initcode: monad_revm::MONAD_MAX_INITCODE_SIZE,
617        })
618    }
619
620    /// Returns contract size limits for networks that override Ethereum defaults.
621    #[cfg(not(feature = "monad"))]
622    pub const fn contract_size_limits(&self) -> Option<NetworkContractSizeLimits> {
623        None
624    }
625
626    pub fn bypass_prevrandao(&self, chain_id: u64) -> bool {
627        if let Ok(
628            Moonbeam | Moonbase | Moonriver | MoonbeamDev | Rsk | RskTestnet | Gnosis | Chiado,
629        ) = NamedChain::try_from(chain_id)
630        {
631            return true;
632        }
633        self.bypass_prevrandao
634    }
635
636    /// Infers an execution profile from `chain_id` unless one was selected explicitly.
637    ///
638    /// Unknown chain IDs preserve the unresolved configuration so endpoint metadata can still
639    /// identify their execution profile. A known chain whose feature is unavailable is rejected
640    /// instead of silently falling back to Ethereum semantics.
641    pub fn try_with_chain_id(self, chain_id: u64) -> Result<Self, String> {
642        if self.has_network_selection() {
643            return Ok(self);
644        }
645
646        match NetworkVariant::from_known_chain_id(chain_id).map_err(|error| {
647            format!("cannot infer execution network from chain ID {chain_id}: {error}")
648        })? {
649            Some(network) => Ok(self.with_rpc_identity(network, chain_id)),
650            None => Ok(self),
651        }
652    }
653
654    /// Best-effort execution-profile inference for trusted, programmatic chain IDs.
655    ///
656    /// User-provided chain IDs must be handled with [`Self::try_with_chain_id`] so an unavailable
657    /// feature cannot silently select Ethereum semantics.
658    pub fn with_chain_id(self, chain_id: u64) -> Self {
659        self.try_with_chain_id(chain_id).unwrap_or(self)
660    }
661
662    /// Applies an RPC endpoint's resolved EVM family to this configuration.
663    ///
664    /// Successful endpoint metadata is authoritative, including when a local Anvil uses a
665    /// well-known chain ID as an execution override. Orthogonal settings are preserved.
666    pub fn with_rpc_network(self, network: NetworkVariant) -> Self {
667        let mut resolved = if network.is_ethereum() { Self::default() } else { network.into() };
668        resolved.bypass_prevrandao = self.bypass_prevrandao;
669        resolved
670    }
671
672    /// Applies an RPC endpoint's execution family and chain-specific Ethereum configuration.
673    ///
674    /// The reported family is authoritative. A known Celo chain ID additionally enables Celo's
675    /// precompiles because Celo shares the Ethereum EVM factory rather than having its own
676    /// [`NetworkVariant`].
677    pub fn with_rpc_identity(self, network: NetworkVariant, chain_id: ChainId) -> Self {
678        let mut resolved = self.with_rpc_network(network);
679        if network.is_ethereum()
680            && matches!(
681                Chain::from_id(chain_id).named(),
682                Some(NamedChain::Celo | NamedChain::CeloSepolia)
683            )
684        {
685            resolved.celo = true;
686        }
687        resolved
688    }
689
690    /// Returns the canonical endpoint-visible execution profile.
691    pub fn canonical_execution_profile(self) -> Self {
692        if self.is_celo() {
693            Self::with_celo()
694        } else if let Some(network) = self.resolved_network()
695            && !network.is_ethereum()
696        {
697            network.into()
698        } else {
699            Self::default()
700        }
701    }
702
703    /// Applies an authoritative execution profile while preserving orthogonal settings.
704    pub fn with_execution_profile(self, profile: Self) -> Self {
705        let mut resolved = if profile.is_celo() {
706            Self::with_celo()
707        } else {
708            profile.resolved_network().map(Into::into).unwrap_or_default()
709        };
710        resolved.bypass_prevrandao = self.bypass_prevrandao;
711        resolved
712    }
713
714    /// Applies an endpoint-reported execution profile while preserving orthogonal settings.
715    pub fn with_rpc_profile(self, profile: Self) -> Self {
716        self.with_execution_profile(profile.canonical_execution_profile())
717    }
718
719    /// Parses the execution profile reported by `anvil_nodeInfo`.
720    pub fn from_node_info_profile(profile: &str) -> Result<Self, String> {
721        if profile == "celo" {
722            return Ok(Self::with_celo());
723        }
724        let network = NetworkVariant::from_node_info_name(profile)?;
725        Ok(if network.is_ethereum() { Self::default() } else { network.into() })
726    }
727
728    /// Resolves an RPC endpoint's complete execution profile.
729    ///
730    /// Explicit metadata is authoritative. When metadata is absent or omits the profile, a
731    /// caller-supplied explicit profile takes precedence over chain-ID inference. Otherwise,
732    /// legacy Anvil responses and ordinary RPC endpoints recover Celo from a canonical Celo chain
733    /// ID and preserve the historical behavior for custom chain IDs.
734    pub fn from_rpc_identity_profile_with_fallback(
735        chain_id: ChainId,
736        node_info_profile: Option<Option<&str>>,
737        unknown_fallback: Option<Self>,
738    ) -> Result<Option<Self>, String> {
739        let known_profile = || {
740            NetworkVariant::from_known_chain_id(chain_id).map(|network| {
741                network.map(|network| Self::default().with_rpc_identity(network, chain_id))
742            })
743        };
744        let fallback_is_explicit =
745            unknown_fallback.is_some_and(|fallback| fallback.has_network_selection());
746        let fallback = unknown_fallback.map(Self::canonical_execution_profile);
747        if let Some(Some(profile)) = node_info_profile {
748            return Self::from_node_info_profile(profile).map(Some);
749        }
750        if fallback_is_explicit && let Some(fallback) = fallback {
751            return Ok(Some(fallback));
752        }
753        match node_info_profile {
754            Some(None) => Ok(known_profile()?.or(fallback).or(Some(Self::default()))),
755            None => Ok(known_profile()?.or(fallback)),
756            Some(Some(_)) => unreachable!(),
757        }
758    }
759
760    /// Validates `hardfork` against the current `NetworkConfigs` and, if consistent, returns an
761    /// updated instance with the network implied by the enabled hardfork.
762    ///
763    /// Returns `Err` when the hardfork's network family conflicts with the configured one.
764    pub fn normalize_for_hardfork(self, hardfork: FoundryHardfork) -> Result<Self, String> {
765        if self.has_network_selection() {
766            let configured = self.execution_network();
767            if configured.hardfork_namespace() != hardfork.namespace() {
768                return Err(format!(
769                    "hardfork `{}` conflicts with network config `{}`",
770                    String::from(hardfork),
771                    self.execution_profile_name(),
772                ));
773            }
774        }
775
776        let network = match hardfork {
777            FoundryHardfork::Ethereum(_) => self,
778            FoundryHardfork::Tempo(_) => Self::with_tempo(),
779            #[cfg(feature = "optimism")]
780            FoundryHardfork::Optimism(_) => Self::with_optimism(),
781            #[cfg(feature = "monad")]
782            FoundryHardfork::Monad(_) => Self::with_monad(),
783        };
784
785        Ok(network)
786    }
787
788    /// Inject precompiles for configured networks.
789    pub fn inject_precompiles(self, precompiles: &mut PrecompilesMap) {
790        if self.is_celo() {
791            precompiles.apply_precompile(&CELO_TRANSFER_ADDRESS, move |_| {
792                Some(celo::transfer::precompile())
793            });
794        }
795    }
796
797    /// Injects chain-specific precompiles active at the given timestamp.
798    pub fn inject_chain_precompiles(
799        self,
800        precompiles: &mut PrecompilesMap,
801        chain_id: ChainId,
802        timestamp: u64,
803    ) {
804        let Some(p256verify) = bsc_p256_precompile(chain_id, timestamp) else { return };
805        precompiles.apply_precompile(&BSC_P256_ADDRESS, move |_| {
806            p256verify.map(|p256verify| {
807                DynPrecompile::new(p256verify.id().clone(), move |input| {
808                    p256verify.execute(input.data, input.gas, input.reservoir)
809                })
810            })
811        });
812    }
813
814    /// Returns precompiles label for configured networks, to be used in traces.
815    pub fn precompiles_label(
816        self,
817        tempo_hardfork: Option<TempoHardfork>,
818        monad_hardfork: Option<MonadHardfork>,
819    ) -> AddressHashMap<String> {
820        #[cfg(not(feature = "monad"))]
821        let _ = monad_hardfork;
822        let mut labels = AddressHashMap::default();
823        if self.is_celo() {
824            labels.insert(CELO_TRANSFER_ADDRESS, CELO_TRANSFER_LABEL.to_string());
825        }
826        if self.is_tempo() {
827            labels.extend(
828                TEMPO_PRECOMPILES
829                    .iter()
830                    .copied()
831                    .filter(|(_, address)| {
832                        tempo_hardfork.is_none_or(|hardfork| {
833                            is_tempo_precompile_active_at(*address, hardfork)
834                        })
835                    })
836                    .map(|(label, address)| (address, label.to_string())),
837            );
838        }
839        #[cfg(feature = "monad")]
840        if self.is_monad() {
841            labels.extend(
842                MONAD_PRECOMPILE_LABELS
843                    .iter()
844                    .copied()
845                    .filter(|(_, address)| {
846                        monad_hardfork.is_none_or(|hardfork| {
847                            is_monad_precompile_active_at(*address, hardfork)
848                        })
849                    })
850                    .map(|(label, address)| (address, label.to_string())),
851            );
852        }
853        labels
854    }
855
856    /// Returns precompiles for configured networks.
857    pub fn precompiles(
858        self,
859        tempo_hardfork: Option<TempoHardfork>,
860        monad_hardfork: Option<MonadHardfork>,
861    ) -> BTreeMap<String, Address> {
862        #[cfg(not(feature = "monad"))]
863        let _ = monad_hardfork;
864        let mut precompiles = BTreeMap::new();
865        if self.is_celo() {
866            precompiles
867                .insert(PRECOMPILE_ID_CELO_TRANSFER.name().to_string(), CELO_TRANSFER_ADDRESS);
868        }
869        if self.is_tempo() {
870            precompiles.extend(
871                TEMPO_PRECOMPILES
872                    .iter()
873                    .copied()
874                    .filter(|(_, address)| {
875                        tempo_hardfork.is_none_or(|hardfork| {
876                            is_tempo_precompile_active_at(*address, hardfork)
877                        })
878                    })
879                    .map(|(label, address)| (label.to_string(), address)),
880            );
881        }
882        #[cfg(feature = "monad")]
883        if self.is_monad() {
884            precompiles.extend(
885                MONAD_PRECOMPILES
886                    .iter()
887                    .copied()
888                    .filter(|(_, address)| {
889                        monad_hardfork.is_none_or(|hardfork| {
890                            is_monad_precompile_active_at(*address, hardfork)
891                        })
892                    })
893                    .map(|(label, address)| (label.to_string(), address)),
894            );
895        }
896        precompiles
897    }
898}
899
900impl From<NetworkVariant> for NetworkConfigs {
901    fn from(network: NetworkVariant) -> Self {
902        match network {
903            NetworkVariant::Ethereum => Self::with_ethereum(),
904            NetworkVariant::Tempo => {
905                Self { network: Some(network), tempo: true, ..Default::default() }
906            }
907            #[cfg(feature = "monad")]
908            NetworkVariant::Monad => {
909                Self { network: Some(network), monad: true, ..Default::default() }
910            }
911            #[cfg(feature = "optimism")]
912            NetworkVariant::Optimism => {
913                Self { network: Some(network), optimism: true, ..Default::default() }
914            }
915        }
916    }
917}
918
919#[cfg(test)]
920mod tests {
921    use super::*;
922    use revm::precompile::{
923        Precompiles,
924        secp256r1::{P256VERIFY_BASE_GAS_FEE, P256VERIFY_BASE_GAS_FEE_OSAKA},
925    };
926
927    // --- Equivalence: new flag == legacy flag ---
928
929    #[test]
930    fn network_variant_predicates() {
931        assert!(NetworkVariant::Ethereum.is_ethereum());
932        assert!(!NetworkVariant::Ethereum.is_tempo());
933        assert!(NetworkVariant::Tempo.is_tempo());
934        assert!(!NetworkVariant::Tempo.is_ethereum());
935
936        #[cfg(feature = "monad")]
937        {
938            assert!(!NetworkVariant::Ethereum.is_monad());
939            assert!(!NetworkVariant::Tempo.is_monad());
940            assert!(NetworkVariant::Monad.is_monad());
941            assert!(!NetworkVariant::Monad.is_ethereum());
942            assert!(!NetworkVariant::Monad.is_tempo());
943        }
944
945        #[cfg(feature = "optimism")]
946        {
947            assert!(NetworkVariant::Optimism.is_optimism());
948            assert!(!NetworkVariant::Optimism.is_ethereum());
949            assert!(!NetworkVariant::Optimism.is_tempo());
950
951            #[cfg(feature = "monad")]
952            assert!(!NetworkVariant::Optimism.is_monad());
953        }
954
955        #[cfg(all(feature = "optimism", feature = "monad"))]
956        assert!(!NetworkVariant::Monad.is_optimism());
957    }
958
959    #[test]
960    #[cfg(feature = "monad")]
961    fn fork_sources_only_isolate_monad() {
962        let mut non_monad = vec![
963            NetworkConfigs::default(),
964            NetworkConfigs::with_ethereum(),
965            NetworkConfigs::with_celo(),
966            NetworkConfigs::with_tempo(),
967        ];
968        #[cfg(feature = "optimism")]
969        non_monad.push(NetworkConfigs::with_optimism());
970
971        for execution in &non_monad {
972            for source in &non_monad {
973                assert!(execution.supports_fork_source(source));
974            }
975            assert!(!execution.supports_fork_source(&NetworkConfigs::with_monad()));
976            assert!(!NetworkConfigs::with_monad().supports_fork_source(execution));
977        }
978        assert!(NetworkConfigs::with_monad().supports_fork_source(&NetworkConfigs::with_monad()));
979    }
980
981    #[test]
982    fn known_chain_identity_does_not_guess_unknown_networks() {
983        assert_eq!(NetworkVariant::from_known_chain_id(98_765_432).unwrap(), None);
984        assert_eq!(
985            NetworkVariant::from_known_chain_id(NamedChain::Mainnet as u64).unwrap(),
986            Some(NetworkVariant::Ethereum)
987        );
988        assert_eq!(NetworkVariant::from(98_765_432), NetworkVariant::Ethereum);
989    }
990
991    #[test]
992    fn fallible_chain_id_inference_preserves_unknown_chains() {
993        let networks = NetworkConfigs { bypass_prevrandao: true, ..Default::default() };
994        assert_eq!(networks.try_with_chain_id(98_765_432).unwrap(), networks);
995    }
996
997    #[test]
998    #[cfg(not(feature = "monad"))]
999    fn fallible_chain_id_inference_rejects_disabled_monad() {
1000        for chain_id in [NamedChain::Monad as u64, NamedChain::MonadTestnet as u64] {
1001            let unavailable = "network family `monad` is not enabled in this build";
1002            let expected =
1003                format!("cannot infer execution network from chain ID {chain_id}: {unavailable}");
1004            assert_eq!(
1005                NetworkConfigs::default().try_with_chain_id(chain_id).unwrap_err(),
1006                expected
1007            );
1008        }
1009    }
1010
1011    #[test]
1012    #[cfg(not(feature = "optimism"))]
1013    fn fallible_chain_id_inference_rejects_disabled_optimism() {
1014        let chain_id = NamedChain::Optimism as u64;
1015        assert_eq!(
1016            NetworkConfigs::default().try_with_chain_id(chain_id).unwrap_err(),
1017            format!(
1018                "cannot infer execution network from chain ID {chain_id}: network family \
1019                 `optimism` is not enabled in this build"
1020            )
1021        );
1022    }
1023
1024    #[test]
1025    fn explicit_ethereum_overrides_known_chain_inference() {
1026        let ethereum = NetworkConfigs::with_ethereum();
1027        for chain_id in [NamedChain::Monad as u64, NamedChain::Optimism as u64] {
1028            assert_eq!(ethereum.try_with_chain_id(chain_id).unwrap(), ethereum);
1029        }
1030    }
1031
1032    #[test]
1033    fn fallible_chain_id_inference_preserves_orthogonal_configuration() {
1034        let networks = NetworkConfigs { bypass_prevrandao: true, ..Default::default() };
1035        let inferred = networks.try_with_chain_id(NamedChain::Tempo as u64).unwrap();
1036
1037        assert!(inferred.is_tempo());
1038        assert!(inferred.bypass_prevrandao(NamedChain::Mainnet as u64));
1039    }
1040
1041    #[test]
1042    #[cfg(feature = "optimism")]
1043    fn fallible_chain_id_inference_detects_optimism() {
1044        let chain_id = NamedChain::Optimism as u64;
1045        assert_eq!(NetworkVariant::from(chain_id), NetworkVariant::Optimism);
1046        assert!(NetworkConfigs::default().try_with_chain_id(chain_id).unwrap().is_optimism());
1047    }
1048
1049    #[test]
1050    fn rpc_identity_preserves_orthogonal_configuration() {
1051        let base = NetworkConfigs { bypass_prevrandao: true, ..NetworkConfigs::default() };
1052        let ethereum = base.with_rpc_network(NetworkVariant::Ethereum);
1053        assert!(ethereum.bypass_prevrandao(NamedChain::Mainnet as u64));
1054
1055        let ethereum = NetworkConfigs::default().with_rpc_network(NetworkVariant::Ethereum);
1056        assert_eq!(ethereum, NetworkConfigs::default());
1057
1058        assert!(
1059            NetworkConfigs::default()
1060                .with_rpc_identity(NetworkVariant::Ethereum, NamedChain::Celo as u64)
1061                .is_celo()
1062        );
1063        assert!(
1064            !NetworkConfigs::default()
1065                .with_rpc_identity(NetworkVariant::Ethereum, NamedChain::Monad as u64)
1066                .is_monad()
1067        );
1068    }
1069
1070    #[test]
1071    fn rpc_metadata_overrides_known_execution_chain_identity() {
1072        assert_eq!(
1073            NetworkVariant::from_rpc_identity(NamedChain::Mainnet as u64, Some(Some("tempo")))
1074                .unwrap(),
1075            Some(NetworkVariant::Tempo)
1076        );
1077        assert_eq!(
1078            NetworkVariant::from_rpc_identity(NamedChain::Tempo as u64, Some(None)).unwrap(),
1079            Some(NetworkVariant::Tempo)
1080        );
1081        assert_eq!(NetworkVariant::from_rpc_identity(98_765_432, None).unwrap(), None);
1082        assert_eq!(
1083            NetworkVariant::from_rpc_identity(98_765_432, Some(None)).unwrap(),
1084            Some(NetworkVariant::Ethereum)
1085        );
1086        #[cfg(feature = "optimism")]
1087        assert_eq!(
1088            NetworkVariant::from_rpc_identity(NamedChain::Optimism as u64, Some(None)).unwrap(),
1089            Some(NetworkVariant::Optimism)
1090        );
1091        assert_eq!(
1092            NetworkVariant::from_rpc_identity(NamedChain::Celo as u64, Some(None)).unwrap(),
1093            Some(NetworkVariant::Ethereum)
1094        );
1095    }
1096
1097    #[test]
1098    fn rpc_profile_distinguishes_celo_from_ethereum_factory() {
1099        let custom_chain_id = 98_765_432;
1100        assert!(
1101            NetworkConfigs::from_rpc_identity_profile_with_fallback(
1102                custom_chain_id,
1103                Some(Some("celo")),
1104                None,
1105            )
1106            .unwrap()
1107            .unwrap()
1108            .is_celo()
1109        );
1110        assert!(
1111            !NetworkConfigs::from_rpc_identity_profile_with_fallback(
1112                NamedChain::Celo as u64,
1113                Some(Some("ethereum")),
1114                None,
1115            )
1116            .unwrap()
1117            .unwrap()
1118            .is_celo()
1119        );
1120        assert!(
1121            NetworkConfigs::from_rpc_identity_profile_with_fallback(
1122                NamedChain::Celo as u64,
1123                Some(None),
1124                None,
1125            )
1126            .unwrap()
1127            .unwrap()
1128            .is_celo()
1129        );
1130        assert!(
1131            NetworkConfigs::from_rpc_identity_profile_with_fallback(
1132                custom_chain_id,
1133                None,
1134                Some(NetworkConfigs::with_celo()),
1135            )
1136            .unwrap()
1137            .unwrap()
1138            .is_celo()
1139        );
1140    }
1141
1142    #[test]
1143    fn rpc_profile_keeps_moonbeam_on_default_ethereum() {
1144        let profile = NetworkConfigs::from_rpc_identity_profile_with_fallback(
1145            NamedChain::Moonbeam as u64,
1146            None,
1147            None,
1148        )
1149        .unwrap()
1150        .unwrap();
1151
1152        assert_eq!(profile, NetworkConfigs::default());
1153        assert!(profile.bypass_prevrandao(NamedChain::Moonbeam as u64));
1154    }
1155
1156    #[test]
1157    fn default_fallback_still_infers_known_execution_profile() {
1158        for node_info in [None, Some(None)] {
1159            assert_eq!(
1160                NetworkConfigs::from_rpc_identity_profile_with_fallback(
1161                    NamedChain::Tempo as u64,
1162                    node_info,
1163                    Some(NetworkConfigs::default()),
1164                )
1165                .unwrap(),
1166                Some(NetworkConfigs::with_tempo())
1167            );
1168        }
1169    }
1170
1171    #[test]
1172    fn explicit_ethereum_overrides_known_execution_profile() {
1173        for node_info in [None, Some(None)] {
1174            assert_eq!(
1175                NetworkConfigs::from_rpc_identity_profile_with_fallback(
1176                    NamedChain::Tempo as u64,
1177                    node_info,
1178                    Some(NetworkConfigs::with_ethereum()),
1179                )
1180                .unwrap(),
1181                Some(NetworkConfigs::default())
1182            );
1183        }
1184    }
1185
1186    #[test]
1187    #[cfg(not(feature = "monad"))]
1188    fn explicit_ethereum_overrides_disabled_monad_without_node_info() {
1189        assert_eq!(
1190            NetworkConfigs::from_rpc_identity_profile_with_fallback(
1191                NamedChain::Monad as u64,
1192                None,
1193                Some(NetworkConfigs::with_ethereum()),
1194            )
1195            .unwrap(),
1196            Some(NetworkConfigs::default())
1197        );
1198    }
1199
1200    #[test]
1201    #[cfg(not(feature = "monad"))]
1202    fn explicit_ethereum_overrides_disabled_monad_with_legacy_node_info() {
1203        assert_eq!(
1204            NetworkConfigs::from_rpc_identity_profile_with_fallback(
1205                NamedChain::Monad as u64,
1206                Some(None),
1207                Some(NetworkConfigs::with_ethereum()),
1208            )
1209            .unwrap(),
1210            Some(NetworkConfigs::default())
1211        );
1212    }
1213
1214    #[test]
1215    #[cfg(not(feature = "monad"))]
1216    fn disabled_monad_without_explicit_profile_still_errors() {
1217        for node_info in [None, Some(None)] {
1218            assert_eq!(
1219                NetworkConfigs::from_rpc_identity_profile_with_fallback(
1220                    NamedChain::Monad as u64,
1221                    node_info,
1222                    None,
1223                )
1224                .unwrap_err(),
1225                "network family `monad` is not enabled in this build"
1226            );
1227        }
1228    }
1229
1230    #[test]
1231    #[cfg(feature = "monad")]
1232    fn rpc_identity_fallback_preserves_explicit_custom_networks() {
1233        let custom_chain_id = 98_765_432;
1234        for node_info in [None, Some(None)] {
1235            assert_eq!(
1236                NetworkVariant::from_rpc_identity_with_fallback(
1237                    custom_chain_id,
1238                    node_info,
1239                    Some(NetworkVariant::Monad),
1240                )
1241                .unwrap(),
1242                Some(NetworkVariant::Monad)
1243            );
1244        }
1245
1246        assert_eq!(
1247            NetworkVariant::from_rpc_identity_with_fallback(
1248                NamedChain::Mainnet as u64,
1249                Some(None),
1250                Some(NetworkVariant::Monad),
1251            )
1252            .unwrap(),
1253            Some(NetworkVariant::Ethereum)
1254        );
1255        assert_eq!(
1256            NetworkVariant::from_rpc_identity_with_fallback(
1257                custom_chain_id,
1258                Some(Some("tempo")),
1259                Some(NetworkVariant::Monad),
1260            )
1261            .unwrap(),
1262            Some(NetworkVariant::Tempo)
1263        );
1264    }
1265
1266    #[test]
1267    fn network_selection_distinguishes_default_and_explicit_ethereum() {
1268        assert!(!NetworkConfigs::default().has_network_selection());
1269        assert!(NetworkConfigs::with_ethereum().has_network_selection());
1270        assert!(NetworkConfigs::with_celo().has_network_selection());
1271    }
1272
1273    #[test]
1274    fn celo_uses_ethereum_factory_with_distinct_precompiles() {
1275        assert!(
1276            !NetworkConfigs::with_celo().has_same_execution_profile(&NetworkConfigs::default())
1277        );
1278        assert!(
1279            NetworkConfigs::with_ethereum().has_same_execution_profile(&NetworkConfigs::default())
1280        );
1281        assert_eq!(NetworkConfigs::with_celo().execution_family_name(), "ethereum");
1282        assert_eq!(NetworkConfigs::with_celo().execution_profile_name(), "celo");
1283    }
1284
1285    #[test]
1286    fn authoritative_execution_profile_preserves_orthogonal_settings() {
1287        let inline = NetworkConfigs { bypass_prevrandao: true, ..NetworkConfigs::with_tempo() };
1288
1289        #[cfg_attr(not(any(feature = "optimism", feature = "monad")), allow(unused_mut))]
1290        let mut profiles = vec![
1291            NetworkConfigs::with_ethereum(),
1292            NetworkConfigs::with_tempo(),
1293            NetworkConfigs::with_celo(),
1294        ];
1295        #[cfg(feature = "optimism")]
1296        profiles.push(NetworkVariant::Optimism.into());
1297        #[cfg(feature = "monad")]
1298        profiles.push(NetworkConfigs::with_monad());
1299
1300        for profile in profiles {
1301            let resolved = inline.with_execution_profile(profile);
1302            assert!(resolved.has_same_execution_profile(&profile));
1303            assert!(resolved.has_network_selection());
1304            assert!(resolved.bypass_prevrandao(NamedChain::Mainnet as u64));
1305        }
1306
1307        let ethereum = inline.with_execution_profile(NetworkConfigs::with_ethereum());
1308        assert_eq!(
1309            ethereum.try_with_chain_id(NamedChain::Tempo as u64).unwrap(),
1310            ethereum,
1311            "an authoritative Ethereum profile must prevent later endpoint inference",
1312        );
1313
1314        let rpc_ethereum = inline.with_rpc_profile(NetworkConfigs::with_ethereum());
1315        assert!(!rpc_ethereum.has_network_selection());
1316    }
1317
1318    #[test]
1319    fn chain_id_inference_preserves_explicit_networks() {
1320        assert!(
1321            NetworkConfigs::default().try_with_chain_id(NamedChain::Celo as u64).unwrap().is_celo()
1322        );
1323        let celo = NetworkConfigs { bypass_prevrandao: true, ..Default::default() }
1324            .try_with_chain_id(NamedChain::Celo as u64)
1325            .unwrap();
1326        assert!(celo.is_celo());
1327        assert!(celo.bypass_prevrandao(NamedChain::Mainnet as u64));
1328
1329        let explicit = [
1330            NetworkConfigs::with_ethereum(),
1331            NetworkConfigs::with_tempo(),
1332            NetworkConfigs::with_celo(),
1333        ];
1334        for networks in explicit {
1335            assert_eq!(networks.try_with_chain_id(NamedChain::Celo as u64).unwrap(), networks);
1336        }
1337
1338        #[cfg(feature = "monad")]
1339        {
1340            let monad = NetworkConfigs::with_monad();
1341            assert_eq!(monad.try_with_chain_id(NamedChain::Celo as u64).unwrap(), monad);
1342        }
1343    }
1344
1345    #[test]
1346    #[cfg(feature = "monad")]
1347    fn rpc_metadata_identifies_custom_monad_networks() {
1348        assert_eq!(NetworkVariant::from_node_info_name("monad").unwrap(), NetworkVariant::Monad);
1349        assert!(NetworkConfigs::default().with_rpc_network(NetworkVariant::Monad).is_monad());
1350    }
1351
1352    #[test]
1353    #[cfg(feature = "monad")]
1354    fn parses_endpoint_hardfork_in_network_namespace() {
1355        assert_eq!(
1356            NetworkVariant::Monad.parse_hardfork("MonadEight").unwrap(),
1357            FoundryHardfork::Monad(MonadHardfork::MonadEight)
1358        );
1359    }
1360
1361    #[test]
1362    fn rpc_metadata_rejects_unknown_network_family() {
1363        assert_eq!(
1364            NetworkVariant::from_node_info_name("unknown").unwrap_err(),
1365            "unsupported network family `unknown` reported by fork endpoint"
1366        );
1367    }
1368
1369    #[test]
1370    fn explicit_ethereum_families_reject_namespaced_hardforks() {
1371        #[cfg_attr(not(feature = "monad"), allow(unused_mut))]
1372        let mut incompatible = vec![FoundryHardfork::Tempo(TempoHardfork::T0)];
1373        #[cfg(feature = "monad")]
1374        incompatible.push(FoundryHardfork::Monad(MonadHardfork::MonadEight));
1375
1376        for networks in [NetworkConfigs::with_ethereum(), NetworkConfigs::with_celo()] {
1377            for hardfork in &incompatible {
1378                assert_eq!(
1379                    networks.normalize_for_hardfork(*hardfork).unwrap_err(),
1380                    format!(
1381                        "hardfork `{}` conflicts with network config `{}`",
1382                        String::from(*hardfork),
1383                        networks.execution_profile_name()
1384                    )
1385                );
1386            }
1387        }
1388
1389        let celo = NetworkConfigs::with_celo();
1390        assert_eq!(
1391            celo.normalize_for_hardfork(FoundryHardfork::Ethereum(
1392                foundry_evm_hardforks::EthereumHardfork::Prague
1393            ))
1394            .unwrap(),
1395            celo
1396        );
1397    }
1398
1399    #[test]
1400    fn new_tempo_flag_equivalent_to_legacy() {
1401        let via_new = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
1402        let via_old = NetworkConfigs { tempo: true, ..Default::default() };
1403        assert_eq!(via_new.is_tempo(), via_old.is_tempo());
1404        assert_eq!(via_new.active_network_name(), via_old.active_network_name());
1405        assert_eq!(via_new.precompiles(None, None), via_old.precompiles(None, None));
1406        assert_eq!(via_new.precompiles_label(None, None), via_old.precompiles_label(None, None));
1407    }
1408
1409    fn bsc_p256_gas_used(chain_id: ChainId, timestamp: u64) -> Option<u64> {
1410        bsc_p256_precompile(chain_id, timestamp)
1411            .flatten()
1412            .map(|precompile| precompile.execute(&[], u64::MAX, 0).unwrap().gas_used)
1413    }
1414
1415    fn assert_bsc_p256_boundaries(chain_id: ChainId, haber_timestamp: u64, osaka_timestamp: u64) {
1416        assert!(matches!(bsc_p256_precompile(chain_id, haber_timestamp - 1), Some(None)));
1417        assert_eq!(bsc_p256_gas_used(chain_id, haber_timestamp), Some(P256VERIFY_BASE_GAS_FEE));
1418        assert_eq!(bsc_p256_gas_used(chain_id, osaka_timestamp - 1), Some(P256VERIFY_BASE_GAS_FEE));
1419        assert_eq!(
1420            bsc_p256_gas_used(chain_id, osaka_timestamp),
1421            Some(P256VERIFY_BASE_GAS_FEE_OSAKA)
1422        );
1423    }
1424
1425    #[test]
1426    fn selects_bsc_p256_at_mainnet_boundaries() {
1427        assert_bsc_p256_boundaries(
1428            BSC_MAINNET_CHAIN_ID,
1429            BSC_MAINNET_HABER_TIMESTAMP,
1430            BSC_MAINNET_OSAKA_TIMESTAMP,
1431        );
1432    }
1433
1434    #[test]
1435    fn selects_bsc_p256_at_testnet_boundaries() {
1436        assert_bsc_p256_boundaries(
1437            BSC_TESTNET_CHAIN_ID,
1438            BSC_TESTNET_HABER_TIMESTAMP,
1439            BSC_TESTNET_OSAKA_TIMESTAMP,
1440        );
1441    }
1442
1443    #[test]
1444    fn removes_bsc_p256_before_haber() {
1445        let mut precompiles = PrecompilesMap::from_static(Precompiles::osaka());
1446        assert!(precompiles.get(&BSC_P256_ADDRESS).is_some());
1447        NetworkConfigs::default().inject_chain_precompiles(
1448            &mut precompiles,
1449            BSC_MAINNET_CHAIN_ID,
1450            BSC_MAINNET_HABER_TIMESTAMP - 1,
1451        );
1452        assert!(precompiles.get(&BSC_P256_ADDRESS).is_none());
1453    }
1454
1455    #[test]
1456    fn canonical_tempo_network_reports_precompiles() {
1457        let cfg = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
1458
1459        assert_eq!(
1460            cfg.precompiles(None, None).get("TIP20ChannelReserve"),
1461            Some(&TIP20_CHANNEL_RESERVE_ADDRESS)
1462        );
1463        assert!(
1464            !cfg.precompiles(Some(TempoHardfork::T4), None).contains_key("TIP20ChannelReserve")
1465        );
1466        assert!(!cfg.precompiles(Some(TempoHardfork::T4), None).contains_key("ReceivePolicyGuard"));
1467        assert!(!cfg.precompiles(Some(TempoHardfork::T2), None).contains_key("AddressRegistry"));
1468        assert!(!cfg.precompiles(Some(TempoHardfork::T2), None).contains_key("SignatureVerifier"));
1469        assert_eq!(
1470            cfg.precompiles(Some(TempoHardfork::T3), None).get("AddressRegistry"),
1471            Some(&ADDRESS_REGISTRY_ADDRESS)
1472        );
1473        assert_eq!(
1474            cfg.precompiles(Some(TempoHardfork::T3), None).get("SignatureVerifier"),
1475            Some(&SIGNATURE_VERIFIER_ADDRESS)
1476        );
1477        assert_eq!(
1478            cfg.precompiles_label(Some(TempoHardfork::T5), None)
1479                .get(&TIP20_CHANNEL_RESERVE_ADDRESS),
1480            Some(&"TIP20ChannelReserve".to_string())
1481        );
1482        assert!(cfg.precompiles_label(None, None).contains_key(&TIP20_CHANNEL_RESERVE_ADDRESS));
1483        assert!(
1484            !cfg.precompiles_label(Some(TempoHardfork::T5), None)
1485                .contains_key(&RECEIVE_POLICY_GUARD_ADDRESS)
1486        );
1487        assert!(
1488            cfg.precompiles_label(Some(TempoHardfork::T6), None)
1489                .contains_key(&RECEIVE_POLICY_GUARD_ADDRESS)
1490        );
1491    }
1492
1493    #[test]
1494    #[cfg(feature = "monad")]
1495    fn canonical_monad_network_reports_hardfork_gated_precompiles() {
1496        let cfg = NetworkConfigs { network: Some(NetworkVariant::Monad), ..Default::default() };
1497
1498        assert_eq!(
1499            cfg.precompiles(None, Some(MonadHardfork::MonadEight)).get("MonadStaking"),
1500            Some(&monad_revm::staking::STAKING_ADDRESS)
1501        );
1502        assert!(
1503            !cfg.precompiles(None, Some(MonadHardfork::MonadEight))
1504                .contains_key("MonadReserveBalance")
1505        );
1506        assert_eq!(
1507            cfg.precompiles(None, Some(MonadHardfork::MonadNine)).get("MonadReserveBalance"),
1508            Some(&monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS)
1509        );
1510        assert_eq!(
1511            cfg.precompiles_label(None, Some(MonadHardfork::MonadNine))
1512                .get(&monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS),
1513            Some(&"ReserveBalance".to_string())
1514        );
1515        assert!(
1516            cfg.precompiles_label(None, None)
1517                .contains_key(&monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS)
1518        );
1519        assert!(
1520            !cfg.precompiles_label(None, Some(MonadHardfork::MonadEight))
1521                .contains_key(&monad_revm::reserve_balance::abi::RESERVE_BALANCE_ADDRESS)
1522        );
1523    }
1524
1525    #[test]
1526    fn storage_credits_precompile_activates_at_t7() {
1527        assert!(!is_tempo_precompile_active_at(STORAGE_CREDITS_ADDRESS, TempoHardfork::T6));
1528        assert!(is_tempo_precompile_active_at(STORAGE_CREDITS_ADDRESS, TempoHardfork::T7));
1529        assert!(TEMPO_PRECOMPILE_ADDRESSES.contains(&STORAGE_CREDITS_ADDRESS));
1530
1531        // The hardfork-filtered precompile map must honor the same T7 activation.
1532        let cfg = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
1533        assert!(!cfg.precompiles(Some(TempoHardfork::T6), None).contains_key("StorageCredits"));
1534        assert!(cfg.precompiles(Some(TempoHardfork::T7), None).contains_key("StorageCredits"));
1535    }
1536
1537    #[test]
1538    fn current_committee_precompile_activates_at_t8() {
1539        assert!(!is_tempo_precompile_active_at(CURRENT_COMMITTEE_ADDRESS, TempoHardfork::T7));
1540        assert!(is_tempo_precompile_active_at(CURRENT_COMMITTEE_ADDRESS, TempoHardfork::T8));
1541        assert!(TEMPO_PRECOMPILE_ADDRESSES.contains(&CURRENT_COMMITTEE_ADDRESS));
1542
1543        let cfg = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
1544        assert!(!cfg.precompiles(Some(TempoHardfork::T7), None).contains_key("CurrentCommittee"));
1545        assert!(cfg.precompiles(Some(TempoHardfork::T8), None).contains_key("CurrentCommittee"));
1546    }
1547
1548    // --- resolved() / active_network_name ---
1549
1550    #[test]
1551    fn active_network_name_tempo() {
1552        let cfg = NetworkConfigs::with_tempo();
1553        assert_eq!(cfg.active_network_name(), Some("tempo"));
1554    }
1555
1556    #[test]
1557    #[cfg(feature = "monad")]
1558    fn active_network_name_monad() {
1559        let cfg = NetworkConfigs::with_monad();
1560        assert_eq!(cfg.active_network_name(), Some("monad"));
1561        assert!(cfg.is_monad());
1562        assert_eq!(cfg.extra_cheatcode_addresses(), &[MONAD_CHEATCODE_ADDRESS]);
1563    }
1564
1565    #[test]
1566    #[cfg(feature = "monad")]
1567    fn contract_size_limits_monad() {
1568        let limits = NetworkConfigs::with_monad().contract_size_limits().unwrap();
1569        assert_eq!(limits.runtime, monad_revm::MONAD_MAX_CODE_SIZE);
1570        assert_eq!(limits.initcode, monad_revm::MONAD_MAX_INITCODE_SIZE);
1571        assert!(NetworkConfigs::default().contract_size_limits().is_none());
1572    }
1573
1574    #[test]
1575    fn active_network_name_default_is_none() {
1576        let cfg = NetworkConfigs::default();
1577        assert_eq!(cfg.active_network_name(), None);
1578        assert!(cfg.extra_cheatcode_addresses().is_empty());
1579    }
1580
1581    // --- Serde round-trip ---
1582
1583    #[test]
1584    fn serde_roundtrip_tempo() {
1585        let original = NetworkConfigs::with_tempo();
1586        let json = serde_json::to_string(&original).unwrap();
1587        let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
1588        assert!(restored.is_tempo());
1589    }
1590
1591    #[test]
1592    fn serde_legacy_tempo_bool_deserialized() {
1593        // Old foundry.toml format: `tempo = true`
1594        let json = r#"{"tempo": true, "celo": false, "bypass_prevrandao": false}"#;
1595        let cfg: NetworkConfigs = serde_json::from_str(json).unwrap();
1596        assert!(cfg.is_tempo());
1597    }
1598
1599    #[test]
1600    #[cfg(feature = "monad")]
1601    fn serde_roundtrip_monad() {
1602        let original = NetworkConfigs::with_monad();
1603        let json = serde_json::to_string(&original).unwrap();
1604        let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
1605        assert!(restored.is_monad());
1606        assert!(!restored.is_tempo());
1607    }
1608
1609    #[test]
1610    #[cfg(feature = "monad")]
1611    fn serde_legacy_monad_bool_deserialized() {
1612        let json = r#"{"monad": true, "celo": false, "bypass_prevrandao": false}"#;
1613        let cfg: NetworkConfigs = serde_json::from_str(json).unwrap();
1614        assert!(cfg.is_monad());
1615    }
1616
1617    #[test]
1618    fn serde_serializes_legacy_alias_as_canonical_network() {
1619        #[cfg_attr(not(feature = "monad"), allow(unused_mut))]
1620        let mut cases = vec![(NetworkConfigs { tempo: true, ..Default::default() }, "tempo")];
1621        #[cfg(feature = "monad")]
1622        cases.push((NetworkConfigs { monad: true, ..Default::default() }, "monad"));
1623
1624        for (cfg, expected) in cases {
1625            let json = serde_json::to_value(cfg).unwrap();
1626            assert_eq!(json["network"], serde_json::json!(expected));
1627            assert!(json.get("tempo").is_none(), "legacy `tempo` key should not be serialized");
1628            assert!(json.get("monad").is_none(), "legacy `monad` key should not be serialized");
1629        }
1630    }
1631
1632    #[test]
1633    fn serde_new_network_field_deserialized() {
1634        let json_tempo = r#"{"network": "tempo", "celo": false, "bypass_prevrandao": false}"#;
1635        let cfg_tempo: NetworkConfigs = serde_json::from_str(json_tempo).unwrap();
1636        assert!(cfg_tempo.is_tempo());
1637
1638        #[cfg(feature = "monad")]
1639        {
1640            let json_monad = r#"{"network": "monad", "celo": false, "bypass_prevrandao": false}"#;
1641            let cfg_monad: NetworkConfigs = serde_json::from_str(json_monad).unwrap();
1642            assert!(cfg_monad.is_monad());
1643        }
1644    }
1645
1646    #[test]
1647    fn validates_flattened_network_selectors() {
1648        let valid = [
1649            NetworkConfigs::default(),
1650            NetworkConfigs::with_ethereum(),
1651            NetworkConfigs::with_celo(),
1652            NetworkConfigs::with_tempo(),
1653            NetworkConfigs {
1654                network: Some(NetworkVariant::Tempo),
1655                tempo: true,
1656                ..Default::default()
1657            },
1658        ];
1659        for networks in valid {
1660            networks.validate().unwrap();
1661        }
1662
1663        let conflicts = [
1664            (
1665                NetworkConfigs {
1666                    network: Some(NetworkVariant::Tempo),
1667                    celo: true,
1668                    ..Default::default()
1669                },
1670                "network selectors `network = \"tempo\"` and `celo = true` conflict",
1671            ),
1672            (
1673                NetworkConfigs { celo: true, tempo: true, ..Default::default() },
1674                "network selectors `celo = true` and `tempo = true` conflict",
1675            ),
1676        ];
1677        for (networks, expected) in conflicts {
1678            assert!(networks.validate().unwrap_err().contains(expected));
1679        }
1680    }
1681
1682    #[test]
1683    #[cfg(feature = "monad")]
1684    fn validates_flattened_monad_network_selectors() {
1685        for networks in [
1686            NetworkConfigs::with_monad(),
1687            NetworkConfigs {
1688                network: Some(NetworkVariant::Monad),
1689                monad: true,
1690                ..Default::default()
1691            },
1692        ] {
1693            networks.validate().unwrap();
1694        }
1695
1696        let conflicts = [
1697            (
1698                NetworkConfigs {
1699                    network: Some(NetworkVariant::Monad),
1700                    celo: true,
1701                    ..Default::default()
1702                },
1703                "network selectors `network = \"monad\"` and `celo = true` conflict",
1704            ),
1705            (
1706                NetworkConfigs {
1707                    network: Some(NetworkVariant::Monad),
1708                    tempo: true,
1709                    ..Default::default()
1710                },
1711                "network selectors `network = \"monad\"` and `tempo = true` conflict",
1712            ),
1713            (
1714                NetworkConfigs { celo: true, monad: true, ..Default::default() },
1715                "network selectors `celo = true` and `monad = true` conflict",
1716            ),
1717            (
1718                NetworkConfigs { tempo: true, monad: true, ..Default::default() },
1719                "network selectors `tempo = true` and `monad = true` conflict",
1720            ),
1721        ];
1722        for (networks, expected) in conflicts {
1723            assert!(networks.validate().unwrap_err().contains(expected));
1724        }
1725    }
1726
1727    #[test]
1728    #[cfg(feature = "monad")]
1729    fn chain_id_detects_monad_network() {
1730        assert_eq!(NetworkVariant::from(143), NetworkVariant::Monad);
1731        assert_eq!(NetworkVariant::from(10143), NetworkVariant::Monad);
1732
1733        assert!(NetworkConfigs::default().try_with_chain_id(143).unwrap().is_monad());
1734    }
1735
1736    #[cfg(feature = "optimism")]
1737    mod optimism {
1738        use super::*;
1739
1740        #[test]
1741        fn new_optimism_flag_equivalent_to_legacy() {
1742            let via_new =
1743                NetworkConfigs { network: Some(NetworkVariant::Optimism), ..Default::default() };
1744            let via_old = NetworkConfigs { optimism: true, ..Default::default() };
1745            assert_eq!(via_new.is_optimism(), via_old.is_optimism());
1746            assert_eq!(via_new.is_tempo(), via_old.is_tempo());
1747            assert_eq!(via_new.active_network_name(), via_old.active_network_name());
1748        }
1749
1750        #[test]
1751        fn matching_optimism_selectors_are_valid() {
1752            NetworkConfigs::with_optimism().validate().unwrap();
1753        }
1754
1755        #[test]
1756        fn active_network_name_optimism() {
1757            let cfg = NetworkConfigs::with_optimism();
1758            assert_eq!(cfg.active_network_name(), Some("optimism"));
1759        }
1760
1761        #[test]
1762        fn conflicting_optimism_and_tempo_selectors_are_rejected() {
1763            let cfg = NetworkConfigs {
1764                network: Some(NetworkVariant::Optimism),
1765                tempo: true,
1766                ..Default::default()
1767            };
1768            assert_eq!(
1769                cfg.validate().unwrap_err(),
1770                "network selectors `network = \"optimism\"` and `tempo = true` conflict; select \
1771                 only one network"
1772            );
1773        }
1774
1775        #[test]
1776        fn serde_roundtrip_optimism() {
1777            let original = NetworkConfigs::with_optimism();
1778            let json = serde_json::to_string(&original).unwrap();
1779            let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
1780            assert!(restored.is_optimism());
1781            assert!(!restored.is_tempo());
1782        }
1783
1784        #[test]
1785        fn serde_optimism_field_deserialized() {
1786            let json_optimism =
1787                r#"{"network": "optimism", "celo": false, "bypass_prevrandao": false}"#;
1788            let cfg_optimism: NetworkConfigs = serde_json::from_str(json_optimism).unwrap();
1789            assert!(cfg_optimism.is_optimism());
1790        }
1791    }
1792}