49 releases (1 stable)
| new 1.0.0 | Nov 11, 2025 |
|---|---|
| 0.27.0 | Nov 9, 2025 |
| 0.8.0 | Jul 31, 2025 |
#10 in Magic Beans
1,620 downloads per month
305KB
5K
SLoC
Odos Rust SDK
A production-ready Rust SDK for Odos - the decentralized exchange aggregator that finds optimal token swap routes across 16+ EVM chains. Built with type safety, reliability, and developer experience in mind.
What Makes This Special
Optimal pricing through advanced routing - Odos analyzes paths across hundreds of DEXes and liquidity sources, splitting trades intelligently to minimize slippage and maximize output. The SDK makes this power accessible in just a few lines of Rust.
Battle-tested for production:
- Smart retry logic with exponential backoff for network resilience
- Structured error codes with clear categorization and trace IDs
- Rate limit detection with
Retry-Aftersupport - Full type safety via Alloy primitives (no string addresses or numeric guessing)
- Connection pooling, configurable timeouts, and graceful degradation
- Comprehensive logging and observability with
tracing
APIs for every use case:
- High-level
SwapBuilder- integrate swaps in minutes - Mid-level
quote→assemble- full control over the flow - Low-level contract bindings - advanced scenarios and direct router access
Quick Start
Add the SDK to your project:
[dependencies]
odos-sdk = "1.0"
Your First Swap
use odos_sdk::prelude::*;
use alloy_primitives::{Address, U256};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize the client
let client = OdosClient::new()?;
// Define your swap
let usdc: Address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".parse()?;
let weth: Address = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".parse()?;
let my_address: Address = "0x...".parse()?;
// Build the transaction - that's it!
let tx = client.swap()
.chain(Chain::ethereum())
.from_token(usdc, U256::from(1_000_000)) // 1 USDC (6 decimals)
.to_token(weth)
.slippage(Slippage::percent(0.5)?)
.signer(my_address)
.build_transaction()
.await?;
// Execute with your wallet
// provider.send_transaction(tx.transaction).await?;
Ok(())
}
Three concepts, one builder, zero complexity. The SDK handles quote fetching, optimal routing, transaction assembly, and error recovery automatically.
Next steps: Check out GETTING_STARTED.md for a complete walkthrough, or jump to EXAMPLES.md for common patterns.
Core Features
Multi-Chain Support
Supports 16+ EVM chains out of the box:
| Category | Chains |
|---|---|
| Layer 1 | Ethereum |
| Layer 2 | Arbitrum, Optimism, Base, Polygon, zkSync, Scroll, Linea, Mantle, Mode |
| Sidechains | BSC, Avalanche, Fraxtal, Sonic, Unichain |
Chain selection is type-safe and simple:
let chain = Chain::ethereum(); // or arbitrum(), optimism(), etc.
let chain = Chain::from_chain_id(42161)?; // From numeric ID
Complete Type Safety
Built on the Alloy ecosystem for bulletproof type safety:
use alloy_primitives::{Address, U256};
// Parse addresses at compile time or runtime
let token: Address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".parse()?;
// Handle amounts with U256 (no floating point errors)
let one_usdc = U256::from(1_000_000); // 6 decimals
// Validated slippage
let slippage = Slippage::percent(0.5)?; // 0.5%
let slippage = Slippage::bps(50)?; // 50 basis points
Resilient Error Handling
Structured errors with clear categorization:
use odos_sdk::{OdosError, error_code::OdosErrorCode};
match client.quote(&request).await {
Ok(quote) => {
println!("Expected output: {}", quote.out_amount());
}
Err(e) => {
// Check error type
if e.is_rate_limit() {
if let Some(retry_after) = e.retry_after() {
println!("Rate limited. Retry after: {:?}", retry_after);
}
}
// Check specific error codes
if let Some(code) = e.error_code() {
if code.is_no_viable_path() {
println!("No routing path found for this pair");
} else if code.is_validation_error() {
println!("Invalid request parameters");
}
}
// Access trace ID for debugging
if let Some(trace_id) = e.trace_id() {
eprintln!("Error trace ID: {}", trace_id);
}
}
}
Error codes match the Odos API documentation with type-safe categorization:
- 1XXX: General API errors
- 2XXX: Quote/routing errors (
NoViablePath,AlgoTimeout, etc.) - 3XXX: Internal service errors
- 4XXX: Validation errors (
InvalidChainId,InvalidTokenAmount, etc.) - 5XXX: Internal errors
Smart Retry Logic
Configurable retry behavior with exponential backoff:
use std::time::Duration;
use odos_sdk::{OdosClient, RetryConfig};
// Conservative preset - only retry network errors
let client = OdosClient::with_retry_config(RetryConfig::conservative())?;
// Custom retry configuration
let client = OdosClient::with_retry_config(RetryConfig {
max_retries: 5,
initial_backoff_ms: 200,
retry_server_errors: true,
retry_predicate: Some(|err| {
// Custom logic to determine if an error should be retried
err.is_timeout() || (err.is_api_error() && !err.is_validation_error())
}),
})?;
Rate limits are detected but not automatically retried - you control the global rate limiting strategy.
Three Ways to Swap
The SDK provides three levels of abstraction. Choose based on your needs:
1. High-Level: SwapBuilder
Perfect for most use cases. One builder, automatic flow:
use odos_sdk::prelude::*;
let tx = client.swap()
.chain(Chain::arbitrum())
.from_token(usdc, amount)
.to_token(weth)
.slippage(Slippage::percent(0.5)?)
.signer(my_address)
.recipient(recipient_address) // Optional: send output to different address
.build_transaction()
.await?;
// Or just get a quote first
let quote = client.swap()
.chain(Chain::ethereum())
.from_token(usdc, amount)
.to_token(weth)
.slippage(Slippage::percent(0.5)?)
.signer(my_address)
.quote()
.await?;
println!("Expected output: {}", quote.out_amount());
2. Mid-Level: Quote + Assemble
More control over the quote and assembly phases:
use odos_sdk::prelude::*;
// Step 1: Request quote
let quote_request = QuoteRequest::builder()
.chain_id(1)
.input_tokens(vec![(usdc, amount).into()])
.output_tokens(vec![(weth, 1).into()])
.slippage_limit_percent(0.5)
.user_addr(my_address)
.compact(false)
.simple(false)
.referral_code(0)
.disable_rfqs(false)
.build();
let quote = client.quote("e_request).await?;
// Show user the expected output
println!("You will receive approximately: {}", quote.out_amount());
// Step 2: User confirms, assemble transaction
let assembly_request = AssemblyRequest::builder()
.chain(NamedChain::Mainnet)
.router_address(NamedChain::Mainnet.v2_router_address()?)
.signer_address(my_address)
.output_recipient(my_address)
.token_address(usdc)
.token_amount(amount)
.path_id(quote.path_id().to_string())
.build();
let tx = client.assemble(&assembly_request).await?;
3. Low-Level: Contract Bindings
Direct router contract access for advanced scenarios:
use odos_sdk::{OdosV2Router, OdosChain};
use alloy_chains::NamedChain;
let router_address = NamedChain::Mainnet.v2_router_address()?;
let router = OdosV2Router::new(router_address, provider);
// Call contract methods directly
let result = router.swap(swap_inputs).send().await?;
Configuration
Basic Configuration
use odos_sdk::{OdosClient, ClientConfig};
use std::time::Duration;
let config = ClientConfig {
timeout: Duration::from_secs(30),
connect_timeout: Duration::from_secs(10),
max_connections: 20,
pool_idle_timeout: Duration::from_secs(90),
..Default::default()
};
let client = OdosClient::with_config(config)?;
API Endpoints
Choose between public and enterprise endpoints:
use odos_sdk::{ClientConfig, Endpoint, ApiHost, ApiVersion};
// Public API (default)
let config = ClientConfig {
endpoint: Endpoint::public_v2(), // or public_v3()
..Default::default()
};
// Enterprise API with higher rate limits
let config = ClientConfig {
endpoint: Endpoint::enterprise_v2(), // or enterprise_v3()
api_key: Some(ApiKey::new("your-api-key")?),
..Default::default()
};
let client = OdosClient::with_config(config)?;
Feature Flags
Customize what gets compiled based on your needs:
# Default: V2 + V3 routers
[dependencies]
odos-sdk = "1.0"
# Minimal: Just API types and HTTP client (no contract bindings)
[dependencies]
odos-sdk = { version = "1.0", default-features = false, features = ["minimal"] }
# All contracts: V2 + V3 + Limit Orders
[dependencies]
odos-sdk = { version = "1.0", default-features = false, features = ["contracts"] }
# Custom combination
[dependencies]
odos-sdk = { version = "1.0", default-features = false, features = ["v2", "v3"] }
Available features:
minimal- Core API types and HTTP client onlyv2- V2 router contract bindingsv3- V3 router contract bindings (includes v2)limit-orders- Limit order contract bindings (includes v2)contracts- All contract bindings (v2 + v3 + limit-orders)default- V2 + V3 routers
Documentation
| Resource | Description |
|---|---|
| GETTING_STARTED.md | Complete walkthrough from setup to your first swap |
| EXAMPLES.md | Real-world patterns: error handling, testing, integration |
| API Docs | Complete API reference with inline examples |
| ERROR_HANDLING_GUIDE.md | Deep dive into error types and recovery strategies |
| CHANGELOG.md | Version history and migration guides |
| SECURITY.md | Security best practices and vulnerability reporting |
Advanced Topics
Rate Limiting Strategy
The SDK detects rate limits but doesn't retry them automatically. Implement your own strategy:
use std::time::Duration;
use tokio::time::sleep;
async fn quote_with_rate_limiting(
client: &OdosClient,
request: &QuoteRequest,
) -> Result<SingleQuoteResponse, OdosError> {
loop {
match client.quote(request).await {
Ok(quote) => return Ok(quote),
Err(e) if e.is_rate_limit() => {
let wait = e.retry_after()
.unwrap_or(Duration::from_secs(5));
eprintln!("Rate limited, waiting {:?}", wait);
sleep(wait).await;
}
Err(e) => return Err(e),
}
}
}
For production applications with high request volumes:
- Share a single
OdosClientinstance across your application - Implement a token bucket or leaky bucket algorithm
- Consider using a rate limiting library like
governor - Monitor rate limit errors and adjust your request rate dynamically
Router Versioning
The SDK supports multiple router versions with different capabilities:
use odos_sdk::{OdosChain, RouterAvailability};
use alloy_chains::NamedChain;
let chain = NamedChain::Mainnet;
let availability = chain.router_availability()?;
if availability.v3 {
// Use V3 router (unified address across all chains)
let router = chain.v3_router_address()?;
} else if availability.v2 {
// Fall back to V2 router
let router = chain.v2_router_address()?;
}
if availability.limit_order {
// Limit orders are supported
let lo_router = chain.lo_router_address()?;
}
V3 router features:
- Deployed at the same address on all supported chains (CREATE2)
- Enhanced gas efficiency
- Improved MEV protection
Chain Support Detection
Check if a chain is supported before attempting operations:
use odos_sdk::OdosChain;
use alloy_chains::NamedChain;
let chain = NamedChain::Mainnet;
// Check general Odos support
if chain.supports_odos() {
println!("Odos is available on this chain");
}
// Check specific router availability
let availability = chain.router_availability()?;
println!("V2: {}, V3: {}, Limit Orders: {}",
availability.v2,
availability.v3,
availability.limit_order
);
Examples
See EXAMPLES.md for comprehensive examples including:
- Multi-token swaps
- Error recovery and retry strategies
- Integration with wallets (ethers-rs, foundry)
- Gas estimation and optimization
- Testing with mocks
- Cross-chain workflows
- Production deployment patterns
Quick example - handling errors gracefully:
use odos_sdk::prelude::*;
async fn robust_swap(
client: &OdosClient,
from: Address,
to: Address,
amount: U256,
) -> Result<AssemblyResponse, OdosError> {
let result = client.swap()
.chain(Chain::ethereum())
.from_token(from, amount)
.to_token(to)
.slippage(Slippage::percent(0.5)?)
.signer(my_address)
.build_transaction()
.await;
match result {
Ok(tx) => Ok(tx),
Err(e) if e.is_no_viable_path() => {
eprintln!("No routing path available - try increasing slippage or different tokens");
Err(e)
}
Err(e) if e.is_timeout() => {
eprintln!("Request timed out - network or service issue");
Err(e)
}
Err(e) if e.is_rate_limit() => {
eprintln!("Rate limited - implement backoff strategy");
Err(e)
}
Err(e) => {
eprintln!("Unexpected error: {}", e);
if let Some(trace_id) = e.trace_id() {
eprintln!("Trace ID for support: {}", trace_id);
}
Err(e)
}
}
}
Contributing
We welcome contributions! Please see CONTRIBUTING.md for:
- Development setup (Rust 1.90+)
- Code standards and formatting
- Testing requirements
- PR process
- Release workflow
Quick development commands:
# Build
cargo build
# Run tests
cargo test
# Lint (CI enforces zero warnings)
cargo clippy --all-targets --all-features -- -D warnings
# Format
cargo fmt
# Security audit
cargo audit
Support
- Issues: GitHub Issues
- Odos Documentation: docs.odos.xyz
- Odos Discord: discord.gg/odos
Security
Security is a top priority. Please report vulnerabilities to security@semiotic.ai. See SECURITY.md for:
- Vulnerability reporting process
- API key security best practices
- Input validation guidelines
- Production deployment checklist
License
Licensed under the Apache License, Version 2.0. See LICENSE for details.
Acknowledgments
Built with the excellent Alloy ecosystem for Ethereum interactions.
Ready to integrate? Start with GETTING_STARTED.md or dive into EXAMPLES.md for practical patterns.
Dependencies
~45–64MB
~1M SLoC