1#![allow(unused_imports)]
15use async_trait::async_trait;
16use derive_builder::Builder;
17use reqwest;
18use rust_decimal::prelude::*;
19use serde::{Deserialize, Serialize};
20use serde_json::{Value, json};
21use std::collections::BTreeMap;
22
23use crate::common::{
24 config::ConfigurationRestApi,
25 models::{ParamBuildError, RestApiResponse},
26 utils::send_request,
27};
28use crate::convert::rest_api::models;
29
30const HAS_TIME_UNIT: bool = false;
31
32#[async_trait]
33pub trait TradeApi: Send + Sync {
34 async fn accept_quote(
35 &self,
36 params: AcceptQuoteParams,
37 ) -> anyhow::Result<RestApiResponse<models::AcceptQuoteResponse>>;
38 async fn cancel_limit_order(
39 &self,
40 params: CancelLimitOrderParams,
41 ) -> anyhow::Result<RestApiResponse<models::CancelLimitOrderResponse>>;
42 async fn get_convert_trade_history(
43 &self,
44 params: GetConvertTradeHistoryParams,
45 ) -> anyhow::Result<RestApiResponse<models::GetConvertTradeHistoryResponse>>;
46 async fn order_status(
47 &self,
48 params: OrderStatusParams,
49 ) -> anyhow::Result<RestApiResponse<models::OrderStatusResponse>>;
50 async fn place_limit_order(
51 &self,
52 params: PlaceLimitOrderParams,
53 ) -> anyhow::Result<RestApiResponse<models::PlaceLimitOrderResponse>>;
54 async fn query_limit_open_orders(
55 &self,
56 params: QueryLimitOpenOrdersParams,
57 ) -> anyhow::Result<RestApiResponse<models::QueryLimitOpenOrdersResponse>>;
58 async fn send_quote_request(
59 &self,
60 params: SendQuoteRequestParams,
61 ) -> anyhow::Result<RestApiResponse<models::SendQuoteRequestResponse>>;
62}
63
64#[derive(Debug, Clone)]
65pub struct TradeApiClient {
66 configuration: ConfigurationRestApi,
67}
68
69impl TradeApiClient {
70 pub fn new(configuration: ConfigurationRestApi) -> Self {
71 Self { configuration }
72 }
73}
74
75#[allow(non_camel_case_types)]
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub enum PlaceLimitOrderSideEnum {
78 #[serde(rename = "BUY")]
79 Buy,
80 #[serde(rename = "SELL")]
81 Sell,
82}
83
84impl PlaceLimitOrderSideEnum {
85 #[must_use]
86 pub fn as_str(&self) -> &'static str {
87 match self {
88 Self::Buy => "BUY",
89 Self::Sell => "SELL",
90 }
91 }
92}
93
94impl std::str::FromStr for PlaceLimitOrderSideEnum {
95 type Err = Box<dyn std::error::Error + Send + Sync>;
96
97 fn from_str(s: &str) -> Result<Self, Self::Err> {
98 match s {
99 "BUY" => Ok(Self::Buy),
100 "SELL" => Ok(Self::Sell),
101 other => Err(format!("invalid PlaceLimitOrderSideEnum: {}", other).into()),
102 }
103 }
104}
105
106#[allow(non_camel_case_types)]
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub enum PlaceLimitOrderExpiredTypeEnum {
109 #[serde(rename = "1_D")]
110 ExpiredType1D,
111 #[serde(rename = "3_D")]
112 ExpiredType3D,
113 #[serde(rename = "7_D")]
114 ExpiredType7D,
115 #[serde(rename = "30_D")]
116 ExpiredType30D,
117}
118
119impl PlaceLimitOrderExpiredTypeEnum {
120 #[must_use]
121 pub fn as_str(&self) -> &'static str {
122 match self {
123 Self::ExpiredType1D => "1_D",
124 Self::ExpiredType3D => "3_D",
125 Self::ExpiredType7D => "7_D",
126 Self::ExpiredType30D => "30_D",
127 }
128 }
129}
130
131impl std::str::FromStr for PlaceLimitOrderExpiredTypeEnum {
132 type Err = Box<dyn std::error::Error + Send + Sync>;
133
134 fn from_str(s: &str) -> Result<Self, Self::Err> {
135 match s {
136 "1_D" => Ok(Self::ExpiredType1D),
137 "3_D" => Ok(Self::ExpiredType3D),
138 "7_D" => Ok(Self::ExpiredType7D),
139 "30_D" => Ok(Self::ExpiredType30D),
140 other => Err(format!("invalid PlaceLimitOrderExpiredTypeEnum: {}", other).into()),
141 }
142 }
143}
144
145#[allow(non_camel_case_types)]
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub enum PlaceLimitOrderWalletTypeEnum {
148 #[serde(rename = "SPOT")]
149 Spot,
150 #[serde(rename = "FUNDING")]
151 Funding,
152 #[serde(rename = "EARN")]
153 Earn,
154 #[serde(rename = "SPOT_FUNDING")]
155 SpotFunding,
156 #[serde(rename = "FUNDING_EARN")]
157 FundingEarn,
158 #[serde(rename = "SPOT_FUNDING_EARN")]
159 SpotFundingEarn,
160 #[serde(rename = "SPOT_EARN")]
161 SpotEarn,
162}
163
164impl PlaceLimitOrderWalletTypeEnum {
165 #[must_use]
166 pub fn as_str(&self) -> &'static str {
167 match self {
168 Self::Spot => "SPOT",
169 Self::Funding => "FUNDING",
170 Self::Earn => "EARN",
171 Self::SpotFunding => "SPOT_FUNDING",
172 Self::FundingEarn => "FUNDING_EARN",
173 Self::SpotFundingEarn => "SPOT_FUNDING_EARN",
174 Self::SpotEarn => "SPOT_EARN",
175 }
176 }
177}
178
179impl std::str::FromStr for PlaceLimitOrderWalletTypeEnum {
180 type Err = Box<dyn std::error::Error + Send + Sync>;
181
182 fn from_str(s: &str) -> Result<Self, Self::Err> {
183 match s {
184 "SPOT" => Ok(Self::Spot),
185 "FUNDING" => Ok(Self::Funding),
186 "EARN" => Ok(Self::Earn),
187 "SPOT_FUNDING" => Ok(Self::SpotFunding),
188 "FUNDING_EARN" => Ok(Self::FundingEarn),
189 "SPOT_FUNDING_EARN" => Ok(Self::SpotFundingEarn),
190 "SPOT_EARN" => Ok(Self::SpotEarn),
191 other => Err(format!("invalid PlaceLimitOrderWalletTypeEnum: {}", other).into()),
192 }
193 }
194}
195
196#[allow(non_camel_case_types)]
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub enum SendQuoteRequestWalletTypeEnum {
199 #[serde(rename = "SPOT")]
200 Spot,
201 #[serde(rename = "FUNDING")]
202 Funding,
203 #[serde(rename = "EARN")]
204 Earn,
205 #[serde(rename = "SPOT_FUNDING")]
206 SpotFunding,
207 #[serde(rename = "FUNDING_EARN")]
208 FundingEarn,
209 #[serde(rename = "SPOT_FUNDING_EARN")]
210 SpotFundingEarn,
211 #[serde(rename = "SPOT_EARN")]
212 SpotEarn,
213}
214
215impl SendQuoteRequestWalletTypeEnum {
216 #[must_use]
217 pub fn as_str(&self) -> &'static str {
218 match self {
219 Self::Spot => "SPOT",
220 Self::Funding => "FUNDING",
221 Self::Earn => "EARN",
222 Self::SpotFunding => "SPOT_FUNDING",
223 Self::FundingEarn => "FUNDING_EARN",
224 Self::SpotFundingEarn => "SPOT_FUNDING_EARN",
225 Self::SpotEarn => "SPOT_EARN",
226 }
227 }
228}
229
230impl std::str::FromStr for SendQuoteRequestWalletTypeEnum {
231 type Err = Box<dyn std::error::Error + Send + Sync>;
232
233 fn from_str(s: &str) -> Result<Self, Self::Err> {
234 match s {
235 "SPOT" => Ok(Self::Spot),
236 "FUNDING" => Ok(Self::Funding),
237 "EARN" => Ok(Self::Earn),
238 "SPOT_FUNDING" => Ok(Self::SpotFunding),
239 "FUNDING_EARN" => Ok(Self::FundingEarn),
240 "SPOT_FUNDING_EARN" => Ok(Self::SpotFundingEarn),
241 "SPOT_EARN" => Ok(Self::SpotEarn),
242 other => Err(format!("invalid SendQuoteRequestWalletTypeEnum: {}", other).into()),
243 }
244 }
245}
246
247#[allow(non_camel_case_types)]
248#[derive(Debug, Clone, Serialize, Deserialize)]
249pub enum SendQuoteRequestValidTimeEnum {
250 #[serde(rename = "10s")]
251 ValidTime10s,
252 #[serde(rename = "30s")]
253 ValidTime30s,
254 #[serde(rename = "1m")]
255 ValidTime1m,
256}
257
258impl SendQuoteRequestValidTimeEnum {
259 #[must_use]
260 pub fn as_str(&self) -> &'static str {
261 match self {
262 Self::ValidTime10s => "10s",
263 Self::ValidTime30s => "30s",
264 Self::ValidTime1m => "1m",
265 }
266 }
267}
268
269impl std::str::FromStr for SendQuoteRequestValidTimeEnum {
270 type Err = Box<dyn std::error::Error + Send + Sync>;
271
272 fn from_str(s: &str) -> Result<Self, Self::Err> {
273 match s {
274 "10s" => Ok(Self::ValidTime10s),
275 "30s" => Ok(Self::ValidTime30s),
276 "1m" => Ok(Self::ValidTime1m),
277 other => Err(format!("invalid SendQuoteRequestValidTimeEnum: {}", other).into()),
278 }
279 }
280}
281
282#[derive(Clone, Debug, Builder, Deserialize)]
287#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
288pub struct AcceptQuoteParams {
289 #[builder(setter(into))]
294 #[serde(rename = "quoteId")]
295 pub quote_id: String,
296 #[builder(setter(into), default)]
300 #[serde(rename = "recvWindow", default)]
301 pub recv_window: Option<i64>,
302}
303
304impl AcceptQuoteParams {
305 #[must_use]
312 pub fn builder(quote_id: String) -> AcceptQuoteParamsBuilder {
313 AcceptQuoteParamsBuilder::default().quote_id(quote_id)
314 }
315}
316#[derive(Clone, Debug, Builder, Deserialize)]
321#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
322pub struct CancelLimitOrderParams {
323 #[builder(setter(into))]
327 #[serde(rename = "orderId")]
328 pub order_id: i64,
329 #[builder(setter(into), default)]
333 #[serde(rename = "recvWindow", default)]
334 pub recv_window: Option<i64>,
335}
336
337impl CancelLimitOrderParams {
338 #[must_use]
345 pub fn builder(order_id: i64) -> CancelLimitOrderParamsBuilder {
346 CancelLimitOrderParamsBuilder::default().order_id(order_id)
347 }
348}
349#[derive(Clone, Debug, Builder, Deserialize)]
354#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
355pub struct GetConvertTradeHistoryParams {
356 #[builder(setter(into))]
361 #[serde(rename = "startTime")]
362 pub start_time: i64,
363 #[builder(setter(into))]
368 #[serde(rename = "endTime")]
369 pub end_time: i64,
370 #[builder(setter(into), default)]
374 #[serde(rename = "limit", default)]
375 pub limit: Option<i64>,
376 #[builder(setter(into), default)]
380 #[serde(rename = "recvWindow", default)]
381 pub recv_window: Option<i64>,
382}
383
384impl GetConvertTradeHistoryParams {
385 #[must_use]
393 pub fn builder(start_time: i64, end_time: i64) -> GetConvertTradeHistoryParamsBuilder {
394 GetConvertTradeHistoryParamsBuilder::default()
395 .start_time(start_time)
396 .end_time(end_time)
397 }
398}
399#[derive(Clone, Debug, Builder, Deserialize, Default)]
404#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
405pub struct OrderStatusParams {
406 #[builder(setter(into), default)]
410 #[serde(rename = "orderId", default)]
411 pub order_id: Option<String>,
412 #[builder(setter(into), default)]
416 #[serde(rename = "quoteId", default)]
417 pub quote_id: Option<String>,
418}
419
420impl OrderStatusParams {
421 #[must_use]
424 pub fn builder() -> OrderStatusParamsBuilder {
425 OrderStatusParamsBuilder::default()
426 }
427}
428#[derive(Clone, Debug, Builder, Deserialize)]
433#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
434pub struct PlaceLimitOrderParams {
435 #[builder(setter(into))]
440 #[serde(rename = "baseAsset")]
441 pub base_asset: String,
442 #[builder(setter(into))]
446 #[serde(rename = "quoteAsset")]
447 pub quote_asset: String,
448 #[builder(setter(into))]
452 #[serde(rename = "limitPrice")]
453 pub limit_price: rust_decimal::Decimal,
454 #[builder(setter(into))]
458 #[serde(rename = "side")]
459 pub side: PlaceLimitOrderSideEnum,
460 #[builder(setter(into))]
464 #[serde(rename = "expiredType")]
465 pub expired_type: PlaceLimitOrderExpiredTypeEnum,
466 #[builder(setter(into), default)]
470 #[serde(rename = "baseAmount", default)]
471 pub base_amount: Option<rust_decimal::Decimal>,
472 #[builder(setter(into), default)]
476 #[serde(rename = "quoteAmount", default)]
477 pub quote_amount: Option<rust_decimal::Decimal>,
478 #[builder(setter(into), default)]
483 #[serde(rename = "walletType", default)]
484 pub wallet_type: Option<PlaceLimitOrderWalletTypeEnum>,
485 #[builder(setter(into), default)]
489 #[serde(rename = "recvWindow", default)]
490 pub recv_window: Option<i64>,
491}
492
493impl PlaceLimitOrderParams {
494 #[must_use]
505 pub fn builder(
506 base_asset: String,
507 quote_asset: String,
508 limit_price: rust_decimal::Decimal,
509 side: PlaceLimitOrderSideEnum,
510 expired_type: PlaceLimitOrderExpiredTypeEnum,
511 ) -> PlaceLimitOrderParamsBuilder {
512 PlaceLimitOrderParamsBuilder::default()
513 .base_asset(base_asset)
514 .quote_asset(quote_asset)
515 .limit_price(limit_price)
516 .side(side)
517 .expired_type(expired_type)
518 }
519}
520#[derive(Clone, Debug, Builder, Deserialize, Default)]
525#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
526pub struct QueryLimitOpenOrdersParams {
527 #[builder(setter(into), default)]
531 #[serde(rename = "recvWindow", default)]
532 pub recv_window: Option<i64>,
533}
534
535impl QueryLimitOpenOrdersParams {
536 #[must_use]
539 pub fn builder() -> QueryLimitOpenOrdersParamsBuilder {
540 QueryLimitOpenOrdersParamsBuilder::default()
541 }
542}
543#[derive(Clone, Debug, Builder, Deserialize)]
548#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
549pub struct SendQuoteRequestParams {
550 #[builder(setter(into))]
555 #[serde(rename = "fromAsset")]
556 pub from_asset: String,
557 #[builder(setter(into))]
562 #[serde(rename = "toAsset")]
563 pub to_asset: String,
564 #[builder(setter(into), default)]
568 #[serde(rename = "fromAmount", default)]
569 pub from_amount: Option<rust_decimal::Decimal>,
570 #[builder(setter(into), default)]
574 #[serde(rename = "toAmount", default)]
575 pub to_amount: Option<rust_decimal::Decimal>,
576 #[builder(setter(into), default)]
581 #[serde(rename = "walletType", default)]
582 pub wallet_type: Option<SendQuoteRequestWalletTypeEnum>,
583 #[builder(setter(into), default)]
587 #[serde(rename = "validTime", default)]
588 pub valid_time: Option<SendQuoteRequestValidTimeEnum>,
589 #[builder(setter(into), default)]
593 #[serde(rename = "recvWindow", default)]
594 pub recv_window: Option<i64>,
595}
596
597impl SendQuoteRequestParams {
598 #[must_use]
606 pub fn builder(from_asset: String, to_asset: String) -> SendQuoteRequestParamsBuilder {
607 SendQuoteRequestParamsBuilder::default()
608 .from_asset(from_asset)
609 .to_asset(to_asset)
610 }
611}
612
613#[async_trait]
614impl TradeApi for TradeApiClient {
615 async fn accept_quote(
616 &self,
617 params: AcceptQuoteParams,
618 ) -> anyhow::Result<RestApiResponse<models::AcceptQuoteResponse>> {
619 let AcceptQuoteParams {
620 quote_id,
621 recv_window,
622 } = params;
623
624 let mut query_params = BTreeMap::new();
625 let body_params = BTreeMap::new();
626
627 query_params.insert("quoteId".to_string(), json!(quote_id));
628
629 if let Some(rw) = recv_window {
630 query_params.insert("recvWindow".to_string(), json!(rw));
631 }
632
633 send_request::<models::AcceptQuoteResponse>(
634 &self.configuration,
635 "/sapi/v1/convert/acceptQuote",
636 reqwest::Method::POST,
637 query_params,
638 body_params,
639 if HAS_TIME_UNIT {
640 self.configuration.time_unit
641 } else {
642 None
643 },
644 true,
645 )
646 .await
647 }
648
649 async fn cancel_limit_order(
650 &self,
651 params: CancelLimitOrderParams,
652 ) -> anyhow::Result<RestApiResponse<models::CancelLimitOrderResponse>> {
653 let CancelLimitOrderParams {
654 order_id,
655 recv_window,
656 } = params;
657
658 let mut query_params = BTreeMap::new();
659 let body_params = BTreeMap::new();
660
661 query_params.insert("orderId".to_string(), json!(order_id));
662
663 if let Some(rw) = recv_window {
664 query_params.insert("recvWindow".to_string(), json!(rw));
665 }
666
667 send_request::<models::CancelLimitOrderResponse>(
668 &self.configuration,
669 "/sapi/v1/convert/limit/cancelOrder",
670 reqwest::Method::POST,
671 query_params,
672 body_params,
673 if HAS_TIME_UNIT {
674 self.configuration.time_unit
675 } else {
676 None
677 },
678 true,
679 )
680 .await
681 }
682
683 async fn get_convert_trade_history(
684 &self,
685 params: GetConvertTradeHistoryParams,
686 ) -> anyhow::Result<RestApiResponse<models::GetConvertTradeHistoryResponse>> {
687 let GetConvertTradeHistoryParams {
688 start_time,
689 end_time,
690 limit,
691 recv_window,
692 } = params;
693
694 let mut query_params = BTreeMap::new();
695 let body_params = BTreeMap::new();
696
697 query_params.insert("startTime".to_string(), json!(start_time));
698
699 query_params.insert("endTime".to_string(), json!(end_time));
700
701 if let Some(rw) = limit {
702 query_params.insert("limit".to_string(), json!(rw));
703 }
704
705 if let Some(rw) = recv_window {
706 query_params.insert("recvWindow".to_string(), json!(rw));
707 }
708
709 send_request::<models::GetConvertTradeHistoryResponse>(
710 &self.configuration,
711 "/sapi/v1/convert/tradeFlow",
712 reqwest::Method::GET,
713 query_params,
714 body_params,
715 if HAS_TIME_UNIT {
716 self.configuration.time_unit
717 } else {
718 None
719 },
720 true,
721 )
722 .await
723 }
724
725 async fn order_status(
726 &self,
727 params: OrderStatusParams,
728 ) -> anyhow::Result<RestApiResponse<models::OrderStatusResponse>> {
729 let OrderStatusParams { order_id, quote_id } = params;
730
731 let mut query_params = BTreeMap::new();
732 let body_params = BTreeMap::new();
733
734 if let Some(rw) = order_id {
735 query_params.insert("orderId".to_string(), json!(rw));
736 }
737
738 if let Some(rw) = quote_id {
739 query_params.insert("quoteId".to_string(), json!(rw));
740 }
741
742 send_request::<models::OrderStatusResponse>(
743 &self.configuration,
744 "/sapi/v1/convert/orderStatus",
745 reqwest::Method::GET,
746 query_params,
747 body_params,
748 if HAS_TIME_UNIT {
749 self.configuration.time_unit
750 } else {
751 None
752 },
753 true,
754 )
755 .await
756 }
757
758 async fn place_limit_order(
759 &self,
760 params: PlaceLimitOrderParams,
761 ) -> anyhow::Result<RestApiResponse<models::PlaceLimitOrderResponse>> {
762 let PlaceLimitOrderParams {
763 base_asset,
764 quote_asset,
765 limit_price,
766 side,
767 expired_type,
768 base_amount,
769 quote_amount,
770 wallet_type,
771 recv_window,
772 } = params;
773
774 let mut query_params = BTreeMap::new();
775 let body_params = BTreeMap::new();
776
777 query_params.insert("baseAsset".to_string(), json!(base_asset));
778
779 query_params.insert("quoteAsset".to_string(), json!(quote_asset));
780
781 query_params.insert("limitPrice".to_string(), json!(limit_price));
782
783 if let Some(rw) = base_amount {
784 query_params.insert("baseAmount".to_string(), json!(rw));
785 }
786
787 if let Some(rw) = quote_amount {
788 query_params.insert("quoteAmount".to_string(), json!(rw));
789 }
790
791 query_params.insert("side".to_string(), json!(side));
792
793 if let Some(rw) = wallet_type {
794 query_params.insert("walletType".to_string(), json!(rw));
795 }
796
797 query_params.insert("expiredType".to_string(), json!(expired_type));
798
799 if let Some(rw) = recv_window {
800 query_params.insert("recvWindow".to_string(), json!(rw));
801 }
802
803 send_request::<models::PlaceLimitOrderResponse>(
804 &self.configuration,
805 "/sapi/v1/convert/limit/placeOrder",
806 reqwest::Method::POST,
807 query_params,
808 body_params,
809 if HAS_TIME_UNIT {
810 self.configuration.time_unit
811 } else {
812 None
813 },
814 true,
815 )
816 .await
817 }
818
819 async fn query_limit_open_orders(
820 &self,
821 params: QueryLimitOpenOrdersParams,
822 ) -> anyhow::Result<RestApiResponse<models::QueryLimitOpenOrdersResponse>> {
823 let QueryLimitOpenOrdersParams { recv_window } = params;
824
825 let mut query_params = BTreeMap::new();
826 let body_params = BTreeMap::new();
827
828 if let Some(rw) = recv_window {
829 query_params.insert("recvWindow".to_string(), json!(rw));
830 }
831
832 send_request::<models::QueryLimitOpenOrdersResponse>(
833 &self.configuration,
834 "/sapi/v1/convert/limit/queryOpenOrders",
835 reqwest::Method::GET,
836 query_params,
837 body_params,
838 if HAS_TIME_UNIT {
839 self.configuration.time_unit
840 } else {
841 None
842 },
843 true,
844 )
845 .await
846 }
847
848 async fn send_quote_request(
849 &self,
850 params: SendQuoteRequestParams,
851 ) -> anyhow::Result<RestApiResponse<models::SendQuoteRequestResponse>> {
852 let SendQuoteRequestParams {
853 from_asset,
854 to_asset,
855 from_amount,
856 to_amount,
857 wallet_type,
858 valid_time,
859 recv_window,
860 } = params;
861
862 let mut query_params = BTreeMap::new();
863 let body_params = BTreeMap::new();
864
865 query_params.insert("fromAsset".to_string(), json!(from_asset));
866
867 query_params.insert("toAsset".to_string(), json!(to_asset));
868
869 if let Some(rw) = from_amount {
870 query_params.insert("fromAmount".to_string(), json!(rw));
871 }
872
873 if let Some(rw) = to_amount {
874 query_params.insert("toAmount".to_string(), json!(rw));
875 }
876
877 if let Some(rw) = wallet_type {
878 query_params.insert("walletType".to_string(), json!(rw));
879 }
880
881 if let Some(rw) = valid_time {
882 query_params.insert("validTime".to_string(), json!(rw));
883 }
884
885 if let Some(rw) = recv_window {
886 query_params.insert("recvWindow".to_string(), json!(rw));
887 }
888
889 send_request::<models::SendQuoteRequestResponse>(
890 &self.configuration,
891 "/sapi/v1/convert/getQuote",
892 reqwest::Method::POST,
893 query_params,
894 body_params,
895 if HAS_TIME_UNIT {
896 self.configuration.time_unit
897 } else {
898 None
899 },
900 true,
901 )
902 .await
903 }
904}
905
906#[cfg(all(test, feature = "convert"))]
907mod tests {
908 use super::*;
909 use crate::TOKIO_SHARED_RT;
910 use crate::{errors::ConnectorError, models::DataFuture, models::RestApiRateLimit};
911 use async_trait::async_trait;
912 use std::collections::HashMap;
913
914 struct DummyRestApiResponse<T> {
915 inner: Box<dyn FnOnce() -> DataFuture<Result<T, ConnectorError>> + Send + Sync>,
916 status: u16,
917 headers: HashMap<String, String>,
918 rate_limits: Option<Vec<RestApiRateLimit>>,
919 }
920
921 impl<T> From<DummyRestApiResponse<T>> for RestApiResponse<T> {
922 fn from(dummy: DummyRestApiResponse<T>) -> Self {
923 Self {
924 data_fn: dummy.inner,
925 status: dummy.status,
926 headers: dummy.headers,
927 rate_limits: dummy.rate_limits,
928 }
929 }
930 }
931
932 struct MockTradeApiClient {
933 force_error: bool,
934 }
935
936 #[async_trait]
937 impl TradeApi for MockTradeApiClient {
938 async fn accept_quote(
939 &self,
940 _params: AcceptQuoteParams,
941 ) -> anyhow::Result<RestApiResponse<models::AcceptQuoteResponse>> {
942 if self.force_error {
943 return Err(ConnectorError::ConnectorClientError {
944 msg: "ResponseError".to_string(),
945 code: None,
946 }
947 .into());
948 }
949
950 let resp_json: Value = serde_json::from_str(r#"{"orderId":"933256278426274426","createTime":1623381330472,"orderStatus":"PROCESS"}"#).unwrap_or_else(|_| serde_json::json!({}));
951 let dummy_response: models::AcceptQuoteResponse =
952 serde_json::from_value(resp_json.clone())
953 .expect("should parse into models::AcceptQuoteResponse");
954
955 let dummy = DummyRestApiResponse {
956 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
957 status: 200,
958 headers: HashMap::new(),
959 rate_limits: None,
960 };
961
962 Ok(dummy.into())
963 }
964
965 async fn cancel_limit_order(
966 &self,
967 _params: CancelLimitOrderParams,
968 ) -> anyhow::Result<RestApiResponse<models::CancelLimitOrderResponse>> {
969 if self.force_error {
970 return Err(ConnectorError::ConnectorClientError {
971 msg: "ResponseError".to_string(),
972 code: None,
973 }
974 .into());
975 }
976
977 let resp_json: Value =
978 serde_json::from_str(r#"{"orderId":1603680255057330400,"status":"CANCELED"}"#)
979 .unwrap_or_else(|_| serde_json::json!({}));
980 let dummy_response: models::CancelLimitOrderResponse =
981 serde_json::from_value(resp_json.clone())
982 .expect("should parse into models::CancelLimitOrderResponse");
983
984 let dummy = DummyRestApiResponse {
985 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
986 status: 200,
987 headers: HashMap::new(),
988 rate_limits: None,
989 };
990
991 Ok(dummy.into())
992 }
993
994 async fn get_convert_trade_history(
995 &self,
996 _params: GetConvertTradeHistoryParams,
997 ) -> anyhow::Result<RestApiResponse<models::GetConvertTradeHistoryResponse>> {
998 if self.force_error {
999 return Err(ConnectorError::ConnectorClientError {
1000 msg: "ResponseError".to_string(),
1001 code: None,
1002 }
1003 .into());
1004 }
1005
1006 let resp_json: Value = serde_json::from_str(r#"{"list":[{"quoteId":"f3b91c525b2644c7bc1e1cd31b6e1aa6","orderId":940708407462087200,"orderStatus":"SUCCESS","fromAsset":"USDT","fromAmount":"20","toAsset":"BNB","toAmount":"0.06154036","ratio":"0.00307702","inverseRatio":"324.99","createTime":1624248872184}],"startTime":1623824139000,"endTime":1626416139000,"limit":100,"moreData":false}"#).unwrap_or_else(|_| serde_json::json!({}));
1007 let dummy_response: models::GetConvertTradeHistoryResponse =
1008 serde_json::from_value(resp_json.clone())
1009 .expect("should parse into models::GetConvertTradeHistoryResponse");
1010
1011 let dummy = DummyRestApiResponse {
1012 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
1013 status: 200,
1014 headers: HashMap::new(),
1015 rate_limits: None,
1016 };
1017
1018 Ok(dummy.into())
1019 }
1020
1021 async fn order_status(
1022 &self,
1023 _params: OrderStatusParams,
1024 ) -> anyhow::Result<RestApiResponse<models::OrderStatusResponse>> {
1025 if self.force_error {
1026 return Err(ConnectorError::ConnectorClientError {
1027 msg: "ResponseError".to_string(),
1028 code: None,
1029 }
1030 .into());
1031 }
1032
1033 let resp_json: Value = serde_json::from_str(r#"{"orderId":933256278426274400,"orderStatus":"SUCCESS","fromAsset":"BTC","fromAmount":"0.00054414","toAsset":"USDT","toAmount":"20","ratio":"36755","inverseRatio":"0.00002721","createTime":1623381330472}"#).unwrap_or_else(|_| serde_json::json!({}));
1034 let dummy_response: models::OrderStatusResponse =
1035 serde_json::from_value(resp_json.clone())
1036 .expect("should parse into models::OrderStatusResponse");
1037
1038 let dummy = DummyRestApiResponse {
1039 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
1040 status: 200,
1041 headers: HashMap::new(),
1042 rate_limits: None,
1043 };
1044
1045 Ok(dummy.into())
1046 }
1047
1048 async fn place_limit_order(
1049 &self,
1050 _params: PlaceLimitOrderParams,
1051 ) -> anyhow::Result<RestApiResponse<models::PlaceLimitOrderResponse>> {
1052 if self.force_error {
1053 return Err(ConnectorError::ConnectorClientError {
1054 msg: "ResponseError".to_string(),
1055 code: None,
1056 }
1057 .into());
1058 }
1059
1060 let resp_json: Value =
1061 serde_json::from_str(r#"{"orderId":1603680255057330400,"status":"PROCESS"}"#)
1062 .unwrap_or_else(|_| serde_json::json!({}));
1063 let dummy_response: models::PlaceLimitOrderResponse =
1064 serde_json::from_value(resp_json.clone())
1065 .expect("should parse into models::PlaceLimitOrderResponse");
1066
1067 let dummy = DummyRestApiResponse {
1068 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
1069 status: 200,
1070 headers: HashMap::new(),
1071 rate_limits: None,
1072 };
1073
1074 Ok(dummy.into())
1075 }
1076
1077 async fn query_limit_open_orders(
1078 &self,
1079 _params: QueryLimitOpenOrdersParams,
1080 ) -> anyhow::Result<RestApiResponse<models::QueryLimitOpenOrdersResponse>> {
1081 if self.force_error {
1082 return Err(ConnectorError::ConnectorClientError {
1083 msg: "ResponseError".to_string(),
1084 code: None,
1085 }
1086 .into());
1087 }
1088
1089 let resp_json: Value = serde_json::from_str(r#"{"list":[{"quoteId":"18sdf87kh9df","orderId":1150901289839,"orderStatus":"SUCCESS","fromAsset":"BNB","fromAmount":"10","toAsset":"USDT","toAmount":"2317.89","ratio":"231.789","inverseRatio":"0.00431427","createTime":1614089498000,"expiredTimestamp":1614099498000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
1090 let dummy_response: models::QueryLimitOpenOrdersResponse =
1091 serde_json::from_value(resp_json.clone())
1092 .expect("should parse into models::QueryLimitOpenOrdersResponse");
1093
1094 let dummy = DummyRestApiResponse {
1095 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
1096 status: 200,
1097 headers: HashMap::new(),
1098 rate_limits: None,
1099 };
1100
1101 Ok(dummy.into())
1102 }
1103
1104 async fn send_quote_request(
1105 &self,
1106 _params: SendQuoteRequestParams,
1107 ) -> anyhow::Result<RestApiResponse<models::SendQuoteRequestResponse>> {
1108 if self.force_error {
1109 return Err(ConnectorError::ConnectorClientError {
1110 msg: "ResponseError".to_string(),
1111 code: None,
1112 }
1113 .into());
1114 }
1115
1116 let resp_json: Value = serde_json::from_str(r#"{"quoteId":"12415572564","ratio":"38163.7","inverseRatio":"0.0000262","validTimestamp":1623319461670,"toAmount":"3816.37","fromAmount":"0.1"}"#).unwrap_or_else(|_| serde_json::json!({}));
1117 let dummy_response: models::SendQuoteRequestResponse =
1118 serde_json::from_value(resp_json.clone())
1119 .expect("should parse into models::SendQuoteRequestResponse");
1120
1121 let dummy = DummyRestApiResponse {
1122 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
1123 status: 200,
1124 headers: HashMap::new(),
1125 rate_limits: None,
1126 };
1127
1128 Ok(dummy.into())
1129 }
1130 }
1131
1132 #[test]
1133 fn accept_quote_required_params_success() {
1134 TOKIO_SHARED_RT.block_on(async {
1135 let client = MockTradeApiClient { force_error: false };
1136
1137 let params = AcceptQuoteParams::builder("1".to_string(),).build().unwrap();
1138
1139 let resp_json: Value = serde_json::from_str(r#"{"orderId":"933256278426274426","createTime":1623381330472,"orderStatus":"PROCESS"}"#).unwrap_or_else(|_| serde_json::json!({}));
1140 let expected_response : models::AcceptQuoteResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::AcceptQuoteResponse");
1141
1142 let resp = client.accept_quote(params).await.expect("Expected a response");
1143 let data_future = resp.data();
1144 let actual_response = data_future.await.unwrap();
1145 assert_eq!(actual_response, expected_response);
1146 });
1147 }
1148
1149 #[test]
1150 fn accept_quote_optional_params_success() {
1151 TOKIO_SHARED_RT.block_on(async {
1152 let client = MockTradeApiClient { force_error: false };
1153
1154 let params = AcceptQuoteParams::builder("1".to_string(),).recv_window(5000).build().unwrap();
1155
1156 let resp_json: Value = serde_json::from_str(r#"{"orderId":"933256278426274426","createTime":1623381330472,"orderStatus":"PROCESS"}"#).unwrap_or_else(|_| serde_json::json!({}));
1157 let expected_response : models::AcceptQuoteResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::AcceptQuoteResponse");
1158
1159 let resp = client.accept_quote(params).await.expect("Expected a response");
1160 let data_future = resp.data();
1161 let actual_response = data_future.await.unwrap();
1162 assert_eq!(actual_response, expected_response);
1163 });
1164 }
1165
1166 #[test]
1167 fn accept_quote_response_error() {
1168 TOKIO_SHARED_RT.block_on(async {
1169 let client = MockTradeApiClient { force_error: true };
1170
1171 let params = AcceptQuoteParams::builder("1".to_string()).build().unwrap();
1172
1173 match client.accept_quote(params).await {
1174 Ok(_) => panic!("Expected an error"),
1175 Err(err) => {
1176 assert_eq!(err.to_string(), "Connector client error: ResponseError");
1177 }
1178 }
1179 });
1180 }
1181
1182 #[test]
1183 fn cancel_limit_order_required_params_success() {
1184 TOKIO_SHARED_RT.block_on(async {
1185 let client = MockTradeApiClient { force_error: false };
1186
1187 let params = CancelLimitOrderParams::builder(1603680255057330400)
1188 .build()
1189 .unwrap();
1190
1191 let resp_json: Value =
1192 serde_json::from_str(r#"{"orderId":1603680255057330400,"status":"CANCELED"}"#)
1193 .unwrap_or_else(|_| serde_json::json!({}));
1194 let expected_response: models::CancelLimitOrderResponse =
1195 serde_json::from_value(resp_json.clone())
1196 .expect("should parse into models::CancelLimitOrderResponse");
1197
1198 let resp = client
1199 .cancel_limit_order(params)
1200 .await
1201 .expect("Expected a response");
1202 let data_future = resp.data();
1203 let actual_response = data_future.await.unwrap();
1204 assert_eq!(actual_response, expected_response);
1205 });
1206 }
1207
1208 #[test]
1209 fn cancel_limit_order_optional_params_success() {
1210 TOKIO_SHARED_RT.block_on(async {
1211 let client = MockTradeApiClient { force_error: false };
1212
1213 let params = CancelLimitOrderParams::builder(1603680255057330400)
1214 .recv_window(5000)
1215 .build()
1216 .unwrap();
1217
1218 let resp_json: Value =
1219 serde_json::from_str(r#"{"orderId":1603680255057330400,"status":"CANCELED"}"#)
1220 .unwrap_or_else(|_| serde_json::json!({}));
1221 let expected_response: models::CancelLimitOrderResponse =
1222 serde_json::from_value(resp_json.clone())
1223 .expect("should parse into models::CancelLimitOrderResponse");
1224
1225 let resp = client
1226 .cancel_limit_order(params)
1227 .await
1228 .expect("Expected a response");
1229 let data_future = resp.data();
1230 let actual_response = data_future.await.unwrap();
1231 assert_eq!(actual_response, expected_response);
1232 });
1233 }
1234
1235 #[test]
1236 fn cancel_limit_order_response_error() {
1237 TOKIO_SHARED_RT.block_on(async {
1238 let client = MockTradeApiClient { force_error: true };
1239
1240 let params = CancelLimitOrderParams::builder(1603680255057330400)
1241 .build()
1242 .unwrap();
1243
1244 match client.cancel_limit_order(params).await {
1245 Ok(_) => panic!("Expected an error"),
1246 Err(err) => {
1247 assert_eq!(err.to_string(), "Connector client error: ResponseError");
1248 }
1249 }
1250 });
1251 }
1252
1253 #[test]
1254 fn get_convert_trade_history_required_params_success() {
1255 TOKIO_SHARED_RT.block_on(async {
1256 let client = MockTradeApiClient { force_error: false };
1257
1258 let params = GetConvertTradeHistoryParams::builder(1623319461670,1641782889000,).build().unwrap();
1259
1260 let resp_json: Value = serde_json::from_str(r#"{"list":[{"quoteId":"f3b91c525b2644c7bc1e1cd31b6e1aa6","orderId":940708407462087200,"orderStatus":"SUCCESS","fromAsset":"USDT","fromAmount":"20","toAsset":"BNB","toAmount":"0.06154036","ratio":"0.00307702","inverseRatio":"324.99","createTime":1624248872184}],"startTime":1623824139000,"endTime":1626416139000,"limit":100,"moreData":false}"#).unwrap_or_else(|_| serde_json::json!({}));
1261 let expected_response : models::GetConvertTradeHistoryResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::GetConvertTradeHistoryResponse");
1262
1263 let resp = client.get_convert_trade_history(params).await.expect("Expected a response");
1264 let data_future = resp.data();
1265 let actual_response = data_future.await.unwrap();
1266 assert_eq!(actual_response, expected_response);
1267 });
1268 }
1269
1270 #[test]
1271 fn get_convert_trade_history_optional_params_success() {
1272 TOKIO_SHARED_RT.block_on(async {
1273 let client = MockTradeApiClient { force_error: false };
1274
1275 let params = GetConvertTradeHistoryParams::builder(1623319461670,1641782889000,).limit(100).recv_window(5000).build().unwrap();
1276
1277 let resp_json: Value = serde_json::from_str(r#"{"list":[{"quoteId":"f3b91c525b2644c7bc1e1cd31b6e1aa6","orderId":940708407462087200,"orderStatus":"SUCCESS","fromAsset":"USDT","fromAmount":"20","toAsset":"BNB","toAmount":"0.06154036","ratio":"0.00307702","inverseRatio":"324.99","createTime":1624248872184}],"startTime":1623824139000,"endTime":1626416139000,"limit":100,"moreData":false}"#).unwrap_or_else(|_| serde_json::json!({}));
1278 let expected_response : models::GetConvertTradeHistoryResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::GetConvertTradeHistoryResponse");
1279
1280 let resp = client.get_convert_trade_history(params).await.expect("Expected a response");
1281 let data_future = resp.data();
1282 let actual_response = data_future.await.unwrap();
1283 assert_eq!(actual_response, expected_response);
1284 });
1285 }
1286
1287 #[test]
1288 fn get_convert_trade_history_response_error() {
1289 TOKIO_SHARED_RT.block_on(async {
1290 let client = MockTradeApiClient { force_error: true };
1291
1292 let params = GetConvertTradeHistoryParams::builder(1623319461670, 1641782889000)
1293 .build()
1294 .unwrap();
1295
1296 match client.get_convert_trade_history(params).await {
1297 Ok(_) => panic!("Expected an error"),
1298 Err(err) => {
1299 assert_eq!(err.to_string(), "Connector client error: ResponseError");
1300 }
1301 }
1302 });
1303 }
1304
1305 #[test]
1306 fn order_status_required_params_success() {
1307 TOKIO_SHARED_RT.block_on(async {
1308 let client = MockTradeApiClient { force_error: false };
1309
1310 let params = OrderStatusParams::builder().build().unwrap();
1311
1312 let resp_json: Value = serde_json::from_str(r#"{"orderId":933256278426274400,"orderStatus":"SUCCESS","fromAsset":"BTC","fromAmount":"0.00054414","toAsset":"USDT","toAmount":"20","ratio":"36755","inverseRatio":"0.00002721","createTime":1623381330472}"#).unwrap_or_else(|_| serde_json::json!({}));
1313 let expected_response : models::OrderStatusResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::OrderStatusResponse");
1314
1315 let resp = client.order_status(params).await.expect("Expected a response");
1316 let data_future = resp.data();
1317 let actual_response = data_future.await.unwrap();
1318 assert_eq!(actual_response, expected_response);
1319 });
1320 }
1321
1322 #[test]
1323 fn order_status_optional_params_success() {
1324 TOKIO_SHARED_RT.block_on(async {
1325 let client = MockTradeApiClient { force_error: false };
1326
1327 let params = OrderStatusParams::builder().order_id("1".to_string()).quote_id("1".to_string()).build().unwrap();
1328
1329 let resp_json: Value = serde_json::from_str(r#"{"orderId":933256278426274400,"orderStatus":"SUCCESS","fromAsset":"BTC","fromAmount":"0.00054414","toAsset":"USDT","toAmount":"20","ratio":"36755","inverseRatio":"0.00002721","createTime":1623381330472}"#).unwrap_or_else(|_| serde_json::json!({}));
1330 let expected_response : models::OrderStatusResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::OrderStatusResponse");
1331
1332 let resp = client.order_status(params).await.expect("Expected a response");
1333 let data_future = resp.data();
1334 let actual_response = data_future.await.unwrap();
1335 assert_eq!(actual_response, expected_response);
1336 });
1337 }
1338
1339 #[test]
1340 fn order_status_response_error() {
1341 TOKIO_SHARED_RT.block_on(async {
1342 let client = MockTradeApiClient { force_error: true };
1343
1344 let params = OrderStatusParams::builder().build().unwrap();
1345
1346 match client.order_status(params).await {
1347 Ok(_) => panic!("Expected an error"),
1348 Err(err) => {
1349 assert_eq!(err.to_string(), "Connector client error: ResponseError");
1350 }
1351 }
1352 });
1353 }
1354
1355 #[test]
1356 fn place_limit_order_required_params_success() {
1357 TOKIO_SHARED_RT.block_on(async {
1358 let client = MockTradeApiClient { force_error: false };
1359
1360 let params = PlaceLimitOrderParams::builder(
1361 "BTC".to_string(),
1362 "USDT".to_string(),
1363 dec!(1),
1364 PlaceLimitOrderSideEnum::Buy,
1365 PlaceLimitOrderExpiredTypeEnum::ExpiredType1D,
1366 )
1367 .build()
1368 .unwrap();
1369
1370 let resp_json: Value =
1371 serde_json::from_str(r#"{"orderId":1603680255057330400,"status":"PROCESS"}"#)
1372 .unwrap_or_else(|_| serde_json::json!({}));
1373 let expected_response: models::PlaceLimitOrderResponse =
1374 serde_json::from_value(resp_json.clone())
1375 .expect("should parse into models::PlaceLimitOrderResponse");
1376
1377 let resp = client
1378 .place_limit_order(params)
1379 .await
1380 .expect("Expected a response");
1381 let data_future = resp.data();
1382 let actual_response = data_future.await.unwrap();
1383 assert_eq!(actual_response, expected_response);
1384 });
1385 }
1386
1387 #[test]
1388 fn place_limit_order_optional_params_success() {
1389 TOKIO_SHARED_RT.block_on(async {
1390 let client = MockTradeApiClient { force_error: false };
1391
1392 let params = PlaceLimitOrderParams::builder(
1393 "BTC".to_string(),
1394 "USDT".to_string(),
1395 dec!(1),
1396 PlaceLimitOrderSideEnum::Buy,
1397 PlaceLimitOrderExpiredTypeEnum::ExpiredType1D,
1398 )
1399 .base_amount(dec!(1))
1400 .quote_amount(dec!(1))
1401 .wallet_type(PlaceLimitOrderWalletTypeEnum::Spot)
1402 .recv_window(5000)
1403 .build()
1404 .unwrap();
1405
1406 let resp_json: Value =
1407 serde_json::from_str(r#"{"orderId":1603680255057330400,"status":"PROCESS"}"#)
1408 .unwrap_or_else(|_| serde_json::json!({}));
1409 let expected_response: models::PlaceLimitOrderResponse =
1410 serde_json::from_value(resp_json.clone())
1411 .expect("should parse into models::PlaceLimitOrderResponse");
1412
1413 let resp = client
1414 .place_limit_order(params)
1415 .await
1416 .expect("Expected a response");
1417 let data_future = resp.data();
1418 let actual_response = data_future.await.unwrap();
1419 assert_eq!(actual_response, expected_response);
1420 });
1421 }
1422
1423 #[test]
1424 fn place_limit_order_response_error() {
1425 TOKIO_SHARED_RT.block_on(async {
1426 let client = MockTradeApiClient { force_error: true };
1427
1428 let params = PlaceLimitOrderParams::builder(
1429 "BTC".to_string(),
1430 "USDT".to_string(),
1431 dec!(1),
1432 PlaceLimitOrderSideEnum::Buy,
1433 PlaceLimitOrderExpiredTypeEnum::ExpiredType1D,
1434 )
1435 .build()
1436 .unwrap();
1437
1438 match client.place_limit_order(params).await {
1439 Ok(_) => panic!("Expected an error"),
1440 Err(err) => {
1441 assert_eq!(err.to_string(), "Connector client error: ResponseError");
1442 }
1443 }
1444 });
1445 }
1446
1447 #[test]
1448 fn query_limit_open_orders_required_params_success() {
1449 TOKIO_SHARED_RT.block_on(async {
1450 let client = MockTradeApiClient { force_error: false };
1451
1452 let params = QueryLimitOpenOrdersParams::builder().build().unwrap();
1453
1454 let resp_json: Value = serde_json::from_str(r#"{"list":[{"quoteId":"18sdf87kh9df","orderId":1150901289839,"orderStatus":"SUCCESS","fromAsset":"BNB","fromAmount":"10","toAsset":"USDT","toAmount":"2317.89","ratio":"231.789","inverseRatio":"0.00431427","createTime":1614089498000,"expiredTimestamp":1614099498000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
1455 let expected_response : models::QueryLimitOpenOrdersResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::QueryLimitOpenOrdersResponse");
1456
1457 let resp = client.query_limit_open_orders(params).await.expect("Expected a response");
1458 let data_future = resp.data();
1459 let actual_response = data_future.await.unwrap();
1460 assert_eq!(actual_response, expected_response);
1461 });
1462 }
1463
1464 #[test]
1465 fn query_limit_open_orders_optional_params_success() {
1466 TOKIO_SHARED_RT.block_on(async {
1467 let client = MockTradeApiClient { force_error: false };
1468
1469 let params = QueryLimitOpenOrdersParams::builder().recv_window(5000).build().unwrap();
1470
1471 let resp_json: Value = serde_json::from_str(r#"{"list":[{"quoteId":"18sdf87kh9df","orderId":1150901289839,"orderStatus":"SUCCESS","fromAsset":"BNB","fromAmount":"10","toAsset":"USDT","toAmount":"2317.89","ratio":"231.789","inverseRatio":"0.00431427","createTime":1614089498000,"expiredTimestamp":1614099498000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
1472 let expected_response : models::QueryLimitOpenOrdersResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::QueryLimitOpenOrdersResponse");
1473
1474 let resp = client.query_limit_open_orders(params).await.expect("Expected a response");
1475 let data_future = resp.data();
1476 let actual_response = data_future.await.unwrap();
1477 assert_eq!(actual_response, expected_response);
1478 });
1479 }
1480
1481 #[test]
1482 fn query_limit_open_orders_response_error() {
1483 TOKIO_SHARED_RT.block_on(async {
1484 let client = MockTradeApiClient { force_error: true };
1485
1486 let params = QueryLimitOpenOrdersParams::builder().build().unwrap();
1487
1488 match client.query_limit_open_orders(params).await {
1489 Ok(_) => panic!("Expected an error"),
1490 Err(err) => {
1491 assert_eq!(err.to_string(), "Connector client error: ResponseError");
1492 }
1493 }
1494 });
1495 }
1496
1497 #[test]
1498 fn send_quote_request_required_params_success() {
1499 TOKIO_SHARED_RT.block_on(async {
1500 let client = MockTradeApiClient { force_error: false };
1501
1502 let params = SendQuoteRequestParams::builder("BTC".to_string(),"USDT".to_string(),).build().unwrap();
1503
1504 let resp_json: Value = serde_json::from_str(r#"{"quoteId":"12415572564","ratio":"38163.7","inverseRatio":"0.0000262","validTimestamp":1623319461670,"toAmount":"3816.37","fromAmount":"0.1"}"#).unwrap_or_else(|_| serde_json::json!({}));
1505 let expected_response : models::SendQuoteRequestResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::SendQuoteRequestResponse");
1506
1507 let resp = client.send_quote_request(params).await.expect("Expected a response");
1508 let data_future = resp.data();
1509 let actual_response = data_future.await.unwrap();
1510 assert_eq!(actual_response, expected_response);
1511 });
1512 }
1513
1514 #[test]
1515 fn send_quote_request_optional_params_success() {
1516 TOKIO_SHARED_RT.block_on(async {
1517 let client = MockTradeApiClient { force_error: false };
1518
1519 let params = SendQuoteRequestParams::builder("BTC".to_string(),"USDT".to_string(),).from_amount(dec!(1)).to_amount(dec!(1)).wallet_type(SendQuoteRequestWalletTypeEnum::Spot).valid_time(SendQuoteRequestValidTimeEnum::ValidTime10s).recv_window(5000).build().unwrap();
1520
1521 let resp_json: Value = serde_json::from_str(r#"{"quoteId":"12415572564","ratio":"38163.7","inverseRatio":"0.0000262","validTimestamp":1623319461670,"toAmount":"3816.37","fromAmount":"0.1"}"#).unwrap_or_else(|_| serde_json::json!({}));
1522 let expected_response : models::SendQuoteRequestResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::SendQuoteRequestResponse");
1523
1524 let resp = client.send_quote_request(params).await.expect("Expected a response");
1525 let data_future = resp.data();
1526 let actual_response = data_future.await.unwrap();
1527 assert_eq!(actual_response, expected_response);
1528 });
1529 }
1530
1531 #[test]
1532 fn send_quote_request_response_error() {
1533 TOKIO_SHARED_RT.block_on(async {
1534 let client = MockTradeApiClient { force_error: true };
1535
1536 let params = SendQuoteRequestParams::builder("BTC".to_string(), "USDT".to_string())
1537 .build()
1538 .unwrap();
1539
1540 match client.send_quote_request(params).await {
1541 Ok(_) => panic!("Expected an error"),
1542 Err(err) => {
1543 assert_eq!(err.to_string(), "Connector client error: ResponseError");
1544 }
1545 }
1546 });
1547 }
1548}