Skip to main content

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