Skip to main content

odos_sdk/types/
chain.rs

1// SPDX-FileCopyrightText: 2025 Semiotic AI, Inc.
2//
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{fmt, str::FromStr};
6
7use alloy_chains::NamedChain;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10use crate::{OdosChain, OdosChainError, OdosChainResult};
11
12/// Type-safe chain identifier with convenient constructors
13///
14/// Provides ergonomic helpers for accessing supported chains while
15/// maintaining full compatibility with `alloy_chains::NamedChain`.
16///
17/// # Examples
18///
19/// ```rust
20/// use odos_sdk::{Chain, OdosChain};
21///
22/// // Convenient constructors
23/// let chain = Chain::ethereum();
24/// let chain = Chain::arbitrum();
25/// let chain = Chain::base();
26///
27/// // From chain ID
28/// let chain = Chain::from_chain_id(1)?;  // Ethereum
29/// let chain = Chain::from_chain_id(42161)?;  // Arbitrum
30///
31/// // Access inner NamedChain
32/// let named = chain.inner();
33///
34/// // Use OdosChain trait methods
35/// let router = chain.v3_router_address()?;
36/// # Ok::<(), Box<dyn std::error::Error>>(())
37/// ```
38#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
39pub struct Chain(NamedChain);
40
41impl Chain {
42    /// Ethereum Mainnet (Chain ID: 1)
43    ///
44    /// # Examples
45    ///
46    /// ```rust
47    /// use odos_sdk::Chain;
48    ///
49    /// let chain = Chain::ethereum();
50    /// assert_eq!(chain.id(), 1);
51    /// ```
52    pub const fn ethereum() -> Self {
53        Self(NamedChain::Mainnet)
54    }
55
56    /// Arbitrum One (Chain ID: 42161)
57    ///
58    /// # Examples
59    ///
60    /// ```rust
61    /// use odos_sdk::Chain;
62    ///
63    /// let chain = Chain::arbitrum();
64    /// assert_eq!(chain.id(), 42161);
65    /// ```
66    pub const fn arbitrum() -> Self {
67        Self(NamedChain::Arbitrum)
68    }
69
70    /// Optimism (Chain ID: 10)
71    ///
72    /// # Examples
73    ///
74    /// ```rust
75    /// use odos_sdk::Chain;
76    ///
77    /// let chain = Chain::optimism();
78    /// assert_eq!(chain.id(), 10);
79    /// ```
80    pub const fn optimism() -> Self {
81        Self(NamedChain::Optimism)
82    }
83
84    /// Polygon (Chain ID: 137)
85    ///
86    /// # Examples
87    ///
88    /// ```rust
89    /// use odos_sdk::Chain;
90    ///
91    /// let chain = Chain::polygon();
92    /// assert_eq!(chain.id(), 137);
93    /// ```
94    pub const fn polygon() -> Self {
95        Self(NamedChain::Polygon)
96    }
97
98    /// Base (Chain ID: 8453)
99    ///
100    /// # Examples
101    ///
102    /// ```rust
103    /// use odos_sdk::Chain;
104    ///
105    /// let chain = Chain::base();
106    /// assert_eq!(chain.id(), 8453);
107    /// ```
108    pub const fn base() -> Self {
109        Self(NamedChain::Base)
110    }
111
112    /// BNB Smart Chain (Chain ID: 56)
113    ///
114    /// # Examples
115    ///
116    /// ```rust
117    /// use odos_sdk::Chain;
118    ///
119    /// let chain = Chain::bsc();
120    /// assert_eq!(chain.id(), 56);
121    /// ```
122    pub const fn bsc() -> Self {
123        Self(NamedChain::BinanceSmartChain)
124    }
125
126    /// Avalanche C-Chain (Chain ID: 43114)
127    ///
128    /// # Examples
129    ///
130    /// ```rust
131    /// use odos_sdk::Chain;
132    ///
133    /// let chain = Chain::avalanche();
134    /// assert_eq!(chain.id(), 43114);
135    /// ```
136    pub const fn avalanche() -> Self {
137        Self(NamedChain::Avalanche)
138    }
139
140    /// Linea (Chain ID: 59144)
141    ///
142    /// # Examples
143    ///
144    /// ```rust
145    /// use odos_sdk::Chain;
146    ///
147    /// let chain = Chain::linea();
148    /// assert_eq!(chain.id(), 59144);
149    /// ```
150    pub const fn linea() -> Self {
151        Self(NamedChain::Linea)
152    }
153
154    /// ZkSync Era (Chain ID: 324)
155    ///
156    /// # Examples
157    ///
158    /// ```rust
159    /// use odos_sdk::Chain;
160    ///
161    /// let chain = Chain::zksync();
162    /// assert_eq!(chain.id(), 324);
163    /// ```
164    pub const fn zksync() -> Self {
165        Self(NamedChain::ZkSync)
166    }
167
168    /// Mantle (Chain ID: 5000)
169    ///
170    /// # Examples
171    ///
172    /// ```rust
173    /// use odos_sdk::Chain;
174    ///
175    /// let chain = Chain::mantle();
176    /// assert_eq!(chain.id(), 5000);
177    /// ```
178    pub const fn mantle() -> Self {
179        Self(NamedChain::Mantle)
180    }
181
182    /// Fraxtal (Chain ID: 252)
183    ///
184    /// # Examples
185    ///
186    /// ```rust
187    /// use odos_sdk::Chain;
188    ///
189    /// let chain = Chain::fraxtal();
190    /// assert_eq!(chain.id(), 252);
191    /// ```
192    pub const fn fraxtal() -> Self {
193        Self(NamedChain::Fraxtal)
194    }
195
196    /// Sonic (Chain ID: 146)
197    ///
198    /// # Examples
199    ///
200    /// ```rust
201    /// use odos_sdk::Chain;
202    ///
203    /// let chain = Chain::sonic();
204    /// assert_eq!(chain.id(), 146);
205    /// ```
206    pub const fn sonic() -> Self {
207        Self(NamedChain::Sonic)
208    }
209
210    /// Unichain (Chain ID: 130)
211    ///
212    /// # Examples
213    ///
214    /// ```rust
215    /// use odos_sdk::Chain;
216    ///
217    /// let chain = Chain::unichain();
218    /// assert_eq!(chain.id(), 130);
219    /// ```
220    pub const fn unichain() -> Self {
221        Self(NamedChain::Unichain)
222    }
223
224    /// Create a chain from a chain ID
225    ///
226    /// # Arguments
227    ///
228    /// * `id` - The EVM chain ID
229    ///
230    /// # Returns
231    ///
232    /// * `Ok(Chain)` - If the chain ID is recognized
233    /// * `Err(OdosChainError)` - If the chain ID is not supported
234    ///
235    /// # Examples
236    ///
237    /// ```rust
238    /// use odos_sdk::Chain;
239    ///
240    /// let chain = Chain::from_chain_id(1)?;      // Ethereum
241    /// let chain = Chain::from_chain_id(42161)?;  // Arbitrum
242    /// let chain = Chain::from_chain_id(8453)?;   // Base
243    ///
244    /// // Unsupported chain
245    /// assert!(Chain::from_chain_id(999999).is_err());
246    /// # Ok::<(), Box<dyn std::error::Error>>(())
247    /// ```
248    pub fn from_chain_id(id: u64) -> OdosChainResult<Self> {
249        let chain = NamedChain::try_from(id).map_err(|_| OdosChainError::UnsupportedChain {
250            chain: format!("Chain ID {id}"),
251        })?;
252
253        if chain.supports_odos() {
254            Ok(Self(chain))
255        } else {
256            Err(OdosChainError::UnsupportedChain {
257                chain: format!("Chain ID {id}"),
258            })
259        }
260    }
261
262    /// Get the chain ID
263    ///
264    /// # Examples
265    ///
266    /// ```rust
267    /// use odos_sdk::Chain;
268    ///
269    /// assert_eq!(Chain::ethereum().id(), 1);
270    /// assert_eq!(Chain::arbitrum().id(), 42161);
271    /// assert_eq!(Chain::base().id(), 8453);
272    /// ```
273    pub fn id(&self) -> u64 {
274        self.0.into()
275    }
276
277    /// Get the inner `NamedChain`
278    ///
279    /// # Examples
280    ///
281    /// ```rust
282    /// use odos_sdk::Chain;
283    /// use alloy_chains::NamedChain;
284    ///
285    /// let chain = Chain::ethereum();
286    /// assert_eq!(chain.inner(), NamedChain::Mainnet);
287    /// ```
288    pub const fn inner(&self) -> NamedChain {
289        self.0
290    }
291
292    /// Parse a supported Odos chain from a common human-readable name or alias.
293    ///
294    /// Accepts common aliases such as `mainnet`, `ethereum`, `arb`, `op`, and
295    /// numeric chain IDs encoded as strings.
296    pub fn from_name(name: &str) -> OdosChainResult<Self> {
297        let normalized = normalize_chain_name(name);
298
299        if let Ok(chain_id) = normalized.parse::<u64>() {
300            return Self::from_chain_id(chain_id);
301        }
302
303        match normalized.as_str() {
304            "mainnet" | "ethereum" | "eth" | "ethereum mainnet" => Ok(Self::ethereum()),
305            "arbitrum" | "arb" | "arbitrum one" => Ok(Self::arbitrum()),
306            "optimism" | "op" => Ok(Self::optimism()),
307            "polygon" | "matic" | "polygon pos" => Ok(Self::polygon()),
308            "base" => Ok(Self::base()),
309            "bsc" | "bnb" | "bnb smart chain" | "binance smart chain" => Ok(Self::bsc()),
310            "avalanche" | "avax" | "avalanche c chain" => Ok(Self::avalanche()),
311            "linea" => Ok(Self::linea()),
312            "zksync" | "zk sync" | "zksync era" => Ok(Self::zksync()),
313            "mantle" => Ok(Self::mantle()),
314            "fraxtal" => Ok(Self::fraxtal()),
315            "sonic" => Ok(Self::sonic()),
316            "unichain" => Ok(Self::unichain()),
317            _ => Err(OdosChainError::UnsupportedChain {
318                chain: name.trim().to_string(),
319            }),
320        }
321    }
322}
323
324fn normalize_chain_name(name: &str) -> String {
325    name.trim()
326        .to_ascii_lowercase()
327        .replace(['-', '_'], " ")
328        .split_whitespace()
329        .collect::<Vec<_>>()
330        .join(" ")
331}
332
333impl fmt::Display for Chain {
334    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335        write!(f, "{}", self.0)
336    }
337}
338
339impl From<NamedChain> for Chain {
340    fn from(chain: NamedChain) -> Self {
341        Self(chain)
342    }
343}
344
345impl From<Chain> for NamedChain {
346    fn from(chain: Chain) -> Self {
347        chain.0
348    }
349}
350
351impl From<Chain> for u64 {
352    fn from(chain: Chain) -> Self {
353        chain.0.into()
354    }
355}
356
357impl FromStr for Chain {
358    type Err = OdosChainError;
359
360    fn from_str(s: &str) -> Result<Self, Self::Err> {
361        Self::from_name(s)
362    }
363}
364
365// Custom serialization using chain ID
366impl Serialize for Chain {
367    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
368    where
369        S: Serializer,
370    {
371        let chain_id: u64 = self.0.into();
372        chain_id.serialize(serializer)
373    }
374}
375
376impl<'de> Deserialize<'de> for Chain {
377    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
378    where
379        D: Deserializer<'de>,
380    {
381        let chain_id = u64::deserialize(deserializer)?;
382        Self::from_chain_id(chain_id).map_err(serde::de::Error::custom)
383    }
384}
385
386// Implement OdosChain trait by delegating to inner NamedChain
387impl OdosChain for Chain {
388    fn lo_router_address(&self) -> OdosChainResult<alloy_primitives::Address> {
389        self.0.lo_router_address()
390    }
391
392    fn v2_router_address(&self) -> OdosChainResult<alloy_primitives::Address> {
393        self.0.v2_router_address()
394    }
395
396    fn v3_router_address(&self) -> OdosChainResult<alloy_primitives::Address> {
397        self.0.v3_router_address()
398    }
399
400    fn supports_odos(&self) -> bool {
401        self.0.supports_odos()
402    }
403
404    fn supports_lo(&self) -> bool {
405        self.0.supports_lo()
406    }
407
408    fn supports_v2(&self) -> bool {
409        self.0.supports_v2()
410    }
411
412    fn supports_v3(&self) -> bool {
413        self.0.supports_v3()
414    }
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420
421    #[test]
422    fn test_chain_constructors() {
423        assert_eq!(Chain::ethereum().id(), 1);
424        assert_eq!(Chain::arbitrum().id(), 42161);
425        assert_eq!(Chain::optimism().id(), 10);
426        assert_eq!(Chain::polygon().id(), 137);
427        assert_eq!(Chain::base().id(), 8453);
428        assert_eq!(Chain::bsc().id(), 56);
429        assert_eq!(Chain::avalanche().id(), 43114);
430        assert_eq!(Chain::linea().id(), 59144);
431        assert_eq!(Chain::zksync().id(), 324);
432        assert_eq!(Chain::mantle().id(), 5000);
433        assert_eq!(Chain::fraxtal().id(), 252);
434        assert_eq!(Chain::sonic().id(), 146);
435        assert_eq!(Chain::unichain().id(), 130);
436    }
437
438    #[test]
439    fn test_from_chain_id() {
440        assert_eq!(Chain::from_chain_id(1).unwrap().id(), 1);
441        assert_eq!(Chain::from_chain_id(42161).unwrap().id(), 42161);
442        assert_eq!(Chain::from_chain_id(8453).unwrap().id(), 8453);
443
444        // Unsupported chain
445        assert!(Chain::from_chain_id(999999).is_err());
446        assert!(Chain::from_chain_id(11155111).is_err());
447    }
448
449    #[test]
450    fn test_from_name() {
451        assert_eq!(Chain::from_name("ethereum").unwrap(), Chain::ethereum());
452        assert_eq!(Chain::from_name("mainnet").unwrap(), Chain::ethereum());
453        assert_eq!(Chain::from_name("arb").unwrap(), Chain::arbitrum());
454        assert_eq!(Chain::from_name("op").unwrap(), Chain::optimism());
455        assert_eq!(Chain::from_name("bnb smart chain").unwrap(), Chain::bsc());
456        assert_eq!(Chain::from_name("8453").unwrap(), Chain::base());
457        assert!(Chain::from_name("sepolia").is_err());
458    }
459
460    #[test]
461    fn test_inner() {
462        assert_eq!(Chain::ethereum().inner(), NamedChain::Mainnet);
463        assert_eq!(Chain::arbitrum().inner(), NamedChain::Arbitrum);
464        assert_eq!(Chain::base().inner(), NamedChain::Base);
465    }
466
467    #[test]
468    fn test_display() {
469        assert_eq!(format!("{}", Chain::ethereum()), "mainnet");
470        assert_eq!(format!("{}", Chain::arbitrum()), "arbitrum");
471        assert_eq!(format!("{}", Chain::base()), "base");
472    }
473
474    #[test]
475    fn test_conversions() {
476        // From NamedChain
477        let chain: Chain = NamedChain::Mainnet.into();
478        assert_eq!(chain.id(), 1);
479
480        // To NamedChain
481        let named: NamedChain = Chain::ethereum().into();
482        assert_eq!(named, NamedChain::Mainnet);
483
484        // To u64
485        let id: u64 = Chain::ethereum().into();
486        assert_eq!(id, 1);
487    }
488
489    #[test]
490    fn test_odos_chain_trait() {
491        let chain = Chain::ethereum();
492
493        // Test trait methods work
494        assert!(chain.supports_odos());
495        assert!(chain.supports_v2());
496        assert!(chain.supports_v3());
497        assert!(chain.v2_router_address().is_ok());
498        assert!(chain.v3_router_address().is_ok());
499    }
500
501    #[test]
502    fn test_equality() {
503        assert_eq!(Chain::ethereum(), Chain::ethereum());
504        assert_ne!(Chain::ethereum(), Chain::arbitrum());
505    }
506
507    #[test]
508    fn test_serialization() {
509        let chain = Chain::ethereum();
510
511        // Serialize (as chain ID)
512        let json = serde_json::to_string(&chain).unwrap();
513        assert_eq!(json, "1"); // Ethereum chain ID
514
515        // Deserialize
516        let deserialized: Chain = serde_json::from_str(&json).unwrap();
517        assert_eq!(deserialized, chain);
518
519        // Test other chains
520        assert_eq!(serde_json::to_string(&Chain::arbitrum()).unwrap(), "42161");
521        assert_eq!(serde_json::to_string(&Chain::base()).unwrap(), "8453");
522    }
523}