1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::str::FromStr;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
6#[serde(rename_all = "lowercase")]
7pub enum ChainType {
8 Evm,
9 Solana,
10 Cosmos,
11 Bitcoin,
12 Tron,
13 Ton,
14 Spark,
15 Filecoin,
16 Sui,
17}
18
19pub const ALL_CHAIN_TYPES: [ChainType; 8] = [
21 ChainType::Evm,
22 ChainType::Solana,
23 ChainType::Bitcoin,
24 ChainType::Cosmos,
25 ChainType::Tron,
26 ChainType::Ton,
27 ChainType::Filecoin,
28 ChainType::Sui,
29];
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct Chain {
34 pub name: &'static str,
35 pub chain_type: ChainType,
36 pub chain_id: &'static str,
37}
38
39pub const KNOWN_CHAINS: &[Chain] = &[
41 Chain {
42 name: "ethereum",
43 chain_type: ChainType::Evm,
44 chain_id: "eip155:1",
45 },
46 Chain {
47 name: "polygon",
48 chain_type: ChainType::Evm,
49 chain_id: "eip155:137",
50 },
51 Chain {
52 name: "arbitrum",
53 chain_type: ChainType::Evm,
54 chain_id: "eip155:42161",
55 },
56 Chain {
57 name: "optimism",
58 chain_type: ChainType::Evm,
59 chain_id: "eip155:10",
60 },
61 Chain {
62 name: "base",
63 chain_type: ChainType::Evm,
64 chain_id: "eip155:8453",
65 },
66 Chain {
67 name: "plasma",
68 chain_type: ChainType::Evm,
69 chain_id: "eip155:9745",
70 },
71 Chain {
72 name: "bsc",
73 chain_type: ChainType::Evm,
74 chain_id: "eip155:56",
75 },
76 Chain {
77 name: "avalanche",
78 chain_type: ChainType::Evm,
79 chain_id: "eip155:43114",
80 },
81 Chain {
82 name: "etherlink",
83 chain_type: ChainType::Evm,
84 chain_id: "eip155:42793",
85 },
86 Chain {
87 name: "solana",
88 chain_type: ChainType::Solana,
89 chain_id: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
90 },
91 Chain {
92 name: "bitcoin",
93 chain_type: ChainType::Bitcoin,
94 chain_id: "bip122:000000000019d6689c085ae165831e93",
95 },
96 Chain {
97 name: "cosmos",
98 chain_type: ChainType::Cosmos,
99 chain_id: "cosmos:cosmoshub-4",
100 },
101 Chain {
102 name: "tron",
103 chain_type: ChainType::Tron,
104 chain_id: "tron:mainnet",
105 },
106 Chain {
107 name: "ton",
108 chain_type: ChainType::Ton,
109 chain_id: "ton:mainnet",
110 },
111 Chain {
112 name: "spark",
113 chain_type: ChainType::Spark,
114 chain_id: "spark:mainnet",
115 },
116 Chain {
117 name: "filecoin",
118 chain_type: ChainType::Filecoin,
119 chain_id: "fil:mainnet",
120 },
121 Chain {
122 name: "sui",
123 chain_type: ChainType::Sui,
124 chain_id: "sui:mainnet",
125 },
126];
127
128pub fn parse_chain(s: &str) -> Result<Chain, String> {
133 let lower = s.to_lowercase();
134
135 let lookup = match lower.as_str() {
137 "evm" => "ethereum",
138 _ => &lower,
139 };
140
141 if let Some(chain) = KNOWN_CHAINS.iter().find(|c| c.name == lookup) {
143 return Ok(*chain);
144 }
145
146 if let Some(chain) = KNOWN_CHAINS.iter().find(|c| c.chain_id == s) {
148 return Ok(*chain);
149 }
150
151 if let Some((namespace, _reference)) = s.split_once(':') {
156 if let Some(ct) = ChainType::from_namespace(namespace) {
157 let leaked: &'static str = Box::leak(s.to_string().into_boxed_str());
158 return Ok(Chain {
159 name: leaked,
160 chain_type: ct,
161 chain_id: leaked,
162 });
163 }
164 }
165
166 Err(format!(
167 "unknown chain: '{}'. Use a chain name (ethereum, solana, bitcoin, ...) or CAIP-2 ID (eip155:1, ...)",
168 s
169 ))
170}
171
172pub fn default_chain_for_type(ct: ChainType) -> Chain {
174 *KNOWN_CHAINS.iter().find(|c| c.chain_type == ct).unwrap()
175}
176
177impl ChainType {
178 pub fn namespace(&self) -> &'static str {
180 match self {
181 ChainType::Evm => "eip155",
182 ChainType::Solana => "solana",
183 ChainType::Cosmos => "cosmos",
184 ChainType::Bitcoin => "bip122",
185 ChainType::Tron => "tron",
186 ChainType::Ton => "ton",
187 ChainType::Spark => "spark",
188 ChainType::Filecoin => "fil",
189 ChainType::Sui => "sui",
190 }
191 }
192
193 pub fn default_coin_type(&self) -> u32 {
195 match self {
196 ChainType::Evm => 60,
197 ChainType::Solana => 501,
198 ChainType::Cosmos => 118,
199 ChainType::Bitcoin => 0,
200 ChainType::Tron => 195,
201 ChainType::Ton => 607,
202 ChainType::Spark => 8797555,
203 ChainType::Filecoin => 461,
204 ChainType::Sui => 784,
205 }
206 }
207
208 pub fn from_namespace(ns: &str) -> Option<ChainType> {
210 match ns {
211 "eip155" => Some(ChainType::Evm),
212 "solana" => Some(ChainType::Solana),
213 "cosmos" => Some(ChainType::Cosmos),
214 "bip122" => Some(ChainType::Bitcoin),
215 "tron" => Some(ChainType::Tron),
216 "ton" => Some(ChainType::Ton),
217 "spark" => Some(ChainType::Spark),
218 "fil" => Some(ChainType::Filecoin),
219 "sui" => Some(ChainType::Sui),
220 _ => None,
221 }
222 }
223}
224
225impl fmt::Display for ChainType {
226 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227 let s = match self {
228 ChainType::Evm => "evm",
229 ChainType::Solana => "solana",
230 ChainType::Cosmos => "cosmos",
231 ChainType::Bitcoin => "bitcoin",
232 ChainType::Tron => "tron",
233 ChainType::Ton => "ton",
234 ChainType::Spark => "spark",
235 ChainType::Filecoin => "filecoin",
236 ChainType::Sui => "sui",
237 };
238 write!(f, "{}", s)
239 }
240}
241
242impl FromStr for ChainType {
243 type Err = String;
244
245 fn from_str(s: &str) -> Result<Self, Self::Err> {
246 match s.to_lowercase().as_str() {
247 "evm" => Ok(ChainType::Evm),
248 "solana" => Ok(ChainType::Solana),
249 "cosmos" => Ok(ChainType::Cosmos),
250 "bitcoin" => Ok(ChainType::Bitcoin),
251 "tron" => Ok(ChainType::Tron),
252 "ton" => Ok(ChainType::Ton),
253 "spark" => Ok(ChainType::Spark),
254 "filecoin" => Ok(ChainType::Filecoin),
255 "sui" => Ok(ChainType::Sui),
256 _ => Err(format!("unknown chain type: {}", s)),
257 }
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn test_serde_roundtrip() {
267 let chain = ChainType::Evm;
268 let json = serde_json::to_string(&chain).unwrap();
269 assert_eq!(json, "\"evm\"");
270 let chain2: ChainType = serde_json::from_str(&json).unwrap();
271 assert_eq!(chain, chain2);
272 }
273
274 #[test]
275 fn test_serde_all_variants() {
276 for (chain, expected) in [
277 (ChainType::Evm, "\"evm\""),
278 (ChainType::Solana, "\"solana\""),
279 (ChainType::Cosmos, "\"cosmos\""),
280 (ChainType::Bitcoin, "\"bitcoin\""),
281 (ChainType::Tron, "\"tron\""),
282 (ChainType::Ton, "\"ton\""),
283 (ChainType::Spark, "\"spark\""),
284 (ChainType::Filecoin, "\"filecoin\""),
285 (ChainType::Sui, "\"sui\""),
286 ] {
287 let json = serde_json::to_string(&chain).unwrap();
288 assert_eq!(json, expected);
289 let deserialized: ChainType = serde_json::from_str(&json).unwrap();
290 assert_eq!(chain, deserialized);
291 }
292 }
293
294 #[test]
295 fn test_namespace_mapping() {
296 assert_eq!(ChainType::Evm.namespace(), "eip155");
297 assert_eq!(ChainType::Solana.namespace(), "solana");
298 assert_eq!(ChainType::Cosmos.namespace(), "cosmos");
299 assert_eq!(ChainType::Bitcoin.namespace(), "bip122");
300 assert_eq!(ChainType::Tron.namespace(), "tron");
301 assert_eq!(ChainType::Ton.namespace(), "ton");
302 assert_eq!(ChainType::Spark.namespace(), "spark");
303 assert_eq!(ChainType::Filecoin.namespace(), "fil");
304 assert_eq!(ChainType::Sui.namespace(), "sui");
305 }
306
307 #[test]
308 fn test_coin_type_mapping() {
309 assert_eq!(ChainType::Evm.default_coin_type(), 60);
310 assert_eq!(ChainType::Solana.default_coin_type(), 501);
311 assert_eq!(ChainType::Cosmos.default_coin_type(), 118);
312 assert_eq!(ChainType::Bitcoin.default_coin_type(), 0);
313 assert_eq!(ChainType::Tron.default_coin_type(), 195);
314 assert_eq!(ChainType::Ton.default_coin_type(), 607);
315 assert_eq!(ChainType::Spark.default_coin_type(), 8797555);
316 assert_eq!(ChainType::Filecoin.default_coin_type(), 461);
317 assert_eq!(ChainType::Sui.default_coin_type(), 784);
318 }
319
320 #[test]
321 fn test_from_namespace() {
322 assert_eq!(ChainType::from_namespace("eip155"), Some(ChainType::Evm));
323 assert_eq!(ChainType::from_namespace("solana"), Some(ChainType::Solana));
324 assert_eq!(ChainType::from_namespace("cosmos"), Some(ChainType::Cosmos));
325 assert_eq!(
326 ChainType::from_namespace("bip122"),
327 Some(ChainType::Bitcoin)
328 );
329 assert_eq!(ChainType::from_namespace("tron"), Some(ChainType::Tron));
330 assert_eq!(ChainType::from_namespace("ton"), Some(ChainType::Ton));
331 assert_eq!(ChainType::from_namespace("spark"), Some(ChainType::Spark));
332 assert_eq!(ChainType::from_namespace("fil"), Some(ChainType::Filecoin));
333 assert_eq!(ChainType::from_namespace("sui"), Some(ChainType::Sui));
334 assert_eq!(ChainType::from_namespace("unknown"), None);
335 }
336
337 #[test]
338 fn test_from_str() {
339 assert_eq!("evm".parse::<ChainType>().unwrap(), ChainType::Evm);
340 assert_eq!("Solana".parse::<ChainType>().unwrap(), ChainType::Solana);
341 assert!("unknown".parse::<ChainType>().is_err());
342 }
343
344 #[test]
345 fn test_display() {
346 assert_eq!(ChainType::Evm.to_string(), "evm");
347 assert_eq!(ChainType::Bitcoin.to_string(), "bitcoin");
348 }
349
350 #[test]
351 fn test_parse_chain_friendly_name() {
352 let chain = parse_chain("ethereum").unwrap();
353 assert_eq!(chain.name, "ethereum");
354 assert_eq!(chain.chain_type, ChainType::Evm);
355 assert_eq!(chain.chain_id, "eip155:1");
356 }
357
358 #[test]
359 fn test_parse_chain_plasma_alias() {
360 let chain = parse_chain("plasma").unwrap();
361 assert_eq!(chain.name, "plasma");
362 assert_eq!(chain.chain_type, ChainType::Evm);
363 assert_eq!(chain.chain_id, "eip155:9745");
364 }
365
366 #[test]
367 fn test_parse_chain_etherlink_alias() {
368 let chain = parse_chain("etherlink").unwrap();
369 assert_eq!(chain.name, "etherlink");
370 assert_eq!(chain.chain_type, ChainType::Evm);
371 assert_eq!(chain.chain_id, "eip155:42793");
372 }
373
374 #[test]
375 fn test_parse_chain_caip2() {
376 let chain = parse_chain("eip155:42161").unwrap();
377 assert_eq!(chain.name, "arbitrum");
378 assert_eq!(chain.chain_type, ChainType::Evm);
379 }
380
381 #[test]
382 fn test_parse_chain_plasma_caip2() {
383 let chain = parse_chain("eip155:9745").unwrap();
384 assert_eq!(chain.name, "plasma");
385 assert_eq!(chain.chain_type, ChainType::Evm);
386 assert_eq!(chain.chain_id, "eip155:9745");
387 }
388
389 #[test]
390 fn test_parse_chain_unknown_evm_caip2() {
391 let chain = parse_chain("eip155:9746").unwrap();
392 assert_eq!(chain.name, "eip155:9746");
393 assert_eq!(chain.chain_type, ChainType::Evm);
394 assert_eq!(chain.chain_id, "eip155:9746");
395 }
396
397 #[test]
398 fn test_parse_chain_legacy_evm() {
399 let chain = parse_chain("evm").unwrap();
400 assert_eq!(chain.name, "ethereum");
401 assert_eq!(chain.chain_type, ChainType::Evm);
402 }
403
404 #[test]
405 fn test_parse_chain_solana() {
406 let chain = parse_chain("solana").unwrap();
407 assert_eq!(chain.chain_type, ChainType::Solana);
408 }
409
410 #[test]
411 fn test_parse_chain_unknown() {
412 assert!(parse_chain("unknown_chain").is_err());
413 }
414
415 #[test]
416 fn test_all_chain_types() {
417 assert_eq!(ALL_CHAIN_TYPES.len(), 8);
418 }
419
420 #[test]
421 fn test_default_chain_for_type() {
422 let chain = default_chain_for_type(ChainType::Evm);
423 assert_eq!(chain.name, "ethereum");
424 assert_eq!(chain.chain_id, "eip155:1");
425 }
426}