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 /// ZkSync Era (Chain ID: 324)
155 ///
156 /// # Examples
157 ///
158 /// ```rust
159 /// use odos_sdk::Chain;
160 ///
161 /// let chain = Chain::zksync();
162 /// assert_eq!(chain.id(), 324);
163 /// ```
164 pub const fn zksync() -> Self {
165 Self(NamedChain::ZkSync)
166 }
167
168 /// Mantle (Chain ID: 5000)
169 ///
170 /// # Examples
171 ///
172 /// ```rust
173 /// use odos_sdk::Chain;
174 ///
175 /// let chain = Chain::mantle();
176 /// assert_eq!(chain.id(), 5000);
177 /// ```
178 pub const fn mantle() -> Self {
179 Self(NamedChain::Mantle)
180 }
181
182 /// Fraxtal (Chain ID: 252)
183 ///
184 /// # Examples
185 ///
186 /// ```rust
187 /// use odos_sdk::Chain;
188 ///
189 /// let chain = Chain::fraxtal();
190 /// assert_eq!(chain.id(), 252);
191 /// ```
192 pub const fn fraxtal() -> Self {
193 Self(NamedChain::Fraxtal)
194 }
195
196 /// Sonic (Chain ID: 146)
197 ///
198 /// # Examples
199 ///
200 /// ```rust
201 /// use odos_sdk::Chain;
202 ///
203 /// let chain = Chain::sonic();
204 /// assert_eq!(chain.id(), 146);
205 /// ```
206 pub const fn sonic() -> Self {
207 Self(NamedChain::Sonic)
208 }
209
210 /// Unichain (Chain ID: 130)
211 ///
212 /// # Examples
213 ///
214 /// ```rust
215 /// use odos_sdk::Chain;
216 ///
217 /// let chain = Chain::unichain();
218 /// assert_eq!(chain.id(), 130);
219 /// ```
220 pub const fn unichain() -> Self {
221 Self(NamedChain::Unichain)
222 }
223
224 /// Create a chain from a chain ID
225 ///
226 /// # Arguments
227 ///
228 /// * `id` - The EVM chain ID
229 ///
230 /// # Returns
231 ///
232 /// * `Ok(Chain)` - If the chain ID is recognized
233 /// * `Err(OdosChainError)` - If the chain ID is not supported
234 ///
235 /// # Examples
236 ///
237 /// ```rust
238 /// use odos_sdk::Chain;
239 ///
240 /// let chain = Chain::from_chain_id(1)?; // Ethereum
241 /// let chain = Chain::from_chain_id(42161)?; // Arbitrum
242 /// let chain = Chain::from_chain_id(8453)?; // Base
243 ///
244 /// // Unsupported chain
245 /// assert!(Chain::from_chain_id(999999).is_err());
246 /// # Ok::<(), Box<dyn std::error::Error>>(())
247 /// ```
248 pub fn from_chain_id(id: u64) -> OdosChainResult<Self> {
249 NamedChain::try_from(id)
250 .map(Self)
251 .map_err(|_| OdosChainError::UnsupportedChain {
252 chain: format!("Chain ID {id}"),
253 })
254 }
255
256 /// Get the chain ID
257 ///
258 /// # Examples
259 ///
260 /// ```rust
261 /// use odos_sdk::Chain;
262 ///
263 /// assert_eq!(Chain::ethereum().id(), 1);
264 /// assert_eq!(Chain::arbitrum().id(), 42161);
265 /// assert_eq!(Chain::base().id(), 8453);
266 /// ```
267 pub fn id(&self) -> u64 {
268 self.0.into()
269 }
270
271 /// Get the inner `NamedChain`
272 ///
273 /// # Examples
274 ///
275 /// ```rust
276 /// use odos_sdk::Chain;
277 /// use alloy_chains::NamedChain;
278 ///
279 /// let chain = Chain::ethereum();
280 /// assert_eq!(chain.inner(), NamedChain::Mainnet);
281 /// ```
282 pub const fn inner(&self) -> NamedChain {
283 self.0
284 }
285}
286
287impl fmt::Display for Chain {
288 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289 write!(f, "{}", self.0)
290 }
291}
292
293impl From<NamedChain> for Chain {
294 fn from(chain: NamedChain) -> Self {
295 Self(chain)
296 }
297}
298
299impl From<Chain> for NamedChain {
300 fn from(chain: Chain) -> Self {
301 chain.0
302 }
303}
304
305impl From<Chain> for u64 {
306 fn from(chain: Chain) -> Self {
307 chain.0.into()
308 }
309}
310
311// Custom serialization using chain ID
312impl Serialize for Chain {
313 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
314 where
315 S: Serializer,
316 {
317 let chain_id: u64 = self.0.into();
318 chain_id.serialize(serializer)
319 }
320}
321
322impl<'de> Deserialize<'de> for Chain {
323 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
324 where
325 D: Deserializer<'de>,
326 {
327 let chain_id = u64::deserialize(deserializer)?;
328 NamedChain::try_from(chain_id)
329 .map(Self)
330 .map_err(serde::de::Error::custom)
331 }
332}
333
334// Implement OdosChain trait by delegating to inner NamedChain
335impl OdosChain for Chain {
336 fn lo_router_address(&self) -> OdosChainResult<alloy_primitives::Address> {
337 self.0.lo_router_address()
338 }
339
340 fn v2_router_address(&self) -> OdosChainResult<alloy_primitives::Address> {
341 self.0.v2_router_address()
342 }
343
344 fn v3_router_address(&self) -> OdosChainResult<alloy_primitives::Address> {
345 self.0.v3_router_address()
346 }
347
348 fn supports_odos(&self) -> bool {
349 self.0.supports_odos()
350 }
351
352 fn supports_lo(&self) -> bool {
353 self.0.supports_lo()
354 }
355
356 fn supports_v2(&self) -> bool {
357 self.0.supports_v2()
358 }
359
360 fn supports_v3(&self) -> bool {
361 self.0.supports_v3()
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368
369 #[test]
370 fn test_chain_constructors() {
371 assert_eq!(Chain::ethereum().id(), 1);
372 assert_eq!(Chain::arbitrum().id(), 42161);
373 assert_eq!(Chain::optimism().id(), 10);
374 assert_eq!(Chain::polygon().id(), 137);
375 assert_eq!(Chain::base().id(), 8453);
376 assert_eq!(Chain::bsc().id(), 56);
377 assert_eq!(Chain::avalanche().id(), 43114);
378 assert_eq!(Chain::linea().id(), 59144);
379 assert_eq!(Chain::zksync().id(), 324);
380 assert_eq!(Chain::mantle().id(), 5000);
381 assert_eq!(Chain::fraxtal().id(), 252);
382 assert_eq!(Chain::sonic().id(), 146);
383 assert_eq!(Chain::unichain().id(), 130);
384 }
385
386 #[test]
387 fn test_from_chain_id() {
388 assert_eq!(Chain::from_chain_id(1).unwrap().id(), 1);
389 assert_eq!(Chain::from_chain_id(42161).unwrap().id(), 42161);
390 assert_eq!(Chain::from_chain_id(8453).unwrap().id(), 8453);
391
392 // Unsupported chain
393 assert!(Chain::from_chain_id(999999).is_err());
394 }
395
396 #[test]
397 fn test_inner() {
398 assert_eq!(Chain::ethereum().inner(), NamedChain::Mainnet);
399 assert_eq!(Chain::arbitrum().inner(), NamedChain::Arbitrum);
400 assert_eq!(Chain::base().inner(), NamedChain::Base);
401 }
402
403 #[test]
404 fn test_display() {
405 assert_eq!(format!("{}", Chain::ethereum()), "mainnet");
406 assert_eq!(format!("{}", Chain::arbitrum()), "arbitrum");
407 assert_eq!(format!("{}", Chain::base()), "base");
408 }
409
410 #[test]
411 fn test_conversions() {
412 // From NamedChain
413 let chain: Chain = NamedChain::Mainnet.into();
414 assert_eq!(chain.id(), 1);
415
416 // To NamedChain
417 let named: NamedChain = Chain::ethereum().into();
418 assert_eq!(named, NamedChain::Mainnet);
419
420 // To u64
421 let id: u64 = Chain::ethereum().into();
422 assert_eq!(id, 1);
423 }
424
425 #[test]
426 fn test_odos_chain_trait() {
427 let chain = Chain::ethereum();
428
429 // Test trait methods work
430 assert!(chain.supports_odos());
431 assert!(chain.supports_v2());
432 assert!(chain.supports_v3());
433 assert!(chain.v2_router_address().is_ok());
434 assert!(chain.v3_router_address().is_ok());
435 }
436
437 #[test]
438 fn test_equality() {
439 assert_eq!(Chain::ethereum(), Chain::ethereum());
440 assert_ne!(Chain::ethereum(), Chain::arbitrum());
441 }
442
443 #[test]
444 fn test_serialization() {
445 let chain = Chain::ethereum();
446
447 // Serialize (as chain ID)
448 let json = serde_json::to_string(&chain).unwrap();
449 assert_eq!(json, "1"); // Ethereum chain ID
450
451 // Deserialize
452 let deserialized: Chain = serde_json::from_str(&json).unwrap();
453 assert_eq!(deserialized, chain);
454
455 // Test other chains
456 assert_eq!(serde_json::to_string(&Chain::arbitrum()).unwrap(), "42161");
457 assert_eq!(serde_json::to_string(&Chain::base()).unwrap(), "8453");
458 }
459}