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_FRAXTAL_ROUTER, ODOS_V2_LINEA_ROUTER, ODOS_V2_MANTLE_ROUTER,
8 ODOS_V2_MODE_ROUTER, ODOS_V2_OP_ROUTER, ODOS_V2_POLYGON_ROUTER, ODOS_V2_SCROLL_ROUTER,
9 ODOS_V2_SONIC_ROUTER, ODOS_V2_UNICHAIN_ROUTER, ODOS_V2_ZKSYNC_ROUTER, ODOS_V3,
10};
11
12/// Errors that can occur when working with Odos chains
13#[derive(Error, Debug, Clone, PartialEq)]
14pub enum OdosChainError {
15 /// The chain is not supported by Odos protocol
16 #[error("Chain {chain:?} is not supported by Odos protocol")]
17 UnsupportedChain { chain: String },
18
19 /// The V2 router is not available on this chain
20 #[error("Odos V2 router is not available on chain {chain:?}")]
21 V2NotAvailable { chain: String },
22
23 /// The V3 router is not available on this chain
24 #[error("Odos V3 router is not available on chain {chain:?}")]
25 V3NotAvailable { chain: String },
26
27 /// Invalid address format
28 #[error("Invalid address format: {address}")]
29 InvalidAddress { address: String },
30}
31
32/// Result type for Odos chain operations
33pub type OdosChainResult<T> = Result<T, OdosChainError>;
34
35/// Trait for chains that support Odos protocol
36///
37/// This trait provides a type-safe way to access Odos router addresses
38/// for supported blockchain networks, integrating seamlessly with the
39/// Alloy ecosystem.
40///
41/// # Examples
42///
43/// ```rust
44/// use odos_sdk::OdosChain;
45/// use alloy_chains::NamedChain;
46///
47/// // Get V2 router address
48/// let v2_router = NamedChain::Mainnet.v2_router_address()?;
49///
50/// // Get V3 router address
51/// let v3_router = NamedChain::Mainnet.v3_router_address()?;
52///
53/// // Get both addresses
54/// let (v2, v3) = NamedChain::Arbitrum.both_router_addresses()?;
55///
56/// // Check support
57/// assert!(NamedChain::Mainnet.supports_odos());
58/// assert!(NamedChain::Mainnet.supports_v3());
59/// # Ok::<(), odos_sdk::OdosChainError>(())
60/// ```
61pub trait OdosChain {
62 /// Get the V2 router address for this chain
63 ///
64 /// # Returns
65 ///
66 /// * `Ok(Address)` - The V2 router contract address
67 /// * `Err(OdosChainError)` - If the chain is not supported or address is invalid
68 ///
69 /// # Example
70 ///
71 /// ```rust
72 /// use odos_sdk::OdosChain;
73 /// use alloy_chains::NamedChain;
74 ///
75 /// let address = NamedChain::Mainnet.v2_router_address()?;
76 /// # Ok::<(), odos_sdk::OdosChainError>(())
77 /// ```
78 fn v2_router_address(&self) -> OdosChainResult<Address>;
79
80 /// Get the V3 router address for this chain
81 ///
82 /// V3 uses the same address across all supported chains,
83 /// following CREATE2 deterministic deployment.
84 ///
85 /// # Returns
86 ///
87 /// * `Ok(Address)` - The V3 router contract address
88 /// * `Err(OdosChainError)` - If the chain is not supported or address is invalid
89 ///
90 /// # Example
91 ///
92 /// ```rust
93 /// use odos_sdk::OdosChain;
94 /// use alloy_chains::NamedChain;
95 ///
96 /// let address = NamedChain::Mainnet.v3_router_address()?;
97 /// # Ok::<(), odos_sdk::OdosChainError>(())
98 /// ```
99 fn v3_router_address(&self) -> OdosChainResult<Address>;
100
101 /// Get both V2 and V3 router addresses for this chain
102 ///
103 /// # Returns
104 ///
105 /// * `Ok((v2_address, v3_address))` - Both router addresses
106 /// * `Err(OdosChainError)` - If the chain is not supported by either version
107 ///
108 /// # Example
109 ///
110 /// ```rust
111 /// use odos_sdk::OdosChain;
112 /// use alloy_chains::NamedChain;
113 ///
114 /// let (v2, v3) = NamedChain::Arbitrum.both_router_addresses()?;
115 /// # Ok::<(), odos_sdk::OdosChainError>(())
116 /// ```
117 fn both_router_addresses(&self) -> OdosChainResult<(Address, Address)> {
118 Ok((self.v2_router_address()?, self.v3_router_address()?))
119 }
120
121 /// Check if this chain supports Odos protocol
122 ///
123 /// # Returns
124 ///
125 /// `true` if either V2 or V3 (or both) are supported on this chain
126 fn supports_odos(&self) -> bool;
127
128 /// Check if this chain supports Odos V2
129 ///
130 /// # Returns
131 ///
132 /// `true` if V2 is supported on this chain
133 fn supports_v2(&self) -> bool;
134
135 /// Check if this chain supports Odos V3
136 ///
137 /// # Returns
138 ///
139 /// `true` if V3 is supported on this chain
140 fn supports_v3(&self) -> bool;
141
142 /// Try to get the V2 router address without errors
143 ///
144 /// # Returns
145 ///
146 /// `Some(address)` if supported, `None` if not supported
147 fn try_v2_router_address(&self) -> Option<Address> {
148 self.v2_router_address().ok()
149 }
150
151 /// Try to get the V3 router address without errors
152 ///
153 /// # Returns
154 ///
155 /// `Some(address)` if supported, `None` if not supported
156 fn try_v3_router_address(&self) -> Option<Address> {
157 self.v3_router_address().ok()
158 }
159
160 /// Try to get both router addresses without errors
161 ///
162 /// # Returns
163 ///
164 /// `Some((v2_address, v3_address))` if both are supported, `None` otherwise
165 fn try_both_router_addresses(&self) -> Option<(Address, Address)> {
166 self.both_router_addresses().ok()
167 }
168}
169
170impl OdosChain for NamedChain {
171 fn v2_router_address(&self) -> OdosChainResult<Address> {
172 use NamedChain::*;
173
174 if !self.supports_odos() {
175 return Err(OdosChainError::V2NotAvailable {
176 chain: format!("{self:?}"),
177 });
178 }
179
180 // If V2 is not available on this chain, fall back to V3
181 if !self.supports_v2() {
182 return self.v3_router_address();
183 }
184
185 Ok(match self {
186 Arbitrum => ODOS_V2_ARBITRUM_ROUTER,
187 Avalanche => ODOS_V2_AVALANCHE_ROUTER,
188 Base => ODOS_V2_BASE_ROUTER,
189 BinanceSmartChain => ODOS_V2_BSC_ROUTER,
190 Fraxtal => ODOS_V2_FRAXTAL_ROUTER,
191 Mainnet => ODOS_V2_ETHEREUM_ROUTER,
192 Optimism => ODOS_V2_OP_ROUTER,
193 Polygon => ODOS_V2_POLYGON_ROUTER,
194 Linea => ODOS_V2_LINEA_ROUTER,
195 Mantle => ODOS_V2_MANTLE_ROUTER,
196 Mode => ODOS_V2_MODE_ROUTER,
197 Scroll => ODOS_V2_SCROLL_ROUTER,
198 Sonic => ODOS_V2_SONIC_ROUTER,
199 ZkSync => ODOS_V2_ZKSYNC_ROUTER,
200 Unichain => ODOS_V2_UNICHAIN_ROUTER,
201 _ => {
202 return Err(OdosChainError::UnsupportedChain {
203 chain: format!("{self:?}"),
204 });
205 }
206 })
207 }
208
209 fn v3_router_address(&self) -> OdosChainResult<Address> {
210 if !self.supports_odos() {
211 return Err(OdosChainError::V3NotAvailable {
212 chain: format!("{self:?}"),
213 });
214 }
215
216 // If V3 is not available on this chain, fall back to V2
217 if !self.supports_v3() {
218 return self.v2_router_address();
219 }
220
221 Ok(ODOS_V3)
222 }
223
224 fn supports_odos(&self) -> bool {
225 use NamedChain::*;
226 matches!(
227 self,
228 Arbitrum
229 | Avalanche
230 | Base
231 | Berachain
232 | BinanceSmartChain
233 | Fraxtal
234 | Mainnet
235 | Optimism
236 | Polygon
237 | Linea
238 | Mantle
239 | Mode
240 | Scroll
241 | Sonic
242 | ZkSync
243 | Unichain
244 )
245 }
246
247 fn supports_v2(&self) -> bool {
248 use NamedChain::*;
249 matches!(
250 self,
251 Arbitrum
252 | Avalanche
253 | Base
254 | BinanceSmartChain
255 | Fraxtal
256 | Mainnet
257 | Optimism
258 | Polygon
259 | Linea
260 | Mantle
261 | Mode
262 | Scroll
263 | Sonic
264 | ZkSync
265 | Unichain
266 )
267 }
268
269 fn supports_v3(&self) -> bool {
270 use NamedChain::*;
271 matches!(
272 self,
273 Arbitrum
274 | Avalanche
275 | Base
276 | Berachain
277 | BinanceSmartChain
278 | Fraxtal
279 | Mainnet
280 | Optimism
281 | Polygon
282 | Linea
283 | Mantle
284 | Mode
285 | Scroll
286 | Sonic
287 | ZkSync
288 | Unichain
289 )
290 }
291}
292
293/// Extension trait for easy router selection
294///
295/// This trait provides convenient methods for choosing between V2 and V3
296/// routers based on your requirements.
297pub trait OdosRouterSelection: OdosChain {
298 /// Get the recommended router address for this chain
299 ///
300 /// Currently defaults to V3 for enhanced features, but this
301 /// may change based on performance characteristics.
302 ///
303 /// # Returns
304 ///
305 /// * `Ok(Address)` - The recommended router address
306 /// * `Err(OdosChainError)` - If the chain is not supported
307 ///
308 /// # Example
309 ///
310 /// ```rust
311 /// use odos_sdk::{OdosChain, OdosRouterSelection};
312 /// use alloy_chains::NamedChain;
313 ///
314 /// let address = NamedChain::Base.recommended_router_address()?;
315 /// # Ok::<(), odos_sdk::OdosChainError>(())
316 /// ```
317 fn recommended_router_address(&self) -> OdosChainResult<Address> {
318 self.v3_router_address()
319 }
320
321 /// Get router address with fallback strategy
322 ///
323 /// Tries V3 first, falls back to V2 if needed.
324 /// This is useful for maximum compatibility.
325 ///
326 /// # Returns
327 ///
328 /// * `Ok(Address)` - V3 address if available, otherwise V2 address
329 /// * `Err(OdosChainError)` - If neither version is supported
330 ///
331 /// # Example
332 ///
333 /// ```rust
334 /// use odos_sdk::{OdosChain, OdosRouterSelection};
335 /// use alloy_chains::NamedChain;
336 ///
337 /// let address = NamedChain::Mainnet.router_address_with_fallback()?;
338 /// # Ok::<(), odos_sdk::OdosChainError>(())
339 /// ```
340 fn router_address_with_fallback(&self) -> OdosChainResult<Address> {
341 self.v3_router_address()
342 .or_else(|_| self.v2_router_address())
343 }
344
345 /// Get router address based on preference
346 ///
347 /// # Arguments
348 ///
349 /// * `prefer_v3` - Whether to prefer V3 when both are available
350 ///
351 /// # Returns
352 ///
353 /// * `Ok(Address)` - The appropriate router address based on preference
354 /// * `Err(OdosChainError)` - If the preferred version is not supported
355 ///
356 /// # Example
357 ///
358 /// ```rust
359 /// use odos_sdk::{OdosChain, OdosRouterSelection};
360 /// use alloy_chains::NamedChain;
361 ///
362 /// let v3_address = NamedChain::Mainnet.router_address_by_preference(true)?;
363 /// let v2_address = NamedChain::Mainnet.router_address_by_preference(false)?;
364 /// # Ok::<(), odos_sdk::OdosChainError>(())
365 /// ```
366 fn router_address_by_preference(&self, prefer_v3: bool) -> OdosChainResult<Address> {
367 if prefer_v3 {
368 self.v3_router_address()
369 } else {
370 self.v2_router_address()
371 }
372 }
373}
374
375impl<T: OdosChain> OdosRouterSelection for T {}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use alloy_chains::NamedChain;
381
382 #[test]
383 fn test_v2_router_addresses() {
384 let chains = [
385 NamedChain::Mainnet,
386 NamedChain::Arbitrum,
387 NamedChain::Optimism,
388 NamedChain::Polygon,
389 NamedChain::Base,
390 ];
391
392 for chain in chains {
393 let address = chain.v2_router_address().unwrap();
394 assert!(address != Address::ZERO);
395 assert_eq!(address.to_string().len(), 42); // 0x + 40 hex chars
396 }
397 }
398
399 #[test]
400 fn test_v3_router_addresses() {
401 let chains = [
402 NamedChain::Mainnet,
403 NamedChain::Arbitrum,
404 NamedChain::Optimism,
405 NamedChain::Polygon,
406 NamedChain::Base,
407 ];
408
409 for chain in chains {
410 let address = chain.v3_router_address().unwrap();
411 assert_eq!(address, ODOS_V3);
412 }
413 }
414
415 #[test]
416 fn test_both_router_addresses() {
417 let (v2_addr, v3_addr) = NamedChain::Mainnet.both_router_addresses().unwrap();
418 assert_eq!(v2_addr, ODOS_V2_ETHEREUM_ROUTER);
419 assert_eq!(v3_addr, ODOS_V3);
420 }
421
422 #[test]
423 fn test_supports_odos() {
424 assert!(NamedChain::Mainnet.supports_odos());
425 assert!(NamedChain::Arbitrum.supports_odos());
426 assert!(NamedChain::Berachain.supports_odos());
427 assert!(!NamedChain::Sepolia.supports_odos());
428 }
429
430 #[test]
431 fn test_supports_v2() {
432 assert!(NamedChain::Mainnet.supports_v2());
433 assert!(NamedChain::Arbitrum.supports_v2());
434 assert!(!NamedChain::Berachain.supports_v2()); // Berachain only has V3
435 assert!(!NamedChain::Sepolia.supports_v2());
436 }
437
438 #[test]
439 fn test_supports_v3() {
440 assert!(NamedChain::Mainnet.supports_v3());
441 assert!(NamedChain::Arbitrum.supports_v3());
442 assert!(NamedChain::Berachain.supports_v3());
443 assert!(!NamedChain::Sepolia.supports_v3());
444 }
445
446 #[test]
447 fn test_berachain_v3_only() {
448 // Berachain only has V3, not V2
449 assert!(!NamedChain::Berachain.supports_v2());
450 assert!(NamedChain::Berachain.supports_v3());
451
452 // Requesting V2 should fallback to V3
453 let v2_result = NamedChain::Berachain.v2_router_address();
454 let v3_result = NamedChain::Berachain.v3_router_address();
455
456 assert!(v2_result.is_ok());
457 assert!(v3_result.is_ok());
458 assert_eq!(v2_result.unwrap(), v3_result.unwrap());
459 }
460
461 #[test]
462 fn test_try_methods() {
463 assert!(NamedChain::Mainnet.try_v2_router_address().is_some());
464 assert!(NamedChain::Mainnet.try_v3_router_address().is_some());
465 assert!(NamedChain::Sepolia.try_v2_router_address().is_none());
466 assert!(NamedChain::Sepolia.try_v3_router_address().is_none());
467
468 assert!(NamedChain::Mainnet.try_both_router_addresses().is_some());
469 assert!(NamedChain::Sepolia.try_both_router_addresses().is_none());
470 }
471
472 #[test]
473 fn test_router_selection() {
474 let chain = NamedChain::Mainnet;
475
476 // Recommended should be V3
477 assert_eq!(
478 chain.recommended_router_address().unwrap(),
479 chain.v3_router_address().unwrap()
480 );
481
482 // Fallback should also be V3 (since both are supported)
483 assert_eq!(
484 chain.router_address_with_fallback().unwrap(),
485 chain.v3_router_address().unwrap()
486 );
487
488 // Preference-based selection
489 assert_eq!(
490 chain.router_address_by_preference(true).unwrap(),
491 chain.v3_router_address().unwrap()
492 );
493 assert_eq!(
494 chain.router_address_by_preference(false).unwrap(),
495 chain.v2_router_address().unwrap()
496 );
497 }
498
499 #[test]
500 fn test_error_handling() {
501 // Test unsupported chain
502 let result = NamedChain::Sepolia.v2_router_address();
503 assert!(result.is_err());
504 assert!(matches!(
505 result.unwrap_err(),
506 OdosChainError::V2NotAvailable { .. }
507 ));
508
509 let result = NamedChain::Sepolia.v3_router_address();
510 assert!(result.is_err());
511 assert!(matches!(
512 result.unwrap_err(),
513 OdosChainError::V3NotAvailable { .. }
514 ));
515 }
516}