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("e_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("e_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("e_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};