use crate::core::{ChainId, ForgeGuardError};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChainInfo {
pub name: String,
pub chain_id: u64,
pub currency: String,
pub explorer_url: String,
pub rpc_urls: Vec<String>,
pub is_evm: bool,
pub supported: bool,
}
pub trait ChainAnalyzer: Send + Sync {
fn chain_id(&self) -> ChainId;
fn info(&self) -> ChainInfo;
fn analyze_deployment(&self, _contract_address: &str) -> Result<Vec<String>, ForgeGuardError> {
Ok(Vec::new())
}
fn estimate_gas(&self, _gas_used: u64) -> Result<f64, ForgeGuardError> {
Ok(0.0)
}
}
#[derive(Clone)]
pub struct ChainRegistry {
chains: HashMap<ChainId, ChainInfo>,
}
impl Default for ChainRegistry {
fn default() -> Self {
let mut registry = Self {
chains: HashMap::new(),
};
registry.register_defaults();
registry
}
}
impl ChainRegistry {
fn register_defaults(&mut self) {
let chains = vec![
ChainInfo {
name: "Ethereum".into(),
chain_id: 1,
currency: "ETH".into(),
explorer_url: "https://etherscan.io".into(),
rpc_urls: vec!["https://eth.llamarpc.com".into()],
is_evm: true,
supported: true,
},
ChainInfo {
name: "Base".into(),
chain_id: 8453,
currency: "ETH".into(),
explorer_url: "https://basescan.org".into(),
rpc_urls: vec!["https://base.llamarpc.com".into()],
is_evm: true,
supported: true,
},
ChainInfo {
name: "Arbitrum".into(),
chain_id: 42161,
currency: "ETH".into(),
explorer_url: "https://arbiscan.io".into(),
rpc_urls: vec!["https://arbitrum.llamarpc.com".into()],
is_evm: true,
supported: true,
},
ChainInfo {
name: "Optimism".into(),
chain_id: 10,
currency: "ETH".into(),
explorer_url: "https://optimistic.etherscan.io".into(),
rpc_urls: vec!["https://optimism.llamarpc.com".into()],
is_evm: true,
supported: true,
},
ChainInfo {
name: "Polygon".into(),
chain_id: 137,
currency: "MATIC".into(),
explorer_url: "https://polygonscan.com".into(),
rpc_urls: vec!["https://polygon.llamarpc.com".into()],
is_evm: true,
supported: true,
},
ChainInfo {
name: "BNB Chain".into(),
chain_id: 56,
currency: "BNB".into(),
explorer_url: "https://bscscan.com".into(),
rpc_urls: vec!["https://bsc.llamarpc.com".into()],
is_evm: true,
supported: true,
},
ChainInfo {
name: "Avalanche".into(),
chain_id: 43114,
currency: "AVAX".into(),
explorer_url: "https://snowtrace.io".into(),
rpc_urls: vec!["https://avalanche.llamarpc.com".into()],
is_evm: true,
supported: true,
},
ChainInfo {
name: "Scroll".into(),
chain_id: 534352,
currency: "ETH".into(),
explorer_url: "https://scrollscan.com".into(),
rpc_urls: vec!["https://scroll.llamarpc.com".into()],
is_evm: true,
supported: true,
},
ChainInfo {
name: "Linea".into(),
chain_id: 59144,
currency: "ETH".into(),
explorer_url: "https://lineascan.build".into(),
rpc_urls: vec!["https://linea.llamarpc.com".into()],
is_evm: true,
supported: true,
},
ChainInfo {
name: "Unichain".into(),
chain_id: 130,
currency: "ETH".into(),
explorer_url: "https://uniscan.xyz".into(),
rpc_urls: vec!["https://unichain.llamarpc.com".into()],
is_evm: true,
supported: true,
},
ChainInfo {
name: "ZKSync".into(),
chain_id: 324,
currency: "ETH".into(),
explorer_url: "https://explorer.zksync.io".into(),
rpc_urls: vec!["https://zksync.llamarpc.com".into()],
is_evm: true,
supported: true,
},
ChainInfo {
name: "HyperEVM".into(),
chain_id: 999,
currency: "HYPE".into(),
explorer_url: "https://explorer.hyperliquid.xyz".into(),
rpc_urls: vec!["https://hyperliquid.llamarpc.com".into()],
is_evm: true,
supported: true,
},
ChainInfo {
name: "Monad".into(),
chain_id: 10143,
currency: "MON".into(),
explorer_url: "https://explorer.monad.xyz".into(),
rpc_urls: vec!["https://monad.llamarpc.com".into()],
is_evm: true,
supported: true,
},
ChainInfo {
name: "Sonic".into(),
chain_id: 146,
currency: "S".into(),
explorer_url: "https://sonicscan.org".into(),
rpc_urls: vec!["https://sonic.llamarpc.com".into()],
is_evm: true,
supported: true,
},
ChainInfo {
name: "Blast".into(),
chain_id: 81457,
currency: "ETH".into(),
explorer_url: "https://blastscan.io".into(),
rpc_urls: vec!["https://blast.llamarpc.com".into()],
is_evm: true,
supported: true,
},
ChainInfo {
name: "Mantle".into(),
chain_id: 5000,
currency: "MNT".into(),
explorer_url: "https://mantlescan.xyz".into(),
rpc_urls: vec!["https://mantle.llamarpc.com".into()],
is_evm: true,
supported: true,
},
ChainInfo {
name: "Robinhood".into(),
chain_id: 31753,
currency: "ETH".into(),
explorer_url: "https://explorer.robinhood.com".into(),
rpc_urls: vec!["https://robinhood.llamarpc.com".into()],
is_evm: true,
supported: true,
},
];
for chain in chains {
let id = ChainId::from_name(&chain.name);
self.chains.insert(id, chain);
}
}
pub fn list_chains(&self) -> Result<Vec<ChainInfo>, ForgeGuardError> {
let mut chains: Vec<ChainInfo> = self.chains.values().cloned().collect();
chains.sort_by(|a, b| a.name.cmp(&b.name));
Ok(chains)
}
pub fn get_chain(&self, name: &str) -> Option<&ChainInfo> {
let id = ChainId::from_name(name);
self.chains.get(&id)
}
pub fn register_chain(&mut self, info: ChainInfo) {
let id = ChainId::from_name(&info.name);
self.chains.insert(id, info);
}
pub fn evm_chains(&self) -> Vec<&ChainInfo> {
self.chains.values().filter(|c| c.is_evm).collect()
}
pub fn get_by_chain_id(&self, chain_id: u64) -> Option<&ChainInfo> {
self.chains.values().find(|c| c.chain_id == chain_id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_registry_default() {
let registry = ChainRegistry::default();
let chains = registry.list_chains().unwrap();
assert_eq!(chains.len(), 17, "Should have 17 supported chains");
}
#[test]
fn test_get_chain_by_name() {
let registry = ChainRegistry::default();
let eth = registry.get_chain("ethereum").unwrap();
assert_eq!(eth.chain_id, 1);
assert_eq!(eth.currency, "ETH");
assert!(eth.is_evm);
assert!(eth.supported);
let base = registry.get_chain("base").unwrap();
assert_eq!(base.chain_id, 8453);
}
#[test]
fn test_get_chain_not_found() {
let registry = ChainRegistry::default();
assert!(registry.get_chain("nonexistent-chain").is_none());
}
#[test]
fn test_get_by_chain_id() {
let registry = ChainRegistry::default();
let chain = registry.get_by_chain_id(1).unwrap();
assert_eq!(chain.name, "Ethereum");
let chain = registry.get_by_chain_id(8453).unwrap();
assert_eq!(chain.name, "Base");
}
#[test]
fn test_evm_chains() {
let registry = ChainRegistry::default();
let evm = registry.evm_chains();
assert_eq!(evm.len(), 17, "All 17 chains should be EVM");
assert!(evm.iter().all(|c| c.is_evm));
}
#[test]
fn test_register_custom_chain() {
let mut registry = ChainRegistry::default();
let custom = ChainInfo {
name: "CustomChain".into(),
chain_id: 99999,
currency: "CST".into(),
explorer_url: "https://example.com".into(),
rpc_urls: vec!["https://rpc.example.com".into()],
is_evm: true,
supported: true,
};
registry.register_chain(custom.clone());
let retrieved = registry.get_chain("CustomChain").unwrap();
assert_eq!(retrieved.chain_id, 99999);
}
#[test]
fn test_chain_name_display() {
let registry = ChainRegistry::default();
let eth = registry.get_chain("ethereum").unwrap();
assert_eq!(eth.name, "Ethereum");
}
}