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