1use alloy_chains::Chain;
7use alloy_rpc_types::BlockNumberOrTag;
8use foundry_compilers::artifacts::EvmVersion;
9use revm::primitives::hardfork::SpecId;
10use serde::{Deserialize, Serialize};
11use std::{
12 str::FromStr,
13 time::{SystemTime, UNIX_EPOCH},
14};
15
16#[cfg(all(feature = "optimism", not(feature = "base")))]
17use alloy_chains::NamedChain;
18#[cfg(feature = "optimism")]
19use op_revm::OpSpecId;
20
21pub use alloy_hardforks::EthereumHardfork;
22pub use tempo_hardfork::TempoHardfork;
23
24#[cfg(feature = "base")]
25pub use base_common_evm::BaseSpecId;
26#[cfg(feature = "base")]
27pub use base_common_genesis::BaseUpgrade;
28
29#[cfg(feature = "monad")]
30pub use monad_revm::MonadHardfork;
31
32#[cfg(feature = "optimism")]
33pub use alloy_op_hardforks::OpHardfork;
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
36#[serde(into = "String")]
37pub enum FoundryHardfork {
38 Ethereum(EthereumHardfork),
39 #[cfg(feature = "optimism")]
40 Optimism(OpHardfork),
41 #[cfg(feature = "base")]
42 Base(BaseUpgrade),
43 Tempo(TempoHardfork),
44 #[cfg(feature = "monad")]
45 Monad(MonadHardfork),
46}
47
48impl From<FoundryHardfork> for String {
49 fn from(fork: FoundryHardfork) -> Self {
50 match fork {
51 FoundryHardfork::Ethereum(h) => format!("{h}"),
52 #[cfg(feature = "optimism")]
53 FoundryHardfork::Optimism(h) => format!("optimism:{h}"),
54 #[cfg(feature = "base")]
55 FoundryHardfork::Base(h) => format!("base:{h}"),
56 FoundryHardfork::Tempo(h) => format!("tempo:{h}"),
57 #[cfg(feature = "monad")]
58 FoundryHardfork::Monad(h) => format!("monad:{h}"),
59 }
60 }
61}
62
63impl<'de> Deserialize<'de> for FoundryHardfork {
64 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
65 where
66 D: serde::Deserializer<'de>,
67 {
68 let s = String::deserialize(deserializer)?;
69 Self::from_str(&s).map_err(serde::de::Error::custom)
70 }
71}
72
73impl FromStr for FoundryHardfork {
74 type Err = String;
75
76 fn from_str(s: &str) -> Result<Self, Self::Err> {
77 let raw = s.trim();
78
79 let Some((ns, fork_raw)) = raw.split_once(':') else {
80 return EthereumHardfork::from_str(raw)
81 .map(Self::Ethereum)
82 .map_err(|_| format!("unknown ethereum hardfork '{raw}'"));
83 };
84
85 let ns = ns.trim().to_ascii_lowercase();
86 let fork = fork_raw.trim().to_ascii_lowercase().replace(['-', ' '], "_");
87
88 match ns.as_str() {
89 "eth" | "ethereum" => EthereumHardfork::from_str(&fork)
90 .map(Self::Ethereum)
91 .map_err(|_| format!("unknown ethereum hardfork '{fork_raw}'")),
92
93 #[cfg(feature = "optimism")]
94 "op" | "optimism" => OpHardfork::from_str(&fork)
95 .map(Self::Optimism)
96 .map_err(|_| format!("unknown optimism hardfork '{fork_raw}'")),
97 #[cfg(not(feature = "optimism"))]
98 "op" | "optimism" => {
99 Err(format!("hardfork namespace '{ns}' requires the `optimism` feature"))
100 }
101
102 #[cfg(feature = "base")]
103 "base" => BaseUpgrade::from_str(&fork)
104 .map(Self::Base)
105 .map_err(|_| format!("unknown base hardfork '{fork_raw}'")),
106 #[cfg(not(feature = "base"))]
107 "base" => Err(format!("hardfork namespace '{ns}' requires the `base` feature")),
108
109 "t" | "tempo" => TempoHardfork::from_str(&fork)
110 .map(Self::Tempo)
111 .map_err(|_| format!("unknown tempo hardfork '{fork_raw}'")),
112
113 #[cfg(feature = "monad")]
114 "m" | "monad" => MonadHardfork::from_str(&fork)
115 .map(Self::Monad)
116 .map_err(|_| format!("unknown monad hardfork '{fork_raw}'")),
117 #[cfg(not(feature = "monad"))]
118 "m" | "monad" => Err(format!("hardfork namespace '{ns}' requires the `monad` feature")),
119
120 _ => Err(format!("unknown hardfork namespace '{ns}'")),
121 }
122 }
123}
124
125impl FoundryHardfork {
126 pub const fn ethereum(h: EthereumHardfork) -> Self {
127 Self::Ethereum(h)
128 }
129
130 #[cfg(feature = "optimism")]
131 pub const fn optimism(h: OpHardfork) -> Self {
132 Self::Optimism(h)
133 }
134
135 #[cfg(feature = "base")]
136 pub const fn base(h: BaseUpgrade) -> Self {
137 Self::Base(h)
138 }
139
140 pub const fn tempo(h: TempoHardfork) -> Self {
141 Self::Tempo(h)
142 }
143
144 #[cfg(feature = "monad")]
145 pub const fn monad(h: MonadHardfork) -> Self {
146 Self::Monad(h)
147 }
148
149 pub fn name(&self) -> String {
151 match self {
152 Self::Ethereum(h) => format!("{h}"),
153 #[cfg(feature = "optimism")]
154 Self::Optimism(h) => format!("{h}"),
155 #[cfg(feature = "base")]
156 Self::Base(h) => format!("{h}"),
157 Self::Tempo(h) => format!("{h}"),
158 #[cfg(feature = "monad")]
159 Self::Monad(h) => format!("{h}"),
160 }
161 }
162
163 pub const fn namespace(&self) -> Option<&'static str> {
167 match self {
168 Self::Ethereum(_) => None,
169 #[cfg(feature = "optimism")]
170 Self::Optimism(_) => Some("optimism"),
171 #[cfg(feature = "base")]
172 Self::Base(_) => Some("base"),
173 Self::Tempo(_) => Some("tempo"),
174 #[cfg(feature = "monad")]
175 Self::Monad(_) => Some("monad"),
176 }
177 }
178
179 pub fn from_chain_and_timestamp(chain_id: u64, timestamp: u64) -> Option<Self> {
183 let chain = Chain::from_id(chain_id);
184 if let Some(fork) = EthereumHardfork::from_chain_and_timestamp(chain, timestamp) {
185 return Some(Self::Ethereum(fork));
186 }
187 #[cfg(feature = "base")]
188 if let Some(fork) = BaseUpgrade::from_chain_and_timestamp(chain_id, timestamp) {
189 return Some(Self::Base(fork));
190 }
191 #[cfg(feature = "optimism")]
192 if let Some(fork) = OpHardfork::from_chain_and_timestamp(chain, timestamp) {
193 return Some(Self::Optimism(fork));
194 }
195 if let Some(fork) = TempoHardfork::from_chain_and_timestamp(chain_id, timestamp) {
196 return Some(Self::Tempo(fork));
197 }
198 #[cfg(feature = "monad")]
199 if let Some(fork) = MonadHardfork::from_chain_and_timestamp(chain_id, timestamp) {
200 return Some(Self::Monad(fork));
201 }
202 None
203 }
204}
205
206impl From<EthereumHardfork> for FoundryHardfork {
207 fn from(value: EthereumHardfork) -> Self {
208 Self::Ethereum(value)
209 }
210}
211
212impl From<FoundryHardfork> for EthereumHardfork {
213 fn from(fork: FoundryHardfork) -> Self {
214 match fork {
215 FoundryHardfork::Ethereum(hardfork) => hardfork,
216 _ => Self::default(),
217 }
218 }
219}
220
221#[cfg(feature = "optimism")]
222impl From<OpHardfork> for FoundryHardfork {
223 fn from(value: OpHardfork) -> Self {
224 Self::Optimism(value)
225 }
226}
227
228#[cfg(feature = "optimism")]
229impl From<FoundryHardfork> for OpHardfork {
230 fn from(fork: FoundryHardfork) -> Self {
231 match fork {
232 FoundryHardfork::Optimism(hardfork) => hardfork,
233 _ => Self::default(),
234 }
235 }
236}
237
238#[cfg(feature = "base")]
239impl From<BaseUpgrade> for FoundryHardfork {
240 fn from(value: BaseUpgrade) -> Self {
241 Self::Base(value)
242 }
243}
244
245#[cfg(feature = "base")]
246impl From<FoundryHardfork> for BaseUpgrade {
247 fn from(fork: FoundryHardfork) -> Self {
248 match fork {
249 FoundryHardfork::Base(upgrade) => upgrade,
250 _ => Self::default(),
251 }
252 }
253}
254
255#[cfg(feature = "base")]
256impl From<FoundryHardfork> for BaseSpecId {
257 fn from(fork: FoundryHardfork) -> Self {
258 match fork {
259 FoundryHardfork::Base(upgrade) => Self::new(upgrade),
260 _ => Self::default(),
261 }
262 }
263}
264
265impl From<TempoHardfork> for FoundryHardfork {
266 fn from(value: TempoHardfork) -> Self {
267 Self::Tempo(value)
268 }
269}
270
271impl From<FoundryHardfork> for TempoHardfork {
272 fn from(fork: FoundryHardfork) -> Self {
273 match fork {
274 FoundryHardfork::Tempo(hardfork) => hardfork,
275 _ => Self::default(),
276 }
277 }
278}
279
280#[cfg(feature = "monad")]
281impl From<MonadHardfork> for FoundryHardfork {
282 fn from(value: MonadHardfork) -> Self {
283 Self::Monad(value)
284 }
285}
286
287#[cfg(feature = "monad")]
288impl From<FoundryHardfork> for MonadHardfork {
289 fn from(fork: FoundryHardfork) -> Self {
290 match fork {
291 FoundryHardfork::Monad(hardfork) => hardfork,
292 _ => Self::default(),
293 }
294 }
295}
296
297impl From<FoundryHardfork> for SpecId {
298 fn from(fork: FoundryHardfork) -> Self {
299 match fork {
300 FoundryHardfork::Ethereum(hardfork) => spec_id_from_ethereum_hardfork(hardfork),
301 #[cfg(feature = "optimism")]
302 FoundryHardfork::Optimism(hardfork) => eth_spec_id_from_optimism_hardfork(hardfork),
303 #[cfg(feature = "base")]
304 FoundryHardfork::Base(hardfork) => BaseSpecId::new(hardfork).into_eth_spec(),
305 FoundryHardfork::Tempo(hardfork) => spec_id_from_tempo_hardfork(hardfork),
306 #[cfg(feature = "monad")]
307 FoundryHardfork::Monad(hardfork) => hardfork.into(),
308 }
309 }
310}
311
312#[cfg(feature = "optimism")]
313impl From<FoundryHardfork> for OpSpecId {
314 fn from(fork: FoundryHardfork) -> Self {
315 match fork {
316 FoundryHardfork::Optimism(hardfork) => spec_id_from_optimism_hardfork(hardfork),
317 _ => Self::default(),
318 }
319 }
320}
321
322pub fn spec_id_from_ethereum_hardfork(hardfork: EthereumHardfork) -> SpecId {
324 match hardfork {
325 EthereumHardfork::Frontier => SpecId::FRONTIER,
326 EthereumHardfork::Homestead => SpecId::HOMESTEAD,
327 EthereumHardfork::Dao => SpecId::HOMESTEAD,
328 EthereumHardfork::Tangerine => SpecId::TANGERINE,
329 EthereumHardfork::SpuriousDragon => SpecId::SPURIOUS_DRAGON,
330 EthereumHardfork::Byzantium => SpecId::BYZANTIUM,
331 EthereumHardfork::Constantinople => SpecId::PETERSBURG,
332 EthereumHardfork::Petersburg => SpecId::PETERSBURG,
333 EthereumHardfork::Istanbul => SpecId::ISTANBUL,
334 EthereumHardfork::MuirGlacier => SpecId::ISTANBUL,
335 EthereumHardfork::Berlin => SpecId::BERLIN,
336 EthereumHardfork::London => SpecId::LONDON,
337 EthereumHardfork::ArrowGlacier => SpecId::LONDON,
338 EthereumHardfork::GrayGlacier => SpecId::LONDON,
339 EthereumHardfork::Paris => SpecId::MERGE,
340 EthereumHardfork::Shanghai => SpecId::SHANGHAI,
341 EthereumHardfork::Cancun => SpecId::CANCUN,
342 EthereumHardfork::Prague => SpecId::PRAGUE,
343 EthereumHardfork::Osaka => SpecId::OSAKA,
344 EthereumHardfork::Bpo1
345 | EthereumHardfork::Bpo2
346 | EthereumHardfork::Bpo3
347 | EthereumHardfork::Bpo4
348 | EthereumHardfork::Bpo5 => SpecId::OSAKA,
349 EthereumHardfork::Amsterdam => SpecId::AMSTERDAM,
350 f => unreachable!("unimplemented {}", f),
351 }
352}
353
354#[cfg(feature = "optimism")]
356pub fn spec_id_from_optimism_hardfork(hardfork: OpHardfork) -> OpSpecId {
357 match hardfork {
358 OpHardfork::Bedrock => OpSpecId::BEDROCK,
359 OpHardfork::Regolith => OpSpecId::REGOLITH,
360 OpHardfork::Canyon => OpSpecId::CANYON,
361 OpHardfork::Ecotone => OpSpecId::ECOTONE,
362 OpHardfork::Fjord => OpSpecId::FJORD,
363 OpHardfork::Granite => OpSpecId::GRANITE,
364 OpHardfork::Holocene => OpSpecId::HOLOCENE,
365 OpHardfork::Isthmus => OpSpecId::ISTHMUS,
366 OpHardfork::Jovian => OpSpecId::JOVIAN,
367 OpHardfork::Karst => OpSpecId::KARST,
368 OpHardfork::Lagoon => OpSpecId::LAGOON,
369 f => unreachable!("unimplemented {}", f),
370 }
371}
372
373#[cfg(feature = "optimism")]
375pub fn eth_spec_id_from_optimism_hardfork(hardfork: OpHardfork) -> SpecId {
376 match hardfork {
377 OpHardfork::Bedrock | OpHardfork::Regolith => SpecId::MERGE,
378 OpHardfork::Canyon => SpecId::SHANGHAI,
379 OpHardfork::Ecotone | OpHardfork::Fjord | OpHardfork::Granite | OpHardfork::Holocene => {
380 SpecId::CANCUN
381 }
382 OpHardfork::Isthmus | OpHardfork::Jovian => SpecId::PRAGUE,
383 OpHardfork::Karst | OpHardfork::Lagoon => SpecId::OSAKA,
384 f => unreachable!("unimplemented {}", f),
385 }
386}
387
388pub const fn spec_id_from_tempo_hardfork(_: TempoHardfork) -> SpecId {
390 SpecId::OSAKA
391}
392
393pub trait FromEvmVersion: From<FoundryHardfork> {
395 fn from_evm_version(version: EvmVersion) -> Self;
396}
397
398pub trait ExecutionSpec: FromEvmVersion {
400 fn evm_version_name(&self) -> String;
402
403 fn from_network_hardfork(_: &str) -> Option<Self> {
405 None
406 }
407
408 fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self>;
410
411 fn historical_hardfork(chain_id: u64, timestamp: u64) -> Option<FoundryHardfork> {
414 FoundryHardfork::from_chain_and_timestamp(chain_id, timestamp)
415 .filter(|&hardfork| Self::from_foundry_hardfork(hardfork).is_some())
416 }
417
418 fn fork_hardfork(
421 chain_id: u64,
422 timestamp: u64,
423 endpoint_hardfork: Option<FoundryHardfork>,
424 ) -> Option<FoundryHardfork> {
425 endpoint_hardfork
426 .filter(|&hardfork| Self::from_foundry_hardfork(hardfork).is_some())
427 .or_else(|| Self::historical_hardfork(chain_id, timestamp))
428 }
429
430 fn reported_hardfork(self) -> Option<FoundryHardfork> {
433 None
434 }
435}
436
437impl FromEvmVersion for SpecId {
438 fn from_evm_version(version: EvmVersion) -> Self {
439 match version {
440 EvmVersion::Homestead => Self::HOMESTEAD,
441 EvmVersion::TangerineWhistle => Self::TANGERINE,
442 EvmVersion::SpuriousDragon => Self::SPURIOUS_DRAGON,
443 EvmVersion::Byzantium => Self::BYZANTIUM,
444 EvmVersion::Constantinople => Self::PETERSBURG,
445 EvmVersion::Petersburg => Self::PETERSBURG,
446 EvmVersion::Istanbul => Self::ISTANBUL,
447 EvmVersion::Berlin => Self::BERLIN,
448 EvmVersion::London => Self::LONDON,
449 EvmVersion::Paris => Self::MERGE,
450 EvmVersion::Shanghai => Self::SHANGHAI,
451 EvmVersion::Cancun => Self::CANCUN,
452 EvmVersion::Prague => Self::PRAGUE,
453 EvmVersion::Osaka => Self::OSAKA,
454 EvmVersion::Amsterdam => Self::AMSTERDAM,
455 }
456 }
457}
458
459impl ExecutionSpec for SpecId {
460 fn evm_version_name(&self) -> String {
462 self.to_string()
463 }
464
465 fn from_network_hardfork(hardfork: &str) -> Option<Self> {
467 EthereumHardfork::from_str(hardfork).ok().map(spec_id_from_ethereum_hardfork)
468 }
469
470 fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self> {
472 match hardfork {
473 FoundryHardfork::Ethereum(hardfork) => Some(spec_id_from_ethereum_hardfork(hardfork)),
474 _ => None,
475 }
476 }
477
478 fn fork_hardfork(
479 _chain_id: u64,
480 _timestamp: u64,
481 _endpoint_hardfork: Option<FoundryHardfork>,
482 ) -> Option<FoundryHardfork> {
483 None
484 }
485}
486
487#[cfg(feature = "optimism")]
488impl FromEvmVersion for OpSpecId {
489 fn from_evm_version(version: EvmVersion) -> Self {
490 match version {
491 EvmVersion::Homestead
492 | EvmVersion::TangerineWhistle
493 | EvmVersion::SpuriousDragon
494 | EvmVersion::Byzantium
495 | EvmVersion::Constantinople
496 | EvmVersion::Petersburg
497 | EvmVersion::Istanbul
498 | EvmVersion::Berlin
499 | EvmVersion::London
500 | EvmVersion::Paris => Self::BEDROCK,
501 EvmVersion::Shanghai => Self::CANYON,
502 EvmVersion::Cancun => Self::ECOTONE,
503 EvmVersion::Prague => Self::ISTHMUS,
504 EvmVersion::Osaka | EvmVersion::Amsterdam => Self::KARST,
505 }
506 }
507}
508
509#[cfg(feature = "optimism")]
510impl ExecutionSpec for OpSpecId {
511 fn evm_version_name(&self) -> String {
513 let name: &'static str = (*self).into();
514 name.to_string()
515 }
516
517 fn from_network_hardfork(hardfork: &str) -> Option<Self> {
519 OpHardfork::from_str(hardfork).ok().map(spec_id_from_optimism_hardfork)
520 }
521
522 fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self> {
524 match hardfork {
525 FoundryHardfork::Optimism(hardfork) => Some(spec_id_from_optimism_hardfork(hardfork)),
526 _ => None,
527 }
528 }
529
530 fn fork_hardfork(
531 chain_id: u64,
532 timestamp: u64,
533 endpoint_hardfork: Option<FoundryHardfork>,
534 ) -> Option<FoundryHardfork> {
535 let endpoint_hardfork =
536 endpoint_hardfork.filter(|&hardfork| Self::from_foundry_hardfork(hardfork).is_some());
537 if endpoint_hardfork.is_some() {
538 return endpoint_hardfork;
539 }
540
541 #[cfg(not(feature = "base"))]
544 if matches!(
545 Chain::from_id(chain_id).named(),
546 Some(NamedChain::Base | NamedChain::BaseSepolia)
547 ) {
548 return None;
549 }
550
551 Self::historical_hardfork(chain_id, timestamp)
552 }
553
554 fn historical_hardfork(chain_id: u64, timestamp: u64) -> Option<FoundryHardfork> {
555 OpHardfork::from_chain_and_timestamp(Chain::from_id(chain_id), timestamp)
556 .map(FoundryHardfork::Optimism)
557 }
558}
559
560impl FromEvmVersion for TempoHardfork {
561 fn from_evm_version(_: EvmVersion) -> Self {
562 latest_active_tempo_hardfork()
563 }
564}
565
566impl ExecutionSpec for TempoHardfork {
567 fn evm_version_name(&self) -> String {
569 self.to_string()
570 }
571
572 fn from_network_hardfork(hardfork: &str) -> Option<Self> {
574 Self::from_str(hardfork).ok()
575 }
576
577 fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self> {
579 match hardfork {
580 FoundryHardfork::Tempo(hardfork) => Some(hardfork),
581 _ => None,
582 }
583 }
584
585 fn reported_hardfork(self) -> Option<FoundryHardfork> {
586 Some(self.into())
587 }
588}
589
590#[cfg(feature = "monad")]
591impl FromEvmVersion for MonadHardfork {
592 fn from_evm_version(_: EvmVersion) -> Self {
593 Self::default()
594 }
595}
596
597#[cfg(feature = "monad")]
598impl ExecutionSpec for MonadHardfork {
599 fn evm_version_name(&self) -> String {
601 self.to_string()
602 }
603
604 fn from_network_hardfork(hardfork: &str) -> Option<Self> {
606 Self::from_str(hardfork).ok()
607 }
608
609 fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self> {
611 match hardfork {
612 #[cfg(feature = "monad")]
613 FoundryHardfork::Monad(hardfork) => Some(hardfork),
614 _ => None,
615 }
616 }
617
618 fn reported_hardfork(self) -> Option<FoundryHardfork> {
619 Some(self.into())
620 }
621}
622
623#[cfg(feature = "base")]
624impl FromEvmVersion for BaseSpecId {
625 fn from_evm_version(version: EvmVersion) -> Self {
626 let upgrade = match version {
627 EvmVersion::Homestead
628 | EvmVersion::TangerineWhistle
629 | EvmVersion::SpuriousDragon
630 | EvmVersion::Byzantium
631 | EvmVersion::Constantinople
632 | EvmVersion::Petersburg
633 | EvmVersion::Istanbul
634 | EvmVersion::Berlin
635 | EvmVersion::London
636 | EvmVersion::Paris => BaseUpgrade::Bedrock,
637 EvmVersion::Shanghai => BaseUpgrade::Canyon,
638 EvmVersion::Cancun => BaseUpgrade::Ecotone,
639 EvmVersion::Prague => BaseUpgrade::Isthmus,
640 EvmVersion::Osaka | EvmVersion::Amsterdam => BaseUpgrade::Azul,
641 };
642 Self::new(upgrade)
643 }
644}
645
646#[cfg(feature = "base")]
647impl ExecutionSpec for BaseSpecId {
648 fn evm_version_name(&self) -> String {
650 self.to_string()
651 }
652
653 fn from_network_hardfork(hardfork: &str) -> Option<Self> {
655 Self::from_str(hardfork).ok()
656 }
657
658 fn from_foundry_hardfork(hardfork: FoundryHardfork) -> Option<Self> {
660 match hardfork {
661 FoundryHardfork::Base(hardfork) => Some(Self::new(hardfork)),
662 _ => None,
663 }
664 }
665
666 fn reported_hardfork(self) -> Option<FoundryHardfork> {
667 Some(FoundryHardfork::Base(self.upgrade()))
668 }
669}
670
671pub fn evm_spec_id<SPEC: FromEvmVersion>(evm_version: EvmVersion) -> SPEC {
673 SPEC::from_evm_version(evm_version)
674}
675
676pub fn latest_active_tempo_hardfork() -> TempoHardfork {
678 let now = SystemTime::now()
680 .duration_since(UNIX_EPOCH)
681 .map(|duration| duration.as_secs())
682 .unwrap_or(u64::MAX);
683 TempoHardfork::from_chain_and_timestamp(4217, now)
684 .or_else(|| TempoHardfork::from_chain_and_timestamp(42431, now))
685 .unwrap_or_default()
686}
687
688pub fn evm_spec_id_from_str<SPEC: ExecutionSpec>(evm_version: &str) -> Option<SPEC> {
690 let evm_version = evm_version.trim();
691
692 if let Ok(version) = EvmVersion::from_str(evm_version) {
693 return Some(evm_spec_id(version));
694 }
695
696 if let Some(spec) = SPEC::from_network_hardfork(evm_version) {
697 return Some(spec);
698 }
699
700 FoundryHardfork::from_str(evm_version).ok().and_then(SPEC::from_foundry_hardfork)
701}
702
703pub fn ethereum_hardfork_from_block_tag(block: impl Into<BlockNumberOrTag>) -> EthereumHardfork {
705 let num = match block.into() {
706 BlockNumberOrTag::Earliest => 0,
707 BlockNumberOrTag::Number(num) => num,
708 _ => u64::MAX,
709 };
710
711 EthereumHardfork::from_mainnet_block_number(num)
712}
713
714#[cfg(test)]
715mod tests {
716 use super::*;
717 use alloy_hardforks::ethereum::mainnet::*;
718 use tempo_hardfork::constants::{mainnet::*, moderato::*};
719
720 #[test]
721 fn test_ethereum_spec_id_mapping() {
722 for (hardfork, expected) in [
723 (EthereumHardfork::Frontier, SpecId::FRONTIER),
724 (EthereumHardfork::Homestead, SpecId::HOMESTEAD),
725 (EthereumHardfork::Dao, SpecId::HOMESTEAD),
726 (EthereumHardfork::Tangerine, SpecId::TANGERINE),
727 (EthereumHardfork::SpuriousDragon, SpecId::SPURIOUS_DRAGON),
728 (EthereumHardfork::Byzantium, SpecId::BYZANTIUM),
729 (EthereumHardfork::Constantinople, SpecId::PETERSBURG),
730 (EthereumHardfork::Petersburg, SpecId::PETERSBURG),
731 (EthereumHardfork::Istanbul, SpecId::ISTANBUL),
732 (EthereumHardfork::MuirGlacier, SpecId::ISTANBUL),
733 (EthereumHardfork::Berlin, SpecId::BERLIN),
734 (EthereumHardfork::London, SpecId::LONDON),
735 (EthereumHardfork::ArrowGlacier, SpecId::LONDON),
736 (EthereumHardfork::GrayGlacier, SpecId::LONDON),
737 (EthereumHardfork::Paris, SpecId::MERGE),
738 (EthereumHardfork::Shanghai, SpecId::SHANGHAI),
739 (EthereumHardfork::Cancun, SpecId::CANCUN),
740 (EthereumHardfork::Prague, SpecId::PRAGUE),
741 (EthereumHardfork::Osaka, SpecId::OSAKA),
742 (EthereumHardfork::Bpo1, SpecId::OSAKA),
743 (EthereumHardfork::Bpo2, SpecId::OSAKA),
744 (EthereumHardfork::Bpo3, SpecId::OSAKA),
745 (EthereumHardfork::Bpo4, SpecId::OSAKA),
746 (EthereumHardfork::Bpo5, SpecId::OSAKA),
747 (EthereumHardfork::Amsterdam, SpecId::AMSTERDAM),
748 ] {
749 assert_eq!(spec_id_from_ethereum_hardfork(hardfork), expected, "{hardfork}");
750 }
751 }
752
753 #[test]
754 fn test_tempo_spec_id_mapping() {
755 assert_eq!(spec_id_from_tempo_hardfork(TempoHardfork::Genesis), SpecId::OSAKA);
756 assert_eq!(spec_id_from_tempo_hardfork(TempoHardfork::T8), SpecId::OSAKA);
757 }
758
759 #[test]
760 fn test_tempo_evm_version_defaults_to_latest_active_hardfork() {
761 let latest = latest_active_tempo_hardfork();
762 assert_eq!(evm_spec_id::<TempoHardfork>(EvmVersion::Osaka), latest);
763 }
764
765 #[test]
766 #[cfg(feature = "monad")]
767 fn test_monad_hardfork_parsing() {
768 assert_eq!(
769 "monad:MonadNine".parse::<FoundryHardfork>().unwrap(),
770 FoundryHardfork::Monad(MonadHardfork::MonadNine)
771 );
772 assert_eq!(
773 "monad:MonadTen".parse::<FoundryHardfork>().unwrap(),
774 FoundryHardfork::Monad(MonadHardfork::MonadTen)
775 );
776 assert_eq!(
777 "m:MonadNext".parse::<FoundryHardfork>().unwrap(),
778 FoundryHardfork::Monad(MonadHardfork::MonadNext)
779 );
780 }
781
782 #[test]
783 fn test_hardfork_namespace_parsing() {
784 let cases = [
785 ("prague", Ok(FoundryHardfork::Ethereum(EthereumHardfork::Prague))),
786 ("eth:prague", Ok(FoundryHardfork::Ethereum(EthereumHardfork::Prague))),
787 ("ethereum:Osaka", Ok(FoundryHardfork::Ethereum(EthereumHardfork::Osaka))),
788 (" ETH : Prague ", Ok(FoundryHardfork::Ethereum(EthereumHardfork::Prague))),
789 ("t:T5", Ok(FoundryHardfork::Tempo(TempoHardfork::T5))),
790 ("tempo:t5", Ok(FoundryHardfork::Tempo(TempoHardfork::T5))),
791 #[cfg(feature = "optimism")]
792 ("op:Isthmus", Ok(FoundryHardfork::Optimism(OpHardfork::Isthmus))),
793 #[cfg(feature = "optimism")]
794 ("optimism:jovian", Ok(FoundryHardfork::Optimism(OpHardfork::Jovian))),
795 #[cfg(not(feature = "optimism"))]
796 ("op:prague", Err("hardfork namespace 'op' requires the `optimism` feature")),
797 #[cfg(feature = "base")]
798 ("base:azul", Ok(FoundryHardfork::Base(BaseUpgrade::Azul))),
799 #[cfg(not(feature = "base"))]
800 ("base:prague", Err("hardfork namespace 'base' requires the `base` feature")),
801 #[cfg(feature = "monad")]
802 ("m:MonadTen", Ok(FoundryHardfork::Monad(MonadHardfork::MonadTen))),
803 #[cfg(feature = "monad")]
804 ("monad:monadnine", Ok(FoundryHardfork::Monad(MonadHardfork::MonadNine))),
805 #[cfg(not(feature = "monad"))]
806 ("monad:prague", Err("hardfork namespace 'monad' requires the `monad` feature")),
807 ("foo:prague", Err("unknown hardfork namespace 'foo'")),
808 ("Foo:Prague", Err("unknown hardfork namespace 'foo'")),
809 ];
810
811 for (input, expected) in cases {
812 assert_eq!(
813 input.parse::<FoundryHardfork>(),
814 expected.map_err(str::to_string),
815 "{input}"
816 );
817 }
818 }
819
820 #[test]
821 #[cfg(feature = "monad")]
822 fn test_monad_hardfork_serialization() {
823 assert_eq!(
824 String::from(FoundryHardfork::Monad(MonadHardfork::MonadEight)),
825 "monad:MonadEight"
826 );
827 assert_eq!(FoundryHardfork::Monad(MonadHardfork::MonadEight).namespace(), Some("monad"));
828 assert_eq!(FoundryHardfork::Monad(MonadHardfork::MonadEight).name(), "MonadEight");
829 }
830
831 #[test]
832 #[cfg(feature = "monad")]
833 fn test_monad_hardfork_spec_id_mapping() {
834 assert_eq!(SpecId::from(FoundryHardfork::Monad(MonadHardfork::MonadEight)), SpecId::PRAGUE);
835 assert_eq!(SpecId::from(FoundryHardfork::Monad(MonadHardfork::MonadNine)), SpecId::OSAKA);
836 assert_eq!(SpecId::from(FoundryHardfork::Monad(MonadHardfork::MonadTen)), SpecId::OSAKA);
837 assert_eq!(
838 MonadHardfork::from(FoundryHardfork::Monad(MonadHardfork::MonadNext)),
839 MonadHardfork::MonadNext
840 );
841 }
842
843 #[test]
844 fn test_tempo_hardfork_from_chain_and_timestamp() {
845 assert_eq!(
846 FoundryHardfork::from_chain_and_timestamp(4217, u64::MAX),
847 Some(FoundryHardfork::Tempo(TempoHardfork::T12))
848 );
849 assert_eq!(
850 FoundryHardfork::from_chain_and_timestamp(42431, u64::MAX),
851 Some(FoundryHardfork::Tempo(TempoHardfork::T12))
852 );
853
854 assert_eq!(
855 FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T6_TIMESTAMP - 1),
856 Some(FoundryHardfork::Tempo(TempoHardfork::T5))
857 );
858 assert_eq!(
859 FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T6_TIMESTAMP),
860 Some(FoundryHardfork::Tempo(TempoHardfork::T6))
861 );
862 assert_eq!(
863 FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T7_TIMESTAMP - 1),
864 Some(FoundryHardfork::Tempo(TempoHardfork::T6))
865 );
866 assert_eq!(
867 FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T7_TIMESTAMP),
868 Some(FoundryHardfork::Tempo(TempoHardfork::T7))
869 );
870 assert_eq!(
871 FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T8_TIMESTAMP - 1),
872 Some(FoundryHardfork::Tempo(TempoHardfork::T7))
873 );
874 assert_eq!(
875 FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T8_TIMESTAMP),
876 Some(FoundryHardfork::Tempo(TempoHardfork::T8))
877 );
878 assert_eq!(
879 FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T9_TIMESTAMP - 1),
880 Some(FoundryHardfork::Tempo(TempoHardfork::T8))
881 );
882 assert_eq!(
883 FoundryHardfork::from_chain_and_timestamp(MAINNET_CHAIN_ID, MAINNET_T9_TIMESTAMP),
884 Some(FoundryHardfork::Tempo(TempoHardfork::T9))
885 );
886 assert_eq!(
887 FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T6_TIMESTAMP - 1),
888 Some(FoundryHardfork::Tempo(TempoHardfork::T5))
889 );
890 assert_eq!(
891 FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T6_TIMESTAMP),
892 Some(FoundryHardfork::Tempo(TempoHardfork::T6))
893 );
894 assert_eq!(
895 FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T7_TIMESTAMP - 1),
896 Some(FoundryHardfork::Tempo(TempoHardfork::T6))
897 );
898 assert_eq!(
899 FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T7_TIMESTAMP),
900 Some(FoundryHardfork::Tempo(TempoHardfork::T7))
901 );
902 assert_eq!(
903 FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T8_TIMESTAMP - 1),
904 Some(FoundryHardfork::Tempo(TempoHardfork::T7))
905 );
906 assert_eq!(
907 FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T8_TIMESTAMP),
908 Some(FoundryHardfork::Tempo(TempoHardfork::T8))
909 );
910 assert_eq!(
911 FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T9_TIMESTAMP - 1),
912 Some(FoundryHardfork::Tempo(TempoHardfork::T8))
913 );
914 assert_eq!(
915 FoundryHardfork::from_chain_and_timestamp(MODERATO_CHAIN_ID, MODERATO_T9_TIMESTAMP),
916 Some(FoundryHardfork::Tempo(TempoHardfork::T9))
917 );
918 }
919
920 #[test]
921 #[cfg(feature = "monad")]
922 fn test_monad_hardfork_from_chain_and_timestamp() {
923 let mainnet_ten = MonadHardfork::MonadTen.mainnet_activation_timestamp().unwrap();
924 let testnet_ten = MonadHardfork::MonadTen.testnet_activation_timestamp().unwrap();
925
926 assert_eq!(
927 FoundryHardfork::from_chain_and_timestamp(
928 monad_revm::MONAD_MAINNET_CHAIN_ID,
929 1_763_648_999,
930 ),
931 Some(FoundryHardfork::Monad(MonadHardfork::MonadEight))
932 );
933 assert_eq!(
934 FoundryHardfork::from_chain_and_timestamp(
935 monad_revm::MONAD_MAINNET_CHAIN_ID,
936 1_773_930_600,
937 ),
938 Some(FoundryHardfork::Monad(MonadHardfork::MonadNine))
939 );
940 assert_eq!(
941 FoundryHardfork::from_chain_and_timestamp(
942 monad_revm::MONAD_MAINNET_CHAIN_ID,
943 mainnet_ten - 1,
944 ),
945 Some(FoundryHardfork::Monad(MonadHardfork::MonadNine))
946 );
947 assert_eq!(
948 FoundryHardfork::from_chain_and_timestamp(
949 monad_revm::MONAD_MAINNET_CHAIN_ID,
950 mainnet_ten,
951 ),
952 Some(FoundryHardfork::Monad(MonadHardfork::MonadTen))
953 );
954 assert_eq!(
955 FoundryHardfork::from_chain_and_timestamp(
956 monad_revm::MONAD_TESTNET_CHAIN_ID,
957 1_773_152_999,
958 ),
959 Some(FoundryHardfork::Monad(MonadHardfork::MonadEight))
960 );
961 assert_eq!(
962 FoundryHardfork::from_chain_and_timestamp(
963 monad_revm::MONAD_TESTNET_CHAIN_ID,
964 1_773_153_000,
965 ),
966 Some(FoundryHardfork::Monad(MonadHardfork::MonadNine))
967 );
968 assert_eq!(
969 FoundryHardfork::from_chain_and_timestamp(
970 monad_revm::MONAD_TESTNET_CHAIN_ID,
971 testnet_ten - 1,
972 ),
973 Some(FoundryHardfork::Monad(MonadHardfork::MonadNine))
974 );
975 assert_eq!(
976 FoundryHardfork::from_chain_and_timestamp(
977 monad_revm::MONAD_TESTNET_CHAIN_ID,
978 testnet_ten,
979 ),
980 Some(FoundryHardfork::Monad(MonadHardfork::MonadTen))
981 );
982 }
983
984 #[test]
985 fn test_evm_spec_id_from_str_parses_network_hardforks() {
986 assert_eq!(evm_spec_id_from_str::<TempoHardfork>("T3"), Some(TempoHardfork::T3));
987 assert_eq!(evm_spec_id_from_str::<TempoHardfork>("tempo:T2"), Some(TempoHardfork::T2));
988 assert_eq!(evm_spec_id_from_str::<TempoHardfork>("tempo:T7"), Some(TempoHardfork::T7));
989 assert_eq!(evm_spec_id_from_str::<TempoHardfork>("tempo:T8"), Some(TempoHardfork::T8));
990 assert_eq!(evm_spec_id_from_str::<TempoHardfork>("tempo:T13"), Some(TempoHardfork::T13));
991 assert_eq!(evm_spec_id_from_str::<TempoHardfork>("T13"), Some(TempoHardfork::T13));
992 assert_eq!(evm_spec_id_from_str::<TempoHardfork>("tempo:T14"), Some(TempoHardfork::T14));
993 assert_eq!(evm_spec_id_from_str::<TempoHardfork>("T14"), Some(TempoHardfork::T14));
994 assert_eq!(evm_spec_id_from_str::<TempoHardfork>("ethereum:prague"), None);
995
996 #[cfg(feature = "monad")]
997 {
998 assert_eq!(
999 evm_spec_id_from_str::<MonadHardfork>("MonadNine"),
1000 Some(MonadHardfork::MonadNine)
1001 );
1002 assert_eq!(
1003 evm_spec_id_from_str::<MonadHardfork>("MonadTen"),
1004 Some(MonadHardfork::MonadTen)
1005 );
1006 assert_eq!(
1007 evm_spec_id_from_str::<MonadHardfork>("monad:MonadEight"),
1008 Some(MonadHardfork::MonadEight)
1009 );
1010 assert_eq!(evm_spec_id_from_str::<MonadHardfork>("tempo:T3"), None);
1011 }
1012 }
1013
1014 #[test]
1015 fn test_hardfork_from_block_tag_numbers() {
1016 assert_eq!(
1017 ethereum_hardfork_from_block_tag(MAINNET_HOMESTEAD_BLOCK - 1),
1018 EthereumHardfork::Frontier
1019 );
1020 assert_eq!(
1021 ethereum_hardfork_from_block_tag(MAINNET_LONDON_BLOCK + 1),
1022 EthereumHardfork::London
1023 );
1024 }
1025
1026 #[test]
1027 fn test_from_chain_and_timestamp_ethereum_mainnet() {
1028 assert_eq!(
1029 FoundryHardfork::from_chain_and_timestamp(1, 0),
1030 Some(FoundryHardfork::Ethereum(EthereumHardfork::Frontier))
1031 );
1032 assert_eq!(
1034 FoundryHardfork::from_chain_and_timestamp(1, 1_681_338_455),
1035 Some(FoundryHardfork::Ethereum(EthereumHardfork::Shanghai))
1036 );
1037 }
1038
1039 #[test]
1040 fn test_from_chain_and_timestamp_sepolia() {
1041 let sepolia_chain_id = 11155111;
1042 assert!(FoundryHardfork::from_chain_and_timestamp(sepolia_chain_id, u64::MAX).is_some());
1043 }
1044
1045 #[test]
1046 fn test_from_chain_and_timestamp_unknown_chain() {
1047 assert_eq!(FoundryHardfork::from_chain_and_timestamp(999999, 0), None);
1048 }
1049
1050 #[cfg(feature = "base")]
1051 mod base {
1052 use super::*;
1053 use base_common_genesis::UpgradeConfig;
1054
1055 #[test]
1056 fn test_base_hardfork_serialization() {
1057 assert_eq!(String::from(FoundryHardfork::Base(BaseUpgrade::Azul)), "base:Azul");
1058 assert_eq!(FoundryHardfork::Base(BaseUpgrade::Azul).namespace(), Some("base"));
1059 assert_eq!(FoundryHardfork::Base(BaseUpgrade::Azul).name(), "Azul");
1060 }
1061
1062 #[test]
1063 fn test_base_hardfork_spec_id_mapping() {
1064 assert_eq!(SpecId::from(FoundryHardfork::Base(BaseUpgrade::Azul)), SpecId::OSAKA);
1065 assert_eq!(SpecId::from(FoundryHardfork::Base(BaseUpgrade::Jovian)), SpecId::PRAGUE);
1066 assert_eq!(
1067 BaseSpecId::from(FoundryHardfork::Base(BaseUpgrade::Azul)),
1068 BaseSpecId::new(BaseUpgrade::Azul)
1069 );
1070 }
1071
1072 #[test]
1073 fn test_base_hardfork_parsing() {
1074 assert_eq!(
1075 "base:Azul".parse::<FoundryHardfork>().unwrap(),
1076 FoundryHardfork::Base(BaseUpgrade::Azul)
1077 );
1078 assert_eq!(
1079 "base:Beryl".parse::<FoundryHardfork>().unwrap(),
1080 FoundryHardfork::Base(BaseUpgrade::Beryl)
1081 );
1082 }
1083
1084 #[test]
1085 fn test_base_hardfork_from_chain_and_timestamp() {
1086 let mainnet_config = UpgradeConfig::BASE_MAINNET;
1087 let sepolia_config = UpgradeConfig::BASE_SEPOLIA;
1088 assert_eq!(
1089 FoundryHardfork::from_chain_and_timestamp(8453, u64::MAX),
1090 Some(FoundryHardfork::Base(BaseUpgrade::Beryl))
1091 );
1092 assert_eq!(
1093 FoundryHardfork::from_chain_and_timestamp(84532, u64::MAX),
1094 Some(FoundryHardfork::Base(BaseUpgrade::Beryl))
1095 );
1096 assert_eq!(
1097 FoundryHardfork::from_chain_and_timestamp(8453, mainnet_config.base.azul.unwrap()),
1098 Some(FoundryHardfork::Base(BaseUpgrade::Azul))
1099 );
1100 assert_eq!(
1101 FoundryHardfork::from_chain_and_timestamp(84532, sepolia_config.base.azul.unwrap()),
1102 Some(FoundryHardfork::Base(BaseUpgrade::Azul))
1103 );
1104 assert_eq!(
1105 FoundryHardfork::from_chain_and_timestamp(
1106 8453,
1107 mainnet_config.base.azul.unwrap() - 1
1108 ),
1109 Some(FoundryHardfork::Base(BaseUpgrade::Jovian))
1110 );
1111 assert_eq!(
1112 FoundryHardfork::from_chain_and_timestamp(
1113 84532,
1114 sepolia_config.base.azul.unwrap() - 1
1115 ),
1116 Some(FoundryHardfork::Base(BaseUpgrade::Jovian))
1117 );
1118 }
1119
1120 #[test]
1121 fn test_evm_spec_id_from_str_parses_base_upgrades() {
1122 assert_eq!(
1123 evm_spec_id_from_str::<BaseSpecId>("Azul"),
1124 Some(BaseSpecId::new(BaseUpgrade::Azul))
1125 );
1126 assert_eq!(
1127 evm_spec_id_from_str::<BaseSpecId>("base:Beryl"),
1128 Some(BaseSpecId::new(BaseUpgrade::Beryl))
1129 );
1130 assert_eq!(evm_spec_id_from_str::<BaseSpecId>("tempo:T3"), None);
1131 }
1132 }
1133
1134 #[cfg(feature = "optimism")]
1135 mod optimism {
1136 use super::*;
1137
1138 #[cfg(feature = "base")]
1139 use base_common_genesis::UpgradeConfig;
1140
1141 #[test]
1142 fn test_optimism_spec_id_mapping() {
1143 assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Bedrock), OpSpecId::BEDROCK);
1144 assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Regolith), OpSpecId::REGOLITH);
1145
1146 assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Holocene), OpSpecId::HOLOCENE);
1148 assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Karst), OpSpecId::KARST);
1149 assert_eq!(spec_id_from_optimism_hardfork(OpHardfork::Lagoon), OpSpecId::LAGOON);
1150 assert_eq!(eth_spec_id_from_optimism_hardfork(OpHardfork::Jovian), SpecId::PRAGUE);
1151 assert_eq!(eth_spec_id_from_optimism_hardfork(OpHardfork::Karst), SpecId::OSAKA);
1152 assert_eq!(eth_spec_id_from_optimism_hardfork(OpHardfork::Lagoon), SpecId::OSAKA);
1153 assert_eq!(evm_spec_id::<OpSpecId>(EvmVersion::Osaka), OpSpecId::KARST);
1154 }
1155
1156 #[test]
1157 fn test_from_chain_and_timestamp_op_mainnet() {
1158 let op_chain_id = 10;
1159 assert!(matches!(
1160 FoundryHardfork::from_chain_and_timestamp(op_chain_id, u64::MAX),
1161 Some(FoundryHardfork::Optimism(_))
1162 ));
1163 }
1164
1165 #[test]
1166 #[cfg(feature = "base")]
1167 fn test_base_chain_historical_hardfork_is_execution_family_specific() {
1168 let chains = [
1169 (8453, UpgradeConfig::BASE_MAINNET.ecotone_time.unwrap()),
1170 (84532, UpgradeConfig::BASE_SEPOLIA.ecotone_time.unwrap()),
1171 ];
1172
1173 for (chain_id, timestamp) in chains {
1174 assert_eq!(
1175 FoundryHardfork::from_chain_and_timestamp(chain_id, timestamp),
1176 Some(FoundryHardfork::Base(BaseUpgrade::Ecotone))
1177 );
1178 assert_eq!(
1179 BaseSpecId::historical_hardfork(chain_id, timestamp),
1180 Some(FoundryHardfork::Base(BaseUpgrade::Ecotone))
1181 );
1182 assert_eq!(
1183 OpSpecId::historical_hardfork(chain_id, timestamp),
1184 Some(FoundryHardfork::Optimism(OpHardfork::Ecotone))
1185 );
1186 assert_eq!(
1187 OpSpecId::fork_hardfork(chain_id, timestamp, None),
1188 Some(FoundryHardfork::Optimism(OpHardfork::Ecotone))
1189 );
1190 }
1191 }
1192
1193 #[test]
1196 #[cfg(not(feature = "base"))]
1197 fn test_from_chain_and_timestamp_base() {
1198 let base_chain_id = 8453;
1199 assert!(matches!(
1200 FoundryHardfork::from_chain_and_timestamp(base_chain_id, u64::MAX),
1201 Some(FoundryHardfork::Optimism(_))
1202 ));
1203 }
1204 }
1205}