1use crate::caip::ChainId;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use std::str::FromStr;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
7#[serde(rename_all = "lowercase")]
8pub enum ChainType {
9 Evm,
10 Solana,
11 Cosmos,
12 Bitcoin,
13 Tron,
14 Ton,
15 Spark,
16 Filecoin,
17 Sui,
18 Xrpl,
19 Nano,
20 Near,
21}
22
23pub const ALL_CHAIN_TYPES: [ChainType; 12] = [
25 ChainType::Evm,
26 ChainType::Solana,
27 ChainType::Bitcoin,
28 ChainType::Cosmos,
29 ChainType::Tron,
30 ChainType::Ton,
31 ChainType::Spark,
32 ChainType::Filecoin,
33 ChainType::Sui,
34 ChainType::Xrpl,
35 ChainType::Nano,
36 ChainType::Near,
37];
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct Chain {
42 pub name: &'static str,
43 pub chain_type: ChainType,
44 pub chain_id: &'static str,
45}
46
47impl Chain {
48 pub fn evm_chain_reference(&self) -> Result<&str, String> {
50 if self.chain_type != ChainType::Evm {
51 return Err(format!("chain '{}' is not an EVM chain", self.chain_id));
52 }
53
54 let chain_id = self
55 .chain_id
56 .parse::<ChainId>()
57 .map_err(|e| e.to_string())?;
58 if chain_id.namespace != "eip155" {
59 return Err(format!(
60 "EVM chain '{}' is missing an eip155 reference",
61 self.chain_id
62 ));
63 }
64
65 self.chain_id
66 .split_once(':')
67 .map(|(_, reference)| reference)
68 .ok_or_else(|| format!("invalid CAIP-2 chain ID: '{}'", self.chain_id))
69 }
70
71 pub fn evm_chain_id_u64(&self) -> Result<u64, String> {
73 self.evm_chain_reference()?
74 .parse()
75 .map_err(|_| format!("cannot extract numeric chain ID from: {}", self.chain_id))
76 }
77}
78
79pub const KNOWN_CHAINS: &[Chain] = &[
81 Chain {
82 name: "ethereum",
83 chain_type: ChainType::Evm,
84 chain_id: "eip155:1",
85 },
86 Chain {
87 name: "polygon",
88 chain_type: ChainType::Evm,
89 chain_id: "eip155:137",
90 },
91 Chain {
92 name: "arbitrum",
93 chain_type: ChainType::Evm,
94 chain_id: "eip155:42161",
95 },
96 Chain {
97 name: "optimism",
98 chain_type: ChainType::Evm,
99 chain_id: "eip155:10",
100 },
101 Chain {
102 name: "base",
103 chain_type: ChainType::Evm,
104 chain_id: "eip155:8453",
105 },
106 Chain {
107 name: "plasma",
108 chain_type: ChainType::Evm,
109 chain_id: "eip155:9745",
110 },
111 Chain {
112 name: "bsc",
113 chain_type: ChainType::Evm,
114 chain_id: "eip155:56",
115 },
116 Chain {
117 name: "avalanche",
118 chain_type: ChainType::Evm,
119 chain_id: "eip155:43114",
120 },
121 Chain {
122 name: "etherlink",
123 chain_type: ChainType::Evm,
124 chain_id: "eip155:42793",
125 },
126 Chain {
127 name: "solana",
128 chain_type: ChainType::Solana,
129 chain_id: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
130 },
131 Chain {
132 name: "bitcoin",
133 chain_type: ChainType::Bitcoin,
134 chain_id: "bip122:000000000019d6689c085ae165831e93",
135 },
136 Chain {
137 name: "cosmos",
138 chain_type: ChainType::Cosmos,
139 chain_id: "cosmos:cosmoshub-4",
140 },
141 Chain {
142 name: "tron",
143 chain_type: ChainType::Tron,
144 chain_id: "tron:mainnet",
145 },
146 Chain {
147 name: "ton",
148 chain_type: ChainType::Ton,
149 chain_id: "ton:mainnet",
150 },
151 Chain {
152 name: "spark",
153 chain_type: ChainType::Spark,
154 chain_id: "spark:mainnet",
155 },
156 Chain {
157 name: "filecoin",
158 chain_type: ChainType::Filecoin,
159 chain_id: "fil:mainnet",
160 },
161 Chain {
162 name: "sui",
163 chain_type: ChainType::Sui,
164 chain_id: "sui:mainnet",
165 },
166 Chain {
167 name: "xrpl",
168 chain_type: ChainType::Xrpl,
169 chain_id: "xrpl:mainnet",
170 },
171 Chain {
172 name: "xrpl-testnet",
173 chain_type: ChainType::Xrpl,
174 chain_id: "xrpl:testnet",
175 },
176 Chain {
177 name: "xrpl-devnet",
178 chain_type: ChainType::Xrpl,
179 chain_id: "xrpl:devnet",
180 },
181 Chain {
182 name: "nano",
183 chain_type: ChainType::Nano,
184 chain_id: "nano:mainnet",
185 },
186 Chain {
187 name: "near",
188 chain_type: ChainType::Near,
189 chain_id: "near:mainnet",
190 },
191 Chain {
192 name: "near-testnet",
193 chain_type: ChainType::Near,
194 chain_id: "near:testnet",
195 },
196 Chain {
197 name: "tempo",
198 chain_type: ChainType::Evm,
199 chain_id: "eip155:4217",
200 },
201 Chain {
202 name: "hyperliquid",
203 chain_type: ChainType::Evm,
204 chain_id: "eip155:999",
205 },
206];
207
208pub fn parse_chain(s: &str) -> Result<Chain, String> {
214 let lower = s.to_lowercase();
215
216 if lower == "evm" {
218 eprintln!(
219 "warning: '--chain evm' is deprecated; use '--chain ethereum' \
220 or a specific chain name (base, arbitrum, polygon, ...)"
221 );
222 return Ok(*KNOWN_CHAINS.iter().find(|c| c.name == "ethereum").unwrap());
223 }
224
225 if let Some(chain) = KNOWN_CHAINS.iter().find(|c| c.name == lower) {
227 return Ok(*chain);
228 }
229
230 if let Some(chain) = KNOWN_CHAINS.iter().find(|c| c.chain_id == s) {
232 return Ok(*chain);
233 }
234
235 if !lower.is_empty() && lower.chars().all(|c| c.is_ascii_digit()) {
237 let caip2 = format!("eip155:{}", lower);
238 if let Some(chain) = KNOWN_CHAINS.iter().find(|c| c.chain_id == caip2) {
239 return Ok(*chain);
240 }
241 let leaked: &'static str = Box::leak(caip2.into_boxed_str());
242 return Ok(Chain {
243 name: leaked,
244 chain_type: ChainType::Evm,
245 chain_id: leaked,
246 });
247 }
248
249 if let Some((namespace, _reference)) = s.split_once(':') {
254 if let Some(ct) = ChainType::from_namespace(namespace) {
255 let leaked: &'static str = Box::leak(s.to_string().into_boxed_str());
256 return Ok(Chain {
257 name: leaked,
258 chain_type: ct,
259 chain_id: leaked,
260 });
261 }
262 }
263
264 Err(format!(
265 "unknown chain: '{s}'\n\n\
266 Supported chains:\n \
267 EVM: ethereum, base, arbitrum, optimism, polygon, bsc, avalanche, plasma, etherlink\n \
268 Solana: solana\n \
269 Bitcoin: bitcoin\n \
270 Other: cosmos, tron, ton, sui, filecoin, spark, xrpl, nano, near\n\n\
271 Or use a CAIP-2 ID (eip155:8453) or bare EVM chain ID (8453)"
272 ))
273}
274
275pub fn default_chain_for_type(ct: ChainType) -> Chain {
277 *KNOWN_CHAINS.iter().find(|c| c.chain_type == ct).unwrap()
278}
279
280impl ChainType {
281 pub fn namespace(&self) -> &'static str {
283 match self {
284 ChainType::Evm => "eip155",
285 ChainType::Solana => "solana",
286 ChainType::Cosmos => "cosmos",
287 ChainType::Bitcoin => "bip122",
288 ChainType::Tron => "tron",
289 ChainType::Ton => "ton",
290 ChainType::Spark => "spark",
291 ChainType::Filecoin => "fil",
292 ChainType::Sui => "sui",
293 ChainType::Xrpl => "xrpl",
294 ChainType::Nano => "nano",
295 ChainType::Near => "near",
296 }
297 }
298
299 pub fn default_coin_type(&self) -> u32 {
301 match self {
302 ChainType::Evm => 60,
303 ChainType::Solana => 501,
304 ChainType::Cosmos => 118,
305 ChainType::Bitcoin => 0,
306 ChainType::Tron => 195,
307 ChainType::Ton => 607,
308 ChainType::Spark => 8797555,
309 ChainType::Filecoin => 461,
310 ChainType::Sui => 784,
311 ChainType::Xrpl => 144,
312 ChainType::Nano => 165,
313 ChainType::Near => 397,
314 }
315 }
316
317 pub fn from_namespace(ns: &str) -> Option<ChainType> {
319 match ns {
320 "eip155" => Some(ChainType::Evm),
321 "solana" => Some(ChainType::Solana),
322 "cosmos" => Some(ChainType::Cosmos),
323 "bip122" => Some(ChainType::Bitcoin),
324 "tron" => Some(ChainType::Tron),
325 "ton" => Some(ChainType::Ton),
326 "spark" => Some(ChainType::Spark),
327 "fil" => Some(ChainType::Filecoin),
328 "sui" => Some(ChainType::Sui),
329 "xrpl" => Some(ChainType::Xrpl),
330 "nano" => Some(ChainType::Nano),
331 "near" => Some(ChainType::Near),
332 _ => None,
333 }
334 }
335}
336
337impl fmt::Display for ChainType {
338 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339 let s = match self {
340 ChainType::Evm => "evm",
341 ChainType::Solana => "solana",
342 ChainType::Cosmos => "cosmos",
343 ChainType::Bitcoin => "bitcoin",
344 ChainType::Tron => "tron",
345 ChainType::Ton => "ton",
346 ChainType::Spark => "spark",
347 ChainType::Filecoin => "filecoin",
348 ChainType::Sui => "sui",
349 ChainType::Xrpl => "xrpl",
350 ChainType::Nano => "nano",
351 ChainType::Near => "near",
352 };
353 write!(f, "{}", s)
354 }
355}
356
357impl FromStr for ChainType {
358 type Err = String;
359
360 fn from_str(s: &str) -> Result<Self, Self::Err> {
361 match s.to_lowercase().as_str() {
362 "evm" => Ok(ChainType::Evm),
363 "solana" => Ok(ChainType::Solana),
364 "cosmos" => Ok(ChainType::Cosmos),
365 "bitcoin" => Ok(ChainType::Bitcoin),
366 "tron" => Ok(ChainType::Tron),
367 "ton" => Ok(ChainType::Ton),
368 "spark" => Ok(ChainType::Spark),
369 "filecoin" => Ok(ChainType::Filecoin),
370 "sui" => Ok(ChainType::Sui),
371 "xrpl" => Ok(ChainType::Xrpl),
372 "nano" => Ok(ChainType::Nano),
373 "near" => Ok(ChainType::Near),
374 _ => Err(format!("unknown chain type: {}", s)),
375 }
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 #[test]
384 fn test_serde_roundtrip() {
385 let chain = ChainType::Evm;
386 let json = serde_json::to_string(&chain).unwrap();
387 assert_eq!(json, "\"evm\"");
388 let chain2: ChainType = serde_json::from_str(&json).unwrap();
389 assert_eq!(chain, chain2);
390 }
391
392 #[test]
393 fn test_serde_all_variants() {
394 for (chain, expected) in [
395 (ChainType::Evm, "\"evm\""),
396 (ChainType::Solana, "\"solana\""),
397 (ChainType::Cosmos, "\"cosmos\""),
398 (ChainType::Bitcoin, "\"bitcoin\""),
399 (ChainType::Tron, "\"tron\""),
400 (ChainType::Ton, "\"ton\""),
401 (ChainType::Spark, "\"spark\""),
402 (ChainType::Filecoin, "\"filecoin\""),
403 (ChainType::Sui, "\"sui\""),
404 (ChainType::Xrpl, "\"xrpl\""),
405 (ChainType::Nano, "\"nano\""),
406 (ChainType::Near, "\"near\""),
407 ] {
408 let json = serde_json::to_string(&chain).unwrap();
409 assert_eq!(json, expected);
410 let deserialized: ChainType = serde_json::from_str(&json).unwrap();
411 assert_eq!(chain, deserialized);
412 }
413 }
414
415 #[test]
416 fn test_namespace_mapping() {
417 assert_eq!(ChainType::Evm.namespace(), "eip155");
418 assert_eq!(ChainType::Solana.namespace(), "solana");
419 assert_eq!(ChainType::Cosmos.namespace(), "cosmos");
420 assert_eq!(ChainType::Bitcoin.namespace(), "bip122");
421 assert_eq!(ChainType::Tron.namespace(), "tron");
422 assert_eq!(ChainType::Ton.namespace(), "ton");
423 assert_eq!(ChainType::Spark.namespace(), "spark");
424 assert_eq!(ChainType::Filecoin.namespace(), "fil");
425 assert_eq!(ChainType::Sui.namespace(), "sui");
426 assert_eq!(ChainType::Xrpl.namespace(), "xrpl");
427 assert_eq!(ChainType::Nano.namespace(), "nano");
428 assert_eq!(ChainType::Near.namespace(), "near");
429 }
430
431 #[test]
432 fn test_coin_type_mapping() {
433 assert_eq!(ChainType::Evm.default_coin_type(), 60);
434 assert_eq!(ChainType::Solana.default_coin_type(), 501);
435 assert_eq!(ChainType::Cosmos.default_coin_type(), 118);
436 assert_eq!(ChainType::Bitcoin.default_coin_type(), 0);
437 assert_eq!(ChainType::Tron.default_coin_type(), 195);
438 assert_eq!(ChainType::Ton.default_coin_type(), 607);
439 assert_eq!(ChainType::Spark.default_coin_type(), 8797555);
440 assert_eq!(ChainType::Filecoin.default_coin_type(), 461);
441 assert_eq!(ChainType::Sui.default_coin_type(), 784);
442 assert_eq!(ChainType::Xrpl.default_coin_type(), 144);
443 assert_eq!(ChainType::Nano.default_coin_type(), 165);
444 assert_eq!(ChainType::Near.default_coin_type(), 397);
445 }
446
447 #[test]
448 fn test_from_namespace() {
449 assert_eq!(ChainType::from_namespace("eip155"), Some(ChainType::Evm));
450 assert_eq!(ChainType::from_namespace("solana"), Some(ChainType::Solana));
451 assert_eq!(ChainType::from_namespace("cosmos"), Some(ChainType::Cosmos));
452 assert_eq!(
453 ChainType::from_namespace("bip122"),
454 Some(ChainType::Bitcoin)
455 );
456 assert_eq!(ChainType::from_namespace("tron"), Some(ChainType::Tron));
457 assert_eq!(ChainType::from_namespace("ton"), Some(ChainType::Ton));
458 assert_eq!(ChainType::from_namespace("spark"), Some(ChainType::Spark));
459 assert_eq!(ChainType::from_namespace("fil"), Some(ChainType::Filecoin));
460 assert_eq!(ChainType::from_namespace("sui"), Some(ChainType::Sui));
461 assert_eq!(ChainType::from_namespace("xrpl"), Some(ChainType::Xrpl));
462 assert_eq!(ChainType::from_namespace("nano"), Some(ChainType::Nano));
463 assert_eq!(ChainType::from_namespace("near"), Some(ChainType::Near));
464 assert_eq!(ChainType::from_namespace("unknown"), None);
465 }
466
467 #[test]
468 fn test_from_str() {
469 assert_eq!("evm".parse::<ChainType>().unwrap(), ChainType::Evm);
470 assert_eq!("Solana".parse::<ChainType>().unwrap(), ChainType::Solana);
471 assert!("unknown".parse::<ChainType>().is_err());
472 }
473
474 #[test]
475 fn test_display() {
476 assert_eq!(ChainType::Evm.to_string(), "evm");
477 assert_eq!(ChainType::Bitcoin.to_string(), "bitcoin");
478 }
479
480 #[test]
481 fn test_parse_chain_friendly_name() {
482 let chain = parse_chain("ethereum").unwrap();
483 assert_eq!(chain.name, "ethereum");
484 assert_eq!(chain.chain_type, ChainType::Evm);
485 assert_eq!(chain.chain_id, "eip155:1");
486 }
487
488 #[test]
489 fn test_parse_chain_plasma_alias() {
490 let chain = parse_chain("plasma").unwrap();
491 assert_eq!(chain.name, "plasma");
492 assert_eq!(chain.chain_type, ChainType::Evm);
493 assert_eq!(chain.chain_id, "eip155:9745");
494 }
495
496 #[test]
497 fn test_parse_chain_etherlink_alias() {
498 let chain = parse_chain("etherlink").unwrap();
499 assert_eq!(chain.name, "etherlink");
500 assert_eq!(chain.chain_type, ChainType::Evm);
501 assert_eq!(chain.chain_id, "eip155:42793");
502 }
503
504 #[test]
505 fn test_parse_chain_caip2() {
506 let chain = parse_chain("eip155:42161").unwrap();
507 assert_eq!(chain.name, "arbitrum");
508 assert_eq!(chain.chain_type, ChainType::Evm);
509 }
510
511 #[test]
512 fn test_parse_chain_plasma_caip2() {
513 let chain = parse_chain("eip155:9745").unwrap();
514 assert_eq!(chain.name, "plasma");
515 assert_eq!(chain.chain_type, ChainType::Evm);
516 assert_eq!(chain.chain_id, "eip155:9745");
517 }
518
519 #[test]
520 fn test_parse_chain_unknown_evm_caip2() {
521 let chain = parse_chain("eip155:9746").unwrap();
522 assert_eq!(chain.name, "eip155:9746");
523 assert_eq!(chain.chain_type, ChainType::Evm);
524 assert_eq!(chain.chain_id, "eip155:9746");
525 }
526
527 #[test]
528 fn test_evm_chain_reference_for_known_chain() {
529 let chain = parse_chain("base").unwrap();
530 assert_eq!(chain.evm_chain_reference().unwrap(), "8453");
531 assert_eq!(chain.evm_chain_id_u64().unwrap(), 8453);
532 }
533
534 #[test]
535 fn test_evm_chain_reference_for_unknown_caip2_chain() {
536 let chain = parse_chain("eip155:999999").unwrap();
537 assert_eq!(chain.evm_chain_reference().unwrap(), "999999");
538 assert_eq!(chain.evm_chain_id_u64().unwrap(), 999999);
539 }
540
541 #[test]
542 fn test_evm_chain_reference_rejects_non_evm_chain() {
543 let chain = parse_chain("solana").unwrap();
544 let err = chain.evm_chain_reference().unwrap_err();
545 assert!(err.contains("not an EVM chain"));
546 }
547
548 #[test]
549 fn test_parse_chain_legacy_evm() {
550 let chain = parse_chain("evm").unwrap();
551 assert_eq!(chain.name, "ethereum");
552 assert_eq!(chain.chain_type, ChainType::Evm);
553 }
554
555 #[test]
556 fn test_parse_chain_solana() {
557 let chain = parse_chain("solana").unwrap();
558 assert_eq!(chain.chain_type, ChainType::Solana);
559 }
560
561 #[test]
562 fn test_parse_chain_xrpl() {
563 let chain = parse_chain("xrpl").unwrap();
564 assert_eq!(chain.chain_type, ChainType::Xrpl);
565 assert_eq!(chain.chain_id, "xrpl:mainnet");
566
567 let testnet = parse_chain("xrpl-testnet").unwrap();
568 assert_eq!(testnet.chain_type, ChainType::Xrpl);
569 assert_eq!(testnet.chain_id, "xrpl:testnet");
570
571 let devnet = parse_chain("xrpl-devnet").unwrap();
572 assert_eq!(devnet.chain_type, ChainType::Xrpl);
573 assert_eq!(devnet.chain_id, "xrpl:devnet");
574
575 let via_caip2 = parse_chain("xrpl:testnet").unwrap();
577 assert_eq!(via_caip2.chain_type, ChainType::Xrpl);
578 assert_eq!(via_caip2.chain_id, "xrpl:testnet");
579 }
580
581 #[test]
582 fn test_parse_chain_bare_numeric_known() {
583 let chain = parse_chain("8453").unwrap();
585 assert_eq!(chain.name, "base");
586 assert_eq!(chain.chain_type, ChainType::Evm);
587 assert_eq!(chain.chain_id, "eip155:8453");
588 }
589
590 #[test]
591 fn test_parse_chain_bare_numeric_mainnet() {
592 let chain = parse_chain("1").unwrap();
593 assert_eq!(chain.name, "ethereum");
594 assert_eq!(chain.chain_id, "eip155:1");
595 }
596
597 #[test]
598 fn test_parse_chain_bare_numeric_unknown() {
599 let chain = parse_chain("99999").unwrap();
601 assert_eq!(chain.chain_type, ChainType::Evm);
602 assert_eq!(chain.chain_id, "eip155:99999");
603 }
604
605 #[test]
606 fn test_parse_chain_unknown() {
607 assert!(parse_chain("unknown_chain").is_err());
608 }
609
610 #[test]
611 fn test_parse_chain_tempo_alias() {
612 let chain = parse_chain("tempo").unwrap();
613 assert_eq!(chain.name, "tempo");
614 assert_eq!(chain.chain_type, ChainType::Evm);
615 assert_eq!(chain.chain_id, "eip155:4217");
616 }
617
618 #[test]
619 fn test_parse_chain_tempo_caip2() {
620 let chain = parse_chain("eip155:4217").unwrap();
621 assert_eq!(chain.name, "tempo");
622 assert_eq!(chain.chain_type, ChainType::Evm);
623 assert_eq!(chain.chain_id, "eip155:4217");
624 }
625
626 #[test]
627 fn test_parse_chain_hyperliquid_alias() {
628 let chain = parse_chain("hyperliquid").unwrap();
629 assert_eq!(chain.name, "hyperliquid");
630 assert_eq!(chain.chain_type, ChainType::Evm);
631 assert_eq!(chain.chain_id, "eip155:999");
632 }
633
634 #[test]
635 fn test_parse_chain_hyperliquid_caip2() {
636 let chain = parse_chain("eip155:999").unwrap();
637 assert_eq!(chain.name, "hyperliquid");
638 assert_eq!(chain.chain_type, ChainType::Evm);
639 assert_eq!(chain.chain_id, "eip155:999");
640 }
641
642 #[test]
643 fn test_all_chain_types() {
644 assert_eq!(ALL_CHAIN_TYPES.len(), 12);
645 }
646
647 #[test]
648 fn test_parse_chain_near() {
649 let chain = parse_chain("near").unwrap();
650 assert_eq!(chain.name, "near");
651 assert_eq!(chain.chain_type, ChainType::Near);
652 assert_eq!(chain.chain_id, "near:mainnet");
653
654 let testnet = parse_chain("near-testnet").unwrap();
655 assert_eq!(testnet.chain_type, ChainType::Near);
656 assert_eq!(testnet.chain_id, "near:testnet");
657
658 let via_caip2 = parse_chain("near:testnet").unwrap();
660 assert_eq!(via_caip2.chain_type, ChainType::Near);
661 assert_eq!(via_caip2.chain_id, "near:testnet");
662 }
663
664 #[test]
665 fn test_default_chain_for_type() {
666 let chain = default_chain_for_type(ChainType::Evm);
667 assert_eq!(chain.name, "ethereum");
668 assert_eq!(chain.chain_id, "eip155:1");
669 }
670}