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