swapspace_api/
lib.rs

1use chrono::{DateTime, NaiveDateTime, Utc};
2use envconfig::Envconfig;
3use regex::Regex;
4use reqwest::{
5    header::{HeaderMap, HeaderValue, InvalidHeaderName, InvalidHeaderValue},
6    Client, Method, StatusCode, Url,
7};
8use serde::{de::DeserializeOwned, Deserialize, Serialize};
9use thiserror::Error;
10
11const BASE_URL: &str = "https://api.swapspace.co/api/v2/";
12
13/// all possible errors from the API and their corresponding error messages
14#[derive(Error, Debug)]
15pub enum Error {
16    #[error(transparent)]
17    ReqwestError(#[from] reqwest::Error),
18    #[error(transparent)]
19    InvalidHeaderValue(#[from] InvalidHeaderValue),
20    #[error(transparent)]
21    InvalidHeaderName(#[from] InvalidHeaderName),
22    #[error(transparent)]
23    UrlError(#[from] url::ParseError),
24    /// 401 Access denied. Invalid api key.
25    #[error("Access denied. Invalid api key.")]
26    InvalidApiKey,
27    /// 401 Access denied. No api key provided.
28    #[error("Access denied. No api key provided.")]
29    NoApiKey,
30    /// 429 Too many requests, your key access is suspended until 2023-01-01 00:00 UTC.
31    #[error("Too many requests, your key access is suspended until {0}")]
32    TooManyRequests(DateTime<Utc>),
33    /// 500 Internal Server Error
34    #[error("Internal Server Error")]
35    InternalServerError,
36    /// 422 Partner not found
37    #[error("Partner not found")]
38    PartnerNotFound,
39    /// 422 Currencies not found: 123-btc
40    #[error("Currencies not found: {0}")]
41    CurrenciesNotFound(String),
42    /// 422 Invalid param "amount"
43    #[error("Invalid param {0}")]
44    InvalidParam(String),
45    /// 422 No partners found using filter: 123
46    #[error("No partners found using filter: #{0}")]
47    NoPartnersFoundUsingFilter(String),
48    /// 400 Missing required param "fromCurrency"
49    #[error("Missing required param {0}")]
50    MissingParam(String),
51    /// 400 The following required params are missing: partner
52    #[error("The following required params are missing: {0}")]
53    MissingRequiredParams(String),
54    /// 422 Currency not found: btc123-btc
55    #[error("Currency not found: {0}")]
56    CurrencyNotFound(String),
57    /// 400 Partner FixedFloat does not support fixed type of exchanges
58    #[error("Partner {0} does not support fixed type of exchanges")]
59    PartnerDoesNotSupportFixed(String),
60    /// 422 Pair cannot be processed by fixedfloat
61    #[error("Pair cannot be processed by {0}")]
62    PartnerCannotProcessPair(String),
63    /// 422 Validation of address failed
64    #[error("Validation of address failed")]
65    ValidationOfAddressFailed,
66    /// 422 The following refund address invalid
67    #[error("The following refund address invalid")]
68    RefundAddressInvalid,
69    /// 422 userIp is incorrect
70    #[error("userIp is incorrect")]
71    UserIpIsIncorrect,
72    /// 422 Amount request failed
73    #[error("Amount request failed")]
74    AmountRequestFailed,
75    /// 403 IP address is forbidden
76    #[error("IP address is forbidden")]
77    IpAddressIsForbidden,
78    /// 422 Amount minimum is 0.00019451
79    #[error("Amount minimum is {0}")]
80    AmountMinimum(f64),
81    /// 422 Amount maximum is 1.8146447
82    #[error("Amount maximum is {0}")]
83    AmountMaximum(f64),
84    /// 404 Exchange not found
85    #[error("Exchange not found")]
86    ExchangeNotFound,
87    #[error("Unmatched error: {0}")]
88    UnmatchedError(String),
89}
90
91impl From<(StatusCode, String)> for Error {
92    fn from(val: (StatusCode, String)) -> Self {
93        match val {
94            (StatusCode::UNAUTHORIZED, text) => match text.as_str() {
95                "Access denied. Invalid api key." => Error::InvalidApiKey,
96                "Access denied. No api key provided." => Error::NoApiKey,
97                _ => Error::UnmatchedError(text),
98            },
99            (StatusCode::TOO_MANY_REQUESTS, text) => text
100                .strip_prefix("Too many requests, your key access is suspended until ")
101                .map_or_else(
102                    || Error::UnmatchedError(text.clone()),
103                    |date| {
104                        NaiveDateTime::parse_from_str(date, "%Y-%m-%d %H:%M UTC.")
105                            .as_ref()
106                            .map(NaiveDateTime::and_utc)
107                            .map_or_else(
108                                |_| Error::UnmatchedError(text.clone()),
109                                Error::TooManyRequests,
110                            )
111                    },
112                ),
113            (StatusCode::BAD_REQUEST, text) => match text.as_str() {
114                t if t.starts_with("Missing required param ") => t
115                    .strip_prefix("Missing required param ")
116                    .map(|param| Error::MissingParam(param.to_string()))
117                    .unwrap(),
118                t if t.starts_with("The following required params are missing: ") => t
119                    .strip_prefix("The following required params are missing: ")
120                    .map(|params| Error::MissingRequiredParams(params.to_string()))
121                    .unwrap(),
122                t if t.starts_with("Partner ")
123                    && t.ends_with(" does not support fixed type of exchanges") =>
124                {
125                    t.strip_prefix("Partner ")
126                        .unwrap()
127                        .strip_suffix(" does not support fixed type of exchanges")
128                        .map(|partner| Error::PartnerDoesNotSupportFixed(partner.to_string()))
129                        .unwrap()
130                }
131                _ => Error::UnmatchedError(text),
132            },
133            (StatusCode::INTERNAL_SERVER_ERROR, _) => Error::InternalServerError,
134            (StatusCode::UNPROCESSABLE_ENTITY, text) => match text.as_str() {
135                "Partner not found" => Error::PartnerNotFound,
136                "Validation of address failed" => Error::ValidationOfAddressFailed,
137                "The following refund address invalid" => Error::RefundAddressInvalid,
138                "userIp is incorrect" => Error::UserIpIsIncorrect,
139                "Amount request failed" => Error::AmountRequestFailed,
140                t if t.starts_with("No partners found using filter: ") => t
141                    .strip_prefix("No partners found using filter: ")
142                    .map(|filter| Error::NoPartnersFoundUsingFilter(filter.to_string()))
143                    .unwrap(),
144                t if t.starts_with("Currencies not found: ") => t
145                    .strip_prefix("Currencies not found: ")
146                    .map(|currency| Error::CurrenciesNotFound(currency.to_string()))
147                    .unwrap(),
148                t if t.starts_with("Invalid param ") => t
149                    .strip_prefix("Invalid param ")
150                    .map(|param| Error::InvalidParam(param.to_string()))
151                    .unwrap(),
152                t if t.starts_with("Missing required param ") => t
153                    .strip_prefix("Missing required param ")
154                    .map(|param| Error::MissingParam(param.to_string()))
155                    .unwrap(),
156                t if t.starts_with("Currency not found: ") => t
157                    .strip_prefix("Currency not found: ")
158                    .map(|currency| Error::CurrencyNotFound(currency.to_string()))
159                    .unwrap(),
160                t if t.starts_with("Pair cannot be processed by ") => t
161                    .strip_prefix("Pair cannot be processed by ")
162                    .map(|partner| Error::PartnerCannotProcessPair(partner.to_string()))
163                    .unwrap(),
164                t if t.starts_with("Amount minimum is ") => t
165                    .strip_prefix("Amount minimum is ")
166                    .map(|amount| {
167                        amount.parse().map_or_else(
168                            |_| Error::UnmatchedError(text.clone()),
169                            Error::AmountMinimum,
170                        )
171                    })
172                    .unwrap(),
173                t if t.starts_with("Amount maximum is ") => t
174                    .strip_prefix("Amount maximum is ")
175                    .map(|amount| {
176                        amount.parse().map_or_else(
177                            |_| Error::UnmatchedError(text.clone()),
178                            Error::AmountMaximum,
179                        )
180                    })
181                    .unwrap(),
182                _ => Error::UnmatchedError(text),
183            },
184            (StatusCode::FORBIDDEN, _) => Error::IpAddressIsForbidden,
185            (StatusCode::NOT_FOUND, _) => Error::ExchangeNotFound,
186            (_, text) => Error::UnmatchedError(text),
187        }
188    }
189}
190
191#[derive(Envconfig)]
192pub struct Config {
193    #[envconfig(from = "SWAPSPACE_API_KEY")]
194    pub swapspace_api_key: String,
195}
196
197impl Default for SwapSpaceApi {
198    fn default() -> Self {
199        let config = Config::init_from_env().expect("Failed to read environment variables");
200        Self::new(config.swapspace_api_key.clone()).expect("Failed to create reqwest client")
201    }
202}
203
204pub struct SwapSpaceApi {
205    pub client: Client,
206    pub base_url: Url,
207}
208
209#[derive(Debug, Clone)]
210pub struct GetAmounts {
211    pub from_currency: String,
212    pub from_network: String,
213    pub to_currency: String,
214    pub to_network: String,
215    pub amount: f64,
216    pub partner: Option<Vec<String>>,
217    pub fixed: bool,
218    pub float: bool,
219}
220
221#[derive(Debug, Clone)]
222pub struct ValidationRegexp(pub Regex);
223
224impl ValidationRegexp {
225    pub fn new(validation_regexp: &str) -> Result<Self, regex::Error> {
226        let regexp = validation_regexp
227            .strip_prefix('/')
228            .unwrap_or(validation_regexp)
229            .strip_suffix('/')
230            .unwrap_or(validation_regexp);
231        Regex::new(regexp).map(Self)
232    }
233}
234
235fn deserialize_validation_regexp<'de, D>(deserializer: D) -> Result<ValidationRegexp, D::Error>
236where
237    D: serde::Deserializer<'de>,
238{
239    let validation_regexp = String::deserialize(deserializer)?;
240    ValidationRegexp::new(&validation_regexp).map_err(serde::de::Error::custom)
241}
242
243#[derive(Default, Clone, Serialize, Deserialize, Debug)]
244pub struct Address(pub String);
245
246impl Address {
247    pub fn new(address: String, validation_regexp: &ValidationRegexp) -> Result<Self, Error> {
248        if !validation_regexp.0.is_match(&address) {
249            Err(Error::ValidationOfAddressFailed)
250        } else {
251            Ok(Self(address))
252        }
253    }
254}
255
256#[derive(Debug, Serialize, Clone)]
257#[serde(rename_all = "camelCase")]
258pub struct ExchangeRequest {
259    pub partner: String,
260    pub from_currency: String,
261    pub from_network: String,
262    pub to_currency: String,
263    pub to_network: String,
264    pub address: Address,
265    pub amount: f64,
266    pub fixed: bool,
267    pub extra_id: String,
268    pub rate_id: String,
269    pub user_ip: String,
270    pub refund: Address,
271}
272
273#[derive(Debug, Deserialize, Clone)]
274#[serde(rename_all = "camelCase")]
275pub struct CurrencyResponse {
276    pub name: String,
277    pub extra_id_name: String,
278    pub icon: String,
279    pub deposit: bool,
280    pub withdrawal: bool,
281    #[serde(deserialize_with = "deserialize_validation_regexp")]
282    pub validation_regexp: ValidationRegexp,
283    pub contract_address: Option<String>,
284    pub code: String,
285    pub network: String,
286    pub has_extra_id: bool,
287    pub id: String,
288    pub popular: bool,
289    pub fiat: Option<bool>,
290    pub buy: Option<bool>,
291    pub network_name: Option<String>,
292}
293pub type Currencies = Vec<CurrencyResponse>;
294
295#[derive(Debug, Deserialize, Clone)]
296#[serde(rename_all = "camelCase")]
297pub struct PartnerResponse {
298    pub fixed: bool,
299    pub float: bool,
300    pub req_fixed_refund: bool,
301    pub req_float_refund: bool,
302    pub name: String,
303    pub path: String,
304    pub fiat_provider: Option<bool>,
305    pub prohibited_countries: Option<Vec<String>>,
306    pub kyc_level: Option<String>,
307}
308pub type Partners = Vec<PartnerResponse>;
309
310#[derive(Debug, Deserialize, Clone, PartialEq)]
311#[serde(rename_all = "camelCase")]
312pub struct AmountResponse {
313    pub partner: String,
314    pub from_amount: f64,
315    pub to_amount: f64,
316    pub from_currency: String,
317    pub from_network: String,
318    pub to_currency: String,
319    pub to_network: String,
320    pub support_rate: u32,
321    pub duration: String,
322    pub fixed: bool,
323    pub min: f64,
324    pub max: f64,
325    pub exists: bool,
326    pub id: String,
327}
328pub type Amounts = Vec<AmountResponse>;
329
330#[derive(Debug, Deserialize, Clone)]
331#[serde(rename_all = "camelCase")]
332pub struct Timestamps {
333    pub created_at: String,
334    pub expires_at: String,
335}
336
337#[derive(Debug, Deserialize, Clone)]
338#[serde(rename_all = "camelCase")]
339pub struct ExchangeCurrency {
340    pub code: String,
341    pub network: String,
342    pub amount: f64,
343    pub address: String,
344    pub extra_id: String,
345    pub transaction_hash: String,
346    pub contract_address: Option<String>,
347}
348
349#[derive(Debug, Deserialize, Clone)]
350#[serde(rename_all = "camelCase")]
351pub struct BlockExplorerUrl {
352    pub from: String,
353    pub to: String,
354}
355
356#[derive(Debug, Deserialize, Clone)]
357#[serde(rename_all = "camelCase")]
358pub struct ExchangeResponse {
359    pub id: String,
360    pub partner: String,
361    pub fixed: bool,
362    pub timestamps: Timestamps,
363    pub from: ExchangeCurrency,
364    pub to: ExchangeCurrency,
365    pub rate: f64,
366    pub status: String,
367    pub confirmations: i32,
368    pub refund_extra_id: String,
369    pub block_explorer_transaction_url: BlockExplorerUrl,
370    pub block_explorer_address_url: BlockExplorerUrl,
371    pub payment_url: Option<String>,
372    pub refund_address: Option<String>,
373    pub error: Option<bool>,
374    pub token: Option<String>,
375    pub warnings: BlockExplorerUrl,
376}
377
378impl From<AmountResponse> for GetAmounts {
379    fn from(amount: AmountResponse) -> Self {
380        Self {
381            from_currency: amount.from_currency,
382            from_network: amount.from_network,
383            to_currency: amount.to_currency,
384            to_network: amount.to_network,
385            amount: amount.from_amount,
386            partner: None,
387            fixed: false,
388            float: false,
389        }
390    }
391}
392
393impl SwapSpaceApi {
394    /// create a new instance of the SwapSpaceApi client using new
395    /// or use the default method to create a new instance
396    /// it would read the api key from the environment variable SWAPSPACE_API_KEY
397    /// ```
398    /// use swapspace_api::SwapSpaceApi;
399    /// let api = SwapSpaceApi::new("api_key".to_string());
400    /// assert_eq!(api.is_ok(), true);
401    /// let api = SwapSpaceApi::default();
402    /// ```
403    pub fn new(api_key: String) -> Result<Self, Error> {
404        if api_key.is_empty() {
405            return Err(Error::NoApiKey);
406        }
407        let mut headers = HeaderMap::new();
408        headers.insert("Authorization", HeaderValue::from_str(&api_key)?);
409        headers.insert("Content-Type", HeaderValue::from_static("application/json"));
410        let client = Client::builder().default_headers(headers).build()?;
411        let base_url = BASE_URL.parse().unwrap();
412        Ok(Self { client, base_url })
413    }
414
415    fn get_full_url(&self, endpoint: &str, params: Option<&GetAmounts>) -> Result<Url, Error> {
416        let mut url = self.base_url.join(endpoint)?;
417        let mut url_builder = url.query_pairs_mut();
418        if let Some(params) = params {
419            url_builder.append_pair("fromCurrency", &params.from_currency);
420            url_builder.append_pair("fromNetwork", &params.from_network);
421            url_builder.append_pair("toCurrency", &params.to_currency);
422            url_builder.append_pair("toNetwork", &params.to_network);
423            url_builder.append_pair("amount", &params.amount.to_string());
424            if let Some(partner) = &params.partner {
425                url_builder.append_pair("partner", &partner.join(","));
426            }
427            if params.fixed {
428                url_builder.append_pair("fixed", "true");
429            }
430            if params.float {
431                url_builder.append_pair("float", "true");
432            }
433        }
434        Ok(url_builder.finish().to_owned())
435    }
436
437    async fn send_request<T: DeserializeOwned>(
438        &self,
439        url: Url,
440        method: Method,
441        json_params: Option<ExchangeRequest>,
442    ) -> Result<T, Error> {
443        #[cfg(feature = "log")]
444        log::debug!("Sending {method} request to {url}");
445        #[cfg(feature = "log")]
446        log::debug!("Params: {json_params:#?}");
447        let request = self.client.request(method, url);
448        let request = match json_params {
449            Some(json_params) => request.json(&json_params),
450            None => request,
451        };
452        let response = request.send().await?;
453        match response.error_for_status_ref() {
454            Ok(_) => Ok(response.json().await?),
455            Err(err) => {
456                let status = err.status();
457                match status {
458                    Some(status) => {
459                        let text = response.text().await?;
460                        Err((status, text).into())
461                    }
462                    None => Err(err.into()),
463                }
464            }
465        }
466    }
467
468    /// Get all available currencies
469    /// https://swapspace.co/api/v2/currencies
470    /// This API endpoint returns the list of available currencies.
471    /// ```
472    /// use swapspace_api::SwapSpaceApi;
473    /// # #[tokio::main]
474    /// # async fn main() {
475    /// let response = SwapSpaceApi::default().get_currencies().await.unwrap();
476    /// assert_eq!(response.len() > 0, true);
477    /// # }
478    /// ```
479    pub async fn get_currencies(&self) -> Result<Currencies, Error> {
480        let url = self.get_full_url("currencies", None)?;
481        self.send_request(url, Method::GET, None).await
482    }
483
484    /// Get all available amounts
485    /// https://swapspace.co/api/v2/amounts
486    /// This endpoint has five required (fromCurrency, fromNetwork, toCurrency , toNetwork and amount) and three optional parameters (partner, fixed and float).
487    /// If you create a request containing only five required parameters, it will return the list of all the available amounts from all the partners.
488    /// If you fill in the partner parameter, it will return the list of amounts available for a specific partner.
489    /// If you fill in the fixed field with a true value, the request will return a list of amounts for fixed rate exchanges.
490    /// If you fill in the float field with a true value, the request will return a list of amounts for floating rate exchanges.
491    /// If you fill in a true value both for fixed and float fields, the request will return amounts both for fixed and floating rate exchanges.
492    /// The unit for duration is the minute. The range of values for the supportRate field is from 0 to 3.
493    /// ```
494    /// use swapspace_api::{SwapSpaceApi, GetAmounts};
495    /// # #[tokio::main]
496    /// # async fn main() {
497    /// let amounts = GetAmounts {
498    ///   from_currency: "btc".to_string(),
499    ///   from_network: "btc".to_string(),
500    ///   to_currency: "eth".to_string(),
501    ///   to_network: "eth".to_string(),
502    ///   amount: 0.1,
503    ///   partner: None,
504    ///   fixed: false,
505    ///   float: false,
506    /// };
507    /// let response = SwapSpaceApi::default().get_amounts(&amounts).await.unwrap();
508    /// assert_eq!(response.len() > 0, true);
509    /// # }
510    /// ```
511    pub async fn get_amounts(&self, get_amounts: &GetAmounts) -> Result<Amounts, Error> {
512        let url = self.get_full_url("amounts", Some(get_amounts))?;
513        self.send_request(url, Method::GET, None).await
514    }
515
516    /// Get the best amount
517    /// https://swapspace.co/api/v2/amounts/best
518    /// This endpoint has five required (fromCurrency, fromNetwork, toCurrency , toNetwork and amount) and three optional parameters (partner, fixed and float).
519    /// If you create a request containing only five required parameters, it will return the best of all the available amounts from all the partners.
520    /// If you fill in the partner parameter, it will return the best amount available for a specific partner.
521    /// If you fill in the fixed field with a true value, the request will return a best amount for fixed rate exchanges.
522    /// If you fill in the float field with a true value, the request will return a best amount for floating rate exchanges.
523    /// If you fill in a true value both for fixed and float fields, the request will return the best amount both for fixed and floating rate exchanges.
524    /// The unit for duration is the minute.
525    /// The range of values for the supportRate field is from 0 to 3.
526    /// ```
527    /// use swapspace_api::{SwapSpaceApi, GetAmounts};
528    /// # #[tokio::main]
529    /// # async fn main() {
530    /// let amounts = GetAmounts {
531    ///  from_currency: "btc".to_string(),
532    ///  from_network: "btc".to_string(),
533    ///  to_currency: "eth".to_string(),
534    ///  to_network: "eth".to_string(),
535    ///  amount: 0.1,
536    ///  partner: None,
537    ///  fixed: false,
538    ///  float: false,
539    ///  };
540    ///  let response = SwapSpaceApi::default().get_amounts_best(&amounts).await.unwrap();
541    ///  assert_eq!(response.exists, true);
542    ///  # }
543    ///  ```
544    pub async fn get_amounts_best(
545        &self,
546        get_amounts: &GetAmounts,
547    ) -> Result<AmountResponse, Error> {
548        let url = self.get_full_url("amounts/best", Some(get_amounts))?;
549        self.send_request(url, Method::GET, None).await
550    }
551
552    /// Get all available partners
553    /// https://swapspace.co/api/v2/partners
554    /// This API endpoint returns the list of available partners.
555    /// ```
556    /// use swapspace_api::SwapSpaceApi;
557    /// # #[tokio::main]
558    /// # async fn main() {
559    /// let response = SwapSpaceApi::default().get_partners().await.unwrap();
560    /// assert_eq!(response.len() > 0, true);
561    /// # }
562    /// ```
563    pub async fn get_partners(&self) -> Result<Partners, Error> {
564        let url = self.get_full_url("partners", None)?;
565        self.send_request(url, Method::GET, None).await
566    }
567
568    /// Post an exchange
569    /// https://swapspace.co/api/v2/exchange
570    /// All the fields mentioned in body are required.
571    /// Field extraId is required only if the currency you want to receive has hasExtraId: true property (you get this info via the List of currencies endpoint).
572    /// If hasExtraId: false, use empty string in the extraId field.
573    /// All the fields mentioned in body are required.
574    /// Field extraId must be filled in if the value of the hasExtraId field is true in the endpoint List of currencies for this currency.
575    /// Otherwise, fill in the extraId field with an empty string.
576    /// After userIp fill in the user’s IP in IPv4 or IPv6 format.
577    /// Refund field is required if fixed: true and reqFixedRefund: true for relevant partner and float: true and reqFloatRefund: true for relevant partner (look List of partners example response).
578    /// But we strongly recommend that you specify refund when creating an exchange, even if it is not required (if a refund is not required or you cannot specify it, then it is permissible to use a refund: '').
579    /// ```
580    /// use swapspace_api::{SwapSpaceApi, ExchangeRequest, Address};
581    /// # #[tokio::main]
582    /// # async fn main() {
583    ///  let data = ExchangeRequest {
584    ///     partner: "simpleswap".to_string(),
585    ///     from_currency: "btc".to_string(),
586    ///     from_network: "btc".to_string(),
587    ///     to_currency: "eth".to_string(),
588    ///     to_network: "eth".to_string(),
589    ///     address: Address("0x32be343b94f860124dc4fee278fdcbd38c102d88".to_string()),
590    ///     amount: 2.0,
591    ///     fixed: true,
592    ///     extra_id: "".to_string(),
593    ///     rate_id: "".to_string(),
594    ///     user_ip: "8.8.8.8".to_string(),
595    ///     refund: Address("1F1tAaz5x1HUXrCNLbtMDqcw6o5GNn4xqX".to_string()),
596    ///  };
597    ///  let response = SwapSpaceApi::default().post_exchange(data).await.unwrap();
598    ///  assert_eq!(response.id.len() > 0, true);
599    ///  # }
600    pub async fn post_exchange(&self, data: ExchangeRequest) -> Result<ExchangeResponse, Error> {
601        let url = self.get_full_url("exchange", None)?;
602        self.send_request(url, Method::POST, Some(data)).await
603    }
604
605    /// Get exchange status
606    /// https://swapspace.co/api/v2/exchange/{id}
607    /// Use the Exchange status endpoint to get the current exchange status.
608    /// As a request data, use path parameter id, which is to be filled in with exchange id you get with the other data for the successful Create new exchange request.
609    /// ```
610    /// use swapspace_api::SwapSpaceApi;
611    /// # #[tokio::main]
612    /// # async fn main() {
613    /// let id = "-9mVIXNbYZcG";
614    /// let response = SwapSpaceApi::default().get_exchange_status(id).await.unwrap();
615    /// assert_eq!(response.id, id);
616    /// # }
617    /// ```
618    pub async fn get_exchange_status(&self, id: &str) -> Result<ExchangeResponse, Error> {
619        let url = self.get_full_url(&format!("exchange/{}", id), None)?;
620        self.send_request(url, Method::GET, None).await
621    }
622}