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", "base", "arbitrum", "solana", etc.
147/// - CAIP-2 chain IDs: "eip155:1", "eip155:8453", etc.
148/// - Bare numeric EVM chain IDs: "8453" → eip155:8453
149/// - Legacy "evm" (deprecated, warns on stderr, resolves to ethereum)
150pub fn parse_chain(s: &str) -> Result<Chain, String> {
151    let lower = s.to_lowercase();
152
153    // Legacy "evm" — deprecated, warn and resolve
154    if lower == "evm" {
155        eprintln!(
156            "warning: '--chain evm' is deprecated; use '--chain ethereum' \
157             or a specific chain name (base, arbitrum, polygon, ...)"
158        );
159        return Ok(*KNOWN_CHAINS.iter().find(|c| c.name == "ethereum").unwrap());
160    }
161
162    // Try friendly name match
163    if let Some(chain) = KNOWN_CHAINS.iter().find(|c| c.name == lower) {
164        return Ok(*chain);
165    }
166
167    // Try CAIP-2 chain ID match
168    if let Some(chain) = KNOWN_CHAINS.iter().find(|c| c.chain_id == s) {
169        return Ok(*chain);
170    }
171
172    // Bare numeric → treat as EVM chain ID (eip155:<n>)
173    if !lower.is_empty() && lower.chars().all(|c| c.is_ascii_digit()) {
174        let caip2 = format!("eip155:{}", lower);
175        if let Some(chain) = KNOWN_CHAINS.iter().find(|c| c.chain_id == caip2) {
176            return Ok(*chain);
177        }
178        let leaked: &'static str = Box::leak(caip2.into_boxed_str());
179        return Ok(Chain {
180            name: leaked,
181            chain_type: ChainType::Evm,
182            chain_id: leaked,
183        });
184    }
185
186    // Try namespace match for unknown CAIP-2 IDs (e.g. eip155:4217, eip155:84532).
187    // Uses the same signer as the namespace's default chain. The chain_id string is
188    // leaked to satisfy the 'static lifetime — acceptable since parse_chain is called
189    // with a small, bounded set of user-supplied chain identifiers.
190    if let Some((namespace, _reference)) = s.split_once(':') {
191        if let Some(ct) = ChainType::from_namespace(namespace) {
192            let leaked: &'static str = Box::leak(s.to_string().into_boxed_str());
193            return Ok(Chain {
194                name: leaked,
195                chain_type: ct,
196                chain_id: leaked,
197            });
198        }
199    }
200
201    Err(format!(
202        "unknown chain: '{s}'\n\n\
203         Supported chains:\n  \
204           EVM:     ethereum, base, arbitrum, optimism, polygon, bsc, avalanche, plasma, etherlink\n  \
205           Solana:  solana\n  \
206           Bitcoin: bitcoin\n  \
207           Other:   cosmos, tron, ton, sui, filecoin, spark, xrpl\n\n\
208         Or use a CAIP-2 ID (eip155:8453) or bare EVM chain ID (8453)"
209    ))
210}
211
212/// Returns the default `Chain` for a given `ChainType` (first match in registry).
213pub fn default_chain_for_type(ct: ChainType) -> Chain {
214    *KNOWN_CHAINS.iter().find(|c| c.chain_type == ct).unwrap()
215}
216
217impl ChainType {
218    /// Returns the CAIP-2 namespace for this chain type.
219    pub fn namespace(&self) -> &'static str {
220        match self {
221            ChainType::Evm => "eip155",
222            ChainType::Solana => "solana",
223            ChainType::Cosmos => "cosmos",
224            ChainType::Bitcoin => "bip122",
225            ChainType::Tron => "tron",
226            ChainType::Ton => "ton",
227            ChainType::Spark => "spark",
228            ChainType::Filecoin => "fil",
229            ChainType::Sui => "sui",
230            ChainType::Xrpl => "xrpl",
231        }
232    }
233
234    /// Returns the BIP-44 coin type for this chain type.
235    pub fn default_coin_type(&self) -> u32 {
236        match self {
237            ChainType::Evm => 60,
238            ChainType::Solana => 501,
239            ChainType::Cosmos => 118,
240            ChainType::Bitcoin => 0,
241            ChainType::Tron => 195,
242            ChainType::Ton => 607,
243            ChainType::Spark => 8797555,
244            ChainType::Filecoin => 461,
245            ChainType::Sui => 784,
246            ChainType::Xrpl => 144,
247        }
248    }
249
250    /// Returns the ChainType for a given CAIP-2 namespace.
251    pub fn from_namespace(ns: &str) -> Option<ChainType> {
252        match ns {
253            "eip155" => Some(ChainType::Evm),
254            "solana" => Some(ChainType::Solana),
255            "cosmos" => Some(ChainType::Cosmos),
256            "bip122" => Some(ChainType::Bitcoin),
257            "tron" => Some(ChainType::Tron),
258            "ton" => Some(ChainType::Ton),
259            "spark" => Some(ChainType::Spark),
260            "fil" => Some(ChainType::Filecoin),
261            "sui" => Some(ChainType::Sui),
262            "xrpl" => Some(ChainType::Xrpl),
263            _ => None,
264        }
265    }
266}
267
268impl fmt::Display for ChainType {
269    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270        let s = match self {
271            ChainType::Evm => "evm",
272            ChainType::Solana => "solana",
273            ChainType::Cosmos => "cosmos",
274            ChainType::Bitcoin => "bitcoin",
275            ChainType::Tron => "tron",
276            ChainType::Ton => "ton",
277            ChainType::Spark => "spark",
278            ChainType::Filecoin => "filecoin",
279            ChainType::Sui => "sui",
280            ChainType::Xrpl => "xrpl",
281        };
282        write!(f, "{}", s)
283    }
284}
285
286impl FromStr for ChainType {
287    type Err = String;
288
289    fn from_str(s: &str) -> Result<Self, Self::Err> {
290        match s.to_lowercase().as_str() {
291            "evm" => Ok(ChainType::Evm),
292            "solana" => Ok(ChainType::Solana),
293            "cosmos" => Ok(ChainType::Cosmos),
294            "bitcoin" => Ok(ChainType::Bitcoin),
295            "tron" => Ok(ChainType::Tron),
296            "ton" => Ok(ChainType::Ton),
297            "spark" => Ok(ChainType::Spark),
298            "filecoin" => Ok(ChainType::Filecoin),
299            "sui" => Ok(ChainType::Sui),
300            "xrpl" => Ok(ChainType::Xrpl),
301            _ => Err(format!("unknown chain type: {}", s)),
302        }
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    #[test]
311    fn test_serde_roundtrip() {
312        let chain = ChainType::Evm;
313        let json = serde_json::to_string(&chain).unwrap();
314        assert_eq!(json, "\"evm\"");
315        let chain2: ChainType = serde_json::from_str(&json).unwrap();
316        assert_eq!(chain, chain2);
317    }
318
319    #[test]
320    fn test_serde_all_variants() {
321        for (chain, expected) in [
322            (ChainType::Evm, "\"evm\""),
323            (ChainType::Solana, "\"solana\""),
324            (ChainType::Cosmos, "\"cosmos\""),
325            (ChainType::Bitcoin, "\"bitcoin\""),
326            (ChainType::Tron, "\"tron\""),
327            (ChainType::Ton, "\"ton\""),
328            (ChainType::Spark, "\"spark\""),
329            (ChainType::Filecoin, "\"filecoin\""),
330            (ChainType::Sui, "\"sui\""),
331            (ChainType::Xrpl, "\"xrpl\""),
332        ] {
333            let json = serde_json::to_string(&chain).unwrap();
334            assert_eq!(json, expected);
335            let deserialized: ChainType = serde_json::from_str(&json).unwrap();
336            assert_eq!(chain, deserialized);
337        }
338    }
339
340    #[test]
341    fn test_namespace_mapping() {
342        assert_eq!(ChainType::Evm.namespace(), "eip155");
343        assert_eq!(ChainType::Solana.namespace(), "solana");
344        assert_eq!(ChainType::Cosmos.namespace(), "cosmos");
345        assert_eq!(ChainType::Bitcoin.namespace(), "bip122");
346        assert_eq!(ChainType::Tron.namespace(), "tron");
347        assert_eq!(ChainType::Ton.namespace(), "ton");
348        assert_eq!(ChainType::Spark.namespace(), "spark");
349        assert_eq!(ChainType::Filecoin.namespace(), "fil");
350        assert_eq!(ChainType::Sui.namespace(), "sui");
351        assert_eq!(ChainType::Xrpl.namespace(), "xrpl");
352    }
353
354    #[test]
355    fn test_coin_type_mapping() {
356        assert_eq!(ChainType::Evm.default_coin_type(), 60);
357        assert_eq!(ChainType::Solana.default_coin_type(), 501);
358        assert_eq!(ChainType::Cosmos.default_coin_type(), 118);
359        assert_eq!(ChainType::Bitcoin.default_coin_type(), 0);
360        assert_eq!(ChainType::Tron.default_coin_type(), 195);
361        assert_eq!(ChainType::Ton.default_coin_type(), 607);
362        assert_eq!(ChainType::Spark.default_coin_type(), 8797555);
363        assert_eq!(ChainType::Filecoin.default_coin_type(), 461);
364        assert_eq!(ChainType::Sui.default_coin_type(), 784);
365        assert_eq!(ChainType::Xrpl.default_coin_type(), 144);
366    }
367
368    #[test]
369    fn test_from_namespace() {
370        assert_eq!(ChainType::from_namespace("eip155"), Some(ChainType::Evm));
371        assert_eq!(ChainType::from_namespace("solana"), Some(ChainType::Solana));
372        assert_eq!(ChainType::from_namespace("cosmos"), Some(ChainType::Cosmos));
373        assert_eq!(
374            ChainType::from_namespace("bip122"),
375            Some(ChainType::Bitcoin)
376        );
377        assert_eq!(ChainType::from_namespace("tron"), Some(ChainType::Tron));
378        assert_eq!(ChainType::from_namespace("ton"), Some(ChainType::Ton));
379        assert_eq!(ChainType::from_namespace("spark"), Some(ChainType::Spark));
380        assert_eq!(ChainType::from_namespace("fil"), Some(ChainType::Filecoin));
381        assert_eq!(ChainType::from_namespace("sui"), Some(ChainType::Sui));
382        assert_eq!(ChainType::from_namespace("xrpl"), Some(ChainType::Xrpl));
383        assert_eq!(ChainType::from_namespace("unknown"), None);
384    }
385
386    #[test]
387    fn test_from_str() {
388        assert_eq!("evm".parse::<ChainType>().unwrap(), ChainType::Evm);
389        assert_eq!("Solana".parse::<ChainType>().unwrap(), ChainType::Solana);
390        assert!("unknown".parse::<ChainType>().is_err());
391    }
392
393    #[test]
394    fn test_display() {
395        assert_eq!(ChainType::Evm.to_string(), "evm");
396        assert_eq!(ChainType::Bitcoin.to_string(), "bitcoin");
397    }
398
399    #[test]
400    fn test_parse_chain_friendly_name() {
401        let chain = parse_chain("ethereum").unwrap();
402        assert_eq!(chain.name, "ethereum");
403        assert_eq!(chain.chain_type, ChainType::Evm);
404        assert_eq!(chain.chain_id, "eip155:1");
405    }
406
407    #[test]
408    fn test_parse_chain_plasma_alias() {
409        let chain = parse_chain("plasma").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_etherlink_alias() {
417        let chain = parse_chain("etherlink").unwrap();
418        assert_eq!(chain.name, "etherlink");
419        assert_eq!(chain.chain_type, ChainType::Evm);
420        assert_eq!(chain.chain_id, "eip155:42793");
421    }
422
423    #[test]
424    fn test_parse_chain_caip2() {
425        let chain = parse_chain("eip155:42161").unwrap();
426        assert_eq!(chain.name, "arbitrum");
427        assert_eq!(chain.chain_type, ChainType::Evm);
428    }
429
430    #[test]
431    fn test_parse_chain_plasma_caip2() {
432        let chain = parse_chain("eip155:9745").unwrap();
433        assert_eq!(chain.name, "plasma");
434        assert_eq!(chain.chain_type, ChainType::Evm);
435        assert_eq!(chain.chain_id, "eip155:9745");
436    }
437
438    #[test]
439    fn test_parse_chain_unknown_evm_caip2() {
440        let chain = parse_chain("eip155:9746").unwrap();
441        assert_eq!(chain.name, "eip155:9746");
442        assert_eq!(chain.chain_type, ChainType::Evm);
443        assert_eq!(chain.chain_id, "eip155:9746");
444    }
445
446    #[test]
447    fn test_parse_chain_legacy_evm() {
448        let chain = parse_chain("evm").unwrap();
449        assert_eq!(chain.name, "ethereum");
450        assert_eq!(chain.chain_type, ChainType::Evm);
451    }
452
453    #[test]
454    fn test_parse_chain_solana() {
455        let chain = parse_chain("solana").unwrap();
456        assert_eq!(chain.chain_type, ChainType::Solana);
457    }
458
459    #[test]
460    fn test_parse_chain_xrpl() {
461        let chain = parse_chain("xrpl").unwrap();
462        assert_eq!(chain.chain_type, ChainType::Xrpl);
463        assert_eq!(chain.chain_id, "xrpl:mainnet");
464
465        let testnet = parse_chain("xrpl-testnet").unwrap();
466        assert_eq!(testnet.chain_type, ChainType::Xrpl);
467        assert_eq!(testnet.chain_id, "xrpl:testnet");
468
469        let devnet = parse_chain("xrpl-devnet").unwrap();
470        assert_eq!(devnet.chain_type, ChainType::Xrpl);
471        assert_eq!(devnet.chain_id, "xrpl:devnet");
472
473        // CAIP-2 IDs also accepted directly
474        let via_caip2 = parse_chain("xrpl:testnet").unwrap();
475        assert_eq!(via_caip2.chain_type, ChainType::Xrpl);
476        assert_eq!(via_caip2.chain_id, "xrpl:testnet");
477    }
478
479    #[test]
480    fn test_parse_chain_bare_numeric_known() {
481        // "8453" → Base (eip155:8453)
482        let chain = parse_chain("8453").unwrap();
483        assert_eq!(chain.name, "base");
484        assert_eq!(chain.chain_type, ChainType::Evm);
485        assert_eq!(chain.chain_id, "eip155:8453");
486    }
487
488    #[test]
489    fn test_parse_chain_bare_numeric_mainnet() {
490        let chain = parse_chain("1").unwrap();
491        assert_eq!(chain.name, "ethereum");
492        assert_eq!(chain.chain_id, "eip155:1");
493    }
494
495    #[test]
496    fn test_parse_chain_bare_numeric_unknown() {
497        // Unknown EVM chain ID still resolves
498        let chain = parse_chain("99999").unwrap();
499        assert_eq!(chain.chain_type, ChainType::Evm);
500        assert_eq!(chain.chain_id, "eip155:99999");
501    }
502
503    #[test]
504    fn test_parse_chain_unknown() {
505        assert!(parse_chain("unknown_chain").is_err());
506    }
507
508    #[test]
509    fn test_all_chain_types() {
510        assert_eq!(ALL_CHAIN_TYPES.len(), 9);
511    }
512
513    #[test]
514    fn test_default_chain_for_type() {
515        let chain = default_chain_for_type(ChainType::Evm);
516        assert_eq!(chain.name, "ethereum");
517        assert_eq!(chain.chain_id, "eip155:1");
518    }
519}