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