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