odos_sdk/
api.rs

1use std::fmt::Display;
2
3use alloy_primitives::{Address, Bytes, U256};
4use bon::Builder;
5use serde::{Deserialize, Serialize};
6use tracing::debug;
7use url::Url;
8
9use crate::{
10    error_code::TraceId,
11    OdosError,
12    OdosRouterV2::{inputTokenInfo, outputTokenInfo, swapTokenInfo},
13    OdosV2Router::{swapCall, OdosV2RouterCalls},
14    Result,
15};
16
17/// Odos API endpoints
18#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
19pub enum Endpoint {
20    /// Public API endpoint <https://docs.odos.xyz/build/api-docs>
21    Public,
22    /// Enterprise API endpoint <https://docs.odos.xyz/build/enterprise-api>
23    Enterprise,
24}
25
26impl Endpoint {
27    /// Get the base URL for the Odos API
28    pub fn base_url(&self) -> Url {
29        match self {
30            Endpoint::Public => Url::parse("https://api.odos.xyz/").unwrap(),
31            Endpoint::Enterprise => Url::parse("https://enterprise-api.odos.xyz/").unwrap(),
32        }
33    }
34
35    /// Get the quote URL for the Odos API v2
36    pub fn quote_url_v2(&self) -> Url {
37        self.base_url().join("sor/quote/v2").unwrap()
38    }
39
40    /// Get the quote URL for the Odos API v3
41    pub fn quote_url_v3(&self) -> Url {
42        self.base_url().join("sor/quote/v3").unwrap()
43    }
44
45    /// Get the assemble URL for the Odos API
46    pub fn assemble_url(&self) -> Url {
47        self.base_url().join("sor/assemble").unwrap()
48    }
49}
50
51/// Input token for the Odos quote API
52#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
53#[serde(rename_all = "camelCase")]
54pub struct InputToken {
55    // Haven't looked much into it, but there's trouble if you try to make this a `Address`
56    token_address: String,
57    // Odos API error message: "Input Amount should be positive integer in string form with < 64 digits[0x6]"
58    amount: String,
59}
60
61impl InputToken {
62    pub fn new(token_address: Address, amount: U256) -> Self {
63        Self {
64            token_address: token_address.to_string(),
65            amount: amount.to_string(),
66        }
67    }
68}
69
70impl From<(Address, U256)> for InputToken {
71    fn from((token_address, amount): (Address, U256)) -> Self {
72        Self::new(token_address, amount)
73    }
74}
75
76impl Display for InputToken {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        write!(
79            f,
80            "InputToken {{ token_address: {}, amount: {} }}",
81            self.token_address, self.amount
82        )
83    }
84}
85
86/// Output token for the Odos quote API
87#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
88#[serde(rename_all = "camelCase")]
89pub struct OutputToken {
90    // Haven't looked much into it, but there's trouble if you try to make this a `Address`
91    token_address: String,
92    proportion: u32,
93}
94
95impl OutputToken {
96    pub fn new(token_address: Address, proportion: u32) -> Self {
97        Self {
98            token_address: token_address.to_string(),
99            proportion,
100        }
101    }
102}
103
104impl From<(Address, u32)> for OutputToken {
105    fn from((token_address, proportion): (Address, u32)) -> Self {
106        Self::new(token_address, proportion)
107    }
108}
109
110impl Display for OutputToken {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        write!(
113            f,
114            "OutputToken {{ token_address: {}, proportion: {} }}",
115            self.token_address, self.proportion
116        )
117    }
118}
119
120/// Request to the Odos quote API: <https://docs.odos.xyz/build/api-docs>
121#[derive(Builder, Clone, Debug, Default, PartialEq, PartialOrd, Deserialize, Serialize)]
122#[serde(rename_all = "camelCase")]
123pub struct QuoteRequest {
124    chain_id: u64,
125    input_tokens: Vec<InputToken>,
126    output_tokens: Vec<OutputToken>,
127    slippage_limit_percent: f64,
128    // Haven't looked much into it, but there's trouble if you try to make this a `Address`
129    user_addr: String,
130    compact: bool,
131    simple: bool,
132    referral_code: u32,
133    disable_rfqs: bool,
134    #[builder(default)]
135    source_blacklist: Vec<String>,
136}
137
138/// Single quote response from the Odos quote API: <https://docs.odos.xyz/build/api-docs>
139#[derive(Clone, Debug, PartialEq, PartialOrd, Deserialize, Serialize)]
140#[serde(rename_all = "camelCase")]
141pub struct SingleQuoteResponse {
142    block_number: u64,
143    data_gas_estimate: u64,
144    gas_estimate: f64,
145    gas_estimate_value: f64,
146    gwei_per_gas: f64,
147    in_amounts: Vec<String>,
148    in_tokens: Vec<Address>,
149    in_values: Vec<f64>,
150    net_out_value: f64,
151    out_amounts: Vec<String>,
152    out_tokens: Vec<Address>,
153    out_values: Vec<f64>,
154    partner_fee_percent: f64,
155    path_id: String,
156    path_viz: Option<String>,
157    percent_diff: f64,
158    price_impact: f64,
159}
160
161impl SingleQuoteResponse {
162    /// Get the data gas estimate of the quote
163    pub fn data_gas_estimate(&self) -> u64 {
164        self.data_gas_estimate
165    }
166
167    /// Get the block number of the quote
168    pub fn get_block_number(&self) -> u64 {
169        self.block_number
170    }
171
172    /// Get the gas estimate of the quote
173    pub fn gas_estimate(&self) -> f64 {
174        self.gas_estimate
175    }
176
177    /// Get the in amounts of the quote
178    pub fn in_amounts_iter(&self) -> impl Iterator<Item = &String> {
179        self.in_amounts.iter()
180    }
181
182    /// Get the in amount of the quote
183    pub fn in_amount_u256(&self) -> Result<U256> {
184        let amount_str = self
185            .in_amounts_iter()
186            .next()
187            .ok_or_else(|| OdosError::missing_data("Missing input amount"))?;
188        let amount: u128 = amount_str
189            .parse()
190            .map_err(|_| OdosError::invalid_input("Invalid input amount format"))?;
191        Ok(U256::from(amount))
192    }
193
194    /// Get the out amount of the quote
195    pub fn out_amount(&self) -> Option<&String> {
196        self.out_amounts.first()
197    }
198
199    /// Get the out amounts of the quote
200    pub fn out_amounts_iter(&self) -> impl Iterator<Item = &String> {
201        self.out_amounts.iter()
202    }
203
204    /// Get the in tokens of the quote
205    pub fn in_tokens_iter(&self) -> impl Iterator<Item = &Address> {
206        self.in_tokens.iter()
207    }
208
209    /// Get the in token of the quote
210    pub fn first_in_token(&self) -> Option<&Address> {
211        self.in_tokens.first()
212    }
213
214    pub fn out_tokens_iter(&self) -> impl Iterator<Item = &Address> {
215        self.out_tokens.iter()
216    }
217
218    /// Get the out token of the quote
219    pub fn first_out_token(&self) -> Option<&Address> {
220        self.out_tokens.first()
221    }
222
223    /// Get the out values of the quote
224    pub fn out_values_iter(&self) -> impl Iterator<Item = &f64> {
225        self.out_values.iter()
226    }
227
228    /// Get the path id of the quote
229    pub fn path_id(&self) -> &str {
230        &self.path_id
231    }
232
233    /// Get the path id as a vector of bytes
234    pub fn path_definition_as_vec_u8(&self) -> Vec<u8> {
235        self.path_id().as_bytes().to_vec()
236    }
237
238    /// Get the swap input token and amount
239    pub fn swap_input_token_and_amount(&self) -> Result<(Address, U256)> {
240        let input_token = *self
241            .in_tokens_iter()
242            .next()
243            .ok_or_else(|| OdosError::missing_data("Missing input token"))?;
244        let input_amount_in_u256 = self.in_amount_u256()?;
245
246        Ok((input_token, input_amount_in_u256))
247    }
248
249    /// Get the price impact of the quote
250    pub fn price_impact(&self) -> f64 {
251        self.price_impact
252    }
253}
254
255/// Error response from the Odos API
256///
257/// When the Odos API returns an error, it includes:
258/// - `detail`: Human-readable error message
259/// - `traceId`: UUID for tracking the error in Odos logs
260/// - `errorCode`: Numeric error code indicating the specific error type
261///
262/// Example error response:
263/// ```json
264/// {
265///   "detail": "Error getting quote, please try again",
266///   "traceId": "10becdc8-a021-4491-8201-a17b657204e0",
267///   "errorCode": 2999
268/// }
269/// ```
270#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
271#[serde(rename_all = "camelCase")]
272pub struct OdosApiErrorResponse {
273    /// Human-readable error message
274    pub detail: String,
275    /// Trace ID for debugging (UUID)
276    pub trace_id: TraceId,
277    /// Numeric error code
278    pub error_code: u16,
279}
280
281/// Swap inputs for the Odos assemble API
282#[derive(Clone, Debug)]
283pub struct SwapInputs {
284    executor: Address,
285    path_definition: Bytes,
286    input_token_info: inputTokenInfo,
287    output_token_info: outputTokenInfo,
288    value_out_min: U256,
289}
290
291impl TryFrom<OdosV2RouterCalls> for SwapInputs {
292    type Error = OdosError;
293
294    fn try_from(swap: OdosV2RouterCalls) -> std::result::Result<Self, Self::Error> {
295        match swap {
296            OdosV2RouterCalls::swap(call) => {
297                debug!(
298                    swap_type = "V2Router",
299                    input.token = %call.tokenInfo.inputToken,
300                    input.amount_wei = %call.tokenInfo.inputAmount,
301                    output.token = %call.tokenInfo.outputToken,
302                    output.min_wei = %call.tokenInfo.outputMin,
303                    executor = %call.executor,
304                    "Extracting swap inputs from V2 router call"
305                );
306
307                let swapCall {
308                    executor,
309                    pathDefinition,
310                    referralCode,
311                    tokenInfo,
312                } = call;
313
314                let _referral_code = referralCode;
315
316                let swapTokenInfo {
317                    inputToken,
318                    inputAmount,
319                    inputReceiver,
320                    outputMin,
321                    outputQuote,
322                    outputReceiver,
323                    outputToken,
324                } = tokenInfo;
325
326                let _output_quote = outputQuote;
327
328                Ok(Self {
329                    executor,
330                    path_definition: pathDefinition,
331                    input_token_info: inputTokenInfo {
332                        tokenAddress: inputToken,
333                        amountIn: inputAmount,
334                        receiver: inputReceiver,
335                    },
336                    output_token_info: outputTokenInfo {
337                        tokenAddress: outputToken,
338                        relativeValue: U256::from(1),
339                        receiver: outputReceiver,
340                    },
341                    value_out_min: outputMin,
342                })
343            }
344            _ => Err(OdosError::invalid_input("Unexpected OdosV2RouterCalls")),
345        }
346    }
347}
348
349impl SwapInputs {
350    /// Get the executor of the swap
351    pub fn executor(&self) -> Address {
352        self.executor
353    }
354
355    /// Get the path definition of the swap
356    pub fn path_definition(&self) -> &Bytes {
357        &self.path_definition
358    }
359
360    /// Get the token address of the swap
361    pub fn token_address(&self) -> Address {
362        self.input_token_info.tokenAddress
363    }
364
365    /// Get the amount in of the swap
366    pub fn amount_in(&self) -> U256 {
367        self.input_token_info.amountIn
368    }
369
370    /// Get the receiver of the swap
371    pub fn receiver(&self) -> Address {
372        self.input_token_info.receiver
373    }
374
375    /// Get the relative value of the swap
376    pub fn relative_value(&self) -> U256 {
377        self.output_token_info.relativeValue
378    }
379
380    /// Get the output token address of the swap
381    pub fn output_token_address(&self) -> Address {
382        self.output_token_info.tokenAddress
383    }
384
385    /// Get the value out min of the swap
386    pub fn value_out_min(&self) -> U256 {
387        self.value_out_min
388    }
389}