Skip to main content

odos_sdk/
api.rs

1// SPDX-FileCopyrightText: 2025 Semiotic AI, Inc.
2//
3// SPDX-License-Identifier: Apache-2.0
4
5use std::fmt::Display;
6
7use alloy_primitives::{Address, U256};
8use bon::Builder;
9use serde::{Deserialize, Serialize};
10use url::Url;
11
12use crate::{error_code::TraceId, OdosError, Result};
13
14#[cfg(feature = "v2")]
15use {
16    crate::OdosRouterV2::{inputTokenInfo, outputTokenInfo, swapTokenInfo},
17    crate::OdosV2Router::{swapCall, OdosV2RouterCalls},
18    alloy_primitives::Bytes,
19    tracing::debug,
20};
21
22#[cfg(feature = "v3")]
23use {
24    crate::IOdosRouterV3::swapTokenInfo as v3SwapTokenInfo, crate::OdosV3Router::OdosV3RouterCalls,
25};
26
27/// API host tier for the Odos API
28///
29/// Odos provides two API host tiers:
30/// - **Public**: Standard API available to all users at <https://api.odos.xyz>
31/// - **Enterprise**: Premium API with enhanced features at <https://enterprise-api.odos.xyz>
32///
33/// Use in combination with [`ApiVersion`] via the [`Endpoint`] type for complete
34/// endpoint configuration.
35///
36/// # Examples
37///
38/// ```rust
39/// use odos_sdk::{ApiHost, ApiVersion, Endpoint};
40///
41/// // Use directly with Endpoint
42/// let endpoint = Endpoint::new(ApiHost::Public, ApiVersion::V2);
43///
44/// // Or use convenience methods
45/// let endpoint = Endpoint::public_v2();
46/// ```
47#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
48#[serde(rename_all = "lowercase")]
49pub enum ApiHost {
50    /// Public API endpoint <https://docs.odos.xyz/build/api-docs>
51    ///
52    /// Standard API available to all users. Suitable for most use cases.
53    Public,
54    /// Enterprise API endpoint <https://docs.odos.xyz/build/enterprise-api>
55    ///
56    /// Premium API with enhanced features, higher rate limits, and dedicated support.
57    /// Requires an API key obtained through the Odos Enterprise program.
58    Enterprise,
59}
60
61impl ApiHost {
62    /// Get the base URL for the API host
63    ///
64    /// Returns the root URL for the selected host tier without any path segments.
65    ///
66    /// # Examples
67    ///
68    /// ```rust
69    /// use odos_sdk::ApiHost;
70    ///
71    /// let public = ApiHost::Public;
72    /// assert_eq!(public.base_url().as_str(), "https://api.odos.xyz/");
73    ///
74    /// let enterprise = ApiHost::Enterprise;
75    /// assert_eq!(enterprise.base_url().as_str(), "https://enterprise-api.odos.xyz/");
76    /// ```
77    pub fn base_url(&self) -> Url {
78        match self {
79            ApiHost::Public => Url::parse("https://api.odos.xyz/").unwrap(),
80            ApiHost::Enterprise => Url::parse("https://enterprise-api.odos.xyz/").unwrap(),
81        }
82    }
83}
84
85/// Version of the Odos API
86///
87/// Odos provides multiple API versions with different features and response formats:
88/// - **V2**: Stable production version with comprehensive swap routing
89/// - **V3**: Latest version with enhanced features and optimizations
90///
91/// Use in combination with [`ApiHost`] via the [`Endpoint`] type for complete
92/// endpoint configuration.
93///
94/// # Examples
95///
96/// ```rust
97/// use odos_sdk::{ApiHost, ApiVersion, Endpoint};
98///
99/// // Recommended: Use V2 for production
100/// let endpoint = Endpoint::new(ApiHost::Public, ApiVersion::V2);
101///
102/// // Or use V3 for latest features
103/// let endpoint = Endpoint::new(ApiHost::Public, ApiVersion::V3);
104/// ```
105#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
106#[serde(rename_all = "lowercase")]
107pub enum ApiVersion {
108    /// API version 2 - Stable production version
109    ///
110    /// Recommended for most production use cases. Provides comprehensive
111    /// swap routing with extensive DEX coverage.
112    V2,
113    /// API version 3 - Latest version with enhanced features
114    ///
115    /// Includes optimizations and new features. Check the Odos documentation
116    /// for specific enhancements over V2.
117    V3,
118}
119
120impl ApiVersion {
121    /// Get the path segment for this version
122    ///
123    /// Returns the path component to use in API URLs (e.g., "v2", "v3").
124    fn path(&self) -> &'static str {
125        match self {
126            ApiVersion::V2 => "v2",
127            ApiVersion::V3 => "v3",
128        }
129    }
130}
131
132/// Complete API endpoint configuration combining host tier and API version
133///
134/// The `Endpoint` type provides an ergonomic way to configure both the API host
135/// tier (Public/Enterprise) and version (V2/V3) together.
136///
137/// # Examples
138///
139/// ## Using convenience constructors (recommended)
140///
141/// ```rust
142/// use odos_sdk::{ClientConfig, Endpoint};
143///
144/// // Public API V2 (default, recommended for production)
145/// let config = ClientConfig {
146///     endpoint: Endpoint::public_v2(),
147///     ..Default::default()
148/// };
149///
150/// // Enterprise API V3 (latest features)
151/// let config = ClientConfig {
152///     endpoint: Endpoint::enterprise_v3(),
153///     ..Default::default()
154/// };
155/// ```
156///
157/// ## Using explicit construction
158///
159/// ```rust
160/// use odos_sdk::{Endpoint, ApiHost, ApiVersion};
161///
162/// let endpoint = Endpoint::new(ApiHost::Enterprise, ApiVersion::V2);
163/// assert_eq!(endpoint.quote_url().as_str(), "https://enterprise-api.odos.xyz/sor/quote/v2");
164/// ```
165///
166/// ## Migration from old API
167///
168/// ```rust
169/// use odos_sdk::{ClientConfig, Endpoint};
170///
171/// // Old way (still works but deprecated)
172/// // let config = ClientConfig {
173/// //     endpoint: EndpointBase::Public,
174/// //     endpoint_version: EndpointVersion::V2,
175/// //     ..Default::default()
176/// // };
177///
178/// // New way
179/// let config = ClientConfig {
180///     endpoint: Endpoint::public_v2(),
181///     ..Default::default()
182/// };
183/// ```
184#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Deserialize, Serialize)]
185pub struct Endpoint {
186    host: ApiHost,
187    version: ApiVersion,
188}
189
190impl Endpoint {
191    /// Create a new endpoint with specific host and version
192    ///
193    /// # Examples
194    ///
195    /// ```rust
196    /// use odos_sdk::{Endpoint, ApiHost, ApiVersion};
197    ///
198    /// let endpoint = Endpoint::new(ApiHost::Public, ApiVersion::V2);
199    /// ```
200    pub const fn new(host: ApiHost, version: ApiVersion) -> Self {
201        Self { host, version }
202    }
203
204    /// Public API V2 endpoint (default, recommended for production)
205    ///
206    /// This is the recommended configuration for most production use cases.
207    ///
208    /// # Examples
209    ///
210    /// ```rust
211    /// use odos_sdk::Endpoint;
212    ///
213    /// let endpoint = Endpoint::public_v2();
214    /// assert_eq!(endpoint.quote_url().as_str(), "https://api.odos.xyz/sor/quote/v2");
215    /// ```
216    pub const fn public_v2() -> Self {
217        Self::new(ApiHost::Public, ApiVersion::V2)
218    }
219
220    /// Public API V3 endpoint
221    ///
222    /// Use for latest features and optimizations on the public API.
223    ///
224    /// # Examples
225    ///
226    /// ```rust
227    /// use odos_sdk::Endpoint;
228    ///
229    /// let endpoint = Endpoint::public_v3();
230    /// assert_eq!(endpoint.quote_url().as_str(), "https://api.odos.xyz/sor/quote/v3");
231    /// ```
232    pub const fn public_v3() -> Self {
233        Self::new(ApiHost::Public, ApiVersion::V3)
234    }
235
236    /// Enterprise API V2 endpoint
237    ///
238    /// Use for enterprise tier with V2 API. Requires an API key.
239    ///
240    /// # Examples
241    ///
242    /// ```rust
243    /// use odos_sdk::Endpoint;
244    ///
245    /// let endpoint = Endpoint::enterprise_v2();
246    /// assert_eq!(endpoint.quote_url().as_str(), "https://enterprise-api.odos.xyz/sor/quote/v2");
247    /// ```
248    pub const fn enterprise_v2() -> Self {
249        Self::new(ApiHost::Enterprise, ApiVersion::V2)
250    }
251
252    /// Enterprise API V3 endpoint
253    ///
254    /// Use for enterprise tier with latest V3 features. Requires an API key.
255    ///
256    /// # Examples
257    ///
258    /// ```rust
259    /// use odos_sdk::Endpoint;
260    ///
261    /// let endpoint = Endpoint::enterprise_v3();
262    /// assert_eq!(endpoint.quote_url().as_str(), "https://enterprise-api.odos.xyz/sor/quote/v3");
263    /// ```
264    pub const fn enterprise_v3() -> Self {
265        Self::new(ApiHost::Enterprise, ApiVersion::V3)
266    }
267
268    /// Get the quote URL for this endpoint
269    ///
270    /// Constructs the full URL for the quote endpoint by combining the base URL
271    /// with the appropriate version path.
272    ///
273    /// # Examples
274    ///
275    /// ```rust
276    /// use odos_sdk::Endpoint;
277    ///
278    /// let endpoint = Endpoint::public_v2();
279    /// assert_eq!(endpoint.quote_url().as_str(), "https://api.odos.xyz/sor/quote/v2");
280    ///
281    /// let endpoint = Endpoint::enterprise_v3();
282    /// assert_eq!(endpoint.quote_url().as_str(), "https://enterprise-api.odos.xyz/sor/quote/v3");
283    /// ```
284    pub fn quote_url(&self) -> Url {
285        self.host
286            .base_url()
287            .join(&format!("sor/quote/{}", self.version.path()))
288            .unwrap()
289    }
290
291    /// Get the assemble URL for this endpoint
292    ///
293    /// The assemble endpoint is version-independent and constructs transaction data
294    /// from a previously obtained quote path ID.
295    ///
296    /// # Examples
297    ///
298    /// ```rust
299    /// use odos_sdk::Endpoint;
300    ///
301    /// let endpoint = Endpoint::public_v2();
302    /// assert_eq!(endpoint.assemble_url().as_str(), "https://api.odos.xyz/sor/assemble");
303    /// ```
304    pub fn assemble_url(&self) -> Url {
305        self.host.base_url().join("sor/assemble").unwrap()
306    }
307
308    /// Get the API host tier
309    ///
310    /// # Examples
311    ///
312    /// ```rust
313    /// use odos_sdk::{Endpoint, ApiHost};
314    ///
315    /// let endpoint = Endpoint::public_v2();
316    /// assert_eq!(endpoint.host(), ApiHost::Public);
317    /// ```
318    pub const fn host(&self) -> ApiHost {
319        self.host
320    }
321
322    /// Get the API version
323    ///
324    /// # Examples
325    ///
326    /// ```rust
327    /// use odos_sdk::{Endpoint, ApiVersion};
328    ///
329    /// let endpoint = Endpoint::public_v2();
330    /// assert_eq!(endpoint.version(), ApiVersion::V2);
331    /// ```
332    pub const fn version(&self) -> ApiVersion {
333        self.version
334    }
335}
336
337impl Default for Endpoint {
338    /// Returns the default endpoint: Public API V2
339    ///
340    /// This is the recommended configuration for most production use cases.
341    fn default() -> Self {
342        Self::public_v2()
343    }
344}
345
346/// Input token for the Odos quote API
347#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
348#[serde(rename_all = "camelCase")]
349pub struct InputToken {
350    token_address: Address,
351    // Odos API error message: "Input Amount should be positive integer in string form with < 64 digits[0x6]"
352    amount: String,
353}
354
355impl InputToken {
356    pub fn new(token_address: Address, amount: U256) -> Self {
357        Self {
358            token_address,
359            amount: amount.to_string(),
360        }
361    }
362}
363
364impl From<(Address, U256)> for InputToken {
365    fn from((token_address, amount): (Address, U256)) -> Self {
366        Self::new(token_address, amount)
367    }
368}
369
370impl Display for InputToken {
371    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372        write!(
373            f,
374            "InputToken {{ token_address: {}, amount: {} }}",
375            self.token_address, self.amount
376        )
377    }
378}
379
380/// Output token for the Odos quote API
381#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize)]
382#[serde(rename_all = "camelCase")]
383pub struct OutputToken {
384    token_address: Address,
385    proportion: u32,
386}
387
388impl OutputToken {
389    pub fn new(token_address: Address, proportion: u32) -> Self {
390        Self {
391            token_address,
392            proportion,
393        }
394    }
395}
396
397impl From<(Address, u32)> for OutputToken {
398    fn from((token_address, proportion): (Address, u32)) -> Self {
399        Self::new(token_address, proportion)
400    }
401}
402
403impl Display for OutputToken {
404    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405        write!(
406            f,
407            "OutputToken {{ token_address: {}, proportion: {} }}",
408            self.token_address, self.proportion
409        )
410    }
411}
412
413/// Request to the Odos quote API: <https://docs.odos.xyz/build/api-docs>
414///
415/// # Using Type-Safe Newtypes
416///
417/// You can use the type-safe [`Slippage`](crate::Slippage), [`Chain`](crate::Chain),
418/// and [`ReferralCode`](crate::ReferralCode) types with their conversion methods:
419///
420/// ```rust
421/// use odos_sdk::{QuoteRequest, Slippage, Chain, ReferralCode};
422/// use alloy_primitives::Address;
423///
424/// let request = QuoteRequest::builder()
425///     .chain_id(Chain::ethereum().id())
426///     .slippage_limit_percent(Slippage::percent(0.5).unwrap().as_percent())
427///     .referral_code(ReferralCode::NONE.code())
428///     // ... other fields
429///     # .input_tokens(vec![])
430///     # .output_tokens(vec![])
431///     # .user_addr(Address::ZERO)
432///     # .compact(false)
433///     # .simple(false)
434///     # .disable_rfqs(false)
435///     .build();
436/// ```
437#[derive(Builder, Clone, Debug, Default, PartialEq, PartialOrd, Deserialize, Serialize)]
438#[serde(rename_all = "camelCase")]
439pub struct QuoteRequest {
440    chain_id: u64,
441    input_tokens: Vec<InputToken>,
442    output_tokens: Vec<OutputToken>,
443    slippage_limit_percent: f64,
444    user_addr: Address,
445    compact: bool,
446    simple: bool,
447    referral_code: u32,
448    disable_rfqs: bool,
449    #[builder(default)]
450    source_blacklist: Vec<String>,
451}
452
453/// Single quote response from the Odos quote API: <https://docs.odos.xyz/build/api-docs>
454#[derive(Clone, Debug, PartialEq, PartialOrd, Deserialize, Serialize)]
455#[serde(rename_all = "camelCase")]
456pub struct SingleQuoteResponse {
457    block_number: u64,
458    data_gas_estimate: u64,
459    gas_estimate: f64,
460    gas_estimate_value: f64,
461    gwei_per_gas: f64,
462    in_amounts: Vec<String>,
463    in_tokens: Vec<Address>,
464    in_values: Vec<f64>,
465    net_out_value: f64,
466    out_amounts: Vec<String>,
467    out_tokens: Vec<Address>,
468    out_values: Vec<f64>,
469    /// Partner fee percentage. Defaults to 0.0 if not present (V3 API compatibility).
470    #[serde(default)]
471    partner_fee_percent: f64,
472    path_id: String,
473    path_viz: Option<String>,
474    percent_diff: f64,
475    price_impact: f64,
476}
477
478impl SingleQuoteResponse {
479    /// Get the first input amount of the quote.
480    pub fn in_amount(&self) -> Option<&String> {
481        self.in_amounts.first()
482    }
483
484    /// Get the data gas estimate of the quote
485    pub fn data_gas_estimate(&self) -> u64 {
486        self.data_gas_estimate
487    }
488
489    /// Get the block number of the quote
490    pub fn get_block_number(&self) -> u64 {
491        self.block_number
492    }
493
494    /// Get the gas estimate of the quote
495    pub fn gas_estimate(&self) -> f64 {
496        self.gas_estimate
497    }
498
499    /// Get the estimated gas cost value of the quote.
500    pub fn gas_estimate_value(&self) -> f64 {
501        self.gas_estimate_value
502    }
503
504    /// Get the gas price used by the quote in gwei.
505    pub fn gwei_per_gas(&self) -> f64 {
506        self.gwei_per_gas
507    }
508
509    /// Get the in amounts of the quote
510    pub fn in_amounts_iter(&self) -> impl Iterator<Item = &String> {
511        self.in_amounts.iter()
512    }
513
514    /// Get the in amount of the quote
515    pub fn in_amount_u256(&self) -> Result<U256> {
516        let amount_str = self
517            .in_amounts_iter()
518            .next()
519            .ok_or_else(|| OdosError::missing_data("Missing input amount"))?;
520        let amount: u128 = amount_str
521            .parse()
522            .map_err(|_| OdosError::invalid_input("Invalid input amount format"))?;
523        Ok(U256::from(amount))
524    }
525
526    /// Get the out amount of the quote
527    pub fn out_amount(&self) -> Option<&String> {
528        self.out_amounts.first()
529    }
530
531    /// Get the out amounts of the quote
532    pub fn out_amounts_iter(&self) -> impl Iterator<Item = &String> {
533        self.out_amounts.iter()
534    }
535
536    /// Get the in tokens of the quote
537    pub fn in_tokens_iter(&self) -> impl Iterator<Item = &Address> {
538        self.in_tokens.iter()
539    }
540
541    /// Get the in token of the quote
542    pub fn first_in_token(&self) -> Option<&Address> {
543        self.in_tokens.first()
544    }
545
546    pub fn out_tokens_iter(&self) -> impl Iterator<Item = &Address> {
547        self.out_tokens.iter()
548    }
549
550    /// Get the out token of the quote
551    pub fn first_out_token(&self) -> Option<&Address> {
552        self.out_tokens.first()
553    }
554
555    /// Get the out values of the quote
556    pub fn out_values_iter(&self) -> impl Iterator<Item = &f64> {
557        self.out_values.iter()
558    }
559
560    /// Get the path id of the quote
561    pub fn path_id(&self) -> &str {
562        &self.path_id
563    }
564
565    /// Get the path id as a vector of bytes
566    pub fn path_definition_as_vec_u8(&self) -> Vec<u8> {
567        self.path_id().as_bytes().to_vec()
568    }
569
570    /// Get the swap input token and amount
571    pub fn swap_input_token_and_amount(&self) -> Result<(Address, U256)> {
572        let input_token = *self
573            .in_tokens_iter()
574            .next()
575            .ok_or_else(|| OdosError::missing_data("Missing input token"))?;
576        let input_amount_in_u256 = self.in_amount_u256()?;
577
578        Ok((input_token, input_amount_in_u256))
579    }
580
581    /// Get the price impact of the quote
582    pub fn price_impact(&self) -> f64 {
583        self.price_impact
584    }
585
586    /// Get the net output value of the quote.
587    pub fn net_out_value(&self) -> f64 {
588        self.net_out_value
589    }
590
591    /// Get the partner fee percent applied to the quote.
592    pub fn partner_fee_percent(&self) -> f64 {
593        self.partner_fee_percent
594    }
595}
596
597/// Error response from the Odos API
598///
599/// When the Odos API returns an error, it includes:
600/// - `detail`: Human-readable error message
601/// - `traceId`: UUID for tracking the error in Odos logs; may be `null` for some
602///   error codes (notably [`AlgoInternal`](crate::error_code::OdosErrorCode::AlgoInternal))
603///   or omitted entirely
604/// - `errorCode`: Numeric error code indicating the specific error type
605///
606/// Example error response:
607/// ```json
608/// {
609///   "detail": "Error getting quote, please try again",
610///   "traceId": "10becdc8-a021-4491-8201-a17b657204e0",
611///   "errorCode": 2999
612/// }
613/// ```
614#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
615#[serde(rename_all = "camelCase")]
616pub struct OdosApiErrorResponse {
617    /// Human-readable error message
618    pub detail: String,
619    /// Trace ID for debugging (UUID); `None` when the API returns `"traceId": null`
620    /// or omits the field.
621    #[serde(default)]
622    pub trace_id: Option<TraceId>,
623    /// Numeric error code
624    pub error_code: u16,
625}
626
627/// Swap inputs for the Odos assemble API
628///
629/// Available only when the `v2` feature is enabled.
630#[cfg(feature = "v2")]
631#[derive(Clone, Debug)]
632pub struct SwapInputs {
633    executor: Address,
634    path_definition: Bytes,
635    input_token_info: inputTokenInfo,
636    output_token_info: outputTokenInfo,
637    value_out_min: U256,
638}
639
640#[cfg(feature = "v2")]
641impl TryFrom<OdosV2RouterCalls> for SwapInputs {
642    type Error = OdosError;
643
644    fn try_from(swap: OdosV2RouterCalls) -> std::result::Result<Self, Self::Error> {
645        match swap {
646            OdosV2RouterCalls::swap(call) => {
647                debug!(
648                    swap_type = "V2Router",
649                    input.token = %call.tokenInfo.inputToken,
650                    input.amount_wei = %call.tokenInfo.inputAmount,
651                    output.token = %call.tokenInfo.outputToken,
652                    output.min_wei = %call.tokenInfo.outputMin,
653                    executor = %call.executor,
654                    "Extracting swap inputs from V2 router call"
655                );
656
657                let swapCall {
658                    executor,
659                    pathDefinition,
660                    referralCode,
661                    tokenInfo,
662                } = call;
663
664                let _referral_code = referralCode;
665
666                let swapTokenInfo {
667                    inputToken,
668                    inputAmount,
669                    inputReceiver,
670                    outputMin,
671                    outputQuote,
672                    outputReceiver,
673                    outputToken,
674                } = tokenInfo;
675
676                let _output_quote = outputQuote;
677
678                Ok(Self {
679                    executor,
680                    path_definition: pathDefinition,
681                    input_token_info: inputTokenInfo {
682                        tokenAddress: inputToken,
683                        amountIn: inputAmount,
684                        receiver: inputReceiver,
685                    },
686                    output_token_info: outputTokenInfo {
687                        tokenAddress: outputToken,
688                        relativeValue: U256::from(1),
689                        receiver: outputReceiver,
690                    },
691                    value_out_min: outputMin,
692                })
693            }
694            _ => Err(OdosError::invalid_input("Unexpected OdosV2RouterCalls")),
695        }
696    }
697}
698
699#[cfg(feature = "v3")]
700impl TryFrom<OdosV3RouterCalls> for SwapInputs {
701    type Error = OdosError;
702
703    fn try_from(swap: OdosV3RouterCalls) -> std::result::Result<Self, Self::Error> {
704        match swap {
705            OdosV3RouterCalls::swap(call) => {
706                debug!(
707                    swap_type = "V3Router",
708                    input.token = %call.tokenInfo.inputToken,
709                    input.amount_wei = %call.tokenInfo.inputAmount,
710                    output.token = %call.tokenInfo.outputToken,
711                    output.min_wei = %call.tokenInfo.outputMin,
712                    executor = %call.executor,
713                    "Extracting swap inputs from V3 router call"
714                );
715
716                let v3SwapTokenInfo {
717                    inputToken,
718                    inputAmount,
719                    inputReceiver,
720                    outputMin,
721                    outputQuote,
722                    outputReceiver,
723                    outputToken,
724                } = call.tokenInfo;
725
726                let _output_quote = outputQuote;
727                let _referral_info = call.referralInfo;
728
729                Ok(Self {
730                    executor: call.executor,
731                    path_definition: call.pathDefinition,
732                    input_token_info: inputTokenInfo {
733                        tokenAddress: inputToken,
734                        amountIn: inputAmount,
735                        receiver: inputReceiver,
736                    },
737                    output_token_info: outputTokenInfo {
738                        tokenAddress: outputToken,
739                        relativeValue: U256::from(1),
740                        receiver: outputReceiver,
741                    },
742                    value_out_min: outputMin,
743                })
744            }
745            _ => Err(OdosError::invalid_input("Unexpected OdosV3RouterCalls")),
746        }
747    }
748}
749
750#[cfg(feature = "v2")]
751impl SwapInputs {
752    /// Get the executor of the swap
753    pub fn executor(&self) -> Address {
754        self.executor
755    }
756
757    /// Get the path definition of the swap
758    pub fn path_definition(&self) -> &Bytes {
759        &self.path_definition
760    }
761
762    /// Get the token address of the swap
763    pub fn token_address(&self) -> Address {
764        self.input_token_info.tokenAddress
765    }
766
767    /// Get the amount in of the swap
768    pub fn amount_in(&self) -> U256 {
769        self.input_token_info.amountIn
770    }
771
772    /// Get the receiver of the swap
773    pub fn receiver(&self) -> Address {
774        self.input_token_info.receiver
775    }
776
777    /// Get the relative value of the swap
778    pub fn relative_value(&self) -> U256 {
779        self.output_token_info.relativeValue
780    }
781
782    /// Get the output token address of the swap
783    pub fn output_token_address(&self) -> Address {
784        self.output_token_info.tokenAddress
785    }
786
787    /// Get the value out min of the swap
788    pub fn value_out_min(&self) -> U256 {
789        self.value_out_min
790    }
791}
792
793#[cfg(test)]
794mod tests {
795    use super::*;
796
797    #[test]
798    fn test_api_host_base_url() {
799        assert_eq!(ApiHost::Public.base_url().as_str(), "https://api.odos.xyz/");
800        assert_eq!(
801            ApiHost::Enterprise.base_url().as_str(),
802            "https://enterprise-api.odos.xyz/"
803        );
804    }
805
806    #[test]
807    fn test_api_version_path() {
808        assert_eq!(ApiVersion::V2.path(), "v2");
809        assert_eq!(ApiVersion::V3.path(), "v3");
810    }
811
812    #[test]
813    fn test_endpoint_constructors() {
814        let endpoint = Endpoint::public_v2();
815        assert_eq!(endpoint.host(), ApiHost::Public);
816        assert_eq!(endpoint.version(), ApiVersion::V2);
817
818        let endpoint = Endpoint::public_v3();
819        assert_eq!(endpoint.host(), ApiHost::Public);
820        assert_eq!(endpoint.version(), ApiVersion::V3);
821
822        let endpoint = Endpoint::enterprise_v2();
823        assert_eq!(endpoint.host(), ApiHost::Enterprise);
824        assert_eq!(endpoint.version(), ApiVersion::V2);
825
826        let endpoint = Endpoint::enterprise_v3();
827        assert_eq!(endpoint.host(), ApiHost::Enterprise);
828        assert_eq!(endpoint.version(), ApiVersion::V3);
829
830        let endpoint = Endpoint::new(ApiHost::Public, ApiVersion::V2);
831        assert_eq!(endpoint.host(), ApiHost::Public);
832        assert_eq!(endpoint.version(), ApiVersion::V2);
833    }
834
835    #[test]
836    fn test_endpoint_quote_urls() {
837        assert_eq!(
838            Endpoint::public_v2().quote_url().as_str(),
839            "https://api.odos.xyz/sor/quote/v2"
840        );
841        assert_eq!(
842            Endpoint::public_v3().quote_url().as_str(),
843            "https://api.odos.xyz/sor/quote/v3"
844        );
845        assert_eq!(
846            Endpoint::enterprise_v2().quote_url().as_str(),
847            "https://enterprise-api.odos.xyz/sor/quote/v2"
848        );
849        assert_eq!(
850            Endpoint::enterprise_v3().quote_url().as_str(),
851            "https://enterprise-api.odos.xyz/sor/quote/v3"
852        );
853    }
854
855    #[test]
856    fn test_endpoint_assemble_urls() {
857        assert_eq!(
858            Endpoint::public_v2().assemble_url().as_str(),
859            "https://api.odos.xyz/sor/assemble"
860        );
861        assert_eq!(
862            Endpoint::public_v3().assemble_url().as_str(),
863            "https://api.odos.xyz/sor/assemble"
864        );
865        assert_eq!(
866            Endpoint::enterprise_v2().assemble_url().as_str(),
867            "https://enterprise-api.odos.xyz/sor/assemble"
868        );
869        assert_eq!(
870            Endpoint::enterprise_v3().assemble_url().as_str(),
871            "https://enterprise-api.odos.xyz/sor/assemble"
872        );
873    }
874
875    #[test]
876    fn test_endpoint_default() {
877        let endpoint = Endpoint::default();
878        assert_eq!(endpoint.host(), ApiHost::Public);
879        assert_eq!(endpoint.version(), ApiVersion::V2);
880        assert_eq!(
881            endpoint.quote_url().as_str(),
882            "https://api.odos.xyz/sor/quote/v2"
883        );
884    }
885
886    #[test]
887    fn test_endpoint_equality() {
888        assert_eq!(
889            Endpoint::public_v2(),
890            Endpoint::new(ApiHost::Public, ApiVersion::V2)
891        );
892        assert_eq!(
893            Endpoint::enterprise_v3(),
894            Endpoint::new(ApiHost::Enterprise, ApiVersion::V3)
895        );
896        assert_ne!(Endpoint::public_v2(), Endpoint::public_v3());
897        assert_ne!(Endpoint::public_v2(), Endpoint::enterprise_v2());
898    }
899
900    #[test]
901    fn test_odos_api_error_response_accepts_null_trace_id() {
902        let body = r#"{"detail":"x","traceId":null,"errorCode":2999}"#;
903        let parsed: OdosApiErrorResponse = serde_json::from_str(body).unwrap();
904        assert_eq!(parsed.trace_id, None);
905        assert_eq!(
906            parsed.error_code,
907            crate::error_code::OdosErrorCode::AlgoInternal.code()
908        );
909    }
910
911    #[test]
912    fn test_odos_api_error_response_accepts_missing_trace_id() {
913        let body = r#"{"detail":"x","errorCode":2999}"#;
914        let parsed: OdosApiErrorResponse = serde_json::from_str(body).unwrap();
915        assert_eq!(parsed.trace_id, None);
916        assert_eq!(
917            parsed.error_code,
918            crate::error_code::OdosErrorCode::AlgoInternal.code()
919        );
920    }
921
922    #[test]
923    fn test_odos_api_error_response_accepts_present_trace_id() {
924        let body =
925            r#"{"detail":"x","traceId":"10becdc8-a021-4491-8201-a17b657204e0","errorCode":2999}"#;
926        let parsed: OdosApiErrorResponse = serde_json::from_str(body).unwrap();
927        assert!(parsed.trace_id.is_some());
928        assert_eq!(
929            parsed.error_code,
930            crate::error_code::OdosErrorCode::AlgoInternal.code()
931        );
932    }
933}