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}
17
18pub const ALL_CHAIN_TYPES: [ChainType; 7] = [
20 ChainType::Evm,
21 ChainType::Solana,
22 ChainType::Bitcoin,
23 ChainType::Cosmos,
24 ChainType::Tron,
25 ChainType::Ton,
26 ChainType::Filecoin,
27];
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct Chain {
32 pub name: &'static str,
33 pub chain_type: ChainType,
34 pub chain_id: &'static str,
35}
36
37pub const KNOWN_CHAINS: &[Chain] = &[
39 Chain {
40 name: "ethereum",
41 chain_type: ChainType::Evm,
42 chain_id: "eip155:1",
43 },
44 Chain {
45 name: "polygon",
46 chain_type: ChainType::Evm,
47 chain_id: "eip155:137",
48 },
49 Chain {
50 name: "arbitrum",
51 chain_type: ChainType::Evm,
52 chain_id: "eip155:42161",
53 },
54 Chain {
55 name: "optimism",
56 chain_type: ChainType::Evm,
57 chain_id: "eip155:10",
58 },
59 Chain {
60 name: "base",
61 chain_type: ChainType::Evm,
62 chain_id: "eip155:8453",
63 },
64 Chain {
65 name: "bsc",
66 chain_type: ChainType::Evm,
67 chain_id: "eip155:56",
68 },
69 Chain {
70 name: "avalanche",
71 chain_type: ChainType::Evm,
72 chain_id: "eip155:43114",
73 },
74 Chain {
75 name: "solana",
76 chain_type: ChainType::Solana,
77 chain_id: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
78 },
79 Chain {
80 name: "bitcoin",
81 chain_type: ChainType::Bitcoin,
82 chain_id: "bip122:000000000019d6689c085ae165831e93",
83 },
84 Chain {
85 name: "cosmos",
86 chain_type: ChainType::Cosmos,
87 chain_id: "cosmos:cosmoshub-4",
88 },
89 Chain {
90 name: "tron",
91 chain_type: ChainType::Tron,
92 chain_id: "tron:mainnet",
93 },
94 Chain {
95 name: "ton",
96 chain_type: ChainType::Ton,
97 chain_id: "ton:mainnet",
98 },
99 Chain {
100 name: "spark",
101 chain_type: ChainType::Spark,
102 chain_id: "spark:mainnet",
103 },
104 Chain {
105 name: "filecoin",
106 chain_type: ChainType::Filecoin,
107 chain_id: "fil:mainnet",
108 },
109];
110
111pub fn parse_chain(s: &str) -> Result<Chain, String> {
116 let lower = s.to_lowercase();
117
118 let lookup = match lower.as_str() {
120 "evm" => "ethereum",
121 _ => &lower,
122 };
123
124 if let Some(chain) = KNOWN_CHAINS.iter().find(|c| c.name == lookup) {
126 return Ok(*chain);
127 }
128
129 if let Some(chain) = KNOWN_CHAINS.iter().find(|c| c.chain_id == s) {
131 return Ok(*chain);
132 }
133
134 Err(format!(
135 "unknown chain: '{}'. Use a chain name (ethereum, solana, bitcoin, ...) or CAIP-2 ID (eip155:1, ...)",
136 s
137 ))
138}
139
140pub fn default_chain_for_type(ct: ChainType) -> Chain {
142 *KNOWN_CHAINS.iter().find(|c| c.chain_type == ct).unwrap()
143}
144
145impl ChainType {
146 pub fn namespace(&self) -> &'static str {
148 match self {
149 ChainType::Evm => "eip155",
150 ChainType::Solana => "solana",
151 ChainType::Cosmos => "cosmos",
152 ChainType::Bitcoin => "bip122",
153 ChainType::Tron => "tron",
154 ChainType::Ton => "ton",
155 ChainType::Spark => "spark",
156 ChainType::Filecoin => "fil",
157 }
158 }
159
160 pub fn default_coin_type(&self) -> u32 {
162 match self {
163 ChainType::Evm => 60,
164 ChainType::Solana => 501,
165 ChainType::Cosmos => 118,
166 ChainType::Bitcoin => 0,
167 ChainType::Tron => 195,
168 ChainType::Ton => 607,
169 ChainType::Spark => 8797555,
170 ChainType::Filecoin => 461,
171 }
172 }
173
174 pub fn from_namespace(ns: &str) -> Option<ChainType> {
176 match ns {
177 "eip155" => Some(ChainType::Evm),
178 "solana" => Some(ChainType::Solana),
179 "cosmos" => Some(ChainType::Cosmos),
180 "bip122" => Some(ChainType::Bitcoin),
181 "tron" => Some(ChainType::Tron),
182 "ton" => Some(ChainType::Ton),
183 "spark" => Some(ChainType::Spark),
184 "fil" => Some(ChainType::Filecoin),
185 _ => None,
186 }
187 }
188}
189
190impl fmt::Display for ChainType {
191 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192 let s = match self {
193 ChainType::Evm => "evm",
194 ChainType::Solana => "solana",
195 ChainType::Cosmos => "cosmos",
196 ChainType::Bitcoin => "bitcoin",
197 ChainType::Tron => "tron",
198 ChainType::Ton => "ton",
199 ChainType::Spark => "spark",
200 ChainType::Filecoin => "filecoin",
201 };
202 write!(f, "{}", s)
203 }
204}
205
206impl FromStr for ChainType {
207 type Err = String;
208
209 fn from_str(s: &str) -> Result<Self, Self::Err> {
210 match s.to_lowercase().as_str() {
211 "evm" => Ok(ChainType::Evm),
212 "solana" => Ok(ChainType::Solana),
213 "cosmos" => Ok(ChainType::Cosmos),
214 "bitcoin" => Ok(ChainType::Bitcoin),
215 "tron" => Ok(ChainType::Tron),
216 "ton" => Ok(ChainType::Ton),
217 "spark" => Ok(ChainType::Spark),
218 "filecoin" => Ok(ChainType::Filecoin),
219 _ => Err(format!("unknown chain type: {}", s)),
220 }
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn test_serde_roundtrip() {
230 let chain = ChainType::Evm;
231 let json = serde_json::to_string(&chain).unwrap();
232 assert_eq!(json, "\"evm\"");
233 let chain2: ChainType = serde_json::from_str(&json).unwrap();
234 assert_eq!(chain, chain2);
235 }
236
237 #[test]
238 fn test_serde_all_variants() {
239 for (chain, expected) in [
240 (ChainType::Evm, "\"evm\""),
241 (ChainType::Solana, "\"solana\""),
242 (ChainType::Cosmos, "\"cosmos\""),
243 (ChainType::Bitcoin, "\"bitcoin\""),
244 (ChainType::Tron, "\"tron\""),
245 (ChainType::Ton, "\"ton\""),
246 (ChainType::Spark, "\"spark\""),
247 (ChainType::Filecoin, "\"filecoin\""),
248 ] {
249 let json = serde_json::to_string(&chain).unwrap();
250 assert_eq!(json, expected);
251 let deserialized: ChainType = serde_json::from_str(&json).unwrap();
252 assert_eq!(chain, deserialized);
253 }
254 }
255
256 #[test]
257 fn test_namespace_mapping() {
258 assert_eq!(ChainType::Evm.namespace(), "eip155");
259 assert_eq!(ChainType::Solana.namespace(), "solana");
260 assert_eq!(ChainType::Cosmos.namespace(), "cosmos");
261 assert_eq!(ChainType::Bitcoin.namespace(), "bip122");
262 assert_eq!(ChainType::Tron.namespace(), "tron");
263 assert_eq!(ChainType::Ton.namespace(), "ton");
264 assert_eq!(ChainType::Spark.namespace(), "spark");
265 assert_eq!(ChainType::Filecoin.namespace(), "fil");
266 }
267
268 #[test]
269 fn test_coin_type_mapping() {
270 assert_eq!(ChainType::Evm.default_coin_type(), 60);
271 assert_eq!(ChainType::Solana.default_coin_type(), 501);
272 assert_eq!(ChainType::Cosmos.default_coin_type(), 118);
273 assert_eq!(ChainType::Bitcoin.default_coin_type(), 0);
274 assert_eq!(ChainType::Tron.default_coin_type(), 195);
275 assert_eq!(ChainType::Ton.default_coin_type(), 607);
276 assert_eq!(ChainType::Spark.default_coin_type(), 8797555);
277 assert_eq!(ChainType::Filecoin.default_coin_type(), 461);
278 }
279
280 #[test]
281 fn test_from_namespace() {
282 assert_eq!(ChainType::from_namespace("eip155"), Some(ChainType::Evm));
283 assert_eq!(ChainType::from_namespace("solana"), Some(ChainType::Solana));
284 assert_eq!(ChainType::from_namespace("cosmos"), Some(ChainType::Cosmos));
285 assert_eq!(
286 ChainType::from_namespace("bip122"),
287 Some(ChainType::Bitcoin)
288 );
289 assert_eq!(ChainType::from_namespace("tron"), Some(ChainType::Tron));
290 assert_eq!(ChainType::from_namespace("ton"), Some(ChainType::Ton));
291 assert_eq!(ChainType::from_namespace("spark"), Some(ChainType::Spark));
292 assert_eq!(ChainType::from_namespace("fil"), Some(ChainType::Filecoin));
293 assert_eq!(ChainType::from_namespace("unknown"), None);
294 }
295
296 #[test]
297 fn test_from_str() {
298 assert_eq!("evm".parse::<ChainType>().unwrap(), ChainType::Evm);
299 assert_eq!("Solana".parse::<ChainType>().unwrap(), ChainType::Solana);
300 assert!("unknown".parse::<ChainType>().is_err());
301 }
302
303 #[test]
304 fn test_display() {
305 assert_eq!(ChainType::Evm.to_string(), "evm");
306 assert_eq!(ChainType::Bitcoin.to_string(), "bitcoin");
307 }
308
309 #[test]
310 fn test_parse_chain_friendly_name() {
311 let chain = parse_chain("ethereum").unwrap();
312 assert_eq!(chain.name, "ethereum");
313 assert_eq!(chain.chain_type, ChainType::Evm);
314 assert_eq!(chain.chain_id, "eip155:1");
315 }
316
317 #[test]
318 fn test_parse_chain_caip2() {
319 let chain = parse_chain("eip155:42161").unwrap();
320 assert_eq!(chain.name, "arbitrum");
321 assert_eq!(chain.chain_type, ChainType::Evm);
322 }
323
324 #[test]
325 fn test_parse_chain_legacy_evm() {
326 let chain = parse_chain("evm").unwrap();
327 assert_eq!(chain.name, "ethereum");
328 assert_eq!(chain.chain_type, ChainType::Evm);
329 }
330
331 #[test]
332 fn test_parse_chain_solana() {
333 let chain = parse_chain("solana").unwrap();
334 assert_eq!(chain.chain_type, ChainType::Solana);
335 }
336
337 #[test]
338 fn test_parse_chain_unknown() {
339 assert!(parse_chain("unknown_chain").is_err());
340 }
341
342 #[test]
343 fn test_all_chain_types() {
344 assert_eq!(ALL_CHAIN_TYPES.len(), 7);
345 }
346
347 #[test]
348 fn test_default_chain_for_type() {
349 let chain = default_chain_for_type(ChainType::Evm);
350 assert_eq!(chain.name, "ethereum");
351 assert_eq!(chain.chain_id, "eip155:1");
352 }
353}