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