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