1use 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;
16use foundry_evm_hardforks::{FoundryHardfork, TempoHardfork};
17use revm::precompile::{
18 Precompile as RevmPrecompile,
19 secp256r1::{P256VERIFY, P256VERIFY_OSAKA},
20};
21use serde::{Deserialize, Serialize};
22use std::collections::BTreeMap;
23use tempo_contracts::precompiles::{
24 ACCOUNT_KEYCHAIN_ADDRESS, ADDRESS_REGISTRY_ADDRESS, CURRENT_COMMITTEE_ADDRESS,
25 NONCE_PRECOMPILE_ADDRESS, RECEIVE_POLICY_GUARD_ADDRESS, SIGNATURE_VERIFIER_ADDRESS,
26 STABLECOIN_DEX_ADDRESS, STORAGE_CREDITS_ADDRESS, TIP_FEE_MANAGER_ADDRESS,
27 TIP20_CHANNEL_RESERVE_ADDRESS, TIP20_FACTORY_ADDRESS, TIP403_REGISTRY_ADDRESS,
28 VALIDATOR_CONFIG_ADDRESS, VALIDATOR_CONFIG_V2_ADDRESS,
29};
30
31pub mod arbitrum;
32pub mod celo;
33
34#[cfg(feature = "optimism")]
35mod optimism;
36
37const TEMPO_PRECOMPILES: &[(&str, Address)] = &[
38 ("Nonce", NONCE_PRECOMPILE_ADDRESS),
39 ("StablecoinDex", STABLECOIN_DEX_ADDRESS),
40 ("TIP20Factory", TIP20_FACTORY_ADDRESS),
41 ("TIP403Registry", TIP403_REGISTRY_ADDRESS),
42 ("FeeManager", TIP_FEE_MANAGER_ADDRESS),
43 ("ValidatorConfig", VALIDATOR_CONFIG_ADDRESS),
44 ("ValidatorConfigV2", VALIDATOR_CONFIG_V2_ADDRESS),
45 ("AccountKeychain", ACCOUNT_KEYCHAIN_ADDRESS),
46 ("SignatureVerifier", SIGNATURE_VERIFIER_ADDRESS),
47 ("AddressRegistry", ADDRESS_REGISTRY_ADDRESS),
48 ("TIP20ChannelReserve", TIP20_CHANNEL_RESERVE_ADDRESS),
49 ("ReceivePolicyGuard", RECEIVE_POLICY_GUARD_ADDRESS),
50 ("StorageCredits", STORAGE_CREDITS_ADDRESS),
51 ("CurrentCommittee", CURRENT_COMMITTEE_ADDRESS),
52];
53
54const BSC_P256_ADDRESS: Address = address!("0000000000000000000000000000000000000100");
56
57const BSC_MAINNET_CHAIN_ID: u64 = 56;
58const BSC_TESTNET_CHAIN_ID: u64 = 97;
59const BSC_MAINNET_HABER_TIMESTAMP: u64 = 1_718_863_500;
60const BSC_TESTNET_HABER_TIMESTAMP: u64 = 1_716_962_820;
61const BSC_MAINNET_OSAKA_TIMESTAMP: u64 = 1_777_343_400;
62const BSC_TESTNET_OSAKA_TIMESTAMP: u64 = 1_774_319_400;
63
64const fn bsc_p256_precompile(chain_id: ChainId, timestamp: u64) -> Option<Option<RevmPrecompile>> {
67 let (haber_timestamp, osaka_timestamp) = match chain_id {
68 BSC_MAINNET_CHAIN_ID => (BSC_MAINNET_HABER_TIMESTAMP, BSC_MAINNET_OSAKA_TIMESTAMP),
69 BSC_TESTNET_CHAIN_ID => (BSC_TESTNET_HABER_TIMESTAMP, BSC_TESTNET_OSAKA_TIMESTAMP),
70 _ => return None,
71 };
72
73 if timestamp < haber_timestamp {
74 Some(None)
75 } else if timestamp < osaka_timestamp {
76 Some(Some(P256VERIFY))
77 } else {
78 Some(Some(P256VERIFY_OSAKA))
79 }
80}
81
82pub const TEMPO_PRECOMPILE_ADDRESSES: &[Address] = &[
84 NONCE_PRECOMPILE_ADDRESS,
85 STABLECOIN_DEX_ADDRESS,
86 TIP20_FACTORY_ADDRESS,
87 TIP403_REGISTRY_ADDRESS,
88 TIP_FEE_MANAGER_ADDRESS,
89 VALIDATOR_CONFIG_ADDRESS,
90 VALIDATOR_CONFIG_V2_ADDRESS,
91 ACCOUNT_KEYCHAIN_ADDRESS,
92 SIGNATURE_VERIFIER_ADDRESS,
93 ADDRESS_REGISTRY_ADDRESS,
94 TIP20_CHANNEL_RESERVE_ADDRESS,
95 RECEIVE_POLICY_GUARD_ADDRESS,
96 STORAGE_CREDITS_ADDRESS,
97 CURRENT_COMMITTEE_ADDRESS,
98];
99
100pub fn is_tempo_precompile_active_at(address: Address, hardfork: TempoHardfork) -> bool {
102 if address == CURRENT_COMMITTEE_ADDRESS {
103 hardfork.is_t8()
104 } else if address == TIP20_CHANNEL_RESERVE_ADDRESS {
105 hardfork.is_t5()
106 } else if address == RECEIVE_POLICY_GUARD_ADDRESS {
107 hardfork.is_t6()
108 } else if address == STORAGE_CREDITS_ADDRESS {
109 hardfork.is_t7()
110 } else if address == ADDRESS_REGISTRY_ADDRESS || address == SIGNATURE_VERIFIER_ADDRESS {
111 hardfork.is_t3()
112 } else {
113 true
114 }
115}
116
117pub fn active_tempo_precompile_addresses(hardfork: TempoHardfork) -> impl Iterator<Item = Address> {
119 TEMPO_PRECOMPILE_ADDRESSES
120 .iter()
121 .copied()
122 .filter(move |&address| is_tempo_precompile_active_at(address, hardfork))
123}
124
125#[derive(
126 Clone,
127 Copy,
128 Debug,
129 Default,
130 PartialEq,
131 Eq,
132 PartialOrd,
133 Ord,
134 Hash,
135 Serialize,
136 Deserialize,
137 clap::ValueEnum,
138)]
139#[serde(rename_all = "lowercase")]
140#[clap(rename_all = "lowercase")]
141pub enum NetworkVariant {
142 #[default]
143 Ethereum,
144 #[cfg(feature = "optimism")]
145 Optimism,
146 Tempo,
147}
148
149impl std::str::FromStr for NetworkVariant {
150 type Err = String;
151
152 fn from_str(s: &str) -> Result<Self, Self::Err> {
153 match s {
154 "ethereum" => Ok(Self::Ethereum),
155 #[cfg(feature = "optimism")]
156 "optimism" => Ok(Self::Optimism),
157 "tempo" => Ok(Self::Tempo),
158 _ => Err(format!("unknown network variant: {s}")),
159 }
160 }
161}
162
163impl NetworkVariant {
164 pub const fn name(&self) -> &'static str {
165 match self {
166 Self::Ethereum => "ethereum",
167 #[cfg(feature = "optimism")]
168 Self::Optimism => "optimism",
169 Self::Tempo => "tempo",
170 }
171 }
172}
173
174impl std::fmt::Display for NetworkVariant {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 f.write_str(self.name())
177 }
178}
179
180impl From<ChainId> for NetworkVariant {
181 fn from(chain_id: ChainId) -> Self {
182 let chain = Chain::from_id(chain_id);
183 if chain.is_tempo() {
184 return Self::Tempo;
185 }
186 #[cfg(feature = "optimism")]
187 if chain.is_optimism() {
188 return Self::Optimism;
189 }
190 Self::Ethereum
191 }
192}
193
194#[derive(Clone, Debug, Default, Parser, Deserialize, Copy, PartialEq, Eq)]
195pub struct NetworkConfigs {
196 #[arg(help_heading = "Networks", long, short, num_args = 1, value_name = "NETWORK", value_enum, conflicts_with_all = ["celo", "tempo"])]
198 #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
199 #[serde(default)]
200 pub(crate) network: Option<NetworkVariant>,
201 #[arg(help_heading = "Networks", long, conflicts_with_all = ["network", "tempo"])]
203 #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
204 celo: bool,
205 #[cfg(feature = "optimism")]
207 #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "tempo"])]
208 #[serde(default)]
211 pub(crate) optimism: bool,
212 #[arg(long, hide = true, conflicts_with_all = ["network", "celo"])]
214 #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
215 #[serde(default)]
218 tempo: bool,
219 #[arg(skip)]
221 #[serde(default)]
222 bypass_prevrandao: bool,
223}
224
225impl Serialize for NetworkConfigs {
230 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
231 use serde::ser::SerializeStruct;
232 let mut s = serializer.serialize_struct("NetworkConfigs", 3)?;
233 s.serialize_field("network", &self.resolved_network())?;
234 s.serialize_field("celo", &self.celo)?;
235 s.serialize_field("bypass_prevrandao", &self.bypass_prevrandao)?;
236 s.end()
237 }
238}
239
240impl NetworkConfigs {
241 pub fn with_celo() -> Self {
242 Self { celo: true, ..Default::default() }
243 }
244
245 pub fn with_tempo() -> Self {
246 Self { network: Some(NetworkVariant::Tempo), tempo: true, ..Default::default() }
247 }
248
249 pub const fn is_tempo(&self) -> bool {
250 matches!(self.resolved_network(), Some(NetworkVariant::Tempo))
251 }
252
253 pub const fn is_celo(&self) -> bool {
254 self.celo
255 }
256
257 pub const fn resolved_network(&self) -> Option<NetworkVariant> {
259 if let Some(n) = self.network {
260 return Some(n);
261 }
262 #[cfg(feature = "optimism")]
263 if self.optimism {
264 return Some(NetworkVariant::Optimism);
265 }
266 if self.tempo {
267 return Some(NetworkVariant::Tempo);
268 }
269 None
270 }
271
272 pub fn active_network_name(&self) -> Option<&'static str> {
274 self.resolved_network().and_then(|n| match n {
275 NetworkVariant::Ethereum => None,
276 _ => Some(n.name()),
277 })
278 }
279
280 #[cfg(feature = "optimism")]
285 pub fn base_fee_params(&self, timestamp: u64) -> BaseFeeParams {
286 if self.is_optimism() {
287 return self.op_base_fee_params(timestamp);
288 }
289 BaseFeeParams::ethereum()
290 }
291
292 #[cfg(not(feature = "optimism"))]
294 pub const fn base_fee_params(&self, timestamp: u64) -> BaseFeeParams {
295 let _ = timestamp;
296 BaseFeeParams::ethereum()
297 }
298
299 pub fn bypass_prevrandao(&self, chain_id: u64) -> bool {
300 if let Ok(
301 Moonbeam | Moonbase | Moonriver | MoonbeamDev | Rsk | RskTestnet | Gnosis | Chiado,
302 ) = NamedChain::try_from(chain_id)
303 {
304 return true;
305 }
306 self.bypass_prevrandao
307 }
308
309 pub fn with_chain_id(self, chain_id: u64) -> Self {
310 let chain = Chain::from_id(chain_id);
311 if self.resolved_network().is_some() {
312 return if !self.celo
313 && matches!(chain.named(), Some(NamedChain::Celo | NamedChain::CeloSepolia))
314 {
315 Self::with_celo()
316 } else {
317 self
318 };
319 }
320 if chain.is_tempo() {
321 return Self::with_tempo();
322 }
323 #[cfg(feature = "optimism")]
324 if chain.is_optimism() {
325 return Self::with_optimism();
326 }
327 self
328 }
329
330 pub fn normalize_for_hardfork(self, hardfork: FoundryHardfork) -> Result<Self, String> {
335 if let Some(configured) =
336 self.active_network_name().filter(|&n| Some(n) != hardfork.namespace())
337 {
338 return Err(format!(
339 "hardfork `{}` conflicts with network config `{configured}`",
340 String::from(hardfork),
341 ));
342 }
343
344 let network = match hardfork {
345 FoundryHardfork::Ethereum(_) => self,
346 FoundryHardfork::Tempo(_) => Self::with_tempo(),
347 #[cfg(feature = "optimism")]
348 FoundryHardfork::Optimism(_) => Self::with_optimism(),
349 };
350
351 Ok(network)
352 }
353
354 pub fn inject_precompiles(self, precompiles: &mut PrecompilesMap) {
356 if self.celo {
357 precompiles.apply_precompile(&CELO_TRANSFER_ADDRESS, move |_| {
358 Some(celo::transfer::precompile())
359 });
360 }
361 }
362
363 pub fn inject_chain_precompiles(
365 self,
366 precompiles: &mut PrecompilesMap,
367 chain_id: ChainId,
368 timestamp: u64,
369 ) {
370 let Some(p256verify) = bsc_p256_precompile(chain_id, timestamp) else { return };
371 precompiles.apply_precompile(&BSC_P256_ADDRESS, move |_| {
372 p256verify.map(|p256verify| {
373 DynPrecompile::new(p256verify.id().clone(), move |input| {
374 p256verify.execute(input.data, input.gas, input.reservoir)
375 })
376 })
377 });
378 }
379
380 pub fn precompiles_label(
382 self,
383 tempo_hardfork: Option<TempoHardfork>,
384 ) -> AddressHashMap<String> {
385 let mut labels = AddressHashMap::default();
386 if self.celo {
387 labels.insert(CELO_TRANSFER_ADDRESS, CELO_TRANSFER_LABEL.to_string());
388 }
389 if self.is_tempo() {
390 labels.extend(
391 TEMPO_PRECOMPILES
392 .iter()
393 .copied()
394 .filter(|(_, address)| {
395 tempo_hardfork.is_none_or(|hardfork| {
396 is_tempo_precompile_active_at(*address, hardfork)
397 })
398 })
399 .map(|(label, address)| (address, label.to_string())),
400 );
401 }
402 labels
403 }
404
405 pub fn precompiles(self, tempo_hardfork: Option<TempoHardfork>) -> BTreeMap<String, Address> {
407 let mut precompiles = BTreeMap::new();
408 if self.celo {
409 precompiles
410 .insert(PRECOMPILE_ID_CELO_TRANSFER.name().to_string(), CELO_TRANSFER_ADDRESS);
411 }
412 if self.is_tempo() {
413 precompiles.extend(
414 TEMPO_PRECOMPILES
415 .iter()
416 .copied()
417 .filter(|(_, address)| {
418 tempo_hardfork.is_none_or(|hardfork| {
419 is_tempo_precompile_active_at(*address, hardfork)
420 })
421 })
422 .map(|(label, address)| (label.to_string(), address)),
423 );
424 }
425 precompiles
426 }
427}
428
429impl From<NetworkVariant> for NetworkConfigs {
430 fn from(network: NetworkVariant) -> Self {
431 match network {
432 NetworkVariant::Ethereum => Self::default(),
433 NetworkVariant::Tempo => {
434 Self { network: Some(network), tempo: true, ..Default::default() }
435 }
436 #[cfg(feature = "optimism")]
437 NetworkVariant::Optimism => {
438 Self { network: Some(network), optimism: true, ..Default::default() }
439 }
440 }
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447 use revm::precompile::{
448 Precompiles,
449 secp256r1::{P256VERIFY_BASE_GAS_FEE, P256VERIFY_BASE_GAS_FEE_OSAKA},
450 };
451
452 #[test]
455 fn new_tempo_flag_equivalent_to_legacy() {
456 let via_new = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
457 let via_old = NetworkConfigs { tempo: true, ..Default::default() };
458 assert_eq!(via_new.is_tempo(), via_old.is_tempo());
459 assert_eq!(via_new.active_network_name(), via_old.active_network_name());
460 assert_eq!(via_new.precompiles(None), via_old.precompiles(None));
461 assert_eq!(via_new.precompiles_label(None), via_old.precompiles_label(None));
462 }
463
464 fn bsc_p256_gas_used(chain_id: ChainId, timestamp: u64) -> Option<u64> {
465 bsc_p256_precompile(chain_id, timestamp)
466 .flatten()
467 .map(|precompile| precompile.execute(&[], u64::MAX, 0).unwrap().gas_used)
468 }
469
470 fn assert_bsc_p256_boundaries(chain_id: ChainId, haber_timestamp: u64, osaka_timestamp: u64) {
471 assert!(matches!(bsc_p256_precompile(chain_id, haber_timestamp - 1), Some(None)));
472 assert_eq!(bsc_p256_gas_used(chain_id, haber_timestamp), Some(P256VERIFY_BASE_GAS_FEE));
473 assert_eq!(bsc_p256_gas_used(chain_id, osaka_timestamp - 1), Some(P256VERIFY_BASE_GAS_FEE));
474 assert_eq!(
475 bsc_p256_gas_used(chain_id, osaka_timestamp),
476 Some(P256VERIFY_BASE_GAS_FEE_OSAKA)
477 );
478 }
479
480 #[test]
481 fn selects_bsc_p256_at_mainnet_boundaries() {
482 assert_bsc_p256_boundaries(
483 BSC_MAINNET_CHAIN_ID,
484 BSC_MAINNET_HABER_TIMESTAMP,
485 BSC_MAINNET_OSAKA_TIMESTAMP,
486 );
487 }
488
489 #[test]
490 fn selects_bsc_p256_at_testnet_boundaries() {
491 assert_bsc_p256_boundaries(
492 BSC_TESTNET_CHAIN_ID,
493 BSC_TESTNET_HABER_TIMESTAMP,
494 BSC_TESTNET_OSAKA_TIMESTAMP,
495 );
496 }
497
498 #[test]
499 fn removes_bsc_p256_before_haber() {
500 let mut precompiles = PrecompilesMap::from_static(Precompiles::osaka());
501 assert!(precompiles.get(&BSC_P256_ADDRESS).is_some());
502 NetworkConfigs::default().inject_chain_precompiles(
503 &mut precompiles,
504 BSC_MAINNET_CHAIN_ID,
505 BSC_MAINNET_HABER_TIMESTAMP - 1,
506 );
507 assert!(precompiles.get(&BSC_P256_ADDRESS).is_none());
508 }
509
510 #[test]
511 fn canonical_tempo_network_reports_precompiles() {
512 let cfg = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
513
514 assert_eq!(
515 cfg.precompiles(None).get("TIP20ChannelReserve"),
516 Some(&TIP20_CHANNEL_RESERVE_ADDRESS)
517 );
518 assert!(!cfg.precompiles(Some(TempoHardfork::T4)).contains_key("TIP20ChannelReserve"));
519 assert!(!cfg.precompiles(Some(TempoHardfork::T4)).contains_key("ReceivePolicyGuard"));
520 assert!(!cfg.precompiles(Some(TempoHardfork::T2)).contains_key("AddressRegistry"));
521 assert!(!cfg.precompiles(Some(TempoHardfork::T2)).contains_key("SignatureVerifier"));
522 assert_eq!(
523 cfg.precompiles(Some(TempoHardfork::T3)).get("AddressRegistry"),
524 Some(&ADDRESS_REGISTRY_ADDRESS)
525 );
526 assert_eq!(
527 cfg.precompiles(Some(TempoHardfork::T3)).get("SignatureVerifier"),
528 Some(&SIGNATURE_VERIFIER_ADDRESS)
529 );
530 assert_eq!(
531 cfg.precompiles_label(Some(TempoHardfork::T5)).get(&TIP20_CHANNEL_RESERVE_ADDRESS),
532 Some(&"TIP20ChannelReserve".to_string())
533 );
534 assert!(cfg.precompiles_label(None).contains_key(&TIP20_CHANNEL_RESERVE_ADDRESS));
535 assert!(
536 !cfg.precompiles_label(Some(TempoHardfork::T5))
537 .contains_key(&RECEIVE_POLICY_GUARD_ADDRESS)
538 );
539 assert!(
540 cfg.precompiles_label(Some(TempoHardfork::T6))
541 .contains_key(&RECEIVE_POLICY_GUARD_ADDRESS)
542 );
543 }
544
545 #[test]
546 fn storage_credits_precompile_activates_at_t7() {
547 assert!(!is_tempo_precompile_active_at(STORAGE_CREDITS_ADDRESS, TempoHardfork::T6));
548 assert!(is_tempo_precompile_active_at(STORAGE_CREDITS_ADDRESS, TempoHardfork::T7));
549 assert!(TEMPO_PRECOMPILE_ADDRESSES.contains(&STORAGE_CREDITS_ADDRESS));
550
551 let cfg = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
553 assert!(!cfg.precompiles(Some(TempoHardfork::T6)).contains_key("StorageCredits"));
554 assert!(cfg.precompiles(Some(TempoHardfork::T7)).contains_key("StorageCredits"));
555 }
556
557 #[test]
558 fn current_committee_precompile_activates_at_t8() {
559 assert!(!is_tempo_precompile_active_at(CURRENT_COMMITTEE_ADDRESS, TempoHardfork::T7));
560 assert!(is_tempo_precompile_active_at(CURRENT_COMMITTEE_ADDRESS, TempoHardfork::T8));
561 assert!(TEMPO_PRECOMPILE_ADDRESSES.contains(&CURRENT_COMMITTEE_ADDRESS));
562
563 let cfg = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
564 assert!(!cfg.precompiles(Some(TempoHardfork::T7)).contains_key("CurrentCommittee"));
565 assert!(cfg.precompiles(Some(TempoHardfork::T8)).contains_key("CurrentCommittee"));
566 }
567
568 #[test]
571 fn active_network_name_tempo() {
572 let cfg = NetworkConfigs::with_tempo();
573 assert_eq!(cfg.active_network_name(), Some("tempo"));
574 }
575
576 #[test]
577 fn active_network_name_default_is_none() {
578 assert_eq!(NetworkConfigs::default().active_network_name(), None);
579 }
580
581 #[test]
584 fn serde_roundtrip_tempo() {
585 let original = NetworkConfigs::with_tempo();
586 let json = serde_json::to_string(&original).unwrap();
587 let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
588 assert!(restored.is_tempo());
589 }
590
591 #[test]
592 fn serde_legacy_tempo_bool_deserialized() {
593 let json = r#"{"tempo": true, "celo": false, "bypass_prevrandao": false}"#;
595 let cfg: NetworkConfigs = serde_json::from_str(json).unwrap();
596 assert!(cfg.is_tempo());
597 }
598
599 #[test]
600 fn serde_serializes_legacy_alias_as_canonical_network() {
601 let cfg = NetworkConfigs { tempo: true, ..Default::default() };
604 let json = serde_json::to_value(cfg).unwrap();
605 assert_eq!(json["network"], serde_json::json!("tempo"));
606 assert!(json.get("tempo").is_none(), "legacy `tempo` key should not be serialized");
607 assert!(json.get("optimism").is_none(), "legacy `optimism` key should not be serialized");
608 }
609
610 #[test]
611 fn serde_new_network_field_deserialized() {
612 let json_tempo = r#"{"network": "tempo", "celo": false, "bypass_prevrandao": false}"#;
613 let cfg_tempo: NetworkConfigs = serde_json::from_str(json_tempo).unwrap();
614 assert!(cfg_tempo.is_tempo());
615 }
616
617 #[cfg(feature = "optimism")]
618 mod optimism {
619 use super::*;
620
621 #[test]
622 fn new_optimism_flag_equivalent_to_legacy() {
623 let via_new =
624 NetworkConfigs { network: Some(NetworkVariant::Optimism), ..Default::default() };
625 let via_old = NetworkConfigs { optimism: true, ..Default::default() };
626 assert_eq!(via_new.is_optimism(), via_old.is_optimism());
627 assert_eq!(via_new.is_tempo(), via_old.is_tempo());
628 assert_eq!(via_new.active_network_name(), via_old.active_network_name());
629 }
630
631 #[test]
632 fn active_network_name_optimism() {
633 let cfg = NetworkConfigs::with_optimism();
634 assert_eq!(cfg.active_network_name(), Some("optimism"));
635 }
636
637 #[test]
638 fn new_flag_wins_over_legacy_when_both_set() {
639 let cfg = NetworkConfigs {
641 network: Some(NetworkVariant::Optimism),
642 tempo: true,
643 ..Default::default()
644 };
645 assert!(cfg.is_optimism());
646 assert!(!cfg.is_tempo());
647 }
648
649 #[test]
650 fn serde_roundtrip_optimism() {
651 let original = NetworkConfigs::with_optimism();
652 let json = serde_json::to_string(&original).unwrap();
653 let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
654 assert!(restored.is_optimism());
655 assert!(!restored.is_tempo());
656 }
657
658 #[test]
659 fn serde_optimism_field_deserialized() {
660 let json_optimism =
661 r#"{"network": "optimism", "celo": false, "bypass_prevrandao": false}"#;
662 let cfg_optimism: NetworkConfigs = serde_json::from_str(json_optimism).unwrap();
663 assert!(cfg_optimism.is_optimism());
664 }
665 }
666}