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 both V2 and V3 are supported on this chain
127    fn supports_odos(&self) -> bool;
128
129    /// Check if this chain supports Odos V3
130    ///
131    /// # Returns
132    ///
133    /// `true` if V3 is supported on this chain
134    fn supports_v3(&self) -> bool {
135        self.supports_odos() // V2 and V3 have identical coverage
136    }
137
138    /// Try to get the V2 router address without errors
139    ///
140    /// # Returns
141    ///
142    /// `Some(address)` if supported, `None` if not supported
143    fn try_v2_router_address(&self) -> Option<Address> {
144        self.v2_router_address().ok()
145    }
146
147    /// Try to get the V3 router address without errors
148    ///
149    /// # Returns
150    ///
151    /// `Some(address)` if supported, `None` if not supported
152    fn try_v3_router_address(&self) -> Option<Address> {
153        self.v3_router_address().ok()
154    }
155
156    /// Try to get both router addresses without errors
157    ///
158    /// # Returns
159    ///
160    /// `Some((v2_address, v3_address))` if both are supported, `None` otherwise
161    fn try_both_router_addresses(&self) -> Option<(Address, Address)> {
162        self.both_router_addresses().ok()
163    }
164}
165
166impl OdosChain for NamedChain {
167    fn v2_router_address(&self) -> OdosChainResult<Address> {
168        use NamedChain::*;
169
170        if !self.supports_odos() {
171            return Err(OdosChainError::V2NotAvailable {
172                chain: format!("{self:?}"),
173            });
174        }
175
176        let address_str = match self {
177            Arbitrum => ODOS_V2_ARBITRUM_ROUTER,
178            Avalanche => ODOS_V2_AVALANCHE_ROUTER,
179            Base => ODOS_V2_BASE_ROUTER,
180            BinanceSmartChain => ODOS_V2_BSC_ROUTER,
181            Fantom => ODOS_V2_FANTOM_ROUTER,
182            Fraxtal => ODOS_V2_FRAXTAL_ROUTER,
183            Mainnet => ODOS_V2_ETHEREUM_ROUTER,
184            Optimism => ODOS_V2_OP_ROUTER,
185            Polygon => ODOS_V2_POLYGON_ROUTER,
186            Linea => ODOS_V2_LINEA_ROUTER,
187            Mantle => ODOS_V2_MANTLE_ROUTER,
188            Mode => ODOS_V2_MODE_ROUTER,
189            Scroll => ODOS_V2_SCROLL_ROUTER,
190            Sonic => ODOS_V2_SONIC_ROUTER,
191            ZkSync => ODOS_V2_ZKSYNC_ROUTER,
192            Unichain => ODOS_V2_UNICHAIN_ROUTER,
193            _ => {
194                return Err(OdosChainError::UnsupportedChain {
195                    chain: format!("{self:?}"),
196                });
197            }
198        };
199
200        address_str
201            .parse()
202            .map_err(|_| OdosChainError::InvalidAddress {
203                address: address_str.to_string(),
204            })
205    }
206
207    fn v3_router_address(&self) -> OdosChainResult<Address> {
208        if !self.supports_v3() {
209            return Err(OdosChainError::V3NotAvailable {
210                chain: format!("{self:?}"),
211            });
212        }
213
214        ODOS_V3.parse().map_err(|_| OdosChainError::InvalidAddress {
215            address: ODOS_V3.to_string(),
216        })
217    }
218
219    fn supports_odos(&self) -> bool {
220        use NamedChain::*;
221        matches!(
222            self,
223            Arbitrum
224                | Avalanche
225                | Base
226                | BinanceSmartChain
227                | Fantom
228                | Fraxtal
229                | Mainnet
230                | Optimism
231                | Polygon
232                | Linea
233                | Mantle
234                | Mode
235                | Scroll
236                | Sonic
237                | ZkSync
238                | Unichain
239        )
240    }
241}
242
243/// Extension trait for easy router selection
244///
245/// This trait provides convenient methods for choosing between V2 and V3
246/// routers based on your requirements.
247pub trait OdosRouterSelection: OdosChain {
248    /// Get the recommended router address for this chain
249    ///
250    /// Currently defaults to V3 for enhanced features, but this
251    /// may change based on performance characteristics.
252    ///
253    /// # Returns
254    ///
255    /// * `Ok(Address)` - The recommended router address
256    /// * `Err(OdosChainError)` - If the chain is not supported
257    ///
258    /// # Example
259    ///
260    /// ```rust
261    /// use odos_sdk::{OdosChain, OdosRouterSelection};
262    /// use alloy_chains::NamedChain;
263    ///
264    /// let address = NamedChain::Base.recommended_router_address()?;
265    /// # Ok::<(), odos_sdk::OdosChainError>(())
266    /// ```
267    fn recommended_router_address(&self) -> OdosChainResult<Address> {
268        self.v3_router_address()
269    }
270
271    /// Get router address with fallback strategy
272    ///
273    /// Tries V3 first, falls back to V2 if needed.
274    /// This is useful for maximum compatibility.
275    ///
276    /// # Returns
277    ///
278    /// * `Ok(Address)` - V3 address if available, otherwise V2 address
279    /// * `Err(OdosChainError)` - If neither version is supported
280    ///
281    /// # Example
282    ///
283    /// ```rust
284    /// use odos_sdk::{OdosChain, OdosRouterSelection};
285    /// use alloy_chains::NamedChain;
286    ///
287    /// let address = NamedChain::Mainnet.router_address_with_fallback()?;
288    /// # Ok::<(), odos_sdk::OdosChainError>(())
289    /// ```
290    fn router_address_with_fallback(&self) -> OdosChainResult<Address> {
291        self.v3_router_address()
292            .or_else(|_| self.v2_router_address())
293    }
294
295    /// Get router address based on preference
296    ///
297    /// # Arguments
298    ///
299    /// * `prefer_v3` - Whether to prefer V3 when both are available
300    ///
301    /// # Returns
302    ///
303    /// * `Ok(Address)` - The appropriate router address based on preference
304    /// * `Err(OdosChainError)` - If the preferred version is not supported
305    ///
306    /// # Example
307    ///
308    /// ```rust
309    /// use odos_sdk::{OdosChain, OdosRouterSelection};
310    /// use alloy_chains::NamedChain;
311    ///
312    /// let v3_address = NamedChain::Mainnet.router_address_by_preference(true)?;
313    /// let v2_address = NamedChain::Mainnet.router_address_by_preference(false)?;
314    /// # Ok::<(), odos_sdk::OdosChainError>(())
315    /// ```
316    fn router_address_by_preference(&self, prefer_v3: bool) -> OdosChainResult<Address> {
317        if prefer_v3 {
318            self.v3_router_address()
319        } else {
320            self.v2_router_address()
321        }
322    }
323}
324
325impl<T: OdosChain> OdosRouterSelection for T {}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330    use alloy_chains::NamedChain;
331
332    #[test]
333    fn test_v2_router_addresses() {
334        let chains = [
335            NamedChain::Mainnet,
336            NamedChain::Arbitrum,
337            NamedChain::Optimism,
338            NamedChain::Polygon,
339            NamedChain::Base,
340        ];
341
342        for chain in chains {
343            let address = chain.v2_router_address().unwrap();
344            assert!(address != Address::ZERO);
345            assert_eq!(address.to_string().len(), 42); // 0x + 40 hex chars
346        }
347    }
348
349    #[test]
350    fn test_v3_router_addresses() {
351        let chains = [
352            NamedChain::Mainnet,
353            NamedChain::Arbitrum,
354            NamedChain::Optimism,
355            NamedChain::Polygon,
356            NamedChain::Base,
357        ];
358
359        for chain in chains {
360            let address = chain.v3_router_address().unwrap();
361            assert_eq!(address, ODOS_V3.parse::<Address>().unwrap());
362        }
363    }
364
365    #[test]
366    fn test_both_router_addresses() {
367        let (v2_addr, v3_addr) = NamedChain::Mainnet.both_router_addresses().unwrap();
368        assert_eq!(v2_addr, ODOS_V2_ETHEREUM_ROUTER.parse::<Address>().unwrap());
369        assert_eq!(v3_addr, ODOS_V3.parse::<Address>().unwrap());
370    }
371
372    #[test]
373    fn test_supports_odos() {
374        assert!(NamedChain::Mainnet.supports_odos());
375        assert!(NamedChain::Arbitrum.supports_odos());
376        assert!(!NamedChain::Sepolia.supports_odos());
377    }
378
379    #[test]
380    fn test_try_methods() {
381        assert!(NamedChain::Mainnet.try_v2_router_address().is_some());
382        assert!(NamedChain::Mainnet.try_v3_router_address().is_some());
383        assert!(NamedChain::Sepolia.try_v2_router_address().is_none());
384        assert!(NamedChain::Sepolia.try_v3_router_address().is_none());
385
386        assert!(NamedChain::Mainnet.try_both_router_addresses().is_some());
387        assert!(NamedChain::Sepolia.try_both_router_addresses().is_none());
388    }
389
390    #[test]
391    fn test_router_selection() {
392        let chain = NamedChain::Mainnet;
393
394        // Recommended should be V3
395        assert_eq!(
396            chain.recommended_router_address().unwrap(),
397            chain.v3_router_address().unwrap()
398        );
399
400        // Fallback should also be V3 (since both are supported)
401        assert_eq!(
402            chain.router_address_with_fallback().unwrap(),
403            chain.v3_router_address().unwrap()
404        );
405
406        // Preference-based selection
407        assert_eq!(
408            chain.router_address_by_preference(true).unwrap(),
409            chain.v3_router_address().unwrap()
410        );
411        assert_eq!(
412            chain.router_address_by_preference(false).unwrap(),
413            chain.v2_router_address().unwrap()
414        );
415    }
416
417    #[test]
418    fn test_error_handling() {
419        // Test unsupported chain
420        let result = NamedChain::Sepolia.v2_router_address();
421        assert!(result.is_err());
422        assert!(matches!(
423            result.unwrap_err(),
424            OdosChainError::V2NotAvailable { .. }
425        ));
426
427        let result = NamedChain::Sepolia.v3_router_address();
428        assert!(result.is_err());
429        assert!(matches!(
430            result.unwrap_err(),
431            OdosChainError::V3NotAvailable { .. }
432        ));
433    }
434}