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}
16
17/// All supported chain families, used for universal wallet derivation.
18pub const ALL_CHAIN_TYPES: [ChainType; 6] = [
19    ChainType::Evm,
20    ChainType::Solana,
21    ChainType::Bitcoin,
22    ChainType::Cosmos,
23    ChainType::Tron,
24    ChainType::Ton,
25];
26
27/// A specific chain (e.g. "ethereum", "arbitrum") with its family type and CAIP-2 ID.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct Chain {
30    pub name: &'static str,
31    pub chain_type: ChainType,
32    pub chain_id: &'static str,
33}
34
35/// Known chains registry.
36pub const KNOWN_CHAINS: &[Chain] = &[
37    Chain {
38        name: "ethereum",
39        chain_type: ChainType::Evm,
40        chain_id: "eip155:1",
41    },
42    Chain {
43        name: "polygon",
44        chain_type: ChainType::Evm,
45        chain_id: "eip155:137",
46    },
47    Chain {
48        name: "arbitrum",
49        chain_type: ChainType::Evm,
50        chain_id: "eip155:42161",
51    },
52    Chain {
53        name: "optimism",
54        chain_type: ChainType::Evm,
55        chain_id: "eip155:10",
56    },
57    Chain {
58        name: "base",
59        chain_type: ChainType::Evm,
60        chain_id: "eip155:8453",
61    },
62    Chain {
63        name: "bsc",
64        chain_type: ChainType::Evm,
65        chain_id: "eip155:56",
66    },
67    Chain {
68        name: "avalanche",
69        chain_type: ChainType::Evm,
70        chain_id: "eip155:43114",
71    },
72    Chain {
73        name: "solana",
74        chain_type: ChainType::Solana,
75        chain_id: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
76    },
77    Chain {
78        name: "bitcoin",
79        chain_type: ChainType::Bitcoin,
80        chain_id: "bip122:000000000019d6689c085ae165831e93",
81    },
82    Chain {
83        name: "cosmos",
84        chain_type: ChainType::Cosmos,
85        chain_id: "cosmos:cosmoshub-4",
86    },
87    Chain {
88        name: "tron",
89        chain_type: ChainType::Tron,
90        chain_id: "tron:mainnet",
91    },
92    Chain {
93        name: "ton",
94        chain_type: ChainType::Ton,
95        chain_id: "ton:mainnet",
96    },
97    Chain {
98        name: "spark",
99        chain_type: ChainType::Spark,
100        chain_id: "spark:mainnet",
101    },
102];
103
104/// Parse a chain string into a `Chain`. Accepts:
105/// - Friendly names: "ethereum", "arbitrum", "solana", etc.
106/// - CAIP-2 chain IDs: "eip155:1", "eip155:42161", etc.
107/// - Legacy family names for backward compat: "evm" → resolves to ethereum
108pub fn parse_chain(s: &str) -> Result<Chain, String> {
109    let lower = s.to_lowercase();
110
111    // Legacy family name backward compat
112    let lookup = match lower.as_str() {
113        "evm" => "ethereum",
114        _ => &lower,
115    };
116
117    // Try friendly name match
118    if let Some(chain) = KNOWN_CHAINS.iter().find(|c| c.name == lookup) {
119        return Ok(*chain);
120    }
121
122    // Try CAIP-2 chain ID match
123    if let Some(chain) = KNOWN_CHAINS.iter().find(|c| c.chain_id == s) {
124        return Ok(*chain);
125    }
126
127    Err(format!(
128        "unknown chain: '{}'. Use a chain name (ethereum, solana, bitcoin, ...) or CAIP-2 ID (eip155:1, ...)",
129        s
130    ))
131}
132
133/// Returns the default `Chain` for a given `ChainType` (first match in registry).
134pub fn default_chain_for_type(ct: ChainType) -> Chain {
135    *KNOWN_CHAINS.iter().find(|c| c.chain_type == ct).unwrap()
136}
137
138impl ChainType {
139    /// Returns the CAIP-2 namespace for this chain type.
140    pub fn namespace(&self) -> &'static str {
141        match self {
142            ChainType::Evm => "eip155",
143            ChainType::Solana => "solana",
144            ChainType::Cosmos => "cosmos",
145            ChainType::Bitcoin => "bip122",
146            ChainType::Tron => "tron",
147            ChainType::Ton => "ton",
148            ChainType::Spark => "spark",
149        }
150    }
151
152    /// Returns the BIP-44 coin type for this chain type.
153    pub fn default_coin_type(&self) -> u32 {
154        match self {
155            ChainType::Evm => 60,
156            ChainType::Solana => 501,
157            ChainType::Cosmos => 118,
158            ChainType::Bitcoin => 0,
159            ChainType::Tron => 195,
160            ChainType::Ton => 607,
161            ChainType::Spark => 8797555,
162        }
163    }
164
165    /// Returns the ChainType for a given CAIP-2 namespace.
166    pub fn from_namespace(ns: &str) -> Option<ChainType> {
167        match ns {
168            "eip155" => Some(ChainType::Evm),
169            "solana" => Some(ChainType::Solana),
170            "cosmos" => Some(ChainType::Cosmos),
171            "bip122" => Some(ChainType::Bitcoin),
172            "tron" => Some(ChainType::Tron),
173            "ton" => Some(ChainType::Ton),
174            "spark" => Some(ChainType::Spark),
175            _ => None,
176        }
177    }
178}
179
180impl fmt::Display for ChainType {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        let s = match self {
183            ChainType::Evm => "evm",
184            ChainType::Solana => "solana",
185            ChainType::Cosmos => "cosmos",
186            ChainType::Bitcoin => "bitcoin",
187            ChainType::Tron => "tron",
188            ChainType::Ton => "ton",
189            ChainType::Spark => "spark",
190        };
191        write!(f, "{}", s)
192    }
193}
194
195impl FromStr for ChainType {
196    type Err = String;
197
198    fn from_str(s: &str) -> Result<Self, Self::Err> {
199        match s.to_lowercase().as_str() {
200            "evm" => Ok(ChainType::Evm),
201            "solana" => Ok(ChainType::Solana),
202            "cosmos" => Ok(ChainType::Cosmos),
203            "bitcoin" => Ok(ChainType::Bitcoin),
204            "tron" => Ok(ChainType::Tron),
205            "ton" => Ok(ChainType::Ton),
206            "spark" => Ok(ChainType::Spark),
207            _ => Err(format!("unknown chain type: {}", s)),
208        }
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_serde_roundtrip() {
218        let chain = ChainType::Evm;
219        let json = serde_json::to_string(&chain).unwrap();
220        assert_eq!(json, "\"evm\"");
221        let chain2: ChainType = serde_json::from_str(&json).unwrap();
222        assert_eq!(chain, chain2);
223    }
224
225    #[test]
226    fn test_serde_all_variants() {
227        for (chain, expected) in [
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        ] {
236            let json = serde_json::to_string(&chain).unwrap();
237            assert_eq!(json, expected);
238            let deserialized: ChainType = serde_json::from_str(&json).unwrap();
239            assert_eq!(chain, deserialized);
240        }
241    }
242
243    #[test]
244    fn test_namespace_mapping() {
245        assert_eq!(ChainType::Evm.namespace(), "eip155");
246        assert_eq!(ChainType::Solana.namespace(), "solana");
247        assert_eq!(ChainType::Cosmos.namespace(), "cosmos");
248        assert_eq!(ChainType::Bitcoin.namespace(), "bip122");
249        assert_eq!(ChainType::Tron.namespace(), "tron");
250        assert_eq!(ChainType::Ton.namespace(), "ton");
251        assert_eq!(ChainType::Spark.namespace(), "spark");
252    }
253
254    #[test]
255    fn test_coin_type_mapping() {
256        assert_eq!(ChainType::Evm.default_coin_type(), 60);
257        assert_eq!(ChainType::Solana.default_coin_type(), 501);
258        assert_eq!(ChainType::Cosmos.default_coin_type(), 118);
259        assert_eq!(ChainType::Bitcoin.default_coin_type(), 0);
260        assert_eq!(ChainType::Tron.default_coin_type(), 195);
261        assert_eq!(ChainType::Ton.default_coin_type(), 607);
262        assert_eq!(ChainType::Spark.default_coin_type(), 8797555);
263    }
264
265    #[test]
266    fn test_from_namespace() {
267        assert_eq!(ChainType::from_namespace("eip155"), Some(ChainType::Evm));
268        assert_eq!(ChainType::from_namespace("solana"), Some(ChainType::Solana));
269        assert_eq!(ChainType::from_namespace("cosmos"), Some(ChainType::Cosmos));
270        assert_eq!(
271            ChainType::from_namespace("bip122"),
272            Some(ChainType::Bitcoin)
273        );
274        assert_eq!(ChainType::from_namespace("tron"), Some(ChainType::Tron));
275        assert_eq!(ChainType::from_namespace("ton"), Some(ChainType::Ton));
276        assert_eq!(ChainType::from_namespace("spark"), Some(ChainType::Spark));
277        assert_eq!(ChainType::from_namespace("unknown"), None);
278    }
279
280    #[test]
281    fn test_from_str() {
282        assert_eq!("evm".parse::<ChainType>().unwrap(), ChainType::Evm);
283        assert_eq!("Solana".parse::<ChainType>().unwrap(), ChainType::Solana);
284        assert!("unknown".parse::<ChainType>().is_err());
285    }
286
287    #[test]
288    fn test_display() {
289        assert_eq!(ChainType::Evm.to_string(), "evm");
290        assert_eq!(ChainType::Bitcoin.to_string(), "bitcoin");
291    }
292
293    #[test]
294    fn test_parse_chain_friendly_name() {
295        let chain = parse_chain("ethereum").unwrap();
296        assert_eq!(chain.name, "ethereum");
297        assert_eq!(chain.chain_type, ChainType::Evm);
298        assert_eq!(chain.chain_id, "eip155:1");
299    }
300
301    #[test]
302    fn test_parse_chain_caip2() {
303        let chain = parse_chain("eip155:42161").unwrap();
304        assert_eq!(chain.name, "arbitrum");
305        assert_eq!(chain.chain_type, ChainType::Evm);
306    }
307
308    #[test]
309    fn test_parse_chain_legacy_evm() {
310        let chain = parse_chain("evm").unwrap();
311        assert_eq!(chain.name, "ethereum");
312        assert_eq!(chain.chain_type, ChainType::Evm);
313    }
314
315    #[test]
316    fn test_parse_chain_solana() {
317        let chain = parse_chain("solana").unwrap();
318        assert_eq!(chain.chain_type, ChainType::Solana);
319    }
320
321    #[test]
322    fn test_parse_chain_unknown() {
323        assert!(parse_chain("unknown_chain").is_err());
324    }
325
326    #[test]
327    fn test_all_chain_types() {
328        assert_eq!(ALL_CHAIN_TYPES.len(), 6);
329    }
330
331    #[test]
332    fn test_default_chain_for_type() {
333        let chain = default_chain_for_type(ChainType::Evm);
334        assert_eq!(chain.name, "ethereum");
335        assert_eq!(chain.chain_id, "eip155:1");
336    }
337}