odos_sdk/
lib.rs

1// SPDX-FileCopyrightText: 2025 Semiotic AI, Inc.
2//
3// SPDX-License-Identifier: Apache-2.0
4//! # Odos SDK
5//!
6//! A production-ready Rust SDK for the Odos protocol - a decentralized exchange aggregator
7//! that provides optimal routing for token swaps across multiple EVM chains.
8//!
9//! ## Features
10//!
11//! - **Multi-chain Support**: 16+ EVM chains including Ethereum, Arbitrum, Optimism, Polygon, Base, etc.
12//! - **Type-safe**: Leverages Rust's type system with Alloy primitives for addresses, chain IDs, and amounts
13//! - **Production-ready**: Built-in retry logic, circuit breakers, timeouts, and error handling
14//! - **Builder Pattern**: Ergonomic API using the `bon` crate for request building
15//! - **Comprehensive Error Handling**: Detailed error types for different failure scenarios
16//!
17//! ## Quick Start
18//!
19//! ### High-Level API with SwapBuilder
20//!
21//! The easiest way to get started is with the [`SwapBuilder`] API:
22//!
23//! ```rust,no_run
24//! use odos_sdk::prelude::*;
25//! use std::str::FromStr;
26//!
27//! # async fn example() -> Result<()> {
28//! // Create a client
29//! let client = OdosClient::new()?;
30//!
31//! // Define tokens and amount
32//! let usdc = Address::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")?; // USDC on Ethereum
33//! let weth = Address::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")?; // WETH on Ethereum
34//! let my_address = Address::from_str("0x742d35Cc6634C0532925a3b8D35f3e7a5edD29c0")?;
35//!
36//! // Build and execute swap in one go
37//! let transaction = client.swap()
38//!     .chain(Chain::ethereum())
39//!     .from_token(usdc, U256::from(1_000_000)) // 1 USDC (6 decimals)
40//!     .to_token(weth)
41//!     .slippage(Slippage::percent(0.5).unwrap()) // 0.5% slippage
42//!     .signer(my_address)
43//!     .build_transaction()
44//!     .await?;
45//!
46//! println!("Transaction ready: {:?}", transaction);
47//! # Ok(())
48//! # }
49//! ```
50//!
51//! ### Low-Level API
52//!
53//! For more control, use the low-level API with [`quote()`](OdosClient::quote) and [`assemble()`](OdosClient::assemble):
54//!
55//! ```rust,no_run
56//! use odos_sdk::prelude::*;
57//! use alloy_primitives::address;
58//! use std::str::FromStr;
59//!
60//! # async fn example() -> Result<()> {
61//! let client = OdosClient::new()?;
62//! let usdc = Address::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")?;
63//! let weth = Address::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")?;
64//!
65//! // Step 1: Get a quote
66//! let quote_request = QuoteRequest::builder()
67//!     .chain_id(1)
68//!     .input_tokens(vec![(usdc, U256::from(1_000_000)).into()])
69//!     .output_tokens(vec![(weth, 1).into()])
70//!     .slippage_limit_percent(0.5)
71//!     .user_addr(address!("742d35Cc6634C0532925a3b8D35f3e7a5edD29c0"))
72//!     .compact(false)
73//!     .simple(false)
74//!     .referral_code(0)
75//!     .disable_rfqs(false)
76//!     .build();
77//!
78//! let quote = client.quote(&quote_request).await?;
79//! println!("Expected output: {} WETH", quote.out_amount().unwrap_or(&"0".to_string()));
80//!
81//! // Step 2: Assemble transaction
82//! let assembly_request = AssemblyRequest::builder()
83//!     .chain(alloy_chains::NamedChain::Mainnet)
84//!     .router_address(alloy_chains::NamedChain::Mainnet.v2_router_address()?)
85//!     .signer_address(Address::from_str("0x742d35Cc6634C0532925a3b8D35f3e7a5edD29c0")?)
86//!     .output_recipient(Address::from_str("0x742d35Cc6634C0532925a3b8D35f3e7a5edD29c0")?)
87//!     .token_address(usdc)
88//!     .token_amount(U256::from(1_000_000))
89//!     .path_id(quote.path_id().to_string())
90//!     .build();
91//!
92//! let transaction = client.assemble(&assembly_request).await?;
93//! # Ok(())
94//! # }
95//! ```
96//!
97//! ## Configuration
98//!
99//! The SDK supports extensive configuration for production use:
100//!
101//! ```rust,no_run
102//! use odos_sdk::*;
103//! use std::time::Duration;
104//!
105//! # fn example() -> Result<()> {
106//! // Full configuration
107//! let config = ClientConfig {
108//!     timeout: Duration::from_secs(30),
109//!     connect_timeout: Duration::from_secs(10),
110//!     retry_config: RetryConfig {
111//!         max_retries: 3,
112//!         initial_backoff_ms: 100,
113//!         retry_server_errors: true,
114//!         retry_predicate: None,
115//!     },
116//!     max_connections: 20,
117//!     pool_idle_timeout: Duration::from_secs(90),
118//!     api_key: None,
119//!     ..Default::default()
120//! };
121//! let client = OdosClient::with_config(config)?;
122//!
123//! // Or use convenience constructors
124//! let client = OdosClient::with_retry_config(RetryConfig::conservative())?;
125//! # Ok(())
126//! # }
127//! ```
128//!
129//! ## Error Handling
130//!
131//! The SDK provides comprehensive error types with strongly-typed error codes:
132//!
133//! ```rust,no_run
134//! use odos_sdk::*;
135//! use alloy_primitives::Address;
136//!
137//! # async fn example() {
138//! # let client = OdosClient::new().unwrap();
139//! # 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();
140//! match client.quote(&quote_request).await {
141//!     Ok(quote) => {
142//!         // Handle successful quote
143//!         println!("Got quote with path ID: {}", quote.path_id());
144//!     }
145//!     Err(err) => {
146//!         // Check for specific error codes
147//!         if let Some(code) = err.error_code() {
148//!             if code.is_invalid_chain_id() {
149//!                 eprintln!("Invalid chain ID - check configuration");
150//!             } else if code.is_no_viable_path() {
151//!                 eprintln!("No routing path found");
152//!             } else if code.is_timeout() {
153//!                 eprintln!("Service timeout: {}", code);
154//!             }
155//!         }
156//!
157//!         // Log trace ID for support
158//!         if let Some(trace_id) = err.trace_id() {
159//!             eprintln!("Trace ID: {}", trace_id);
160//!         }
161//!
162//!         // Handle by error type
163//!         match err {
164//!             OdosError::Api { status, message, .. } => {
165//!                 eprintln!("API error {}: {}", status, message);
166//!             }
167//!             OdosError::Timeout(msg) => {
168//!                 eprintln!("Request timed out: {}", msg);
169//!             }
170//!             OdosError::RateLimit { message, retry_after, .. } => {
171//!                 if let Some(duration) = retry_after {
172//!                     eprintln!("Rate limited: {}. Retry after {} seconds", message, duration.as_secs());
173//!                 } else {
174//!                     eprintln!("Rate limited: {}", message);
175//!                 }
176//!             }
177//!             _ => eprintln!("Error: {}", err),
178//!         }
179//!     }
180//! }
181//! # }
182//! ```
183//!
184//! ### Strongly-Typed Error Codes
185//!
186//! The SDK provides error codes matching the [Odos API documentation](https://docs.odos.xyz/build/api_errors):
187//!
188//! - **General (1XXX)**: `ApiError`
189//! - **Algo/Quote (2XXX)**: `NoViablePath`, `AlgoTimeout`, `AlgoInternal`
190//! - **Internal Service (3XXX)**: `TxnAssemblyTimeout`, `GasUnavailable`
191//! - **Validation (4XXX)**: `InvalidChainId`, `BlockedUserAddr`, `InvalidTokenAmount`
192//! - **Internal (5XXX)**: `InternalError`, `SwapUnavailable`
193//!
194//! ```rust,no_run
195//! use odos_sdk::{OdosError, error_code::OdosErrorCode};
196//!
197//! # fn handle_error(error: OdosError) {
198//! if let Some(code) = error.error_code() {
199//!     // Check categories
200//!     if code.is_validation_error() {
201//!         println!("Validation error - check request parameters");
202//!     }
203//!
204//!     // Check retryability
205//!     if code.is_retryable() {
206//!         println!("Error can be retried: {}", code);
207//!     }
208//! }
209//! # }
210//! ```
211//!
212//! ## Rate Limiting
213//!
214//! The Odos API enforces rate limits to ensure fair usage. The SDK handles rate limits intelligently:
215//!
216//! - **HTTP 429 responses** are detected and classified as [`OdosError::RateLimit`]
217//! - Rate limit errors are **NOT retried** (return immediately with `Retry-After` header)
218//! - The SDK **captures `Retry-After` headers** for application-level handling
219//! - Applications should handle rate limits globally with proper backoff coordination
220//!
221//! ### Best Practices for Avoiding Rate Limits
222//!
223//! 1. **Share a single client** across your application instead of creating new clients per request
224//! 2. **Implement application-level rate limiting** if making many concurrent requests
225//! 3. **Handle rate limit errors gracefully** and back off at the application level if needed
226//!
227//! ### Example: Handling Rate Limits
228//!
229//! ```rust,no_run
230//! use odos_sdk::*;
231//! use alloy_primitives::{Address, U256};
232//! use std::time::Duration;
233//!
234//! # async fn example() -> Result<()> {
235//! # let client = OdosClient::new()?;
236//! # let quote_request = QuoteRequest::builder()
237//! #     .chain_id(1)
238//! #     .input_tokens(vec![])
239//! #     .output_tokens(vec![])
240//! #     .slippage_limit_percent(1.0)
241//! #     .user_addr(Address::ZERO)
242//! #     .compact(false)
243//! #     .simple(false)
244//! #     .referral_code(0)
245//! #     .disable_rfqs(false)
246//! #     .build();
247//! match client.quote(&quote_request).await {
248//!     Ok(quote) => {
249//!         println!("Got quote: {}", quote.path_id());
250//!     }
251//!     Err(e) if e.is_rate_limit() => {
252//!         // Rate limit exceeded even after SDK retries
253//!         // Consider backing off at application level
254//!         eprintln!("Rate limited - waiting before retry");
255//!         tokio::time::sleep(Duration::from_secs(5)).await;
256//!         // Retry or handle accordingly
257//!     }
258//!     Err(e) => {
259//!         eprintln!("Error: {}", e);
260//!     }
261//! }
262//! # Ok(())
263//! # }
264//! ```
265//!
266//! ### Configuring Retry Behavior
267//!
268//! You can customize retry behavior for your use case:
269//!
270//! ```rust,no_run
271//! use odos_sdk::*;
272//!
273//! # fn example() -> Result<()> {
274//! // Conservative: only retry network errors
275//! let client = OdosClient::with_retry_config(RetryConfig::conservative())?;
276//!
277//! // No retries: handle all errors at application level
278//! let client = OdosClient::with_retry_config(RetryConfig::no_retries())?;
279//!
280//! // Custom configuration
281//! let retry_config = RetryConfig {
282//!     max_retries: 5,
283//!     initial_backoff_ms: 200,
284//!     retry_server_errors: false,  // Don't retry 5xx errors
285//!     retry_predicate: None,
286//! };
287//! let client = OdosClient::with_retry_config(retry_config)?;
288//! # Ok(())
289//! # }
290//! ```
291//!
292//! **Note:** Rate limit errors (429) are never retried regardless of configuration.
293//! This prevents retry cascades that make rate limiting worse.
294//!
295//! ## Provider Construction (Alloy Best Practices)
296//!
297//! ### Dynamic Chain Support with AnyNetwork
298//!
299//! For applications that need to work with multiple chains dynamically (without knowing
300//! the chain at compile time), use `AnyNetwork`:
301//!
302//! ```rust,ignore
303//! use alloy_network::AnyNetwork;
304//! use alloy_provider::ProviderBuilder;
305//!
306//! // Works with any EVM chain without network-specific types
307//! let provider = ProviderBuilder::new()
308//!     .network::<AnyNetwork>()
309//!     .connect_http(rpc_url.parse()?);
310//!
311//! // Routers work with AnyNetwork
312//! let router: V3Router<AnyNetwork, _> = V3Router::new(router_address, provider);
313//! ```
314//!
315//! **Trade-offs:**
316//! - ✅ Single code path for all chains
317//! - ✅ Simpler multi-chain applications
318//! - ⚠️ Loses network-specific receipt fields (e.g., OP-stack L1 gas info)
319//! - ⚠️ Less compile-time type safety
320//!
321//! When executing swaps on-chain, use Alloy's [`ProviderBuilder`](alloy_provider::ProviderBuilder)
322//! with recommended fillers for proper nonce management, gas estimation, and chain ID handling:
323//!
324//! ```rust,ignore
325//! use alloy_provider::ProviderBuilder;
326//! use alloy_signer_local::PrivateKeySigner;
327//! use alloy_network::EthereumWallet;
328//!
329//! // Create a signer from a private key
330//! let signer: PrivateKeySigner = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
331//!     .parse()
332//!     .expect("valid private key");
333//!
334//! // Create a provider with recommended fillers (nonce, gas, chain ID)
335//! let provider = ProviderBuilder::new()
336//!     .with_recommended_fillers()
337//!     .wallet(EthereumWallet::new(signer))
338//!     .connect_http("https://eth.llamarpc.com".parse()?);
339//!
340//! // Use the provider with routers
341//! let router = odos_sdk::V3Router::new(router_address, provider);
342//! ```
343//!
344//! ### OP-Stack Chains (Base, Optimism, Fraxtal)
345//!
346//! For OP-stack chains, use the `Optimism` network type to access L1 gas information:
347//!
348//! ```rust,ignore
349//! #[cfg(feature = "op-stack")]
350//! {
351//!     use odos_sdk::op_stack::Optimism;
352//!     use alloy_provider::ProviderBuilder;
353//!
354//!     // Create a provider for OP-stack chains
355//!     let provider = ProviderBuilder::new()
356//!         .with_recommended_fillers()
357//!         .network::<Optimism>()
358//!         .connect_http("https://mainnet.base.org".parse()?);
359//!
360//!     // Transaction receipts will include L1 gas information
361//!     let receipt = provider.get_transaction_receipt(tx_hash).await?;
362//!     if let Some(l1_fee) = receipt.inner.l1_fee {
363//!         println!("L1 fee: {l1_fee}");
364//!     }
365//! }
366//! ```
367//!
368//! ### Sharing Providers
369//!
370//! For concurrent swap operations, share a single provider instance:
371//!
372//! ```rust,ignore
373//! use std::sync::Arc;
374//! use alloy_provider::ProviderBuilder;
375//!
376//! // Create a shared provider
377//! let provider = Arc::new(
378//!     ProviderBuilder::new()
379//!         .with_recommended_fillers()
380//!         .connect_http("https://eth.llamarpc.com".parse()?)
381//! );
382//!
383//! // Clone the Arc for each concurrent operation
384//! let provider_clone = Arc::clone(&provider);
385//! tokio::spawn(async move {
386//!     // Use provider_clone for concurrent swap
387//! });
388//! ```
389//!
390//! ### WebSocket Providers for Real-Time Monitoring
391//!
392//! Use WebSocket providers for real-time swap event monitoring:
393//!
394//! ```rust,ignore
395//! use alloy_provider::ProviderBuilder;
396//! use alloy_rpc_types::Filter;
397//! use odos_sdk::events::SwapEventFilter;
398//! use futures_util::StreamExt;
399//!
400//! // Connect via WebSocket for subscriptions
401//! let ws_provider = ProviderBuilder::new()
402//!     .connect_ws("wss://eth.llamarpc.com".parse()?)
403//!     .await?;
404//!
405//! // Create a filter for swap events
406//! let filter = SwapEventFilter::new(router_address)
407//!     .from_latest()
408//!     .build_v3_filter();
409//!
410//! // Subscribe to real-time swap events
411//! let mut stream = ws_provider.subscribe_logs(&filter).await?;
412//!
413//! while let Some(log) = stream.next().await {
414//!     match log {
415//!         Ok(log) => println!("New swap detected: {:?}", log),
416//!         Err(e) => eprintln!("Subscription error: {e}"),
417//!     }
418//! }
419//! ```
420
421mod api;
422mod api_key;
423mod assemble;
424mod chain;
425mod client;
426mod contract;
427mod error;
428pub mod error_code;
429#[cfg(any(feature = "v2", feature = "v3"))]
430pub mod events;
431#[cfg(test)]
432mod integration_tests;
433#[cfg(feature = "limit-orders")]
434mod limit_order_v2;
435pub mod multicall;
436#[cfg(feature = "op-stack")]
437pub mod op_stack;
438mod router_type;
439mod sor;
440mod swap;
441mod swap_builder;
442mod transfer;
443mod types;
444
445// Prelude for convenient imports
446pub mod prelude;
447
448#[cfg(feature = "v2")]
449mod v2_router;
450#[cfg(feature = "v3")]
451mod v3_router;
452
453// API types
454pub use api::{
455    ApiHost, ApiVersion, Endpoint, InputToken, OdosApiErrorResponse, OutputToken, QuoteRequest,
456    SingleQuoteResponse,
457};
458
459// SwapInputs is only available with v2 feature (contains V2 router types)
460#[cfg(feature = "v2")]
461pub use api::SwapInputs;
462
463// API key management
464pub use api_key::ApiKey;
465
466// Transaction assembly
467pub use assemble::{
468    parse_value, AssembleRequest, AssemblyResponse, Simulation, SimulationError, TransactionData,
469};
470
471// Chain support
472pub use chain::{OdosChain, OdosChainError, OdosChainResult, OdosRouterSelection};
473
474// HTTP client configuration
475pub use client::{ClientConfig, OdosHttpClient, RetryConfig};
476
477// Contract addresses and chain helpers
478pub use contract::{
479    get_lo_router_by_chain_id, get_supported_chains, get_supported_lo_chains,
480    get_supported_v2_chains, get_supported_v3_chains, get_v2_router_by_chain_id,
481    get_v3_router_by_chain_id, ODOS_LO_ARBITRUM_ROUTER, ODOS_LO_AVALANCHE_ROUTER,
482    ODOS_LO_BASE_ROUTER, ODOS_LO_BSC_ROUTER, ODOS_LO_ETHEREUM_ROUTER, ODOS_LO_FRAXTAL_ROUTER,
483    ODOS_LO_LINEA_ROUTER, ODOS_LO_MANTLE_ROUTER, ODOS_LO_OP_ROUTER, ODOS_LO_POLYGON_ROUTER,
484    ODOS_LO_SCROLL_ROUTER, ODOS_LO_SONIC_ROUTER, ODOS_LO_UNICHAIN_ROUTER, ODOS_LO_ZKSYNC_ROUTER,
485    ODOS_V2_ARBITRUM_ROUTER, ODOS_V2_AVALANCHE_ROUTER, ODOS_V2_BASE_ROUTER, ODOS_V2_BSC_ROUTER,
486    ODOS_V2_ETHEREUM_ROUTER, ODOS_V2_FRAXTAL_ROUTER, ODOS_V2_LINEA_ROUTER, ODOS_V2_MANTLE_ROUTER,
487    ODOS_V2_OP_ROUTER, ODOS_V2_POLYGON_ROUTER, ODOS_V2_SCROLL_ROUTER, ODOS_V2_SONIC_ROUTER,
488    ODOS_V2_UNICHAIN_ROUTER, ODOS_V2_ZKSYNC_ROUTER, ODOS_V3,
489};
490
491// Error handling
492pub use error::{OdosError, Result};
493
494// Limit order contract bindings
495#[cfg(feature = "limit-orders")]
496pub use limit_order_v2::LimitOrderV2;
497
498// Limit order event types (different from V2/V3 Swap events)
499#[cfg(feature = "limit-orders")]
500pub use limit_order_v2::{
501    AllowedFillerAdded, AllowedFillerRemoved, LimitOrderCancelled, LimitOrderFilled,
502    LiquidatorAddressChanged, MultiLimitOrderCancelled, MultiLimitOrderFilled, OrderPreSigned,
503    SwapRouterFunds,
504};
505
506// Router type selection
507pub use router_type::{RouterAvailability, RouterType};
508
509// Smart Order Router client
510#[allow(deprecated)]
511pub use sor::{OdosClient, OdosSor};
512
513// Swap execution context
514#[allow(deprecated)]
515pub use swap::{AssemblyRequest, SwapContext};
516
517// High-level swap builder
518pub use swap_builder::SwapBuilder;
519
520// Transfer types
521pub use transfer::TransferRouterFunds;
522
523// Type-safe domain types
524pub use types::{Chain, ReferralCode, Slippage};
525
526// V2 router contract bindings
527#[cfg(feature = "v2")]
528pub use v2_router::{OdosRouterV2, OdosV2Router, V2Router};
529
530// V3 router contract bindings
531#[cfg(feature = "v3")]
532pub use v3_router::{IOdosRouterV3, OdosV3Router, V3Router};