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_op_hardforks::{OpChainHardforks, OpHardforks};
15use alloy_primitives::{Address, ChainId, map::AddressHashMap};
16use clap::Parser;
17use foundry_evm_hardforks::FoundryHardfork;
18use serde::{Deserialize, Serialize};
19use std::collections::BTreeMap;
20
21pub mod celo;
22
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
24#[serde(rename_all = "lowercase")]
25#[clap(rename_all = "lowercase")]
26pub enum NetworkVariant {
27 #[default]
28 Ethereum,
29 Optimism,
30 Tempo,
31}
32
33impl NetworkVariant {
34 pub const fn name(&self) -> &'static str {
35 match self {
36 Self::Ethereum => "ethereum",
37 Self::Optimism => "optimism",
38 Self::Tempo => "tempo",
39 }
40 }
41}
42
43impl std::fmt::Display for NetworkVariant {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 f.write_str(self.name())
46 }
47}
48
49impl From<ChainId> for NetworkVariant {
50 fn from(chain_id: ChainId) -> Self {
51 let chain = Chain::from_id(chain_id);
52 if chain.is_tempo() {
53 Self::Tempo
54 } else if chain.is_optimism() {
55 Self::Optimism
56 } else {
57 Self::Ethereum
58 }
59 }
60}
61
62#[derive(Clone, Debug, Default, Parser, Deserialize, Copy, PartialEq, Eq)]
63pub struct NetworkConfigs {
64 #[arg(help_heading = "Networks", long, short, num_args = 1, value_name = "NETWORK", value_enum, conflicts_with_all = ["celo", "optimism", "tempo"])]
66 #[serde(default)]
67 network: Option<NetworkVariant>,
68 #[arg(help_heading = "Networks", long, conflicts_with_all = ["network", "optimism", "tempo"])]
70 celo: bool,
71 #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "tempo"])]
73 #[serde(default)]
76 optimism: bool,
77 #[arg(long, hide = true, conflicts_with_all = ["network", "celo", "optimism"])]
79 #[serde(default)]
82 tempo: bool,
83 #[arg(skip)]
85 #[serde(default)]
86 bypass_prevrandao: bool,
87}
88
89impl Serialize for NetworkConfigs {
94 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
95 use serde::ser::SerializeStruct;
96 let mut s = serializer.serialize_struct("NetworkConfigs", 3)?;
97 s.serialize_field("network", &self.resolved_network())?;
98 s.serialize_field("celo", &self.celo)?;
99 s.serialize_field("bypass_prevrandao", &self.bypass_prevrandao)?;
100 s.end()
101 }
102}
103
104impl NetworkConfigs {
105 pub fn with_optimism() -> Self {
106 Self { network: Some(NetworkVariant::Optimism), optimism: true, ..Default::default() }
107 }
108
109 pub fn with_celo() -> Self {
110 Self { celo: true, ..Default::default() }
111 }
112
113 pub fn with_tempo() -> Self {
114 Self { network: Some(NetworkVariant::Tempo), tempo: true, ..Default::default() }
115 }
116
117 pub fn is_optimism(&self) -> bool {
118 matches!(self.resolved_network(), Some(NetworkVariant::Optimism))
119 }
120
121 pub fn is_tempo(&self) -> bool {
122 matches!(self.resolved_network(), Some(NetworkVariant::Tempo))
123 }
124
125 pub const fn is_celo(&self) -> bool {
126 self.celo
127 }
128
129 fn resolved_network(&self) -> Option<NetworkVariant> {
131 self.network.or(if self.optimism {
132 Some(NetworkVariant::Optimism)
133 } else if self.tempo {
134 Some(NetworkVariant::Tempo)
135 } else {
136 None
137 })
138 }
139
140 pub fn active_network_name(&self) -> Option<&'static str> {
142 self.resolved_network().and_then(|n| match n {
143 NetworkVariant::Ethereum => None,
144 _ => Some(n.name()),
145 })
146 }
147
148 pub fn base_fee_params(&self, timestamp: u64) -> BaseFeeParams {
153 if self.is_optimism() {
154 let op_hardforks = OpChainHardforks::op_mainnet();
155 if op_hardforks.is_canyon_active_at_timestamp(timestamp) {
156 BaseFeeParams::optimism_canyon()
157 } else {
158 BaseFeeParams::optimism()
159 }
160 } else {
161 BaseFeeParams::ethereum()
162 }
163 }
164
165 pub fn bypass_prevrandao(&self, chain_id: u64) -> bool {
166 if let Ok(
167 Moonbeam | Moonbase | Moonriver | MoonbeamDev | Rsk | RskTestnet | Gnosis | Chiado,
168 ) = NamedChain::try_from(chain_id)
169 {
170 return true;
171 }
172 self.bypass_prevrandao
173 }
174
175 pub fn with_chain_id(self, chain_id: u64) -> Self {
176 let chain = Chain::from_id(chain_id);
177 if self.resolved_network().is_none() {
178 if chain.is_tempo() {
179 Self::with_tempo()
180 } else if chain.is_optimism() {
181 Self::with_optimism()
182 } else {
183 self
184 }
185 } else if !self.celo
186 && matches!(chain.named(), Some(NamedChain::Celo | NamedChain::CeloSepolia))
187 {
188 Self::with_celo()
189 } else {
190 self
191 }
192 }
193
194 pub fn normalize_for_hardfork(self, hardfork: FoundryHardfork) -> Result<Self, String> {
199 if let Some(configured) =
200 self.active_network_name().filter(|&n| Some(n) != hardfork.namespace())
201 {
202 return Err(format!(
203 "hardfork `{}` conflicts with network config `{configured}`",
204 String::from(hardfork),
205 ));
206 }
207
208 let network = match hardfork {
209 FoundryHardfork::Ethereum(_) => self,
210 FoundryHardfork::Tempo(_) => Self::with_tempo(),
211 FoundryHardfork::Optimism(_) => Self::with_optimism(),
212 };
213
214 Ok(network)
215 }
216
217 pub fn inject_precompiles(self, precompiles: &mut PrecompilesMap) {
219 if self.celo {
220 precompiles.apply_precompile(&CELO_TRANSFER_ADDRESS, move |_| {
221 Some(celo::transfer::precompile())
222 });
223 }
224 }
225
226 pub fn precompiles_label(self) -> AddressHashMap<String> {
228 let mut labels = AddressHashMap::default();
229 if self.celo {
230 labels.insert(CELO_TRANSFER_ADDRESS, CELO_TRANSFER_LABEL.to_string());
231 }
232 labels
233 }
234
235 pub fn precompiles(self) -> BTreeMap<String, Address> {
237 let mut precompiles = BTreeMap::new();
238 if self.celo {
239 precompiles
240 .insert(PRECOMPILE_ID_CELO_TRANSFER.name().to_string(), CELO_TRANSFER_ADDRESS);
241 }
242 precompiles
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
253 fn new_tempo_flag_equivalent_to_legacy() {
254 let via_new = NetworkConfigs { network: Some(NetworkVariant::Tempo), ..Default::default() };
255 let via_old = NetworkConfigs { tempo: true, ..Default::default() };
256 assert_eq!(via_new.is_tempo(), via_old.is_tempo());
257 assert_eq!(via_new.is_optimism(), via_old.is_optimism());
258 assert_eq!(via_new.active_network_name(), via_old.active_network_name());
259 }
260
261 #[test]
262 fn new_optimism_flag_equivalent_to_legacy() {
263 let via_new =
264 NetworkConfigs { network: Some(NetworkVariant::Optimism), ..Default::default() };
265 let via_old = NetworkConfigs { optimism: true, ..Default::default() };
266 assert_eq!(via_new.is_optimism(), via_old.is_optimism());
267 assert_eq!(via_new.is_tempo(), via_old.is_tempo());
268 assert_eq!(via_new.active_network_name(), via_old.active_network_name());
269 }
270
271 #[test]
274 fn active_network_name_tempo() {
275 let cfg = NetworkConfigs::with_tempo();
276 assert_eq!(cfg.active_network_name(), Some("tempo"));
277 }
278
279 #[test]
280 fn active_network_name_optimism() {
281 let cfg = NetworkConfigs::with_optimism();
282 assert_eq!(cfg.active_network_name(), Some("optimism"));
283 }
284
285 #[test]
286 fn active_network_name_default_is_none() {
287 assert_eq!(NetworkConfigs::default().active_network_name(), None);
288 }
289
290 #[test]
293 fn new_flag_wins_over_legacy_when_both_set() {
294 let cfg = NetworkConfigs {
296 network: Some(NetworkVariant::Optimism),
297 tempo: true,
298 ..Default::default()
299 };
300 assert!(cfg.is_optimism());
301 assert!(!cfg.is_tempo());
302 }
303
304 #[test]
307 fn serde_roundtrip_tempo() {
308 let original = NetworkConfigs::with_tempo();
309 let json = serde_json::to_string(&original).unwrap();
310 let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
311 assert!(restored.is_tempo());
312 assert!(!restored.is_optimism());
313 }
314
315 #[test]
316 fn serde_roundtrip_optimism() {
317 let original = NetworkConfigs::with_optimism();
318 let json = serde_json::to_string(&original).unwrap();
319 let restored: NetworkConfigs = serde_json::from_str(&json).unwrap();
320 assert!(restored.is_optimism());
321 assert!(!restored.is_tempo());
322 }
323
324 #[test]
325 fn serde_legacy_tempo_bool_deserialized() {
326 let json = r#"{"tempo": true, "celo": false, "bypass_prevrandao": false}"#;
328 let cfg: NetworkConfigs = serde_json::from_str(json).unwrap();
329 assert!(cfg.is_tempo());
330 }
331
332 #[test]
333 fn serde_serializes_legacy_alias_as_canonical_network() {
334 let cfg = NetworkConfigs { tempo: true, ..Default::default() };
337 let json = serde_json::to_value(cfg).unwrap();
338 assert_eq!(json["network"], serde_json::json!("tempo"));
339 assert!(json.get("tempo").is_none(), "legacy `tempo` key should not be serialized");
340 assert!(json.get("optimism").is_none(), "legacy `optimism` key should not be serialized");
341 }
342
343 #[test]
344 fn serde_new_network_field_deserialized() {
345 let json_tempo = r#"{"network": "tempo", "celo": false, "bypass_prevrandao": false}"#;
346 let cfg_tempo: NetworkConfigs = serde_json::from_str(json_tempo).unwrap();
347 assert!(cfg_tempo.is_tempo());
348 let json_optimism = r#"{"network": "optimism", "celo": false, "bypass_prevrandao": false}"#;
349 let cfg_optimism: NetworkConfigs = serde_json::from_str(json_optimism).unwrap();
350 assert!(cfg_optimism.is_optimism());
351 }
352}