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