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}
15
16/// All supported chain families, used for universal wallet derivation.
17pub 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/// A specific chain (e.g. "ethereum", "arbitrum") with its family type and CAIP-2 ID.
27#[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
34/// Known chains registry.
35pub 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
98/// Parse a chain string into a `Chain`. Accepts:
99/// - Friendly names: "ethereum", "arbitrum", "solana", etc.
100/// - CAIP-2 chain IDs: "eip155:1", "eip155:42161", etc.
101/// - Legacy family names for backward compat: "evm" → resolves to ethereum
102pub fn parse_chain(s: &str) -> Result<Chain, String> {
103    let lower = s.to_lowercase();
104
105    // Legacy family name backward compat
106    let lookup = match lower.as_str() {
107        "evm" => "ethereum",
108        _ => &lower,
109    };
110
111    // Try friendly name match
112    if let Some(chain) = KNOWN_CHAINS.iter().find(|c| c.name == lookup) {
113        return Ok(*chain);
114    }
115
116    // Try CAIP-2 chain ID match
117    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
127/// Returns the default `Chain` for a given `ChainType` (first match in registry).
128pub fn default_chain_for_type(ct: ChainType) -> Chain {
129    *KNOWN_CHAINS.iter().find(|c| c.chain_type == ct).unwrap()
130}
131
132impl ChainType {
133    /// Returns the CAIP-2 namespace for this chain type.
134    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    /// Returns the BIP-44 coin type for this chain type.
146    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    /// Returns the ChainType for a given CAIP-2 namespace.
158    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}