odos_sdk/
chain.rs

1use alloy_chains::NamedChain;
2use alloy_primitives::Address;
3use thiserror::Error;
4
5use crate::{
6    ODOS_V2_ARBITRUM_ROUTER, ODOS_V2_AVALANCHE_ROUTER, ODOS_V2_BASE_ROUTER, ODOS_V2_BSC_ROUTER,
7    ODOS_V2_ETHEREUM_ROUTER, ODOS_V2_FANTOM_ROUTER, ODOS_V2_FRAXTAL_ROUTER, ODOS_V2_LINEA_ROUTER,
8    ODOS_V2_MANTLE_ROUTER, ODOS_V2_MODE_ROUTER, ODOS_V2_OP_ROUTER, ODOS_V2_POLYGON_ROUTER,
9    ODOS_V2_SCROLL_ROUTER, ODOS_V2_SONIC_ROUTER, ODOS_V2_UNICHAIN_ROUTER, ODOS_V2_ZKSYNC_ROUTER,
10    ODOS_V3,
11};
12
13/// Errors that can occur when working with Odos chains
14#[derive(Error, Debug, Clone, PartialEq)]
15pub enum OdosChainError {
16    /// The chain is not supported by Odos protocol
17    #[error("Chain {chain:?} is not supported by Odos protocol")]
18    UnsupportedChain { chain: String },
19
20    /// The V2 router is not available on this chain
21    #[error("Odos V2 router is not available on chain {chain:?}")]
22    V2NotAvailable { chain: String },
23
24    /// The V3 router is not available on this chain  
25    #[error("Odos V3 router is not available on chain {chain:?}")]
26    V3NotAvailable { chain: String },
27
28    /// Invalid address format
29    #[error("Invalid address format: {address}")]
30    InvalidAddress { address: String },
31}
32
33/// Result type for Odos chain operations
34pub type OdosChainResult<T> = Result<T, OdosChainError>;
35
36/// Trait for chains that support Odos protocol
37///
38/// This trait provides a type-safe way to access Odos router addresses
39/// for supported blockchain networks, integrating seamlessly with the
40/// Alloy ecosystem.
41///
42/// # Examples
43///
44/// ```rust
45/// use odos_sdk::OdosChain;
46/// use alloy_chains::NamedChain;
47///
48/// // Get V2 router address
49/// let v2_router = NamedChain::Mainnet.v2_router_address()?;
50///
51/// // Get V3 router address
52/// let v3_router = NamedChain::Mainnet.v3_router_address()?;
53///
54/// // Get both addresses
55/// let (v2, v3) = NamedChain::Arbitrum.both_router_addresses()?;
56///
57/// // Check support
58/// assert!(NamedChain::Mainnet.supports_odos());
59/// assert!(NamedChain::Mainnet.supports_v3());
60/// # Ok::<(), odos_sdk::OdosChainError>(())
61/// ```
62pub trait OdosChain {
63    /// Get the V2 router address for this chain
64    ///
65    /// # Returns
66    ///
67    /// * `Ok(Address)` - The V2 router contract address
68    /// * `Err(OdosChainError)` - If the chain is not supported or address is invalid
69    ///
70    /// # Example
71    ///
72    /// ```rust
73    /// use odos_sdk::OdosChain;
74    /// use alloy_chains::NamedChain;
75    ///
76    /// let address = NamedChain::Mainnet.v2_router_address()?;
77    /// # Ok::<(), odos_sdk::OdosChainError>(())
78    /// ```
79    fn v2_router_address(&self) -> OdosChainResult<Address>;
80
81    /// Get the V3 router address for this chain
82    ///
83    /// V3 uses the same address across all supported chains,
84    /// following CREATE2 deterministic deployment.
85    ///
86    /// # Returns
87    ///
88    /// * `Ok(Address)` - The V3 router contract address
89    /// * `Err(OdosChainError)` - If the chain is not supported or address is invalid
90    ///
91    /// # Example
92    ///
93    /// ```rust
94    /// use odos_sdk::OdosChain;
95    /// use alloy_chains::NamedChain;
96    ///
97    /// let address = NamedChain::Mainnet.v3_router_address()?;
98    /// # Ok::<(), odos_sdk::OdosChainError>(())
99    /// ```
100    fn v3_router_address(&self) -> OdosChainResult<Address>;
101
102    /// Get both V2 and V3 router addresses for this chain
103    ///
104    /// # Returns
105    ///
106    /// * `Ok((v2_address, v3_address))` - Both router addresses
107    /// * `Err(OdosChainError)` - If the chain is not supported by either version
108    ///
109    /// # Example
110    ///
111    /// ```rust
112    /// use odos_sdk::OdosChain;
113    /// use alloy_chains::NamedChain;
114    ///
115    /// let (v2, v3) = NamedChain::Arbitrum.both_router_addresses()?;
116    /// # Ok::<(), odos_sdk::OdosChainError>(())
117    /// ```
118    fn both_router_addresses(&self) -> OdosChainResult<(Address, Address)> {
119        Ok((self.v2_router_address()?, self.v3_router_address()?))
120    }
121
122    /// Check if this chain supports Odos protocol
123    ///
124    /// # Returns
125    ///
126    /// `true` if either V2 or V3 (or both) are supported on this chain
127    fn supports_odos(&self) -> bool;
128
129    /// Check if this chain supports Odos V2
130    ///
131    /// # Returns
132    ///
133    /// `true` if V2 is supported on this chain
134    fn supports_v2(&self) -> bool;
135
136    /// Check if this chain supports Odos V3
137    ///
138    /// # Returns
139    ///
140    /// `true` if V3 is supported on this chain
141    fn supports_v3(&self) -> bool;
142
143    /// Try to get the V2 router address without errors
144    ///
145    /// # Returns
146    ///
147    /// `Some(address)` if supported, `None` if not supported
148    fn try_v2_router_address(&self) -> Option<Address> {
149        self.v2_router_address().ok()
150    }
151
152    /// Try to get the V3 router address without errors
153    ///
154    /// # Returns
155    ///
156    /// `Some(address)` if supported, `None` if not supported
157    fn try_v3_router_address(&self) -> Option<Address> {
158        self.v3_router_address().ok()
159    }
160
161    /// Try to get both router addresses without errors
162    ///
163    /// # Returns
164    ///
165    /// `Some((v2_address, v3_address))` if both are supported, `None` otherwise
166    fn try_both_router_addresses(&self) -> Option<(Address, Address)> {
167        self.both_router_addresses().ok()
168    }
169}
170
171impl OdosChain for NamedChain {
172    fn v2_router_address(&self) -> OdosChainResult<Address> {
173        use NamedChain::*;
174
175        if !self.supports_odos() {
176            return Err(OdosChainError::V2NotAvailable {
177                chain: format!("{self:?}"),
178            });
179        }
180
181        // If V2 is not available on this chain, fall back to V3
182        if !self.supports_v2() {
183            return self.v3_router_address();
184        }
185
186        let address_str = match self {
187            Arbitrum => ODOS_V2_ARBITRUM_ROUTER,
188            Avalanche => ODOS_V2_AVALANCHE_ROUTER,
189            Base => ODOS_V2_BASE_ROUTER,
190            BinanceSmartChain => ODOS_V2_BSC_ROUTER,
191            Fantom => ODOS_V2_FANTOM_ROUTER,
192            Fraxtal => ODOS_V2_FRAXTAL_ROUTER,
193            Mainnet => ODOS_V2_ETHEREUM_ROUTER,
194            Optimism => ODOS_V2_OP_ROUTER,
195            Polygon => ODOS_V2_POLYGON_ROUTER,
196            Linea => ODOS_V2_LINEA_ROUTER,
197            Mantle => ODOS_V2_MANTLE_ROUTER,
198            Mode => ODOS_V2_MODE_ROUTER,
199            Scroll => ODOS_V2_SCROLL_ROUTER,
200            Sonic => ODOS_V2_SONIC_ROUTER,
201            ZkSync => ODOS_V2_ZKSYNC_ROUTER,
202            Unichain => ODOS_V2_UNICHAIN_ROUTER,
203            _ => {
204                return Err(OdosChainError::UnsupportedChain {
205                    chain: format!("{self:?}"),
206                });
207            }
208        };
209
210        address_str
211            .parse()
212            .map_err(|_| OdosChainError::InvalidAddress {
213                address: address_str.to_string(),
214            })
215    }
216
217    fn v3_router_address(&self) -> OdosChainResult<Address> {
218        if !self.supports_odos() {
219            return Err(OdosChainError::V3NotAvailable {
220                chain: format!("{self:?}"),
221            });
222        }
223
224        // If V3 is not available on this chain, fall back to V2
225        if !self.supports_v3() {
226            return self.v2_router_address();
227        }
228
229        ODOS_V3.parse().map_err(|_| OdosChainError::InvalidAddress {
230            address: ODOS_V3.to_string(),
231        })
232    }
233
234    fn supports_odos(&self) -> bool {
235        use NamedChain::*;
236        matches!(
237            self,
238            Arbitrum
239                | Avalanche
240                | Base
241                | Berachain
242                | BinanceSmartChain
243                | Fantom
244                | Fraxtal
245                | Mainnet
246                | Optimism
247                | Polygon
248                | Linea
249                | Mantle
250                | Mode
251                | Scroll
252                | Sonic
253                | ZkSync
254                | Unichain
255        )
256    }
257
258    fn supports_v2(&self) -> bool {
259        use NamedChain::*;
260        matches!(
261            self,
262            Arbitrum
263                | Avalanche
264                | Base
265                | BinanceSmartChain
266                | Fantom
267                | Fraxtal
268                | Mainnet
269                | Optimism
270                | Polygon
271                | Linea
272                | Mantle
273                | Mode
274                | Scroll
275                | Sonic
276                | ZkSync
277                | Unichain
278        )
279    }
280
281    fn supports_v3(&self) -> bool {
282        use NamedChain::*;
283        matches!(
284            self,
285            Arbitrum
286                | Avalanche
287                | Base
288                | Berachain
289                | BinanceSmartChain
290                | Fantom
291                | Fraxtal
292                | Mainnet
293                | Optimism
294                | Polygon
295                | Linea
296                | Mantle
297                | Mode
298                | Scroll
299                | Sonic
300                | ZkSync
301                | Unichain
302        )
303    }
304}
305
306/// Extension trait for easy router selection
307///
308/// This trait provides convenient methods for choosing between V2 and V3
309/// routers based on your requirements.
310pub trait OdosRouterSelection: OdosChain {
311    /// Get the recommended router address for this chain
312    ///
313    /// Currently defaults to V3 for enhanced features, but this
314    /// may change based on performance characteristics.
315    ///
316    /// # Returns
317    ///
318    /// * `Ok(Address)` - The recommended router address
319    /// * `Err(OdosChainError)` - If the chain is not supported
320    ///
321    /// # Example
322    ///
323    /// ```rust
324    /// use odos_sdk::{OdosChain, OdosRouterSelection};
325    /// use alloy_chains::NamedChain;
326    ///
327    /// let address = NamedChain::Base.recommended_router_address()?;
328    /// # Ok::<(), odos_sdk::OdosChainError>(())
329    /// ```
330    fn recommended_router_address(&self) -> OdosChainResult<Address> {
331        self.v3_router_address()
332    }
333
334    /// Get router address with fallback strategy
335    ///
336    /// Tries V3 first, falls back to V2 if needed.
337    /// This is useful for maximum compatibility.
338    ///
339    /// # Returns
340    ///
341    /// * `Ok(Address)` - V3 address if available, otherwise V2 address
342    /// * `Err(OdosChainError)` - If neither version is supported
343    ///
344    /// # Example
345    ///
346    /// ```rust
347    /// use odos_sdk::{OdosChain, OdosRouterSelection};
348    /// use alloy_chains::NamedChain;
349    ///
350    /// let address = NamedChain::Mainnet.router_address_with_fallback()?;
351    /// # Ok::<(), odos_sdk::OdosChainError>(())
352    /// ```
353    fn router_address_with_fallback(&self) -> OdosChainResult<Address> {
354        self.v3_router_address()
355            .or_else(|_| self.v2_router_address())
356    }
357
358    /// Get router address based on preference
359    ///
360    /// # Arguments
361    ///
362    /// * `prefer_v3` - Whether to prefer V3 when both are available
363    ///
364    /// # Returns
365    ///
366    /// * `Ok(Address)` - The appropriate router address based on preference
367    /// * `Err(OdosChainError)` - If the preferred version is not supported
368    ///
369    /// # Example
370    ///
371    /// ```rust
372    /// use odos_sdk::{OdosChain, OdosRouterSelection};
373    /// use alloy_chains::NamedChain;
374    ///
375    /// let v3_address = NamedChain::Mainnet.router_address_by_preference(true)?;
376    /// let v2_address = NamedChain::Mainnet.router_address_by_preference(false)?;
377    /// # Ok::<(), odos_sdk::OdosChainError>(())
378    /// ```
379    fn router_address_by_preference(&self, prefer_v3: bool) -> OdosChainResult<Address> {
380        if prefer_v3 {
381            self.v3_router_address()
382        } else {
383            self.v2_router_address()
384        }
385    }
386}
387
388impl<T: OdosChain> OdosRouterSelection for T {}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393    use alloy_chains::NamedChain;
394
395    #[test]
396    fn test_v2_router_addresses() {
397        let chains = [
398            NamedChain::Mainnet,
399            NamedChain::Arbitrum,
400            NamedChain::Optimism,
401            NamedChain::Polygon,
402            NamedChain::Base,
403        ];
404
405        for chain in chains {
406            let address = chain.v2_router_address().unwrap();
407            assert!(address != Address::ZERO);
408            assert_eq!(address.to_string().len(), 42); // 0x + 40 hex chars
409        }
410    }
411
412    #[test]
413    fn test_v3_router_addresses() {
414        let chains = [
415            NamedChain::Mainnet,
416            NamedChain::Arbitrum,
417            NamedChain::Optimism,
418            NamedChain::Polygon,
419            NamedChain::Base,
420        ];
421
422        for chain in chains {
423            let address = chain.v3_router_address().unwrap();
424            assert_eq!(address, ODOS_V3.parse::<Address>().unwrap());
425        }
426    }
427
428    #[test]
429    fn test_both_router_addresses() {
430        let (v2_addr, v3_addr) = NamedChain::Mainnet.both_router_addresses().unwrap();
431        assert_eq!(v2_addr, ODOS_V2_ETHEREUM_ROUTER.parse::<Address>().unwrap());
432        assert_eq!(v3_addr, ODOS_V3.parse::<Address>().unwrap());
433    }
434
435    #[test]
436    fn test_supports_odos() {
437        assert!(NamedChain::Mainnet.supports_odos());
438        assert!(NamedChain::Arbitrum.supports_odos());
439        assert!(NamedChain::Berachain.supports_odos());
440        assert!(!NamedChain::Sepolia.supports_odos());
441    }
442
443    #[test]
444    fn test_supports_v2() {
445        assert!(NamedChain::Mainnet.supports_v2());
446        assert!(NamedChain::Arbitrum.supports_v2());
447        assert!(!NamedChain::Berachain.supports_v2()); // Berachain only has V3
448        assert!(!NamedChain::Sepolia.supports_v2());
449    }
450
451    #[test]
452    fn test_supports_v3() {
453        assert!(NamedChain::Mainnet.supports_v3());
454        assert!(NamedChain::Arbitrum.supports_v3());
455        assert!(NamedChain::Berachain.supports_v3());
456        assert!(!NamedChain::Sepolia.supports_v3());
457    }
458
459    #[test]
460    fn test_berachain_v3_only() {
461        // Berachain only has V3, not V2
462        assert!(!NamedChain::Berachain.supports_v2());
463        assert!(NamedChain::Berachain.supports_v3());
464
465        // Requesting V2 should fallback to V3
466        let v2_result = NamedChain::Berachain.v2_router_address();
467        let v3_result = NamedChain::Berachain.v3_router_address();
468
469        assert!(v2_result.is_ok());
470        assert!(v3_result.is_ok());
471        assert_eq!(v2_result.unwrap(), v3_result.unwrap());
472    }
473
474    #[test]
475    fn test_try_methods() {
476        assert!(NamedChain::Mainnet.try_v2_router_address().is_some());
477        assert!(NamedChain::Mainnet.try_v3_router_address().is_some());
478        assert!(NamedChain::Sepolia.try_v2_router_address().is_none());
479        assert!(NamedChain::Sepolia.try_v3_router_address().is_none());
480
481        assert!(NamedChain::Mainnet.try_both_router_addresses().is_some());
482        assert!(NamedChain::Sepolia.try_both_router_addresses().is_none());
483    }
484
485    #[test]
486    fn test_router_selection() {
487        let chain = NamedChain::Mainnet;
488
489        // Recommended should be V3
490        assert_eq!(
491            chain.recommended_router_address().unwrap(),
492            chain.v3_router_address().unwrap()
493        );
494
495        // Fallback should also be V3 (since both are supported)
496        assert_eq!(
497            chain.router_address_with_fallback().unwrap(),
498            chain.v3_router_address().unwrap()
499        );
500
501        // Preference-based selection
502        assert_eq!(
503            chain.router_address_by_preference(true).unwrap(),
504            chain.v3_router_address().unwrap()
505        );
506        assert_eq!(
507            chain.router_address_by_preference(false).unwrap(),
508            chain.v2_router_address().unwrap()
509        );
510    }
511
512    #[test]
513    fn test_error_handling() {
514        // Test unsupported chain
515        let result = NamedChain::Sepolia.v2_router_address();
516        assert!(result.is_err());
517        assert!(matches!(
518            result.unwrap_err(),
519            OdosChainError::V2NotAvailable { .. }
520        ));
521
522        let result = NamedChain::Sepolia.v3_router_address();
523        assert!(result.is_err());
524        assert!(matches!(
525            result.unwrap_err(),
526            OdosChainError::V3NotAvailable { .. }
527        ));
528    }
529}