odos_sdk/
chain.rs

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