1use std::collections::HashMap;
14use std::sync::OnceLock;
15
16use serde::Deserialize;
17
18#[derive(Debug, Clone, Deserialize)]
20pub struct ChainFeature {
21 pub name: String,
23}
24
25#[derive(Debug, Clone, Deserialize)]
27pub struct ChainCurrency {
28 pub name: String,
30 pub symbol: String,
32 pub decimals: i64,
34}
35
36#[derive(Debug, Clone, Deserialize)]
38pub struct ChainEns {
39 pub registry: String,
41}
42
43#[derive(Debug, Clone, Deserialize)]
45pub struct ChainExplorer {
46 pub name: String,
48 pub url: String,
50 #[serde(default)]
52 pub standard: String,
53}
54
55#[derive(Debug, Clone, Deserialize)]
57pub struct ChainInfo {
58 pub name: String,
60 #[serde(default)]
62 pub chain: String,
63 #[serde(default)]
65 pub icon: String,
66 #[serde(default)]
68 pub rpc: Vec<String>,
69 #[serde(default)]
71 pub features: Vec<ChainFeature>,
72 #[serde(default)]
74 pub faucets: Vec<String>,
75 #[serde(rename = "nativeCurrency", default)]
77 pub native_currency: Option<ChainCurrency>,
78 #[serde(rename = "infoURL", default)]
80 pub info_url: String,
81 #[serde(rename = "shortName", default)]
83 pub short_name: String,
84 #[serde(rename = "chainId")]
86 pub chain_id: u64,
87 #[serde(rename = "networkId", default)]
89 pub network_id: u64,
90 #[serde(default)]
92 pub slip44: Option<i64>,
93 #[serde(default)]
95 pub ens: Option<ChainEns>,
96 #[serde(default)]
98 pub explorers: Vec<ChainExplorer>,
99}
100
101impl ChainInfo {
102 pub fn has_feature(&self, feat: &str) -> bool {
104 self.features.iter().any(|f| f.name == feat)
105 }
106
107 pub fn transaction_url(&self, tx_hash: &str) -> Option<String> {
110 self.explorers
111 .first()
112 .map(|e| format!("{}/tx/{}", e.url, tx_hash))
113 }
114
115 pub fn explorer_url(&self) -> Option<&str> {
117 self.explorers.first().map(|e| e.url.as_str())
118 }
119}
120
121const CHAINS_JSON: &str = include_str!("chains_data.json");
123
124fn registry() -> &'static HashMap<u64, ChainInfo> {
125 static REGISTRY: OnceLock<HashMap<u64, ChainInfo>> = OnceLock::new();
126 REGISTRY.get_or_init(|| {
127 let raw: HashMap<String, ChainInfo> =
128 serde_json::from_str(CHAINS_JSON).expect("embedded chains_data.json must be valid");
129 raw.into_iter()
130 .filter_map(|(k, v)| k.parse::<u64>().ok().map(|id| (id, v)))
131 .collect()
132 })
133}
134
135pub fn get(id: u64) -> Option<&'static ChainInfo> {
137 registry().get(&id)
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn ethereum_mainnet() {
146 let ci = get(1).expect("chain 1");
147 assert_eq!(ci.name, "Ethereum Mainnet");
148 assert_eq!(ci.chain_id, 1);
149 assert_eq!(ci.native_currency.as_ref().unwrap().symbol, "ETH");
150 }
151
152 #[test]
153 fn cached_same_pointer() {
154 let a = get(1).unwrap();
155 let b = get(1).unwrap();
156 assert!(std::ptr::eq(a, b));
157 }
158
159 #[test]
160 fn unknown_chain() {
161 assert!(get(0).is_none());
162 }
163
164 #[test]
165 fn features() {
166 let ci = get(1).unwrap();
167 assert!(ci.has_feature("EIP155"));
168 assert!(ci.has_feature("EIP1559"));
169 assert!(!ci.has_feature("nonexistent"));
170 }
171
172 #[test]
173 fn transaction_and_explorer_urls() {
174 let ci = get(1).unwrap();
175 assert_eq!(
176 ci.transaction_url("0xabc123").unwrap(),
177 "https://etherscan.io/tx/0xabc123"
178 );
179 assert_eq!(ci.explorer_url().unwrap(), "https://etherscan.io");
180 }
181
182 #[test]
183 fn no_explorer() {
184 let ci = ChainInfo {
185 name: "Test".to_string(),
186 chain: String::new(),
187 icon: String::new(),
188 rpc: vec![],
189 features: vec![],
190 faucets: vec![],
191 native_currency: None,
192 info_url: String::new(),
193 short_name: String::new(),
194 chain_id: 0,
195 network_id: 0,
196 slip44: None,
197 ens: None,
198 explorers: vec![],
199 };
200 assert!(ci.transaction_url("0xabc").is_none());
201 assert!(ci.explorer_url().is_none());
202 }
203}