Skip to main content

ows_core/
chain.rs

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
18/// All supported chain families, used for universal wallet derivation.
19pub 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/// A specific chain (e.g. "ethereum", "arbitrum") with its family type and CAIP-2 ID.
30#[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
37/// Known chains registry.
38pub 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
111/// Parse a chain string into a `Chain`. Accepts:
112/// - Friendly names: "ethereum", "arbitrum", "solana", etc.
113/// - CAIP-2 chain IDs: "eip155:1", "eip155:42161", etc.
114/// - Legacy family names for backward compat: "evm" → resolves to ethereum
115pub fn parse_chain(s: &str) -> Result<Chain, String> {
116    let lower = s.to_lowercase();
117
118    // Legacy family name backward compat
119    let lookup = match lower.as_str() {
120        "evm" => "ethereum",
121        _ => &lower,
122    };
123
124    // Try friendly name match
125    if let Some(chain) = KNOWN_CHAINS.iter().find(|c| c.name == lookup) {
126        return Ok(*chain);
127    }
128
129    // Try CAIP-2 chain ID match
130    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
140/// Returns the default `Chain` for a given `ChainType` (first match in registry).
141pub fn default_chain_for_type(ct: ChainType) -> Chain {
142    *KNOWN_CHAINS.iter().find(|c| c.chain_type == ct).unwrap()
143}
144
145impl ChainType {
146    /// Returns the CAIP-2 namespace for this chain type.
147    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    /// Returns the BIP-44 coin type for this chain type.
161    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    /// Returns the ChainType for a given CAIP-2 namespace.
175    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}