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