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_MODE_ROUTER, ODOS_LO_OP_ROUTER, ODOS_LO_POLYGON_ROUTER,
13    ODOS_LO_SCROLL_ROUTER, ODOS_LO_SONIC_ROUTER, ODOS_LO_UNICHAIN_ROUTER, ODOS_LO_ZKSYNC_ROUTER,
14    ODOS_V2_ARBITRUM_ROUTER, ODOS_V2_AVALANCHE_ROUTER, ODOS_V2_BASE_ROUTER, ODOS_V2_BSC_ROUTER,
15    ODOS_V2_ETHEREUM_ROUTER, ODOS_V2_FRAXTAL_ROUTER, ODOS_V2_LINEA_ROUTER, ODOS_V2_MANTLE_ROUTER,
16    ODOS_V2_MODE_ROUTER, ODOS_V2_OP_ROUTER, ODOS_V2_POLYGON_ROUTER, ODOS_V2_SCROLL_ROUTER,
17    ODOS_V2_SONIC_ROUTER, ODOS_V2_UNICHAIN_ROUTER, 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            Mode => ODOS_LO_MODE_ROUTER,
241            Scroll => ODOS_LO_SCROLL_ROUTER,
242            Sonic => ODOS_LO_SONIC_ROUTER,
243            ZkSync => ODOS_LO_ZKSYNC_ROUTER,
244            Unichain => ODOS_LO_UNICHAIN_ROUTER,
245            _ => {
246                return Err(OdosChainError::LimitOrderNotAvailable {
247                    chain: format!("{self:?}"),
248                });
249            }
250        })
251    }
252
253    fn v2_router_address(&self) -> OdosChainResult<Address> {
254        use NamedChain::*;
255
256        if !self.supports_odos() {
257            return Err(OdosChainError::V2NotAvailable {
258                chain: format!("{self:?}"),
259            });
260        }
261
262        // If V2 is not available on this chain, fall back to V3
263        if !self.supports_v2() {
264            return self.v3_router_address();
265        }
266
267        Ok(match self {
268            Arbitrum => ODOS_V2_ARBITRUM_ROUTER,
269            Avalanche => ODOS_V2_AVALANCHE_ROUTER,
270            Base => ODOS_V2_BASE_ROUTER,
271            BinanceSmartChain => ODOS_V2_BSC_ROUTER,
272            Fraxtal => ODOS_V2_FRAXTAL_ROUTER,
273            Mainnet => ODOS_V2_ETHEREUM_ROUTER,
274            Optimism => ODOS_V2_OP_ROUTER,
275            Polygon => ODOS_V2_POLYGON_ROUTER,
276            Linea => ODOS_V2_LINEA_ROUTER,
277            Mantle => ODOS_V2_MANTLE_ROUTER,
278            Mode => ODOS_V2_MODE_ROUTER,
279            Scroll => ODOS_V2_SCROLL_ROUTER,
280            Sonic => ODOS_V2_SONIC_ROUTER,
281            ZkSync => ODOS_V2_ZKSYNC_ROUTER,
282            Unichain => ODOS_V2_UNICHAIN_ROUTER,
283            _ => {
284                return Err(OdosChainError::UnsupportedChain {
285                    chain: format!("{self:?}"),
286                });
287            }
288        })
289    }
290
291    fn v3_router_address(&self) -> OdosChainResult<Address> {
292        if !self.supports_odos() {
293            return Err(OdosChainError::V3NotAvailable {
294                chain: format!("{self:?}"),
295            });
296        }
297
298        // If V3 is not available on this chain, fall back to V2
299        if !self.supports_v3() {
300            return self.v2_router_address();
301        }
302
303        Ok(ODOS_V3)
304    }
305
306    fn supports_odos(&self) -> bool {
307        use NamedChain::*;
308        matches!(
309            self,
310            Arbitrum
311                | Avalanche
312                | Base
313                | BinanceSmartChain
314                | Fraxtal
315                | Mainnet
316                | Optimism
317                | Polygon
318                | Linea
319                | Mantle
320                | Mode
321                | Scroll
322                | Sonic
323                | ZkSync
324                | Unichain
325        )
326    }
327
328    fn supports_lo(&self) -> bool {
329        use NamedChain::*;
330        matches!(
331            self,
332            Arbitrum
333                | Avalanche
334                | Base
335                | BinanceSmartChain
336                | Fraxtal
337                | Mainnet
338                | Optimism
339                | Polygon
340                | Linea
341                | Mantle
342                | Mode
343                | Scroll
344                | Sonic
345                | ZkSync
346                | Unichain
347        )
348    }
349
350    fn supports_v2(&self) -> bool {
351        use NamedChain::*;
352        matches!(
353            self,
354            Arbitrum
355                | Avalanche
356                | Base
357                | BinanceSmartChain
358                | Fraxtal
359                | Mainnet
360                | Optimism
361                | Polygon
362                | Linea
363                | Mantle
364                | Mode
365                | Scroll
366                | Sonic
367                | ZkSync
368                | Unichain
369        )
370    }
371
372    fn supports_v3(&self) -> bool {
373        use NamedChain::*;
374        matches!(
375            self,
376            Arbitrum
377                | Avalanche
378                | Base
379                | BinanceSmartChain
380                | Fraxtal
381                | Mainnet
382                | Optimism
383                | Polygon
384                | Linea
385                | Mantle
386                | Mode
387                | Scroll
388                | Sonic
389                | ZkSync
390                | Unichain
391        )
392    }
393}
394
395/// Extension trait for easy router selection
396///
397/// This trait provides convenient methods for choosing between V2 and V3
398/// routers based on your requirements.
399pub trait OdosRouterSelection: OdosChain {
400    /// Get the recommended router address for this chain
401    ///
402    /// Currently defaults to V3 for enhanced features, but this
403    /// may change based on performance characteristics.
404    ///
405    /// # Returns
406    ///
407    /// * `Ok(Address)` - The recommended router address
408    /// * `Err(OdosChainError)` - If the chain is not supported
409    ///
410    /// # Example
411    ///
412    /// ```rust
413    /// use odos_sdk::{OdosChain, OdosRouterSelection};
414    /// use alloy_chains::NamedChain;
415    ///
416    /// let address = NamedChain::Base.recommended_router_address()?;
417    /// # Ok::<(), odos_sdk::OdosChainError>(())
418    /// ```
419    fn recommended_router_address(&self) -> OdosChainResult<Address> {
420        self.v3_router_address()
421    }
422
423    /// Get router address with fallback strategy
424    ///
425    /// Tries V3 first, falls back to V2 if needed.
426    /// This is useful for maximum compatibility.
427    ///
428    /// # Returns
429    ///
430    /// * `Ok(Address)` - V3 address if available, otherwise V2 address
431    /// * `Err(OdosChainError)` - If neither version is supported
432    ///
433    /// # Example
434    ///
435    /// ```rust
436    /// use odos_sdk::{OdosChain, OdosRouterSelection};
437    /// use alloy_chains::NamedChain;
438    ///
439    /// let address = NamedChain::Mainnet.router_address_with_fallback()?;
440    /// # Ok::<(), odos_sdk::OdosChainError>(())
441    /// ```
442    fn router_address_with_fallback(&self) -> OdosChainResult<Address> {
443        self.v3_router_address()
444            .or_else(|_| self.v2_router_address())
445    }
446
447    /// Get router address based on preference
448    ///
449    /// # Arguments
450    ///
451    /// * `prefer_v3` - Whether to prefer V3 when both are available
452    ///
453    /// # Returns
454    ///
455    /// * `Ok(Address)` - The appropriate router address based on preference
456    /// * `Err(OdosChainError)` - If the preferred version is not supported
457    ///
458    /// # Example
459    ///
460    /// ```rust
461    /// use odos_sdk::{OdosChain, OdosRouterSelection};
462    /// use alloy_chains::NamedChain;
463    ///
464    /// let v3_address = NamedChain::Mainnet.router_address_by_preference(true)?;
465    /// let v2_address = NamedChain::Mainnet.router_address_by_preference(false)?;
466    /// # Ok::<(), odos_sdk::OdosChainError>(())
467    /// ```
468    fn router_address_by_preference(&self, prefer_v3: bool) -> OdosChainResult<Address> {
469        if prefer_v3 {
470            self.v3_router_address()
471        } else {
472            self.v2_router_address()
473        }
474    }
475}
476
477impl<T: OdosChain> OdosRouterSelection for T {}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482    use alloy_chains::NamedChain;
483
484    #[test]
485    fn test_lo_router_addresses() {
486        let chains = [
487            NamedChain::Mainnet,
488            NamedChain::Optimism,
489            NamedChain::Polygon,
490            NamedChain::BinanceSmartChain,
491        ];
492
493        for chain in chains {
494            let address = chain.lo_router_address().unwrap();
495            assert!(address != Address::ZERO);
496            assert_eq!(address.to_string().len(), 42); // 0x + 40 hex chars
497        }
498    }
499
500    #[test]
501    fn test_v2_router_addresses() {
502        let chains = [
503            NamedChain::Mainnet,
504            NamedChain::Arbitrum,
505            NamedChain::Optimism,
506            NamedChain::Polygon,
507            NamedChain::Base,
508        ];
509
510        for chain in chains {
511            let address = chain.v2_router_address().unwrap();
512            assert!(address != Address::ZERO);
513            assert_eq!(address.to_string().len(), 42); // 0x + 40 hex chars
514        }
515    }
516
517    #[test]
518    fn test_v3_router_addresses() {
519        let chains = [
520            NamedChain::Mainnet,
521            NamedChain::Arbitrum,
522            NamedChain::Optimism,
523            NamedChain::Polygon,
524            NamedChain::Base,
525        ];
526
527        for chain in chains {
528            let address = chain.v3_router_address().unwrap();
529            assert_eq!(address, ODOS_V3);
530        }
531    }
532
533    #[test]
534    fn test_supports_odos() {
535        assert!(NamedChain::Mainnet.supports_odos());
536        assert!(NamedChain::Arbitrum.supports_odos());
537        assert!(!NamedChain::Sepolia.supports_odos());
538    }
539
540    #[test]
541    fn test_supports_lo() {
542        assert!(NamedChain::Mainnet.supports_lo());
543        assert!(NamedChain::Optimism.supports_lo());
544        assert!(NamedChain::Polygon.supports_lo());
545        assert!(NamedChain::BinanceSmartChain.supports_lo());
546        assert!(NamedChain::Arbitrum.supports_lo());
547        assert!(NamedChain::Base.supports_lo());
548        assert!(!NamedChain::Sepolia.supports_lo());
549    }
550
551    #[test]
552    fn test_supports_v2() {
553        assert!(NamedChain::Mainnet.supports_v2());
554        assert!(NamedChain::Arbitrum.supports_v2());
555        assert!(!NamedChain::Sepolia.supports_v2());
556    }
557
558    #[test]
559    fn test_supports_v3() {
560        assert!(NamedChain::Mainnet.supports_v3());
561        assert!(NamedChain::Arbitrum.supports_v3());
562        assert!(!NamedChain::Sepolia.supports_v3());
563    }
564
565    #[test]
566    fn test_router_availability() {
567        // Ethereum: all routers
568        let avail = NamedChain::Mainnet.router_availability();
569        assert!(avail.limit_order);
570        assert!(avail.v2);
571        assert!(avail.v3);
572        assert_eq!(avail.count(), 3);
573
574        // Arbitrum: all routers
575        let avail = NamedChain::Arbitrum.router_availability();
576        assert!(avail.limit_order);
577        assert!(avail.v2);
578        assert!(avail.v3);
579        assert_eq!(avail.count(), 3);
580
581        // Sepolia: none
582        let avail = NamedChain::Sepolia.router_availability();
583        assert!(!avail.limit_order);
584        assert!(!avail.v2);
585        assert!(!avail.v3);
586        assert_eq!(avail.count(), 0);
587        assert!(!avail.has_any());
588    }
589
590    #[test]
591    fn test_try_methods() {
592        assert!(NamedChain::Mainnet.try_lo_router_address().is_some());
593        assert!(NamedChain::Mainnet.try_v2_router_address().is_some());
594        assert!(NamedChain::Mainnet.try_v3_router_address().is_some());
595
596        assert!(NamedChain::Sepolia.try_lo_router_address().is_none());
597        assert!(NamedChain::Sepolia.try_v2_router_address().is_none());
598        assert!(NamedChain::Sepolia.try_v3_router_address().is_none());
599
600        // Arbitrum has all routers
601        assert!(NamedChain::Arbitrum.try_lo_router_address().is_some());
602        assert!(NamedChain::Arbitrum.try_v2_router_address().is_some());
603        assert!(NamedChain::Arbitrum.try_v3_router_address().is_some());
604    }
605
606    #[test]
607    fn test_router_selection() {
608        let chain = NamedChain::Mainnet;
609
610        // Recommended should be V3
611        assert_eq!(
612            chain.recommended_router_address().unwrap(),
613            chain.v3_router_address().unwrap()
614        );
615
616        // Fallback should also be V3 (since both are supported)
617        assert_eq!(
618            chain.router_address_with_fallback().unwrap(),
619            chain.v3_router_address().unwrap()
620        );
621
622        // Preference-based selection
623        assert_eq!(
624            chain.router_address_by_preference(true).unwrap(),
625            chain.v3_router_address().unwrap()
626        );
627        assert_eq!(
628            chain.router_address_by_preference(false).unwrap(),
629            chain.v2_router_address().unwrap()
630        );
631    }
632
633    #[test]
634    fn test_error_handling() {
635        // Test unsupported chain
636        let result = NamedChain::Sepolia.lo_router_address();
637        assert!(result.is_err());
638        assert!(matches!(
639            result.unwrap_err(),
640            OdosChainError::LimitOrderNotAvailable { .. }
641        ));
642
643        let result = NamedChain::Sepolia.v2_router_address();
644        assert!(result.is_err());
645        assert!(matches!(
646            result.unwrap_err(),
647            OdosChainError::V2NotAvailable { .. }
648        ));
649
650        let result = NamedChain::Sepolia.v3_router_address();
651        assert!(result.is_err());
652        assert!(matches!(
653            result.unwrap_err(),
654            OdosChainError::V3NotAvailable { .. }
655        ));
656    }
657}