odos_sdk/
lib.rs

1//! # Odos SDK
2//!
3//! A production-ready Rust SDK for the Odos protocol - a decentralized exchange aggregator
4//! that provides optimal routing for token swaps across multiple EVM chains.
5//!
6//! ## Features
7//!
8//! - **Multi-chain Support**: 16+ EVM chains including Ethereum, Arbitrum, Optimism, Polygon, Base, etc.
9//! - **Type-safe**: Leverages Rust's type system with Alloy primitives for addresses, chain IDs, and amounts
10//! - **Production-ready**: Built-in retry logic, circuit breakers, timeouts, and error handling
11//! - **Builder Pattern**: Ergonomic API using the `bon` crate for request building
12//! - **Comprehensive Error Handling**: Detailed error types for different failure scenarios
13//!
14//! ## Quick Start
15//!
16//! ### High-Level API with SwapBuilder
17//!
18//! The easiest way to get started is with the [`SwapBuilder`] API:
19//!
20//! ```rust,no_run
21//! use odos_sdk::prelude::*;
22//! use std::str::FromStr;
23//!
24//! # async fn example() -> Result<()> {
25//! // Create a client
26//! let client = OdosClient::new()?;
27//!
28//! // Define tokens and amount
29//! let usdc = Address::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")?; // USDC on Ethereum
30//! let weth = Address::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")?; // WETH on Ethereum
31//! let my_address = Address::from_str("0x742d35Cc6634C0532925a3b8D35f3e7a5edD29c0")?;
32//!
33//! // Build and execute swap in one go
34//! let transaction = client.swap()
35//!     .chain(Chain::ethereum())
36//!     .from_token(usdc, U256::from(1_000_000)) // 1 USDC (6 decimals)
37//!     .to_token(weth)
38//!     .slippage(Slippage::percent(0.5).unwrap()) // 0.5% slippage
39//!     .signer(my_address)
40//!     .build_transaction()
41//!     .await?;
42//!
43//! println!("Transaction ready: {:?}", transaction);
44//! # Ok(())
45//! # }
46//! ```
47//!
48//! ### Low-Level API
49//!
50//! For more control, use the low-level API with [`quote()`](OdosClient::quote) and [`assemble()`](OdosClient::assemble):
51//!
52//! ```rust,no_run
53//! use odos_sdk::prelude::*;
54//! use alloy_primitives::address;
55//! use std::str::FromStr;
56//!
57//! # async fn example() -> Result<()> {
58//! let client = OdosClient::new()?;
59//! let usdc = Address::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")?;
60//! let weth = Address::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")?;
61//!
62//! // Step 1: Get a quote
63//! let quote_request = QuoteRequest::builder()
64//!     .chain_id(1)
65//!     .input_tokens(vec![(usdc, U256::from(1_000_000)).into()])
66//!     .output_tokens(vec![(weth, 1).into()])
67//!     .slippage_limit_percent(0.5)
68//!     .user_addr(address!("742d35Cc6634C0532925a3b8D35f3e7a5edD29c0"))
69//!     .compact(false)
70//!     .simple(false)
71//!     .referral_code(0)
72//!     .disable_rfqs(false)
73//!     .build();
74//!
75//! let quote = client.quote(&quote_request).await?;
76//! println!("Expected output: {} WETH", quote.out_amount().unwrap_or(&"0".to_string()));
77//!
78//! // Step 2: Assemble transaction
79//! let assembly_request = AssemblyRequest::builder()
80//!     .chain(alloy_chains::NamedChain::Mainnet)
81//!     .router_address(alloy_chains::NamedChain::Mainnet.v2_router_address()?)
82//!     .signer_address(Address::from_str("0x742d35Cc6634C0532925a3b8D35f3e7a5edD29c0")?)
83//!     .output_recipient(Address::from_str("0x742d35Cc6634C0532925a3b8D35f3e7a5edD29c0")?)
84//!     .token_address(usdc)
85//!     .token_amount(U256::from(1_000_000))
86//!     .path_id(quote.path_id().to_string())
87//!     .build();
88//!
89//! let transaction = client.assemble(&assembly_request).await?;
90//! # Ok(())
91//! # }
92//! ```
93//!
94//! ## Configuration
95//!
96//! The SDK supports extensive configuration for production use:
97//!
98//! ```rust,no_run
99//! use odos_sdk::*;
100//! use std::time::Duration;
101//!
102//! # fn example() -> Result<()> {
103//! // Full configuration
104//! let config = ClientConfig {
105//!     timeout: Duration::from_secs(30),
106//!     connect_timeout: Duration::from_secs(10),
107//!     retry_config: RetryConfig {
108//!         max_retries: 3,
109//!         initial_backoff_ms: 100,
110//!         retry_server_errors: true,
111//!         retry_predicate: None,
112//!     },
113//!     max_connections: 20,
114//!     pool_idle_timeout: Duration::from_secs(90),
115//!     api_key: None,
116//!     ..Default::default()
117//! };
118//! let client = OdosClient::with_config(config)?;
119//!
120//! // Or use convenience constructors
121//! let client = OdosClient::with_retry_config(RetryConfig::conservative())?;
122//! # Ok(())
123//! # }
124//! ```
125//!
126//! ## Error Handling
127//!
128//! The SDK provides comprehensive error types with strongly-typed error codes:
129//!
130//! ```rust,no_run
131//! use odos_sdk::*;
132//! use alloy_primitives::Address;
133//!
134//! # async fn example() {
135//! # let client = OdosClient::new().unwrap();
136//! # let quote_request = QuoteRequest::builder().chain_id(1).input_tokens(vec![]).output_tokens(vec![]).slippage_limit_percent(1.0).user_addr(Address::ZERO).compact(false).simple(false).referral_code(0).disable_rfqs(false).build();
137//! match client.quote(&quote_request).await {
138//!     Ok(quote) => {
139//!         // Handle successful quote
140//!         println!("Got quote with path ID: {}", quote.path_id());
141//!     }
142//!     Err(err) => {
143//!         // Check for specific error codes
144//!         if let Some(code) = err.error_code() {
145//!             if code.is_invalid_chain_id() {
146//!                 eprintln!("Invalid chain ID - check configuration");
147//!             } else if code.is_no_viable_path() {
148//!                 eprintln!("No routing path found");
149//!             } else if code.is_timeout() {
150//!                 eprintln!("Service timeout: {}", code);
151//!             }
152//!         }
153//!
154//!         // Log trace ID for support
155//!         if let Some(trace_id) = err.trace_id() {
156//!             eprintln!("Trace ID: {}", trace_id);
157//!         }
158//!
159//!         // Handle by error type
160//!         match err {
161//!             OdosError::Api { status, message, .. } => {
162//!                 eprintln!("API error {}: {}", status, message);
163//!             }
164//!             OdosError::Timeout(msg) => {
165//!                 eprintln!("Request timed out: {}", msg);
166//!             }
167//!             OdosError::RateLimit { message, retry_after, .. } => {
168//!                 if let Some(duration) = retry_after {
169//!                     eprintln!("Rate limited: {}. Retry after {} seconds", message, duration.as_secs());
170//!                 } else {
171//!                     eprintln!("Rate limited: {}", message);
172//!                 }
173//!             }
174//!             _ => eprintln!("Error: {}", err),
175//!         }
176//!     }
177//! }
178//! # }
179//! ```
180//!
181//! ### Strongly-Typed Error Codes
182//!
183//! The SDK provides error codes matching the [Odos API documentation](https://docs.odos.xyz/build/api_errors):
184//!
185//! - **General (1XXX)**: `ApiError`
186//! - **Algo/Quote (2XXX)**: `NoViablePath`, `AlgoTimeout`, `AlgoInternal`
187//! - **Internal Service (3XXX)**: `TxnAssemblyTimeout`, `GasUnavailable`
188//! - **Validation (4XXX)**: `InvalidChainId`, `BlockedUserAddr`, `InvalidTokenAmount`
189//! - **Internal (5XXX)**: `InternalError`, `SwapUnavailable`
190//!
191//! ```rust,no_run
192//! use odos_sdk::{OdosError, error_code::OdosErrorCode};
193//!
194//! # fn handle_error(error: OdosError) {
195//! if let Some(code) = error.error_code() {
196//!     // Check categories
197//!     if code.is_validation_error() {
198//!         println!("Validation error - check request parameters");
199//!     }
200//!
201//!     // Check retryability
202//!     if code.is_retryable() {
203//!         println!("Error can be retried: {}", code);
204//!     }
205//! }
206//! # }
207//! ```
208//!
209//! ## Rate Limiting
210//!
211//! The Odos API enforces rate limits to ensure fair usage. The SDK handles rate limits intelligently:
212//!
213//! - **HTTP 429 responses** are detected and classified as [`OdosError::RateLimit`]
214//! - Rate limit errors are **NOT retried** (return immediately with `Retry-After` header)
215//! - The SDK **captures `Retry-After` headers** for application-level handling
216//! - Applications should handle rate limits globally with proper backoff coordination
217//!
218//! ### Best Practices for Avoiding Rate Limits
219//!
220//! 1. **Share a single client** across your application instead of creating new clients per request
221//! 2. **Implement application-level rate limiting** if making many concurrent requests
222//! 3. **Handle rate limit errors gracefully** and back off at the application level if needed
223//!
224//! ### Example: Handling Rate Limits
225//!
226//! ```rust,no_run
227//! use odos_sdk::*;
228//! use alloy_primitives::{Address, U256};
229//! use std::time::Duration;
230//!
231//! # async fn example() -> Result<()> {
232//! # let client = OdosClient::new()?;
233//! # let quote_request = QuoteRequest::builder()
234//! #     .chain_id(1)
235//! #     .input_tokens(vec![])
236//! #     .output_tokens(vec![])
237//! #     .slippage_limit_percent(1.0)
238//! #     .user_addr(Address::ZERO)
239//! #     .compact(false)
240//! #     .simple(false)
241//! #     .referral_code(0)
242//! #     .disable_rfqs(false)
243//! #     .build();
244//! match client.quote(&quote_request).await {
245//!     Ok(quote) => {
246//!         println!("Got quote: {}", quote.path_id());
247//!     }
248//!     Err(e) if e.is_rate_limit() => {
249//!         // Rate limit exceeded even after SDK retries
250//!         // Consider backing off at application level
251//!         eprintln!("Rate limited - waiting before retry");
252//!         tokio::time::sleep(Duration::from_secs(5)).await;
253//!         // Retry or handle accordingly
254//!     }
255//!     Err(e) => {
256//!         eprintln!("Error: {}", e);
257//!     }
258//! }
259//! # Ok(())
260//! # }
261//! ```
262//!
263//! ### Configuring Retry Behavior
264//!
265//! You can customize retry behavior for your use case:
266//!
267//! ```rust,no_run
268//! use odos_sdk::*;
269//!
270//! # fn example() -> Result<()> {
271//! // Conservative: only retry network errors
272//! let client = OdosClient::with_retry_config(RetryConfig::conservative())?;
273//!
274//! // No retries: handle all errors at application level
275//! let client = OdosClient::with_retry_config(RetryConfig::no_retries())?;
276//!
277//! // Custom configuration
278//! let retry_config = RetryConfig {
279//!     max_retries: 5,
280//!     initial_backoff_ms: 200,
281//!     retry_server_errors: false,  // Don't retry 5xx errors
282//!     retry_predicate: None,
283//! };
284//! let client = OdosClient::with_retry_config(retry_config)?;
285//! # Ok(())
286//! # }
287//! ```
288//!
289//! **Note:** Rate limit errors (429) are never retried regardless of configuration.
290//! This prevents retry cascades that make rate limiting worse.
291
292mod api;
293mod api_key;
294mod assemble;
295mod chain;
296mod client;
297mod contract;
298mod error;
299pub mod error_code;
300#[cfg(test)]
301mod integration_tests;
302#[cfg(feature = "limit-orders")]
303mod limit_order_v2;
304mod router_type;
305mod sor;
306mod swap;
307mod swap_builder;
308mod transfer;
309mod types;
310
311// Prelude for convenient imports
312pub mod prelude;
313
314#[cfg(feature = "v2")]
315mod v2_router;
316#[cfg(feature = "v3")]
317mod v3_router;
318
319// API types
320pub use api::{
321    ApiHost, ApiVersion, Endpoint, InputToken, OdosApiErrorResponse, OutputToken, QuoteRequest,
322    SingleQuoteResponse,
323};
324
325// SwapInputs is only available with v2 feature (contains V2 router types)
326#[cfg(feature = "v2")]
327pub use api::SwapInputs;
328
329// API key management
330pub use api_key::ApiKey;
331
332// Transaction assembly
333pub use assemble::{
334    parse_value, AssembleRequest, AssemblyResponse, Simulation, SimulationError, TransactionData,
335};
336
337// Chain support
338pub use chain::{OdosChain, OdosChainError, OdosChainResult, OdosRouterSelection};
339
340// HTTP client configuration
341pub use client::{ClientConfig, OdosHttpClient, RetryConfig};
342
343// Contract addresses and chain helpers
344pub use contract::{
345    get_lo_router_by_chain_id, get_supported_chains, get_supported_lo_chains,
346    get_supported_v2_chains, get_supported_v3_chains, get_v2_router_by_chain_id,
347    get_v3_router_by_chain_id, ODOS_LO_ARBITRUM_ROUTER, ODOS_LO_AVALANCHE_ROUTER,
348    ODOS_LO_BASE_ROUTER, ODOS_LO_BSC_ROUTER, ODOS_LO_ETHEREUM_ROUTER, ODOS_LO_FRAXTAL_ROUTER,
349    ODOS_LO_LINEA_ROUTER, ODOS_LO_MANTLE_ROUTER, ODOS_LO_MODE_ROUTER, ODOS_LO_OP_ROUTER,
350    ODOS_LO_POLYGON_ROUTER, ODOS_LO_SCROLL_ROUTER, ODOS_LO_SONIC_ROUTER, ODOS_LO_UNICHAIN_ROUTER,
351    ODOS_LO_ZKSYNC_ROUTER, ODOS_V2_ARBITRUM_ROUTER, ODOS_V2_AVALANCHE_ROUTER, ODOS_V2_BASE_ROUTER,
352    ODOS_V2_BSC_ROUTER, ODOS_V2_ETHEREUM_ROUTER, ODOS_V2_FRAXTAL_ROUTER, ODOS_V2_LINEA_ROUTER,
353    ODOS_V2_MANTLE_ROUTER, ODOS_V2_MODE_ROUTER, ODOS_V2_OP_ROUTER, ODOS_V2_POLYGON_ROUTER,
354    ODOS_V2_SCROLL_ROUTER, ODOS_V2_SONIC_ROUTER, ODOS_V2_UNICHAIN_ROUTER, ODOS_V2_ZKSYNC_ROUTER,
355    ODOS_V3,
356};
357
358// Error handling
359pub use error::{OdosError, Result};
360
361// Limit order contract bindings
362#[cfg(feature = "limit-orders")]
363pub use limit_order_v2::LimitOrderV2;
364
365// Router type selection
366pub use router_type::{RouterAvailability, RouterType};
367
368// Smart Order Router client
369#[allow(deprecated)]
370pub use sor::{OdosClient, OdosSor};
371
372// Swap execution context
373#[allow(deprecated)]
374pub use swap::{AssemblyRequest, SwapContext};
375
376// High-level swap builder
377pub use swap_builder::SwapBuilder;
378
379// Transfer types
380pub use transfer::TransferRouterFunds;
381
382// Type-safe domain types
383pub use types::{Chain, ReferralCode, Slippage};
384
385// V2 router contract bindings
386#[cfg(feature = "v2")]
387pub use v2_router::{OdosRouterV2, OdosV2Router, V2Router};
388
389// V3 router contract bindings
390#[cfg(feature = "v3")]
391pub use v3_router::{IOdosRouterV3, OdosV3Router, V3Router};