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::PrecompilesMap;
14use alloy_primitives::{Address, ChainId, map::AddressHashMap};
15use clap::Parser;
16use foundry_evm_hardforks::FoundryHardfork;
17use serde::{Deserialize, Serialize};
18use std::collections::BTreeMap;
19
20pub mod celo;
21
22#[cfg(feature = "optimism")]
23mod optimism;
24
25#[derive(
26 Clone,
27 Copy,
28 Debug,
29 Default,
30 PartialEq,
31 Eq,
32 PartialOrd,
33 Ord,
34 Hash,
35 Serialize,
36 Deserialize,
37 clap::ValueEnum,
38)]
39#[serde(rename_all = "lowercase")]
40#[clap(rename_all = "lowercase")]
41pub enum NetworkVariant {
42 #[default]
43 Ethereum,
44 #[cfg(feature = "optimism")]
45 Optimism,
46 Tempo,
47}
48
49impl std::str::FromStr for NetworkVariant {
50 type Err = String;
51
52 fn from_str(s: &str) -> Result<Self, Self::Err> {
53 match s {
54 "ethereum" => Ok(Self::Ethereum),
55 #[cfg(feature = "optimism")]
56 "optimism" => Ok(Self::Optimism),
57 "tempo" => Ok(Self::Tempo),
58 _ => Err(format!("unknown network variant: {s}")),
59 }
60 }
61}
62
63impl NetworkVariant {
64 pub const fn name(&self) -> &'static str {
65 match self {
66 Self::Ethereum => "ethereum",
67 #[cfg(feature = "optimism")]
68 Self::Optimism => "optimism",
69 Self::Tempo => "tempo",
70 }
71 }
72}
73
74impl std::fmt::Display for NetworkVariant {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 f.write_str(self.name())
77 }
78}
79
80impl From<ChainId> for NetworkVariant {
81 fn from(chain_id: ChainId) -> Self {
82 let chain = Chain::from_id(chain_id);
83 if chain.is_tempo() {
84 return Self::Tempo;
85 }
86 #[cfg(feature = "optimism")]
87 if chain.is_optimism() {
88 return Self::Optimism;
89 }
90 Self::Ethereum
91 }
92}
93
94#[derive(Clone, Debug, Default, Parser, Deserialize, Copy, PartialEq, Eq)]
95pub struct NetworkConfigs {
96 #[arg(help_heading = "Networks", long, short, num_args = 1, value_name = "NETWORK", value_enum, conflicts_with_all = ["celo", "tempo"])]
98 #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
99 #[serde(default)]
100 pub(crate) network: Option<NetworkVariant>,
101 #[arg(help_heading = "Networks", long, conflicts_with_all = ["network", "tempo"])]
103 #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
104 celo: bool,
105 #[cfg(feature = "optimism")]
107 #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "tempo"])]
108 #[serde(default)]
111 pub(crate) optimism: bool,
112 #[arg(long, hide = true, conflicts_with_all = ["network", "celo"])]
114 #[cfg_attr(feature = "optimism", arg(conflicts_with = "optimism"))]
115 #[serde(default)]
118 tempo: bool,
119 #[arg(skip)]
121 #[serde(default)]
122 bypass_prevrandao: bool,
123}
124
125impl Serialize for NetworkConfigs {
130 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
131 use serde::ser::SerializeStruct;
132 let mut s = serializer.serialize_struct("NetworkConfigs", 3)?;
133 s.serialize_field("network", &self.resolved_network())?;
134 s.serialize_field("celo", &self.celo)?;
135 s.serialize_field("bypass_prevrandao", &self.bypass_prevrandao)?;
136 s.end()
137 }
138}
139
140impl NetworkConfigs {
141 pub fn with_celo() -> Self {
142 Self { celo: true, ..Default::default() }
143 }
144
145 pub fn with_tempo() -> Self {
146 Self { network: Some(NetworkVariant::Tempo), tempo: true, ..Default::default() }
147 }
148
149 pub const fn is_tempo(&self) -> bool {
150 matches!(self.resolved_network(), Some(NetworkVariant::Tempo))
151 }
152
153 pub const fn is_celo(&self) -> bool {
154 self.celo
155 }
156
157 const fn resolved_network(&self) -> Option<NetworkVariant> {
159 if let Some(n) = self.network {
160 return Some(n);
161 }
162 #[cfg(feature = "optimism")]
163 if self.optimism {
164 return Some(NetworkVariant::Optimism);
165 }
166 if self.tempo {
167 return Some(NetworkVariant::Tempo);
168 }
169 None
170 }
171
172 pub fn active_network_name(&self) -> Option<&'static str> {
174 self.resolved_network().and_then(|n| match n {
175 NetworkVariant::Ethereum => None,
176 _ => Some(n.name()),
177 })
178 }
179
180 pub fn base_fee_params(&self, timestamp: u64) -> BaseFeeParams {
185 #[cfg(feature = "optimism")]
186 if self.is_optimism() {
187 return self.op_base_fee_params(timestamp);
188 }
189 let _ = timestamp;
190 BaseFeeParams::ethereum()
191 }
192
193 pub fn bypass_prevrandao(&self, chain_id: u64) -> bool {
194 if let Ok(
195 Moonbeam | Moonbase | Moonriver | MoonbeamDev | Rsk | RskTestnet | Gnosis | Chiado,
196 ) = NamedChain::try_from(chain_id)
197 {
198 return true;
199 }
200 self.bypass_prevrandao
201 }
202
203 pub fn with_chain_id(self, chain_id: u64) -> Self {
204 let chain = Chain::from_id(chain_id);
205 if self.resolved_network().is_some() {
206 return if !self.celo
207 && matches!(chain.named(), Some(NamedChain::Celo | NamedChain::CeloSepolia))
208 {
209 Self::with_celo()
210 } else {
211 self
212 };
213 }
214 if chain.is_tempo() {
215 return Self::with_tempo();
216 }
217 #[cfg(feature = "optimism")]
218 if chain.is_optimism() {
219 return Self::with_optimism();
220 }
221 self
222 }
223
224 pub fn normalize_for_hardfork(self, hardfork: FoundryHardfork) -> Result<Self, String> {
229 if let Some(configured) =
230 self.active_network_name().filter(|&n| Some(n) != hardfork.namespace())
231 {
232 return Err(format!(
233 "hardfork `{}` conflicts with network config `{configured}`",
234 String::from(hardfork),
235 ));
236 }
237
238 let network = match hardfork {
239 FoundryHardfork::Ethereum(_) => self,
240 FoundryHardfork::Tempo(_) => Self::with_tempo(),
241 #[cfg(feature = "optimism")]
242 FoundryHardfork::Optimism(_) => Self::with_optimism(),
243 };
244
245 Ok(network)
246 }
247
248 pub fn inject_precompiles(self, precompiles: &mut PrecompilesMap) {
250 if self.celo {
251 precompiles.apply_precompile(&CELO_TRANSFER_ADDRESS, move |_| {
252 Some(celo::transfer::precompile())
253 });
254 }
255 }
256
257 pub fn precompiles_label(self) -> AddressHashMap<String> {
259 let mut labels = AddressHashMap::default();
260 if self.celo {
261 labels.insert(CELO_TRANSFER_ADDRESS, CELO_TRANSFER_LABEL.to_string());
262 }
263 labels
264 }
265
266 pub fn precompiles(self) -> BTreeMap<String, Address> {
268 let mut precompiles = BTreeMap::new();
269 if self.celo {
270 precompiles
271 .insert(PRECOMPILE_ID_CELO_TRANSFER.name().to_string(), CELO_TRANSFER_ADDRESS);
272 }
273 precompiles
274 }
275}
276
277impl From<NetworkVariant> for NetworkConfigs {
278 fn from(network: NetworkVariant) -> Self {
279 match network {
280 NetworkVariant::Ethereum => Self::default(),
281 NetworkVariant::Tempo => {
282 Self { network: Some(network), tempo: true, ..Default::default() }
283 }
284 #[cfg(feature = "optimism")]
285 NetworkVariant::Optimism => {
286 Self { network: Some(network), optimism: true, ..Default::default() }
287 }
288 }
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
299 fn new_tempo_flag_equivalent_to_legacy() {
300 let via_new = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
301 let via_old = NetworkConfigs { tempo: true, ..Default::default() };
302 assert_eq!(via_new.is_tempo(), via_old.is_tempo());
303 assert_eq!(via_new.active_network_name(), via_old.active_network_name());
304 }
305
306 #[test]
309 fn active_network_name_tempo() {
310 let cfg = NetworkConfigs::with_tempo();
311 assert_eq!(cfg.active_network_name(), Some("tempo"));
312 }
313
314 #[test]
315 fn active_network_name_default_is_none() {
316 assert_eq!(NetworkConfigs::default().active_network_name(), None);
317 }
318
319 #[test]
322 fn serde_roundtrip_tempo() {
323 let original = NetworkConfigs::with_tempo();
324 let json = serde_json::to_string(&original).unwrap();
325 let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
326 assert!(restored.is_tempo());
327 }
328
329 #[test]
330 fn serde_legacy_tempo_bool_deserialized() {
331 let json = r#"{"tempo": true, "celo": false, "bypass_prevrandao": false}"#;
333 let cfg: NetworkConfigs = serde_json::from_str(json).unwrap();
334 assert!(cfg.is_tempo());
335 }
336
337 #[test]
338 fn serde_serializes_legacy_alias_as_canonical_network() {
339 let cfg = NetworkConfigs { tempo: true, ..Default::default() };
342 let json = serde_json::to_value(cfg).unwrap();
343 assert_eq!(json["network"], serde_json::json!("tempo"));
344 assert!(json.get("tempo").is_none(), "legacy `tempo` key should not be serialized");
345 assert!(json.get("optimism").is_none(), "legacy `optimism` key should not be serialized");
346 }
347
348 #[test]
349 fn serde_new_network_field_deserialized() {
350 let json_tempo = r#"{"network": "tempo", "celo": false, "bypass_prevrandao": false}"#;
351 let cfg_tempo: NetworkConfigs = serde_json::from_str(json_tempo).unwrap();
352 assert!(cfg_tempo.is_tempo());
353 }
354
355 #[cfg(feature = "optimism")]
356 mod optimism {
357 use super::*;
358
359 #[test]
360 fn new_optimism_flag_equivalent_to_legacy() {
361 let via_new =
362 NetworkConfigs { network: Some(NetworkVariant::Optimism), ..Default::default() };
363 let via_old = NetworkConfigs { optimism: true, ..Default::default() };
364 assert_eq!(via_new.is_optimism(), via_old.is_optimism());
365 assert_eq!(via_new.is_tempo(), via_old.is_tempo());
366 assert_eq!(via_new.active_network_name(), via_old.active_network_name());
367 }
368
369 #[test]
370 fn active_network_name_optimism() {
371 let cfg = NetworkConfigs::with_optimism();
372 assert_eq!(cfg.active_network_name(), Some("optimism"));
373 }
374
375 #[test]
376 fn new_flag_wins_over_legacy_when_both_set() {
377 let cfg = NetworkConfigs {
379 network: Some(NetworkVariant::Optimism),
380 tempo: true,
381 ..Default::default()
382 };
383 assert!(cfg.is_optimism());
384 assert!(!cfg.is_tempo());
385 }
386
387 #[test]
388 fn serde_roundtrip_optimism() {
389 let original = NetworkConfigs::with_optimism();
390 let json = serde_json::to_string(&original).unwrap();
391 let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
392 assert!(restored.is_optimism());
393 assert!(!restored.is_tempo());
394 }
395
396 #[test]
397 fn serde_optimism_field_deserialized() {
398 let json_optimism =
399 r#"{"network": "optimism", "celo": false, "bypass_prevrandao": false}"#;
400 let cfg_optimism: NetworkConfigs = serde_json::from_str(json_optimism).unwrap();
401 assert!(cfg_optimism.is_optimism());
402 }
403 }
404}