odos_sdk/types/chain.rs
1// SPDX-FileCopyrightText: 2025 Semiotic AI, Inc.
2//
3// SPDX-License-Identifier: Apache-2.0
4
5use std::fmt;
6
7use alloy_chains::NamedChain;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10use crate::{OdosChain, OdosChainError, OdosChainResult};
11
12/// Type-safe chain identifier with convenient constructors
13///
14/// Provides ergonomic helpers for accessing supported chains while
15/// maintaining full compatibility with `alloy_chains::NamedChain`.
16///
17/// # Examples
18///
19/// ```rust
20/// use odos_sdk::{Chain, OdosChain};
21///
22/// // Convenient constructors
23/// let chain = Chain::ethereum();
24/// let chain = Chain::arbitrum();
25/// let chain = Chain::base();
26///
27/// // From chain ID
28/// let chain = Chain::from_chain_id(1)?; // Ethereum
29/// let chain = Chain::from_chain_id(42161)?; // Arbitrum
30///
31/// // Access inner NamedChain
32/// let named = chain.inner();
33///
34/// // Use OdosChain trait methods
35/// let router = chain.v3_router_address()?;
36/// # Ok::<(), Box<dyn std::error::Error>>(())
37/// ```
38#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
39pub struct Chain(NamedChain);
40
41impl Chain {
42 /// Ethereum Mainnet (Chain ID: 1)
43 ///
44 /// # Examples
45 ///
46 /// ```rust
47 /// use odos_sdk::Chain;
48 ///
49 /// let chain = Chain::ethereum();
50 /// assert_eq!(chain.id(), 1);
51 /// ```
52 pub const fn ethereum() -> Self {
53 Self(NamedChain::Mainnet)
54 }
55
56 /// Arbitrum One (Chain ID: 42161)
57 ///
58 /// # Examples
59 ///
60 /// ```rust
61 /// use odos_sdk::Chain;
62 ///
63 /// let chain = Chain::arbitrum();
64 /// assert_eq!(chain.id(), 42161);
65 /// ```
66 pub const fn arbitrum() -> Self {
67 Self(NamedChain::Arbitrum)
68 }
69
70 /// Optimism (Chain ID: 10)
71 ///
72 /// # Examples
73 ///
74 /// ```rust
75 /// use odos_sdk::Chain;
76 ///
77 /// let chain = Chain::optimism();
78 /// assert_eq!(chain.id(), 10);
79 /// ```
80 pub const fn optimism() -> Self {
81 Self(NamedChain::Optimism)
82 }
83
84 /// Polygon (Chain ID: 137)
85 ///
86 /// # Examples
87 ///
88 /// ```rust
89 /// use odos_sdk::Chain;
90 ///
91 /// let chain = Chain::polygon();
92 /// assert_eq!(chain.id(), 137);
93 /// ```
94 pub const fn polygon() -> Self {
95 Self(NamedChain::Polygon)
96 }
97
98 /// Base (Chain ID: 8453)
99 ///
100 /// # Examples
101 ///
102 /// ```rust
103 /// use odos_sdk::Chain;
104 ///
105 /// let chain = Chain::base();
106 /// assert_eq!(chain.id(), 8453);
107 /// ```
108 pub const fn base() -> Self {
109 Self(NamedChain::Base)
110 }
111
112 /// BNB Smart Chain (Chain ID: 56)
113 ///
114 /// # Examples
115 ///
116 /// ```rust
117 /// use odos_sdk::Chain;
118 ///
119 /// let chain = Chain::bsc();
120 /// assert_eq!(chain.id(), 56);
121 /// ```
122 pub const fn bsc() -> Self {
123 Self(NamedChain::BinanceSmartChain)
124 }
125
126 /// Avalanche C-Chain (Chain ID: 43114)
127 ///
128 /// # Examples
129 ///
130 /// ```rust
131 /// use odos_sdk::Chain;
132 ///
133 /// let chain = Chain::avalanche();
134 /// assert_eq!(chain.id(), 43114);
135 /// ```
136 pub const fn avalanche() -> Self {
137 Self(NamedChain::Avalanche)
138 }
139
140 /// Linea (Chain ID: 59144)
141 ///
142 /// # Examples
143 ///
144 /// ```rust
145 /// use odos_sdk::Chain;
146 ///
147 /// let chain = Chain::linea();
148 /// assert_eq!(chain.id(), 59144);
149 /// ```
150 pub const fn linea() -> Self {
151 Self(NamedChain::Linea)
152 }
153
154 /// Scroll (Chain ID: 534352)
155 ///
156 /// # Examples
157 ///
158 /// ```rust
159 /// use odos_sdk::Chain;
160 ///
161 /// let chain = Chain::scroll();
162 /// assert_eq!(chain.id(), 534352);
163 /// ```
164 pub const fn scroll() -> Self {
165 Self(NamedChain::Scroll)
166 }
167
168 /// ZkSync Era (Chain ID: 324)
169 ///
170 /// # Examples
171 ///
172 /// ```rust
173 /// use odos_sdk::Chain;
174 ///
175 /// let chain = Chain::zksync();
176 /// assert_eq!(chain.id(), 324);
177 /// ```
178 pub const fn zksync() -> Self {
179 Self(NamedChain::ZkSync)
180 }
181
182 /// Mantle (Chain ID: 5000)
183 ///
184 /// # Examples
185 ///
186 /// ```rust
187 /// use odos_sdk::Chain;
188 ///
189 /// let chain = Chain::mantle();
190 /// assert_eq!(chain.id(), 5000);
191 /// ```
192 pub const fn mantle() -> Self {
193 Self(NamedChain::Mantle)
194 }
195
196 /// Fraxtal (Chain ID: 252)
197 ///
198 /// # Examples
199 ///
200 /// ```rust
201 /// use odos_sdk::Chain;
202 ///
203 /// let chain = Chain::fraxtal();
204 /// assert_eq!(chain.id(), 252);
205 /// ```
206 pub const fn fraxtal() -> Self {
207 Self(NamedChain::Fraxtal)
208 }
209
210 /// Sonic (Chain ID: 146)
211 ///
212 /// # Examples
213 ///
214 /// ```rust
215 /// use odos_sdk::Chain;
216 ///
217 /// let chain = Chain::sonic();
218 /// assert_eq!(chain.id(), 146);
219 /// ```
220 pub const fn sonic() -> Self {
221 Self(NamedChain::Sonic)
222 }
223
224 /// Unichain (Chain ID: 130)
225 ///
226 /// # Examples
227 ///
228 /// ```rust
229 /// use odos_sdk::Chain;
230 ///
231 /// let chain = Chain::unichain();
232 /// assert_eq!(chain.id(), 130);
233 /// ```
234 pub const fn unichain() -> Self {
235 Self(NamedChain::Unichain)
236 }
237
238 /// Create a chain from a chain ID
239 ///
240 /// # Arguments
241 ///
242 /// * `id` - The EVM chain ID
243 ///
244 /// # Returns
245 ///
246 /// * `Ok(Chain)` - If the chain ID is recognized
247 /// * `Err(OdosChainError)` - If the chain ID is not supported
248 ///
249 /// # Examples
250 ///
251 /// ```rust
252 /// use odos_sdk::Chain;
253 ///
254 /// let chain = Chain::from_chain_id(1)?; // Ethereum
255 /// let chain = Chain::from_chain_id(42161)?; // Arbitrum
256 /// let chain = Chain::from_chain_id(8453)?; // Base
257 ///
258 /// // Unsupported chain
259 /// assert!(Chain::from_chain_id(999999).is_err());
260 /// # Ok::<(), Box<dyn std::error::Error>>(())
261 /// ```
262 pub fn from_chain_id(id: u64) -> OdosChainResult<Self> {
263 NamedChain::try_from(id)
264 .map(Self)
265 .map_err(|_| OdosChainError::UnsupportedChain {
266 chain: format!("Chain ID {id}"),
267 })
268 }
269
270 /// Get the chain ID
271 ///
272 /// # Examples
273 ///
274 /// ```rust
275 /// use odos_sdk::Chain;
276 ///
277 /// assert_eq!(Chain::ethereum().id(), 1);
278 /// assert_eq!(Chain::arbitrum().id(), 42161);
279 /// assert_eq!(Chain::base().id(), 8453);
280 /// ```
281 pub fn id(&self) -> u64 {
282 self.0.into()
283 }
284
285 /// Get the inner `NamedChain`
286 ///
287 /// # Examples
288 ///
289 /// ```rust
290 /// use odos_sdk::Chain;
291 /// use alloy_chains::NamedChain;
292 ///
293 /// let chain = Chain::ethereum();
294 /// assert_eq!(chain.inner(), NamedChain::Mainnet);
295 /// ```
296 pub const fn inner(&self) -> NamedChain {
297 self.0
298 }
299}
300
301impl fmt::Display for Chain {
302 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303 write!(f, "{}", self.0)
304 }
305}
306
307impl From<NamedChain> for Chain {
308 fn from(chain: NamedChain) -> Self {
309 Self(chain)
310 }
311}
312
313impl From<Chain> for NamedChain {
314 fn from(chain: Chain) -> Self {
315 chain.0
316 }
317}
318
319impl From<Chain> for u64 {
320 fn from(chain: Chain) -> Self {
321 chain.0.into()
322 }
323}
324
325// Custom serialization using chain ID
326impl Serialize for Chain {
327 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
328 where
329 S: Serializer,
330 {
331 let chain_id: u64 = self.0.into();
332 chain_id.serialize(serializer)
333 }
334}
335
336impl<'de> Deserialize<'de> for Chain {
337 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
338 where
339 D: Deserializer<'de>,
340 {
341 let chain_id = u64::deserialize(deserializer)?;
342 NamedChain::try_from(chain_id)
343 .map(Self)
344 .map_err(serde::de::Error::custom)
345 }
346}
347
348// Implement OdosChain trait by delegating to inner NamedChain
349impl OdosChain for Chain {
350 fn lo_router_address(&self) -> OdosChainResult<alloy_primitives::Address> {
351 self.0.lo_router_address()
352 }
353
354 fn v2_router_address(&self) -> OdosChainResult<alloy_primitives::Address> {
355 self.0.v2_router_address()
356 }
357
358 fn v3_router_address(&self) -> OdosChainResult<alloy_primitives::Address> {
359 self.0.v3_router_address()
360 }
361
362 fn supports_odos(&self) -> bool {
363 self.0.supports_odos()
364 }
365
366 fn supports_lo(&self) -> bool {
367 self.0.supports_lo()
368 }
369
370 fn supports_v2(&self) -> bool {
371 self.0.supports_v2()
372 }
373
374 fn supports_v3(&self) -> bool {
375 self.0.supports_v3()
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 #[test]
384 fn test_chain_constructors() {
385 assert_eq!(Chain::ethereum().id(), 1);
386 assert_eq!(Chain::arbitrum().id(), 42161);
387 assert_eq!(Chain::optimism().id(), 10);
388 assert_eq!(Chain::polygon().id(), 137);
389 assert_eq!(Chain::base().id(), 8453);
390 assert_eq!(Chain::bsc().id(), 56);
391 assert_eq!(Chain::avalanche().id(), 43114);
392 assert_eq!(Chain::linea().id(), 59144);
393 assert_eq!(Chain::scroll().id(), 534352);
394 assert_eq!(Chain::zksync().id(), 324);
395 assert_eq!(Chain::mantle().id(), 5000);
396 assert_eq!(Chain::fraxtal().id(), 252);
397 assert_eq!(Chain::sonic().id(), 146);
398 assert_eq!(Chain::unichain().id(), 130);
399 }
400
401 #[test]
402 fn test_from_chain_id() {
403 assert_eq!(Chain::from_chain_id(1).unwrap().id(), 1);
404 assert_eq!(Chain::from_chain_id(42161).unwrap().id(), 42161);
405 assert_eq!(Chain::from_chain_id(8453).unwrap().id(), 8453);
406
407 // Unsupported chain
408 assert!(Chain::from_chain_id(999999).is_err());
409 }
410
411 #[test]
412 fn test_inner() {
413 assert_eq!(Chain::ethereum().inner(), NamedChain::Mainnet);
414 assert_eq!(Chain::arbitrum().inner(), NamedChain::Arbitrum);
415 assert_eq!(Chain::base().inner(), NamedChain::Base);
416 }
417
418 #[test]
419 fn test_display() {
420 assert_eq!(format!("{}", Chain::ethereum()), "mainnet");
421 assert_eq!(format!("{}", Chain::arbitrum()), "arbitrum");
422 assert_eq!(format!("{}", Chain::base()), "base");
423 }
424
425 #[test]
426 fn test_conversions() {
427 // From NamedChain
428 let chain: Chain = NamedChain::Mainnet.into();
429 assert_eq!(chain.id(), 1);
430
431 // To NamedChain
432 let named: NamedChain = Chain::ethereum().into();
433 assert_eq!(named, NamedChain::Mainnet);
434
435 // To u64
436 let id: u64 = Chain::ethereum().into();
437 assert_eq!(id, 1);
438 }
439
440 #[test]
441 fn test_odos_chain_trait() {
442 let chain = Chain::ethereum();
443
444 // Test trait methods work
445 assert!(chain.supports_odos());
446 assert!(chain.supports_v2());
447 assert!(chain.supports_v3());
448 assert!(chain.v2_router_address().is_ok());
449 assert!(chain.v3_router_address().is_ok());
450 }
451
452 #[test]
453 fn test_equality() {
454 assert_eq!(Chain::ethereum(), Chain::ethereum());
455 assert_ne!(Chain::ethereum(), Chain::arbitrum());
456 }
457
458 #[test]
459 fn test_serialization() {
460 let chain = Chain::ethereum();
461
462 // Serialize (as chain ID)
463 let json = serde_json::to_string(&chain).unwrap();
464 assert_eq!(json, "1"); // Ethereum chain ID
465
466 // Deserialize
467 let deserialized: Chain = serde_json::from_str(&json).unwrap();
468 assert_eq!(deserialized, chain);
469
470 // Test other chains
471 assert_eq!(serde_json::to_string(&Chain::arbitrum()).unwrap(), "42161");
472 assert_eq!(serde_json::to_string(&Chain::base()).unwrap(), "8453");
473 }
474}