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: "solana",
83 chain_type: ChainType::Solana,
84 chain_id: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
85 },
86 Chain {
87 name: "bitcoin",
88 chain_type: ChainType::Bitcoin,
89 chain_id: "bip122:000000000019d6689c085ae165831e93",
90 },
91 Chain {
92 name: "cosmos",
93 chain_type: ChainType::Cosmos,
94 chain_id: "cosmos:cosmoshub-4",
95 },
96 Chain {
97 name: "tron",
98 chain_type: ChainType::Tron,
99 chain_id: "tron:mainnet",
100 },
101 Chain {
102 name: "ton",
103 chain_type: ChainType::Ton,
104 chain_id: "ton:mainnet",
105 },
106 Chain {
107 name: "spark",
108 chain_type: ChainType::Spark,
109 chain_id: "spark:mainnet",
110 },
111 Chain {
112 name: "filecoin",
113 chain_type: ChainType::Filecoin,
114 chain_id: "fil:mainnet",
115 },
116 Chain {
117 name: "sui",
118 chain_type: ChainType::Sui,
119 chain_id: "sui:mainnet",
120 },
121];
122
123pub fn parse_chain(s: &str) -> Result<Chain, String> {
128 let lower = s.to_lowercase();
129
130 let lookup = match lower.as_str() {
132 "evm" => "ethereum",
133 _ => &lower,
134 };
135
136 if let Some(chain) = KNOWN_CHAINS.iter().find(|c| c.name == lookup) {
138 return Ok(*chain);
139 }
140
141 if let Some(chain) = KNOWN_CHAINS.iter().find(|c| c.chain_id == s) {
143 return Ok(*chain);
144 }
145
146 if let Some((namespace, _reference)) = s.split_once(':') {
151 if let Some(ct) = ChainType::from_namespace(namespace) {
152 let leaked: &'static str = Box::leak(s.to_string().into_boxed_str());
153 return Ok(Chain {
154 name: leaked,
155 chain_type: ct,
156 chain_id: leaked,
157 });
158 }
159 }
160
161 Err(format!(
162 "unknown chain: '{}'. Use a chain name (ethereum, solana, bitcoin, ...) or CAIP-2 ID (eip155:1, ...)",
163 s
164 ))
165}
166
167pub fn default_chain_for_type(ct: ChainType) -> Chain {
169 *KNOWN_CHAINS.iter().find(|c| c.chain_type == ct).unwrap()
170}
171
172impl ChainType {
173 pub fn namespace(&self) -> &'static str {
175 match self {
176 ChainType::Evm => "eip155",
177 ChainType::Solana => "solana",
178 ChainType::Cosmos => "cosmos",
179 ChainType::Bitcoin => "bip122",
180 ChainType::Tron => "tron",
181 ChainType::Ton => "ton",
182 ChainType::Spark => "spark",
183 ChainType::Filecoin => "fil",
184 ChainType::Sui => "sui",
185 }
186 }
187
188 pub fn default_coin_type(&self) -> u32 {
190 match self {
191 ChainType::Evm => 60,
192 ChainType::Solana => 501,
193 ChainType::Cosmos => 118,
194 ChainType::Bitcoin => 0,
195 ChainType::Tron => 195,
196 ChainType::Ton => 607,
197 ChainType::Spark => 8797555,
198 ChainType::Filecoin => 461,
199 ChainType::Sui => 784,
200 }
201 }
202
203 pub fn from_namespace(ns: &str) -> Option<ChainType> {
205 match ns {
206 "eip155" => Some(ChainType::Evm),
207 "solana" => Some(ChainType::Solana),
208 "cosmos" => Some(ChainType::Cosmos),
209 "bip122" => Some(ChainType::Bitcoin),
210 "tron" => Some(ChainType::Tron),
211 "ton" => Some(ChainType::Ton),
212 "spark" => Some(ChainType::Spark),
213 "fil" => Some(ChainType::Filecoin),
214 "sui" => Some(ChainType::Sui),
215 _ => None,
216 }
217 }
218}
219
220impl fmt::Display for ChainType {
221 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222 let s = match self {
223 ChainType::Evm => "evm",
224 ChainType::Solana => "solana",
225 ChainType::Cosmos => "cosmos",
226 ChainType::Bitcoin => "bitcoin",
227 ChainType::Tron => "tron",
228 ChainType::Ton => "ton",
229 ChainType::Spark => "spark",
230 ChainType::Filecoin => "filecoin",
231 ChainType::Sui => "sui",
232 };
233 write!(f, "{}", s)
234 }
235}
236
237impl FromStr for ChainType {
238 type Err = String;
239
240 fn from_str(s: &str) -> Result<Self, Self::Err> {
241 match s.to_lowercase().as_str() {
242 "evm" => Ok(ChainType::Evm),
243 "solana" => Ok(ChainType::Solana),
244 "cosmos" => Ok(ChainType::Cosmos),
245 "bitcoin" => Ok(ChainType::Bitcoin),
246 "tron" => Ok(ChainType::Tron),
247 "ton" => Ok(ChainType::Ton),
248 "spark" => Ok(ChainType::Spark),
249 "filecoin" => Ok(ChainType::Filecoin),
250 "sui" => Ok(ChainType::Sui),
251 _ => Err(format!("unknown chain type: {}", s)),
252 }
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn test_serde_roundtrip() {
262 let chain = ChainType::Evm;
263 let json = serde_json::to_string(&chain).unwrap();
264 assert_eq!(json, "\"evm\"");
265 let chain2: ChainType = serde_json::from_str(&json).unwrap();
266 assert_eq!(chain, chain2);
267 }
268
269 #[test]
270 fn test_serde_all_variants() {
271 for (chain, expected) in [
272 (ChainType::Evm, "\"evm\""),
273 (ChainType::Solana, "\"solana\""),
274 (ChainType::Cosmos, "\"cosmos\""),
275 (ChainType::Bitcoin, "\"bitcoin\""),
276 (ChainType::Tron, "\"tron\""),
277 (ChainType::Ton, "\"ton\""),
278 (ChainType::Spark, "\"spark\""),
279 (ChainType::Filecoin, "\"filecoin\""),
280 (ChainType::Sui, "\"sui\""),
281 ] {
282 let json = serde_json::to_string(&chain).unwrap();
283 assert_eq!(json, expected);
284 let deserialized: ChainType = serde_json::from_str(&json).unwrap();
285 assert_eq!(chain, deserialized);
286 }
287 }
288
289 #[test]
290 fn test_namespace_mapping() {
291 assert_eq!(ChainType::Evm.namespace(), "eip155");
292 assert_eq!(ChainType::Solana.namespace(), "solana");
293 assert_eq!(ChainType::Cosmos.namespace(), "cosmos");
294 assert_eq!(ChainType::Bitcoin.namespace(), "bip122");
295 assert_eq!(ChainType::Tron.namespace(), "tron");
296 assert_eq!(ChainType::Ton.namespace(), "ton");
297 assert_eq!(ChainType::Spark.namespace(), "spark");
298 assert_eq!(ChainType::Filecoin.namespace(), "fil");
299 assert_eq!(ChainType::Sui.namespace(), "sui");
300 }
301
302 #[test]
303 fn test_coin_type_mapping() {
304 assert_eq!(ChainType::Evm.default_coin_type(), 60);
305 assert_eq!(ChainType::Solana.default_coin_type(), 501);
306 assert_eq!(ChainType::Cosmos.default_coin_type(), 118);
307 assert_eq!(ChainType::Bitcoin.default_coin_type(), 0);
308 assert_eq!(ChainType::Tron.default_coin_type(), 195);
309 assert_eq!(ChainType::Ton.default_coin_type(), 607);
310 assert_eq!(ChainType::Spark.default_coin_type(), 8797555);
311 assert_eq!(ChainType::Filecoin.default_coin_type(), 461);
312 assert_eq!(ChainType::Sui.default_coin_type(), 784);
313 }
314
315 #[test]
316 fn test_from_namespace() {
317 assert_eq!(ChainType::from_namespace("eip155"), Some(ChainType::Evm));
318 assert_eq!(ChainType::from_namespace("solana"), Some(ChainType::Solana));
319 assert_eq!(ChainType::from_namespace("cosmos"), Some(ChainType::Cosmos));
320 assert_eq!(
321 ChainType::from_namespace("bip122"),
322 Some(ChainType::Bitcoin)
323 );
324 assert_eq!(ChainType::from_namespace("tron"), Some(ChainType::Tron));
325 assert_eq!(ChainType::from_namespace("ton"), Some(ChainType::Ton));
326 assert_eq!(ChainType::from_namespace("spark"), Some(ChainType::Spark));
327 assert_eq!(ChainType::from_namespace("fil"), Some(ChainType::Filecoin));
328 assert_eq!(ChainType::from_namespace("sui"), Some(ChainType::Sui));
329 assert_eq!(ChainType::from_namespace("unknown"), None);
330 }
331
332 #[test]
333 fn test_from_str() {
334 assert_eq!("evm".parse::<ChainType>().unwrap(), ChainType::Evm);
335 assert_eq!("Solana".parse::<ChainType>().unwrap(), ChainType::Solana);
336 assert!("unknown".parse::<ChainType>().is_err());
337 }
338
339 #[test]
340 fn test_display() {
341 assert_eq!(ChainType::Evm.to_string(), "evm");
342 assert_eq!(ChainType::Bitcoin.to_string(), "bitcoin");
343 }
344
345 #[test]
346 fn test_parse_chain_friendly_name() {
347 let chain = parse_chain("ethereum").unwrap();
348 assert_eq!(chain.name, "ethereum");
349 assert_eq!(chain.chain_type, ChainType::Evm);
350 assert_eq!(chain.chain_id, "eip155:1");
351 }
352
353 #[test]
354 fn test_parse_chain_plasma_alias() {
355 let chain = parse_chain("plasma").unwrap();
356 assert_eq!(chain.name, "plasma");
357 assert_eq!(chain.chain_type, ChainType::Evm);
358 assert_eq!(chain.chain_id, "eip155:9745");
359 }
360
361 #[test]
362 fn test_parse_chain_caip2() {
363 let chain = parse_chain("eip155:42161").unwrap();
364 assert_eq!(chain.name, "arbitrum");
365 assert_eq!(chain.chain_type, ChainType::Evm);
366 }
367
368 #[test]
369 fn test_parse_chain_plasma_caip2() {
370 let chain = parse_chain("eip155:9745").unwrap();
371 assert_eq!(chain.name, "plasma");
372 assert_eq!(chain.chain_type, ChainType::Evm);
373 assert_eq!(chain.chain_id, "eip155:9745");
374 }
375
376 #[test]
377 fn test_parse_chain_unknown_evm_caip2() {
378 let chain = parse_chain("eip155:9746").unwrap();
379 assert_eq!(chain.name, "eip155:9746");
380 assert_eq!(chain.chain_type, ChainType::Evm);
381 assert_eq!(chain.chain_id, "eip155:9746");
382 }
383
384 #[test]
385 fn test_parse_chain_legacy_evm() {
386 let chain = parse_chain("evm").unwrap();
387 assert_eq!(chain.name, "ethereum");
388 assert_eq!(chain.chain_type, ChainType::Evm);
389 }
390
391 #[test]
392 fn test_parse_chain_solana() {
393 let chain = parse_chain("solana").unwrap();
394 assert_eq!(chain.chain_type, ChainType::Solana);
395 }
396
397 #[test]
398 fn test_parse_chain_unknown() {
399 assert!(parse_chain("unknown_chain").is_err());
400 }
401
402 #[test]
403 fn test_all_chain_types() {
404 assert_eq!(ALL_CHAIN_TYPES.len(), 8);
405 }
406
407 #[test]
408 fn test_default_chain_for_type() {
409 let chain = default_chain_for_type(ChainType::Evm);
410 assert_eq!(chain.name, "ethereum");
411 assert_eq!(chain.chain_id, "eip155:1");
412 }
413}