odos_sdk/
chain.rs

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