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