odos_sdk/
api.rs

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