Skip to main content

forge_guard/chains/
mod.rs

1//! Multi-chain support — chain-specific analyzers and configuration.
2
3use crate::core::{ChainId, ForgeGuardError};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Information about a supported blockchain.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ChainInfo {
10    pub name: String,
11    pub chain_id: u64,
12    pub currency: String,
13    pub explorer_url: String,
14    pub rpc_urls: Vec<String>,
15    pub is_evm: bool,
16    pub supported: bool,
17}
18
19/// A chain-specific analyzer for EVM and non-EVM chains.
20pub trait ChainAnalyzer: Send + Sync {
21    /// The chain this analyzer supports.
22    fn chain_id(&self) -> ChainId;
23
24    /// Get chain information.
25    fn info(&self) -> ChainInfo;
26
27    /// Analyze deployment for this chain's specifics.
28    fn analyze_deployment(&self, _contract_address: &str) -> Result<Vec<String>, ForgeGuardError> {
29        Ok(Vec::new())
30    }
31
32    /// Estimate gas for this chain.
33    fn estimate_gas(&self, _gas_used: u64) -> Result<f64, ForgeGuardError> {
34        Ok(0.0)
35    }
36}
37
38/// Registry of all supported chains and their analyzers.
39#[derive(Clone)]
40pub struct ChainRegistry {
41    chains: HashMap<ChainId, ChainInfo>,
42}
43
44impl Default for ChainRegistry {
45    fn default() -> Self {
46        let mut registry = Self {
47            chains: HashMap::new(),
48        };
49        registry.register_defaults();
50        registry
51    }
52}
53
54impl ChainRegistry {
55    /// Register all default supported chains.
56    fn register_defaults(&mut self) {
57        let chains = vec![
58            ChainInfo {
59                name: "Ethereum".into(),
60                chain_id: 1,
61                currency: "ETH".into(),
62                explorer_url: "https://etherscan.io".into(),
63                rpc_urls: vec!["https://eth.llamarpc.com".into()],
64                is_evm: true,
65                supported: true,
66            },
67            ChainInfo {
68                name: "Base".into(),
69                chain_id: 8453,
70                currency: "ETH".into(),
71                explorer_url: "https://basescan.org".into(),
72                rpc_urls: vec!["https://base.llamarpc.com".into()],
73                is_evm: true,
74                supported: true,
75            },
76            ChainInfo {
77                name: "Arbitrum".into(),
78                chain_id: 42161,
79                currency: "ETH".into(),
80                explorer_url: "https://arbiscan.io".into(),
81                rpc_urls: vec!["https://arbitrum.llamarpc.com".into()],
82                is_evm: true,
83                supported: true,
84            },
85            ChainInfo {
86                name: "Optimism".into(),
87                chain_id: 10,
88                currency: "ETH".into(),
89                explorer_url: "https://optimistic.etherscan.io".into(),
90                rpc_urls: vec!["https://optimism.llamarpc.com".into()],
91                is_evm: true,
92                supported: true,
93            },
94            ChainInfo {
95                name: "Polygon".into(),
96                chain_id: 137,
97                currency: "MATIC".into(),
98                explorer_url: "https://polygonscan.com".into(),
99                rpc_urls: vec!["https://polygon.llamarpc.com".into()],
100                is_evm: true,
101                supported: true,
102            },
103            ChainInfo {
104                name: "BNB Chain".into(),
105                chain_id: 56,
106                currency: "BNB".into(),
107                explorer_url: "https://bscscan.com".into(),
108                rpc_urls: vec!["https://bsc.llamarpc.com".into()],
109                is_evm: true,
110                supported: true,
111            },
112            ChainInfo {
113                name: "Avalanche".into(),
114                chain_id: 43114,
115                currency: "AVAX".into(),
116                explorer_url: "https://snowtrace.io".into(),
117                rpc_urls: vec!["https://avalanche.llamarpc.com".into()],
118                is_evm: true,
119                supported: true,
120            },
121            ChainInfo {
122                name: "Scroll".into(),
123                chain_id: 534352,
124                currency: "ETH".into(),
125                explorer_url: "https://scrollscan.com".into(),
126                rpc_urls: vec!["https://scroll.llamarpc.com".into()],
127                is_evm: true,
128                supported: true,
129            },
130            ChainInfo {
131                name: "Linea".into(),
132                chain_id: 59144,
133                currency: "ETH".into(),
134                explorer_url: "https://lineascan.build".into(),
135                rpc_urls: vec!["https://linea.llamarpc.com".into()],
136                is_evm: true,
137                supported: true,
138            },
139            ChainInfo {
140                name: "Unichain".into(),
141                chain_id: 130,
142                currency: "ETH".into(),
143                explorer_url: "https://uniscan.xyz".into(),
144                rpc_urls: vec!["https://unichain.llamarpc.com".into()],
145                is_evm: true,
146                supported: true,
147            },
148            ChainInfo {
149                name: "ZKSync".into(),
150                chain_id: 324,
151                currency: "ETH".into(),
152                explorer_url: "https://explorer.zksync.io".into(),
153                rpc_urls: vec!["https://zksync.llamarpc.com".into()],
154                is_evm: true,
155                supported: true,
156            },
157            ChainInfo {
158                name: "HyperEVM".into(),
159                chain_id: 999,
160                currency: "HYPE".into(),
161                explorer_url: "https://explorer.hyperliquid.xyz".into(),
162                rpc_urls: vec!["https://hyperliquid.llamarpc.com".into()],
163                is_evm: true,
164                supported: true,
165            },
166            ChainInfo {
167                name: "Monad".into(),
168                chain_id: 10143,
169                currency: "MON".into(),
170                explorer_url: "https://explorer.monad.xyz".into(),
171                rpc_urls: vec!["https://monad.llamarpc.com".into()],
172                is_evm: true,
173                supported: true,
174            },
175            ChainInfo {
176                name: "Sonic".into(),
177                chain_id: 146,
178                currency: "S".into(),
179                explorer_url: "https://sonicscan.org".into(),
180                rpc_urls: vec!["https://sonic.llamarpc.com".into()],
181                is_evm: true,
182                supported: true,
183            },
184            ChainInfo {
185                name: "Blast".into(),
186                chain_id: 81457,
187                currency: "ETH".into(),
188                explorer_url: "https://blastscan.io".into(),
189                rpc_urls: vec!["https://blast.llamarpc.com".into()],
190                is_evm: true,
191                supported: true,
192            },
193            ChainInfo {
194                name: "Mantle".into(),
195                chain_id: 5000,
196                currency: "MNT".into(),
197                explorer_url: "https://mantlescan.xyz".into(),
198                rpc_urls: vec!["https://mantle.llamarpc.com".into()],
199                is_evm: true,
200                supported: true,
201            },
202            ChainInfo {
203                name: "Robinhood".into(),
204                chain_id: 31753,
205                currency: "ETH".into(),
206                explorer_url: "https://explorer.robinhood.com".into(),
207                rpc_urls: vec!["https://robinhood.llamarpc.com".into()],
208                is_evm: true,
209                supported: true,
210            },
211        ];
212
213        for chain in chains {
214            let id = ChainId::from_name(&chain.name);
215            self.chains.insert(id, chain);
216        }
217    }
218
219    /// List all registered chains.
220    pub fn list_chains(&self) -> Result<Vec<ChainInfo>, ForgeGuardError> {
221        let mut chains: Vec<ChainInfo> = self.chains.values().cloned().collect();
222        chains.sort_by(|a, b| a.name.cmp(&b.name));
223        Ok(chains)
224    }
225
226    /// Get chain info by name.
227    pub fn get_chain(&self, name: &str) -> Option<&ChainInfo> {
228        let id = ChainId::from_name(name);
229        self.chains.get(&id)
230    }
231
232    /// Register a custom chain.
233    pub fn register_chain(&mut self, info: ChainInfo) {
234        let id = ChainId::from_name(&info.name);
235        self.chains.insert(id, info);
236    }
237
238    /// Get all EVM-compatible chains.
239    pub fn evm_chains(&self) -> Vec<&ChainInfo> {
240        self.chains.values().filter(|c| c.is_evm).collect()
241    }
242
243    /// Get a chain by its numeric chain ID.
244    pub fn get_by_chain_id(&self, chain_id: u64) -> Option<&ChainInfo> {
245        self.chains.values().find(|c| c.chain_id == chain_id)
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn test_registry_default() {
255        let registry = ChainRegistry::default();
256        let chains = registry.list_chains().unwrap();
257        assert_eq!(chains.len(), 17, "Should have 17 supported chains");
258    }
259
260    #[test]
261    fn test_get_chain_by_name() {
262        let registry = ChainRegistry::default();
263        let eth = registry.get_chain("ethereum").unwrap();
264        assert_eq!(eth.chain_id, 1);
265        assert_eq!(eth.currency, "ETH");
266        assert!(eth.is_evm);
267        assert!(eth.supported);
268
269        let base = registry.get_chain("base").unwrap();
270        assert_eq!(base.chain_id, 8453);
271    }
272
273    #[test]
274    fn test_get_chain_not_found() {
275        let registry = ChainRegistry::default();
276        assert!(registry.get_chain("nonexistent-chain").is_none());
277    }
278
279    #[test]
280    fn test_get_by_chain_id() {
281        let registry = ChainRegistry::default();
282        let chain = registry.get_by_chain_id(1).unwrap();
283        assert_eq!(chain.name, "Ethereum");
284
285        let chain = registry.get_by_chain_id(8453).unwrap();
286        assert_eq!(chain.name, "Base");
287    }
288
289    #[test]
290    fn test_evm_chains() {
291        let registry = ChainRegistry::default();
292        let evm = registry.evm_chains();
293        assert_eq!(evm.len(), 17, "All 17 chains should be EVM");
294        assert!(evm.iter().all(|c| c.is_evm));
295    }
296
297    #[test]
298    fn test_register_custom_chain() {
299        let mut registry = ChainRegistry::default();
300        let custom = ChainInfo {
301            name: "CustomChain".into(),
302            chain_id: 99999,
303            currency: "CST".into(),
304            explorer_url: "https://example.com".into(),
305            rpc_urls: vec!["https://rpc.example.com".into()],
306            is_evm: true,
307            supported: true,
308        };
309        registry.register_chain(custom.clone());
310
311        let retrieved = registry.get_chain("CustomChain").unwrap();
312        assert_eq!(retrieved.chain_id, 99999);
313    }
314
315    #[test]
316    fn test_chain_name_display() {
317        let registry = ChainRegistry::default();
318        let eth = registry.get_chain("ethereum").unwrap();
319        assert_eq!(eth.name, "Ethereum");
320    }
321}