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> {
151 let lower = s.to_lowercase();
152
153 if lower == "evm" {
155 eprintln!(
156 "warning: '--chain evm' is deprecated; use '--chain ethereum' \
157 or a specific chain name (base, arbitrum, polygon, ...)"
158 );
159 return Ok(*KNOWN_CHAINS.iter().find(|c| c.name == "ethereum").unwrap());
160 }
161
162 if let Some(chain) = KNOWN_CHAINS.iter().find(|c| c.name == lower) {
164 return Ok(*chain);
165 }
166
167 if let Some(chain) = KNOWN_CHAINS.iter().find(|c| c.chain_id == s) {
169 return Ok(*chain);
170 }
171
172 if !lower.is_empty() && lower.chars().all(|c| c.is_ascii_digit()) {
174 let caip2 = format!("eip155:{}", lower);
175 if let Some(chain) = KNOWN_CHAINS.iter().find(|c| c.chain_id == caip2) {
176 return Ok(*chain);
177 }
178 let leaked: &'static str = Box::leak(caip2.into_boxed_str());
179 return Ok(Chain {
180 name: leaked,
181 chain_type: ChainType::Evm,
182 chain_id: leaked,
183 });
184 }
185
186 if let Some((namespace, _reference)) = s.split_once(':') {
191 if let Some(ct) = ChainType::from_namespace(namespace) {
192 let leaked: &'static str = Box::leak(s.to_string().into_boxed_str());
193 return Ok(Chain {
194 name: leaked,
195 chain_type: ct,
196 chain_id: leaked,
197 });
198 }
199 }
200
201 Err(format!(
202 "unknown chain: '{s}'\n\n\
203 Supported chains:\n \
204 EVM: ethereum, base, arbitrum, optimism, polygon, bsc, avalanche, plasma, etherlink\n \
205 Solana: solana\n \
206 Bitcoin: bitcoin\n \
207 Other: cosmos, tron, ton, sui, filecoin, spark, xrpl\n\n\
208 Or use a CAIP-2 ID (eip155:8453) or bare EVM chain ID (8453)"
209 ))
210}
211
212pub fn default_chain_for_type(ct: ChainType) -> Chain {
214 *KNOWN_CHAINS.iter().find(|c| c.chain_type == ct).unwrap()
215}
216
217impl ChainType {
218 pub fn namespace(&self) -> &'static str {
220 match self {
221 ChainType::Evm => "eip155",
222 ChainType::Solana => "solana",
223 ChainType::Cosmos => "cosmos",
224 ChainType::Bitcoin => "bip122",
225 ChainType::Tron => "tron",
226 ChainType::Ton => "ton",
227 ChainType::Spark => "spark",
228 ChainType::Filecoin => "fil",
229 ChainType::Sui => "sui",
230 ChainType::Xrpl => "xrpl",
231 }
232 }
233
234 pub fn default_coin_type(&self) -> u32 {
236 match self {
237 ChainType::Evm => 60,
238 ChainType::Solana => 501,
239 ChainType::Cosmos => 118,
240 ChainType::Bitcoin => 0,
241 ChainType::Tron => 195,
242 ChainType::Ton => 607,
243 ChainType::Spark => 8797555,
244 ChainType::Filecoin => 461,
245 ChainType::Sui => 784,
246 ChainType::Xrpl => 144,
247 }
248 }
249
250 pub fn from_namespace(ns: &str) -> Option<ChainType> {
252 match ns {
253 "eip155" => Some(ChainType::Evm),
254 "solana" => Some(ChainType::Solana),
255 "cosmos" => Some(ChainType::Cosmos),
256 "bip122" => Some(ChainType::Bitcoin),
257 "tron" => Some(ChainType::Tron),
258 "ton" => Some(ChainType::Ton),
259 "spark" => Some(ChainType::Spark),
260 "fil" => Some(ChainType::Filecoin),
261 "sui" => Some(ChainType::Sui),
262 "xrpl" => Some(ChainType::Xrpl),
263 _ => None,
264 }
265 }
266}
267
268impl fmt::Display for ChainType {
269 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270 let s = match self {
271 ChainType::Evm => "evm",
272 ChainType::Solana => "solana",
273 ChainType::Cosmos => "cosmos",
274 ChainType::Bitcoin => "bitcoin",
275 ChainType::Tron => "tron",
276 ChainType::Ton => "ton",
277 ChainType::Spark => "spark",
278 ChainType::Filecoin => "filecoin",
279 ChainType::Sui => "sui",
280 ChainType::Xrpl => "xrpl",
281 };
282 write!(f, "{}", s)
283 }
284}
285
286impl FromStr for ChainType {
287 type Err = String;
288
289 fn from_str(s: &str) -> Result<Self, Self::Err> {
290 match s.to_lowercase().as_str() {
291 "evm" => Ok(ChainType::Evm),
292 "solana" => Ok(ChainType::Solana),
293 "cosmos" => Ok(ChainType::Cosmos),
294 "bitcoin" => Ok(ChainType::Bitcoin),
295 "tron" => Ok(ChainType::Tron),
296 "ton" => Ok(ChainType::Ton),
297 "spark" => Ok(ChainType::Spark),
298 "filecoin" => Ok(ChainType::Filecoin),
299 "sui" => Ok(ChainType::Sui),
300 "xrpl" => Ok(ChainType::Xrpl),
301 _ => Err(format!("unknown chain type: {}", s)),
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 fn test_serde_roundtrip() {
312 let chain = ChainType::Evm;
313 let json = serde_json::to_string(&chain).unwrap();
314 assert_eq!(json, "\"evm\"");
315 let chain2: ChainType = serde_json::from_str(&json).unwrap();
316 assert_eq!(chain, chain2);
317 }
318
319 #[test]
320 fn test_serde_all_variants() {
321 for (chain, expected) in [
322 (ChainType::Evm, "\"evm\""),
323 (ChainType::Solana, "\"solana\""),
324 (ChainType::Cosmos, "\"cosmos\""),
325 (ChainType::Bitcoin, "\"bitcoin\""),
326 (ChainType::Tron, "\"tron\""),
327 (ChainType::Ton, "\"ton\""),
328 (ChainType::Spark, "\"spark\""),
329 (ChainType::Filecoin, "\"filecoin\""),
330 (ChainType::Sui, "\"sui\""),
331 (ChainType::Xrpl, "\"xrpl\""),
332 ] {
333 let json = serde_json::to_string(&chain).unwrap();
334 assert_eq!(json, expected);
335 let deserialized: ChainType = serde_json::from_str(&json).unwrap();
336 assert_eq!(chain, deserialized);
337 }
338 }
339
340 #[test]
341 fn test_namespace_mapping() {
342 assert_eq!(ChainType::Evm.namespace(), "eip155");
343 assert_eq!(ChainType::Solana.namespace(), "solana");
344 assert_eq!(ChainType::Cosmos.namespace(), "cosmos");
345 assert_eq!(ChainType::Bitcoin.namespace(), "bip122");
346 assert_eq!(ChainType::Tron.namespace(), "tron");
347 assert_eq!(ChainType::Ton.namespace(), "ton");
348 assert_eq!(ChainType::Spark.namespace(), "spark");
349 assert_eq!(ChainType::Filecoin.namespace(), "fil");
350 assert_eq!(ChainType::Sui.namespace(), "sui");
351 assert_eq!(ChainType::Xrpl.namespace(), "xrpl");
352 }
353
354 #[test]
355 fn test_coin_type_mapping() {
356 assert_eq!(ChainType::Evm.default_coin_type(), 60);
357 assert_eq!(ChainType::Solana.default_coin_type(), 501);
358 assert_eq!(ChainType::Cosmos.default_coin_type(), 118);
359 assert_eq!(ChainType::Bitcoin.default_coin_type(), 0);
360 assert_eq!(ChainType::Tron.default_coin_type(), 195);
361 assert_eq!(ChainType::Ton.default_coin_type(), 607);
362 assert_eq!(ChainType::Spark.default_coin_type(), 8797555);
363 assert_eq!(ChainType::Filecoin.default_coin_type(), 461);
364 assert_eq!(ChainType::Sui.default_coin_type(), 784);
365 assert_eq!(ChainType::Xrpl.default_coin_type(), 144);
366 }
367
368 #[test]
369 fn test_from_namespace() {
370 assert_eq!(ChainType::from_namespace("eip155"), Some(ChainType::Evm));
371 assert_eq!(ChainType::from_namespace("solana"), Some(ChainType::Solana));
372 assert_eq!(ChainType::from_namespace("cosmos"), Some(ChainType::Cosmos));
373 assert_eq!(
374 ChainType::from_namespace("bip122"),
375 Some(ChainType::Bitcoin)
376 );
377 assert_eq!(ChainType::from_namespace("tron"), Some(ChainType::Tron));
378 assert_eq!(ChainType::from_namespace("ton"), Some(ChainType::Ton));
379 assert_eq!(ChainType::from_namespace("spark"), Some(ChainType::Spark));
380 assert_eq!(ChainType::from_namespace("fil"), Some(ChainType::Filecoin));
381 assert_eq!(ChainType::from_namespace("sui"), Some(ChainType::Sui));
382 assert_eq!(ChainType::from_namespace("xrpl"), Some(ChainType::Xrpl));
383 assert_eq!(ChainType::from_namespace("unknown"), None);
384 }
385
386 #[test]
387 fn test_from_str() {
388 assert_eq!("evm".parse::<ChainType>().unwrap(), ChainType::Evm);
389 assert_eq!("Solana".parse::<ChainType>().unwrap(), ChainType::Solana);
390 assert!("unknown".parse::<ChainType>().is_err());
391 }
392
393 #[test]
394 fn test_display() {
395 assert_eq!(ChainType::Evm.to_string(), "evm");
396 assert_eq!(ChainType::Bitcoin.to_string(), "bitcoin");
397 }
398
399 #[test]
400 fn test_parse_chain_friendly_name() {
401 let chain = parse_chain("ethereum").unwrap();
402 assert_eq!(chain.name, "ethereum");
403 assert_eq!(chain.chain_type, ChainType::Evm);
404 assert_eq!(chain.chain_id, "eip155:1");
405 }
406
407 #[test]
408 fn test_parse_chain_plasma_alias() {
409 let chain = parse_chain("plasma").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_etherlink_alias() {
417 let chain = parse_chain("etherlink").unwrap();
418 assert_eq!(chain.name, "etherlink");
419 assert_eq!(chain.chain_type, ChainType::Evm);
420 assert_eq!(chain.chain_id, "eip155:42793");
421 }
422
423 #[test]
424 fn test_parse_chain_caip2() {
425 let chain = parse_chain("eip155:42161").unwrap();
426 assert_eq!(chain.name, "arbitrum");
427 assert_eq!(chain.chain_type, ChainType::Evm);
428 }
429
430 #[test]
431 fn test_parse_chain_plasma_caip2() {
432 let chain = parse_chain("eip155:9745").unwrap();
433 assert_eq!(chain.name, "plasma");
434 assert_eq!(chain.chain_type, ChainType::Evm);
435 assert_eq!(chain.chain_id, "eip155:9745");
436 }
437
438 #[test]
439 fn test_parse_chain_unknown_evm_caip2() {
440 let chain = parse_chain("eip155:9746").unwrap();
441 assert_eq!(chain.name, "eip155:9746");
442 assert_eq!(chain.chain_type, ChainType::Evm);
443 assert_eq!(chain.chain_id, "eip155:9746");
444 }
445
446 #[test]
447 fn test_parse_chain_legacy_evm() {
448 let chain = parse_chain("evm").unwrap();
449 assert_eq!(chain.name, "ethereum");
450 assert_eq!(chain.chain_type, ChainType::Evm);
451 }
452
453 #[test]
454 fn test_parse_chain_solana() {
455 let chain = parse_chain("solana").unwrap();
456 assert_eq!(chain.chain_type, ChainType::Solana);
457 }
458
459 #[test]
460 fn test_parse_chain_xrpl() {
461 let chain = parse_chain("xrpl").unwrap();
462 assert_eq!(chain.chain_type, ChainType::Xrpl);
463 assert_eq!(chain.chain_id, "xrpl:mainnet");
464
465 let testnet = parse_chain("xrpl-testnet").unwrap();
466 assert_eq!(testnet.chain_type, ChainType::Xrpl);
467 assert_eq!(testnet.chain_id, "xrpl:testnet");
468
469 let devnet = parse_chain("xrpl-devnet").unwrap();
470 assert_eq!(devnet.chain_type, ChainType::Xrpl);
471 assert_eq!(devnet.chain_id, "xrpl:devnet");
472
473 let via_caip2 = parse_chain("xrpl:testnet").unwrap();
475 assert_eq!(via_caip2.chain_type, ChainType::Xrpl);
476 assert_eq!(via_caip2.chain_id, "xrpl:testnet");
477 }
478
479 #[test]
480 fn test_parse_chain_bare_numeric_known() {
481 let chain = parse_chain("8453").unwrap();
483 assert_eq!(chain.name, "base");
484 assert_eq!(chain.chain_type, ChainType::Evm);
485 assert_eq!(chain.chain_id, "eip155:8453");
486 }
487
488 #[test]
489 fn test_parse_chain_bare_numeric_mainnet() {
490 let chain = parse_chain("1").unwrap();
491 assert_eq!(chain.name, "ethereum");
492 assert_eq!(chain.chain_id, "eip155:1");
493 }
494
495 #[test]
496 fn test_parse_chain_bare_numeric_unknown() {
497 let chain = parse_chain("99999").unwrap();
499 assert_eq!(chain.chain_type, ChainType::Evm);
500 assert_eq!(chain.chain_id, "eip155:99999");
501 }
502
503 #[test]
504 fn test_parse_chain_unknown() {
505 assert!(parse_chain("unknown_chain").is_err());
506 }
507
508 #[test]
509 fn test_all_chain_types() {
510 assert_eq!(ALL_CHAIN_TYPES.len(), 9);
511 }
512
513 #[test]
514 fn test_default_chain_for_type() {
515 let chain = default_chain_for_type(ChainType::Evm);
516 assert_eq!(chain.name, "ethereum");
517 assert_eq!(chain.chain_id, "eip155:1");
518 }
519}