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