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//! ```rust,no_run
17//! use odos_sdk::*;
18//! use alloy_primitives::{Address, U256};
19//! use std::str::FromStr;
20//!
21//! # async fn example() -> Result<()> {
22//! // Create a client
23//! let client = OdosSor::new()?;
24//!
25//! // Build a quote request
26//! let quote_request = QuoteRequest::builder()
27//! .chain_id(1) // Ethereum mainnet
28//! .input_tokens(vec![(
29//! Address::from_str("0xA0b86a33E6441d35a6b083d5b02a8e3F6CE21a2E")?, // WETH
30//! U256::from(1000000000000000000u64) // 1 ETH
31//! ).into()])
32//! .output_tokens(vec![(
33//! Address::from_str("0xA0b86a33E6441d35a6b083d5b02a8e3F6CE21a2E")?, // USDC
34//! 1
35//! ).into()])
36//! .slippage_limit_percent(1.0)
37//! .user_addr("0x742d35Cc6634C0532925a3b8D35f3e7a5edD29c0".to_string())
38//! .compact(false)
39//! .simple(false)
40//! .referral_code(0)
41//! .disable_rfqs(false)
42//! .build();
43//!
44//! // Get a quote
45//! let quote = client.get_swap_quote("e_request).await?;
46//!
47//! // Build transaction data
48//! let swap_context = SwapContext::builder()
49//! .chain(alloy_chains::NamedChain::Mainnet)
50//! .router_address(alloy_chains::NamedChain::Mainnet.v2_router_address()?)
51//! .signer_address(Address::from_str("0x742d35Cc6634C0532925a3b8D35f3e7a5edD29c0")?)
52//! .output_recipient(Address::from_str("0x742d35Cc6634C0532925a3b8D35f3e7a5edD29c0")?)
53//! .token_address(Address::from_str("0xA0b86a33E6441d35a6b083d5b02a8e3F6CE21a2E")?)
54//! .token_amount(U256::from(1000000000000000000u64))
55//! .path_id(quote.path_id().to_string())
56//! .build();
57//!
58//! let transaction = client.build_base_transaction(&swap_context).await?;
59//! # Ok(())
60//! # }
61//! ```
62//!
63//! ## Configuration
64//!
65//! The SDK supports extensive configuration for production use:
66//!
67//! ```rust,no_run
68//! use odos_sdk::*;
69//! use std::time::Duration;
70//!
71//! # fn example() -> Result<()> {
72//! // Full configuration
73//! let config = ClientConfig {
74//! timeout: Duration::from_secs(30),
75//! connect_timeout: Duration::from_secs(10),
76//! retry_config: RetryConfig {
77//! max_retries: 3,
78//! initial_backoff_ms: 100,
79//! retry_server_errors: true,
80//! retry_predicate: None,
81//! },
82//! max_connections: 20,
83//! pool_idle_timeout: Duration::from_secs(90),
84//! api_key: None,
85//! ..Default::default()
86//! };
87//! let client = OdosSor::with_config(config)?;
88//!
89//! // Or use convenience constructors
90//! let client = OdosSor::with_retry_config(RetryConfig::conservative())?;
91//! # Ok(())
92//! # }
93//! ```
94//!
95//! ## Error Handling
96//!
97//! The SDK provides comprehensive error types with strongly-typed error codes:
98//!
99//! ```rust,no_run
100//! use odos_sdk::*;
101//!
102//! # async fn example() {
103//! # let client = OdosSor::new().unwrap();
104//! # let quote_request = QuoteRequest::builder().chain_id(1).input_tokens(vec![]).output_tokens(vec![]).slippage_limit_percent(1.0).user_addr("test".to_string()).compact(false).simple(false).referral_code(0).disable_rfqs(false).build();
105//! match client.get_swap_quote("e_request).await {
106//! Ok(quote) => {
107//! // Handle successful quote
108//! println!("Got quote with path ID: {}", quote.path_id());
109//! }
110//! Err(err) => {
111//! // Check for specific error codes
112//! if let Some(code) = err.error_code() {
113//! if code.is_invalid_chain_id() {
114//! eprintln!("Invalid chain ID - check configuration");
115//! } else if code.is_no_viable_path() {
116//! eprintln!("No routing path found");
117//! } else if code.is_timeout() {
118//! eprintln!("Service timeout: {}", code);
119//! }
120//! }
121//!
122//! // Log trace ID for support
123//! if let Some(trace_id) = err.trace_id() {
124//! eprintln!("Trace ID: {}", trace_id);
125//! }
126//!
127//! // Handle by error type
128//! match err {
129//! OdosError::Api { status, message, .. } => {
130//! eprintln!("API error {}: {}", status, message);
131//! }
132//! OdosError::Timeout(msg) => {
133//! eprintln!("Request timed out: {}", msg);
134//! }
135//! OdosError::RateLimit { message, retry_after, .. } => {
136//! if let Some(duration) = retry_after {
137//! eprintln!("Rate limited: {}. Retry after {} seconds", message, duration.as_secs());
138//! } else {
139//! eprintln!("Rate limited: {}", message);
140//! }
141//! }
142//! _ => eprintln!("Error: {}", err),
143//! }
144//! }
145//! }
146//! # }
147//! ```
148//!
149//! ### Strongly-Typed Error Codes
150//!
151//! The SDK provides error codes matching the [Odos API documentation](https://docs.odos.xyz/build/api_errors):
152//!
153//! - **General (1XXX)**: `ApiError`
154//! - **Algo/Quote (2XXX)**: `NoViablePath`, `AlgoTimeout`, `AlgoInternal`
155//! - **Internal Service (3XXX)**: `TxnAssemblyTimeout`, `GasUnavailable`
156//! - **Validation (4XXX)**: `InvalidChainId`, `BlockedUserAddr`, `InvalidTokenAmount`
157//! - **Internal (5XXX)**: `InternalError`, `SwapUnavailable`
158//!
159//! ```rust,no_run
160//! use odos_sdk::{OdosError, error_code::OdosErrorCode};
161//!
162//! # fn handle_error(error: OdosError) {
163//! if let Some(code) = error.error_code() {
164//! // Check categories
165//! if code.is_validation_error() {
166//! println!("Validation error - check request parameters");
167//! }
168//!
169//! // Check retryability
170//! if code.is_retryable() {
171//! println!("Error can be retried: {}", code);
172//! }
173//! }
174//! # }
175//! ```
176//!
177//! ## Rate Limiting
178//!
179//! The Odos API enforces rate limits to ensure fair usage. The SDK handles rate limits intelligently:
180//!
181//! - **HTTP 429 responses** are detected and classified as [`OdosError::RateLimit`]
182//! - Rate limit errors are **NOT retried** (return immediately with `Retry-After` header)
183//! - The SDK **captures `Retry-After` headers** for application-level handling
184//! - Applications should handle rate limits globally with proper backoff coordination
185//!
186//! ### Best Practices for Avoiding Rate Limits
187//!
188//! 1. **Share a single client** across your application instead of creating new clients per request
189//! 2. **Implement application-level rate limiting** if making many concurrent requests
190//! 3. **Handle rate limit errors gracefully** and back off at the application level if needed
191//!
192//! ### Example: Handling Rate Limits
193//!
194//! ```rust,no_run
195//! use odos_sdk::*;
196//! use alloy_primitives::{Address, U256};
197//! use std::time::Duration;
198//!
199//! # async fn example() -> Result<()> {
200//! # let client = OdosSor::new()?;
201//! # let quote_request = QuoteRequest::builder()
202//! # .chain_id(1)
203//! # .input_tokens(vec![])
204//! # .output_tokens(vec![])
205//! # .slippage_limit_percent(1.0)
206//! # .user_addr("test".to_string())
207//! # .compact(false)
208//! # .simple(false)
209//! # .referral_code(0)
210//! # .disable_rfqs(false)
211//! # .build();
212//! match client.get_swap_quote("e_request).await {
213//! Ok(quote) => {
214//! println!("Got quote: {}", quote.path_id());
215//! }
216//! Err(e) if e.is_rate_limit() => {
217//! // Rate limit exceeded even after SDK retries
218//! // Consider backing off at application level
219//! eprintln!("Rate limited - waiting before retry");
220//! tokio::time::sleep(Duration::from_secs(5)).await;
221//! // Retry or handle accordingly
222//! }
223//! Err(e) => {
224//! eprintln!("Error: {}", e);
225//! }
226//! }
227//! # Ok(())
228//! # }
229//! ```
230//!
231//! ### Configuring Retry Behavior
232//!
233//! You can customize retry behavior for your use case:
234//!
235//! ```rust,no_run
236//! use odos_sdk::*;
237//!
238//! # fn example() -> Result<()> {
239//! // Conservative: only retry network errors
240//! let client = OdosSor::with_retry_config(RetryConfig::conservative())?;
241//!
242//! // No retries: handle all errors at application level
243//! let client = OdosSor::with_retry_config(RetryConfig::no_retries())?;
244//!
245//! // Custom configuration
246//! let retry_config = RetryConfig {
247//! max_retries: 5,
248//! initial_backoff_ms: 200,
249//! retry_server_errors: false, // Don't retry 5xx errors
250//! retry_predicate: None,
251//! };
252//! let client = OdosSor::with_retry_config(retry_config)?;
253//! # Ok(())
254//! # }
255//! ```
256//!
257//! **Note:** Rate limit errors (429) are never retried regardless of configuration.
258//! This prevents retry cascades that make rate limiting worse.
259
260mod api;
261mod api_key;
262mod assemble;
263mod chain;
264mod client;
265mod contract;
266mod error;
267pub mod error_code;
268#[cfg(test)]
269mod integration_tests;
270mod limit_order_v2;
271mod sor;
272mod swap;
273mod transfer;
274mod v2_router;
275mod v3_router;
276
277pub use api::*;
278pub use api_key::*;
279pub use assemble::*;
280pub use chain::*;
281pub use client::*;
282pub use contract::*;
283pub use error::*;
284pub use limit_order_v2::*;
285pub use sor::*;
286pub use swap::*;
287pub use transfer::*;
288pub use v2_router::*;
289pub use v3_router::*;