1#![allow(unused_imports)]
20use anyhow::Context;
21use async_trait::async_trait;
22use derive_builder::Builder;
23use rust_decimal::prelude::*;
24use serde::{Deserialize, Serialize};
25use serde_json::Value;
26use std::{collections::BTreeMap, sync::Arc};
27
28use crate::common::{
29 errors::WebsocketError,
30 models::{ParamBuildError, WebsocketApiResponse},
31 utils::remove_empty_value,
32 websocket::{WebsocketApi, WebsocketMessageSendOptions},
33};
34use crate::spot::websocket_api::models;
35
36#[async_trait]
37pub trait AccountApi: Send + Sync {
38 async fn account_commission(
39 &self,
40 params: AccountCommissionParams,
41 ) -> anyhow::Result<WebsocketApiResponse<Box<models::AccountCommissionResponseResult>>>;
42 async fn account_rate_limits_orders(
43 &self,
44 params: AccountRateLimitsOrdersParams,
45 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::AccountRateLimitsOrdersResponseResultInner>>>;
46 async fn account_status(
47 &self,
48 params: AccountStatusParams,
49 ) -> anyhow::Result<WebsocketApiResponse<Box<models::AccountStatusResponseResult>>>;
50 async fn all_order_lists(
51 &self,
52 params: AllOrderListsParams,
53 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::AllOrderListsResponseResultInner>>>;
54 async fn all_orders(
55 &self,
56 params: AllOrdersParams,
57 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::AllOrdersResponseResultInner>>>;
58 async fn my_allocations(
59 &self,
60 params: MyAllocationsParams,
61 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::MyAllocationsResponseResultInner>>>;
62 async fn my_filters(
63 &self,
64 params: MyFiltersParams,
65 ) -> anyhow::Result<WebsocketApiResponse<Box<models::MyFiltersResponseResult>>>;
66 async fn my_prevented_matches(
67 &self,
68 params: MyPreventedMatchesParams,
69 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::MyPreventedMatchesResponseResultInner>>>;
70 async fn my_trades(
71 &self,
72 params: MyTradesParams,
73 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::MyTradesResponseResultInner>>>;
74 async fn open_order_lists_status(
75 &self,
76 params: OpenOrderListsStatusParams,
77 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::OpenOrderListsStatusResponseResultInner>>>;
78 async fn open_orders_status(
79 &self,
80 params: OpenOrdersStatusParams,
81 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::OpenOrdersStatusResponseResultInner>>>;
82 async fn order_amendments(
83 &self,
84 params: OrderAmendmentsParams,
85 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::OrderAmendmentsResponseResultInner>>>;
86 async fn order_list_status(
87 &self,
88 params: OrderListStatusParams,
89 ) -> anyhow::Result<WebsocketApiResponse<Box<models::AllOrderListsResponseResultInner>>>;
90 async fn order_status(
91 &self,
92 params: OrderStatusParams,
93 ) -> anyhow::Result<WebsocketApiResponse<Box<models::OrderStatusResponseResult>>>;
94}
95
96#[derive(Clone)]
97pub struct AccountApiClient {
98 websocket_api_base: Arc<WebsocketApi>,
99}
100
101impl AccountApiClient {
102 pub fn new(websocket_api_base: Arc<WebsocketApi>) -> Self {
103 Self { websocket_api_base }
104 }
105}
106
107#[derive(Clone, Debug, Builder)]
112#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
113pub struct AccountCommissionParams {
114 #[builder(setter(into))]
119 pub symbol: String,
120 #[builder(setter(into), default)]
124 pub id: Option<String>,
125}
126
127impl AccountCommissionParams {
128 #[must_use]
135 pub fn builder(symbol: String) -> AccountCommissionParamsBuilder {
136 AccountCommissionParamsBuilder::default().symbol(symbol)
137 }
138}
139#[derive(Clone, Debug, Builder, Default)]
144#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
145pub struct AccountRateLimitsOrdersParams {
146 #[builder(setter(into), default)]
150 pub id: Option<String>,
151 #[builder(setter(into), default)]
155 pub recv_window: Option<rust_decimal::Decimal>,
156}
157
158impl AccountRateLimitsOrdersParams {
159 #[must_use]
162 pub fn builder() -> AccountRateLimitsOrdersParamsBuilder {
163 AccountRateLimitsOrdersParamsBuilder::default()
164 }
165}
166#[derive(Clone, Debug, Builder, Default)]
171#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
172pub struct AccountStatusParams {
173 #[builder(setter(into), default)]
177 pub id: Option<String>,
178 #[builder(setter(into), default)]
182 pub omit_zero_balances: Option<bool>,
183 #[builder(setter(into), default)]
187 pub recv_window: Option<rust_decimal::Decimal>,
188}
189
190impl AccountStatusParams {
191 #[must_use]
194 pub fn builder() -> AccountStatusParamsBuilder {
195 AccountStatusParamsBuilder::default()
196 }
197}
198#[derive(Clone, Debug, Builder, Default)]
203#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
204pub struct AllOrderListsParams {
205 #[builder(setter(into), default)]
209 pub id: Option<String>,
210 #[builder(setter(into), default)]
214 pub from_id: Option<i32>,
215 #[builder(setter(into), default)]
220 pub start_time: Option<i64>,
221 #[builder(setter(into), default)]
226 pub end_time: Option<i64>,
227 #[builder(setter(into), default)]
231 pub limit: Option<i32>,
232 #[builder(setter(into), default)]
236 pub recv_window: Option<rust_decimal::Decimal>,
237}
238
239impl AllOrderListsParams {
240 #[must_use]
243 pub fn builder() -> AllOrderListsParamsBuilder {
244 AllOrderListsParamsBuilder::default()
245 }
246}
247#[derive(Clone, Debug, Builder)]
252#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
253pub struct AllOrdersParams {
254 #[builder(setter(into))]
259 pub symbol: String,
260 #[builder(setter(into), default)]
264 pub id: Option<String>,
265 #[builder(setter(into), default)]
269 pub order_id: Option<i64>,
270 #[builder(setter(into), default)]
275 pub start_time: Option<i64>,
276 #[builder(setter(into), default)]
281 pub end_time: Option<i64>,
282 #[builder(setter(into), default)]
286 pub limit: Option<i32>,
287 #[builder(setter(into), default)]
291 pub recv_window: Option<rust_decimal::Decimal>,
292}
293
294impl AllOrdersParams {
295 #[must_use]
302 pub fn builder(symbol: String) -> AllOrdersParamsBuilder {
303 AllOrdersParamsBuilder::default().symbol(symbol)
304 }
305}
306#[derive(Clone, Debug, Builder)]
311#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
312pub struct MyAllocationsParams {
313 #[builder(setter(into))]
318 pub symbol: String,
319 #[builder(setter(into), default)]
323 pub id: Option<String>,
324 #[builder(setter(into), default)]
329 pub start_time: Option<i64>,
330 #[builder(setter(into), default)]
335 pub end_time: Option<i64>,
336 #[builder(setter(into), default)]
341 pub from_allocation_id: Option<i32>,
342 #[builder(setter(into), default)]
346 pub limit: Option<i32>,
347 #[builder(setter(into), default)]
351 pub order_id: Option<i64>,
352 #[builder(setter(into), default)]
356 pub recv_window: Option<rust_decimal::Decimal>,
357}
358
359impl MyAllocationsParams {
360 #[must_use]
367 pub fn builder(symbol: String) -> MyAllocationsParamsBuilder {
368 MyAllocationsParamsBuilder::default().symbol(symbol)
369 }
370}
371#[derive(Clone, Debug, Builder)]
376#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
377pub struct MyFiltersParams {
378 #[builder(setter(into))]
383 pub symbol: String,
384 #[builder(setter(into), default)]
388 pub id: Option<String>,
389 #[builder(setter(into), default)]
393 pub recv_window: Option<rust_decimal::Decimal>,
394}
395
396impl MyFiltersParams {
397 #[must_use]
404 pub fn builder(symbol: String) -> MyFiltersParamsBuilder {
405 MyFiltersParamsBuilder::default().symbol(symbol)
406 }
407}
408#[derive(Clone, Debug, Builder)]
413#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
414pub struct MyPreventedMatchesParams {
415 #[builder(setter(into))]
420 pub symbol: String,
421 #[builder(setter(into), default)]
425 pub id: Option<String>,
426 #[builder(setter(into), default)]
431 pub prevented_match_id: Option<i64>,
432 #[builder(setter(into), default)]
436 pub order_id: Option<i64>,
437 #[builder(setter(into), default)]
442 pub from_prevented_match_id: Option<i64>,
443 #[builder(setter(into), default)]
447 pub limit: Option<i32>,
448 #[builder(setter(into), default)]
452 pub recv_window: Option<rust_decimal::Decimal>,
453}
454
455impl MyPreventedMatchesParams {
456 #[must_use]
463 pub fn builder(symbol: String) -> MyPreventedMatchesParamsBuilder {
464 MyPreventedMatchesParamsBuilder::default().symbol(symbol)
465 }
466}
467#[derive(Clone, Debug, Builder)]
472#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
473pub struct MyTradesParams {
474 #[builder(setter(into))]
479 pub symbol: String,
480 #[builder(setter(into), default)]
484 pub id: Option<String>,
485 #[builder(setter(into), default)]
489 pub order_id: Option<i64>,
490 #[builder(setter(into), default)]
495 pub start_time: Option<i64>,
496 #[builder(setter(into), default)]
501 pub end_time: Option<i64>,
502 #[builder(setter(into), default)]
506 pub from_id: Option<i32>,
507 #[builder(setter(into), default)]
511 pub limit: Option<i32>,
512 #[builder(setter(into), default)]
516 pub recv_window: Option<rust_decimal::Decimal>,
517}
518
519impl MyTradesParams {
520 #[must_use]
527 pub fn builder(symbol: String) -> MyTradesParamsBuilder {
528 MyTradesParamsBuilder::default().symbol(symbol)
529 }
530}
531#[derive(Clone, Debug, Builder, Default)]
536#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
537pub struct OpenOrderListsStatusParams {
538 #[builder(setter(into), default)]
542 pub id: Option<String>,
543 #[builder(setter(into), default)]
547 pub recv_window: Option<rust_decimal::Decimal>,
548}
549
550impl OpenOrderListsStatusParams {
551 #[must_use]
554 pub fn builder() -> OpenOrderListsStatusParamsBuilder {
555 OpenOrderListsStatusParamsBuilder::default()
556 }
557}
558#[derive(Clone, Debug, Builder, Default)]
563#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
564pub struct OpenOrdersStatusParams {
565 #[builder(setter(into), default)]
569 pub id: Option<String>,
570 #[builder(setter(into), default)]
574 pub symbol: Option<String>,
575 #[builder(setter(into), default)]
579 pub recv_window: Option<rust_decimal::Decimal>,
580}
581
582impl OpenOrdersStatusParams {
583 #[must_use]
586 pub fn builder() -> OpenOrdersStatusParamsBuilder {
587 OpenOrdersStatusParamsBuilder::default()
588 }
589}
590#[derive(Clone, Debug, Builder)]
595#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
596pub struct OrderAmendmentsParams {
597 #[builder(setter(into))]
602 pub symbol: String,
603 #[builder(setter(into))]
608 pub order_id: i64,
609 #[builder(setter(into), default)]
613 pub id: Option<String>,
614 #[builder(setter(into), default)]
619 pub from_execution_id: Option<i64>,
620 #[builder(setter(into), default)]
624 pub limit: Option<i64>,
625 #[builder(setter(into), default)]
629 pub recv_window: Option<rust_decimal::Decimal>,
630}
631
632impl OrderAmendmentsParams {
633 #[must_use]
641 pub fn builder(symbol: String, order_id: i64) -> OrderAmendmentsParamsBuilder {
642 OrderAmendmentsParamsBuilder::default()
643 .symbol(symbol)
644 .order_id(order_id)
645 }
646}
647#[derive(Clone, Debug, Builder, Default)]
652#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
653pub struct OrderListStatusParams {
654 #[builder(setter(into), default)]
658 pub id: Option<String>,
659 #[builder(setter(into), default)]
663 pub orig_client_order_id: Option<String>,
664 #[builder(setter(into), default)]
668 pub order_list_id: Option<i32>,
669 #[builder(setter(into), default)]
673 pub recv_window: Option<rust_decimal::Decimal>,
674}
675
676impl OrderListStatusParams {
677 #[must_use]
680 pub fn builder() -> OrderListStatusParamsBuilder {
681 OrderListStatusParamsBuilder::default()
682 }
683}
684#[derive(Clone, Debug, Builder)]
689#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
690pub struct OrderStatusParams {
691 #[builder(setter(into))]
696 pub symbol: String,
697 #[builder(setter(into), default)]
701 pub id: Option<String>,
702 #[builder(setter(into), default)]
706 pub order_id: Option<i64>,
707 #[builder(setter(into), default)]
711 pub orig_client_order_id: Option<String>,
712 #[builder(setter(into), default)]
716 pub recv_window: Option<rust_decimal::Decimal>,
717}
718
719impl OrderStatusParams {
720 #[must_use]
727 pub fn builder(symbol: String) -> OrderStatusParamsBuilder {
728 OrderStatusParamsBuilder::default().symbol(symbol)
729 }
730}
731
732#[async_trait]
733impl AccountApi for AccountApiClient {
734 async fn account_commission(
735 &self,
736 params: AccountCommissionParams,
737 ) -> anyhow::Result<WebsocketApiResponse<Box<models::AccountCommissionResponseResult>>> {
738 let AccountCommissionParams { symbol, id } = params;
739
740 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
741 payload.insert("symbol".to_string(), serde_json::json!(symbol));
742 if let Some(value) = id {
743 payload.insert("id".to_string(), serde_json::json!(value));
744 }
745 let payload = remove_empty_value(payload);
746
747 self.websocket_api_base
748 .send_message::<Box<models::AccountCommissionResponseResult>>(
749 "/account.commission".trim_start_matches('/'),
750 payload,
751 WebsocketMessageSendOptions::new().signed(),
752 )
753 .await
754 .map_err(anyhow::Error::from)?
755 .into_iter()
756 .next()
757 .ok_or(WebsocketError::NoResponse)
758 .map_err(anyhow::Error::from)
759 }
760
761 async fn account_rate_limits_orders(
762 &self,
763 params: AccountRateLimitsOrdersParams,
764 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::AccountRateLimitsOrdersResponseResultInner>>>
765 {
766 let AccountRateLimitsOrdersParams { id, recv_window } = params;
767
768 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
769 if let Some(value) = id {
770 payload.insert("id".to_string(), serde_json::json!(value));
771 }
772 if let Some(value) = recv_window {
773 payload.insert("recvWindow".to_string(), serde_json::json!(value));
774 }
775 let payload = remove_empty_value(payload);
776
777 self.websocket_api_base
778 .send_message::<Vec<models::AccountRateLimitsOrdersResponseResultInner>>(
779 "/account.rateLimits.orders".trim_start_matches('/'),
780 payload,
781 WebsocketMessageSendOptions::new().signed(),
782 )
783 .await
784 .map_err(anyhow::Error::from)?
785 .into_iter()
786 .next()
787 .ok_or(WebsocketError::NoResponse)
788 .map_err(anyhow::Error::from)
789 }
790
791 async fn account_status(
792 &self,
793 params: AccountStatusParams,
794 ) -> anyhow::Result<WebsocketApiResponse<Box<models::AccountStatusResponseResult>>> {
795 let AccountStatusParams {
796 id,
797 omit_zero_balances,
798 recv_window,
799 } = params;
800
801 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
802 if let Some(value) = id {
803 payload.insert("id".to_string(), serde_json::json!(value));
804 }
805 if let Some(value) = omit_zero_balances {
806 payload.insert("omitZeroBalances".to_string(), serde_json::json!(value));
807 }
808 if let Some(value) = recv_window {
809 payload.insert("recvWindow".to_string(), serde_json::json!(value));
810 }
811 let payload = remove_empty_value(payload);
812
813 self.websocket_api_base
814 .send_message::<Box<models::AccountStatusResponseResult>>(
815 "/account.status".trim_start_matches('/'),
816 payload,
817 WebsocketMessageSendOptions::new().signed(),
818 )
819 .await
820 .map_err(anyhow::Error::from)?
821 .into_iter()
822 .next()
823 .ok_or(WebsocketError::NoResponse)
824 .map_err(anyhow::Error::from)
825 }
826
827 async fn all_order_lists(
828 &self,
829 params: AllOrderListsParams,
830 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::AllOrderListsResponseResultInner>>> {
831 let AllOrderListsParams {
832 id,
833 from_id,
834 start_time,
835 end_time,
836 limit,
837 recv_window,
838 } = params;
839
840 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
841 if let Some(value) = id {
842 payload.insert("id".to_string(), serde_json::json!(value));
843 }
844 if let Some(value) = from_id {
845 payload.insert("fromId".to_string(), serde_json::json!(value));
846 }
847 if let Some(value) = start_time {
848 payload.insert("startTime".to_string(), serde_json::json!(value));
849 }
850 if let Some(value) = end_time {
851 payload.insert("endTime".to_string(), serde_json::json!(value));
852 }
853 if let Some(value) = limit {
854 payload.insert("limit".to_string(), serde_json::json!(value));
855 }
856 if let Some(value) = recv_window {
857 payload.insert("recvWindow".to_string(), serde_json::json!(value));
858 }
859 let payload = remove_empty_value(payload);
860
861 self.websocket_api_base
862 .send_message::<Vec<models::AllOrderListsResponseResultInner>>(
863 "/allOrderLists".trim_start_matches('/'),
864 payload,
865 WebsocketMessageSendOptions::new().signed(),
866 )
867 .await
868 .map_err(anyhow::Error::from)?
869 .into_iter()
870 .next()
871 .ok_or(WebsocketError::NoResponse)
872 .map_err(anyhow::Error::from)
873 }
874
875 async fn all_orders(
876 &self,
877 params: AllOrdersParams,
878 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::AllOrdersResponseResultInner>>> {
879 let AllOrdersParams {
880 symbol,
881 id,
882 order_id,
883 start_time,
884 end_time,
885 limit,
886 recv_window,
887 } = params;
888
889 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
890 payload.insert("symbol".to_string(), serde_json::json!(symbol));
891 if let Some(value) = id {
892 payload.insert("id".to_string(), serde_json::json!(value));
893 }
894 if let Some(value) = order_id {
895 payload.insert("orderId".to_string(), serde_json::json!(value));
896 }
897 if let Some(value) = start_time {
898 payload.insert("startTime".to_string(), serde_json::json!(value));
899 }
900 if let Some(value) = end_time {
901 payload.insert("endTime".to_string(), serde_json::json!(value));
902 }
903 if let Some(value) = limit {
904 payload.insert("limit".to_string(), serde_json::json!(value));
905 }
906 if let Some(value) = recv_window {
907 payload.insert("recvWindow".to_string(), serde_json::json!(value));
908 }
909 let payload = remove_empty_value(payload);
910
911 self.websocket_api_base
912 .send_message::<Vec<models::AllOrdersResponseResultInner>>(
913 "/allOrders".trim_start_matches('/'),
914 payload,
915 WebsocketMessageSendOptions::new().signed(),
916 )
917 .await
918 .map_err(anyhow::Error::from)?
919 .into_iter()
920 .next()
921 .ok_or(WebsocketError::NoResponse)
922 .map_err(anyhow::Error::from)
923 }
924
925 async fn my_allocations(
926 &self,
927 params: MyAllocationsParams,
928 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::MyAllocationsResponseResultInner>>> {
929 let MyAllocationsParams {
930 symbol,
931 id,
932 start_time,
933 end_time,
934 from_allocation_id,
935 limit,
936 order_id,
937 recv_window,
938 } = params;
939
940 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
941 payload.insert("symbol".to_string(), serde_json::json!(symbol));
942 if let Some(value) = id {
943 payload.insert("id".to_string(), serde_json::json!(value));
944 }
945 if let Some(value) = start_time {
946 payload.insert("startTime".to_string(), serde_json::json!(value));
947 }
948 if let Some(value) = end_time {
949 payload.insert("endTime".to_string(), serde_json::json!(value));
950 }
951 if let Some(value) = from_allocation_id {
952 payload.insert("fromAllocationId".to_string(), serde_json::json!(value));
953 }
954 if let Some(value) = limit {
955 payload.insert("limit".to_string(), serde_json::json!(value));
956 }
957 if let Some(value) = order_id {
958 payload.insert("orderId".to_string(), serde_json::json!(value));
959 }
960 if let Some(value) = recv_window {
961 payload.insert("recvWindow".to_string(), serde_json::json!(value));
962 }
963 let payload = remove_empty_value(payload);
964
965 self.websocket_api_base
966 .send_message::<Vec<models::MyAllocationsResponseResultInner>>(
967 "/myAllocations".trim_start_matches('/'),
968 payload,
969 WebsocketMessageSendOptions::new().signed(),
970 )
971 .await
972 .map_err(anyhow::Error::from)?
973 .into_iter()
974 .next()
975 .ok_or(WebsocketError::NoResponse)
976 .map_err(anyhow::Error::from)
977 }
978
979 async fn my_filters(
980 &self,
981 params: MyFiltersParams,
982 ) -> anyhow::Result<WebsocketApiResponse<Box<models::MyFiltersResponseResult>>> {
983 let MyFiltersParams {
984 symbol,
985 id,
986 recv_window,
987 } = params;
988
989 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
990 payload.insert("symbol".to_string(), serde_json::json!(symbol));
991 if let Some(value) = id {
992 payload.insert("id".to_string(), serde_json::json!(value));
993 }
994 if let Some(value) = recv_window {
995 payload.insert("recvWindow".to_string(), serde_json::json!(value));
996 }
997 let payload = remove_empty_value(payload);
998
999 self.websocket_api_base
1000 .send_message::<Box<models::MyFiltersResponseResult>>(
1001 "/myFilters".trim_start_matches('/'),
1002 payload,
1003 WebsocketMessageSendOptions::new().signed(),
1004 )
1005 .await
1006 .map_err(anyhow::Error::from)?
1007 .into_iter()
1008 .next()
1009 .ok_or(WebsocketError::NoResponse)
1010 .map_err(anyhow::Error::from)
1011 }
1012
1013 async fn my_prevented_matches(
1014 &self,
1015 params: MyPreventedMatchesParams,
1016 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::MyPreventedMatchesResponseResultInner>>>
1017 {
1018 let MyPreventedMatchesParams {
1019 symbol,
1020 id,
1021 prevented_match_id,
1022 order_id,
1023 from_prevented_match_id,
1024 limit,
1025 recv_window,
1026 } = params;
1027
1028 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
1029 payload.insert("symbol".to_string(), serde_json::json!(symbol));
1030 if let Some(value) = id {
1031 payload.insert("id".to_string(), serde_json::json!(value));
1032 }
1033 if let Some(value) = prevented_match_id {
1034 payload.insert("preventedMatchId".to_string(), serde_json::json!(value));
1035 }
1036 if let Some(value) = order_id {
1037 payload.insert("orderId".to_string(), serde_json::json!(value));
1038 }
1039 if let Some(value) = from_prevented_match_id {
1040 payload.insert("fromPreventedMatchId".to_string(), serde_json::json!(value));
1041 }
1042 if let Some(value) = limit {
1043 payload.insert("limit".to_string(), serde_json::json!(value));
1044 }
1045 if let Some(value) = recv_window {
1046 payload.insert("recvWindow".to_string(), serde_json::json!(value));
1047 }
1048 let payload = remove_empty_value(payload);
1049
1050 self.websocket_api_base
1051 .send_message::<Vec<models::MyPreventedMatchesResponseResultInner>>(
1052 "/myPreventedMatches".trim_start_matches('/'),
1053 payload,
1054 WebsocketMessageSendOptions::new().signed(),
1055 )
1056 .await
1057 .map_err(anyhow::Error::from)?
1058 .into_iter()
1059 .next()
1060 .ok_or(WebsocketError::NoResponse)
1061 .map_err(anyhow::Error::from)
1062 }
1063
1064 async fn my_trades(
1065 &self,
1066 params: MyTradesParams,
1067 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::MyTradesResponseResultInner>>> {
1068 let MyTradesParams {
1069 symbol,
1070 id,
1071 order_id,
1072 start_time,
1073 end_time,
1074 from_id,
1075 limit,
1076 recv_window,
1077 } = params;
1078
1079 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
1080 payload.insert("symbol".to_string(), serde_json::json!(symbol));
1081 if let Some(value) = id {
1082 payload.insert("id".to_string(), serde_json::json!(value));
1083 }
1084 if let Some(value) = order_id {
1085 payload.insert("orderId".to_string(), serde_json::json!(value));
1086 }
1087 if let Some(value) = start_time {
1088 payload.insert("startTime".to_string(), serde_json::json!(value));
1089 }
1090 if let Some(value) = end_time {
1091 payload.insert("endTime".to_string(), serde_json::json!(value));
1092 }
1093 if let Some(value) = from_id {
1094 payload.insert("fromId".to_string(), serde_json::json!(value));
1095 }
1096 if let Some(value) = limit {
1097 payload.insert("limit".to_string(), serde_json::json!(value));
1098 }
1099 if let Some(value) = recv_window {
1100 payload.insert("recvWindow".to_string(), serde_json::json!(value));
1101 }
1102 let payload = remove_empty_value(payload);
1103
1104 self.websocket_api_base
1105 .send_message::<Vec<models::MyTradesResponseResultInner>>(
1106 "/myTrades".trim_start_matches('/'),
1107 payload,
1108 WebsocketMessageSendOptions::new().signed(),
1109 )
1110 .await
1111 .map_err(anyhow::Error::from)?
1112 .into_iter()
1113 .next()
1114 .ok_or(WebsocketError::NoResponse)
1115 .map_err(anyhow::Error::from)
1116 }
1117
1118 async fn open_order_lists_status(
1119 &self,
1120 params: OpenOrderListsStatusParams,
1121 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::OpenOrderListsStatusResponseResultInner>>>
1122 {
1123 let OpenOrderListsStatusParams { id, recv_window } = params;
1124
1125 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
1126 if let Some(value) = id {
1127 payload.insert("id".to_string(), serde_json::json!(value));
1128 }
1129 if let Some(value) = recv_window {
1130 payload.insert("recvWindow".to_string(), serde_json::json!(value));
1131 }
1132 let payload = remove_empty_value(payload);
1133
1134 self.websocket_api_base
1135 .send_message::<Vec<models::OpenOrderListsStatusResponseResultInner>>(
1136 "/openOrderLists.status".trim_start_matches('/'),
1137 payload,
1138 WebsocketMessageSendOptions::new().signed(),
1139 )
1140 .await
1141 .map_err(anyhow::Error::from)?
1142 .into_iter()
1143 .next()
1144 .ok_or(WebsocketError::NoResponse)
1145 .map_err(anyhow::Error::from)
1146 }
1147
1148 async fn open_orders_status(
1149 &self,
1150 params: OpenOrdersStatusParams,
1151 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::OpenOrdersStatusResponseResultInner>>>
1152 {
1153 let OpenOrdersStatusParams {
1154 id,
1155 symbol,
1156 recv_window,
1157 } = params;
1158
1159 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
1160 if let Some(value) = id {
1161 payload.insert("id".to_string(), serde_json::json!(value));
1162 }
1163 if let Some(value) = symbol {
1164 payload.insert("symbol".to_string(), serde_json::json!(value));
1165 }
1166 if let Some(value) = recv_window {
1167 payload.insert("recvWindow".to_string(), serde_json::json!(value));
1168 }
1169 let payload = remove_empty_value(payload);
1170
1171 self.websocket_api_base
1172 .send_message::<Vec<models::OpenOrdersStatusResponseResultInner>>(
1173 "/openOrders.status".trim_start_matches('/'),
1174 payload,
1175 WebsocketMessageSendOptions::new().signed(),
1176 )
1177 .await
1178 .map_err(anyhow::Error::from)?
1179 .into_iter()
1180 .next()
1181 .ok_or(WebsocketError::NoResponse)
1182 .map_err(anyhow::Error::from)
1183 }
1184
1185 async fn order_amendments(
1186 &self,
1187 params: OrderAmendmentsParams,
1188 ) -> anyhow::Result<WebsocketApiResponse<Vec<models::OrderAmendmentsResponseResultInner>>> {
1189 let OrderAmendmentsParams {
1190 symbol,
1191 order_id,
1192 id,
1193 from_execution_id,
1194 limit,
1195 recv_window,
1196 } = params;
1197
1198 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
1199 payload.insert("symbol".to_string(), serde_json::json!(symbol));
1200 payload.insert("orderId".to_string(), serde_json::json!(order_id));
1201 if let Some(value) = id {
1202 payload.insert("id".to_string(), serde_json::json!(value));
1203 }
1204 if let Some(value) = from_execution_id {
1205 payload.insert("fromExecutionId".to_string(), serde_json::json!(value));
1206 }
1207 if let Some(value) = limit {
1208 payload.insert("limit".to_string(), serde_json::json!(value));
1209 }
1210 if let Some(value) = recv_window {
1211 payload.insert("recvWindow".to_string(), serde_json::json!(value));
1212 }
1213 let payload = remove_empty_value(payload);
1214
1215 self.websocket_api_base
1216 .send_message::<Vec<models::OrderAmendmentsResponseResultInner>>(
1217 "/order.amendments".trim_start_matches('/'),
1218 payload,
1219 WebsocketMessageSendOptions::new().signed(),
1220 )
1221 .await
1222 .map_err(anyhow::Error::from)?
1223 .into_iter()
1224 .next()
1225 .ok_or(WebsocketError::NoResponse)
1226 .map_err(anyhow::Error::from)
1227 }
1228
1229 async fn order_list_status(
1230 &self,
1231 params: OrderListStatusParams,
1232 ) -> anyhow::Result<WebsocketApiResponse<Box<models::AllOrderListsResponseResultInner>>> {
1233 let OrderListStatusParams {
1234 id,
1235 orig_client_order_id,
1236 order_list_id,
1237 recv_window,
1238 } = params;
1239
1240 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
1241 if let Some(value) = id {
1242 payload.insert("id".to_string(), serde_json::json!(value));
1243 }
1244 if let Some(value) = orig_client_order_id {
1245 payload.insert("origClientOrderId".to_string(), serde_json::json!(value));
1246 }
1247 if let Some(value) = order_list_id {
1248 payload.insert("orderListId".to_string(), serde_json::json!(value));
1249 }
1250 if let Some(value) = recv_window {
1251 payload.insert("recvWindow".to_string(), serde_json::json!(value));
1252 }
1253 let payload = remove_empty_value(payload);
1254
1255 self.websocket_api_base
1256 .send_message::<Box<models::AllOrderListsResponseResultInner>>(
1257 "/orderList.status".trim_start_matches('/'),
1258 payload,
1259 WebsocketMessageSendOptions::new().signed(),
1260 )
1261 .await
1262 .map_err(anyhow::Error::from)?
1263 .into_iter()
1264 .next()
1265 .ok_or(WebsocketError::NoResponse)
1266 .map_err(anyhow::Error::from)
1267 }
1268
1269 async fn order_status(
1270 &self,
1271 params: OrderStatusParams,
1272 ) -> anyhow::Result<WebsocketApiResponse<Box<models::OrderStatusResponseResult>>> {
1273 let OrderStatusParams {
1274 symbol,
1275 id,
1276 order_id,
1277 orig_client_order_id,
1278 recv_window,
1279 } = params;
1280
1281 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
1282 payload.insert("symbol".to_string(), serde_json::json!(symbol));
1283 if let Some(value) = id {
1284 payload.insert("id".to_string(), serde_json::json!(value));
1285 }
1286 if let Some(value) = order_id {
1287 payload.insert("orderId".to_string(), serde_json::json!(value));
1288 }
1289 if let Some(value) = orig_client_order_id {
1290 payload.insert("origClientOrderId".to_string(), serde_json::json!(value));
1291 }
1292 if let Some(value) = recv_window {
1293 payload.insert("recvWindow".to_string(), serde_json::json!(value));
1294 }
1295 let payload = remove_empty_value(payload);
1296
1297 self.websocket_api_base
1298 .send_message::<Box<models::OrderStatusResponseResult>>(
1299 "/order.status".trim_start_matches('/'),
1300 payload,
1301 WebsocketMessageSendOptions::new().signed(),
1302 )
1303 .await
1304 .map_err(anyhow::Error::from)?
1305 .into_iter()
1306 .next()
1307 .ok_or(WebsocketError::NoResponse)
1308 .map_err(anyhow::Error::from)
1309 }
1310}
1311
1312#[cfg(all(test, feature = "spot"))]
1313mod tests {
1314 use super::*;
1315 use crate::TOKIO_SHARED_RT;
1316 use crate::common::websocket::{WebsocketApi, WebsocketConnection, WebsocketHandler};
1317 use crate::config::ConfigurationWebsocketApi;
1318 use crate::errors::WebsocketError;
1319 use crate::models::WebsocketApiRateLimit;
1320 use serde_json::{Value, json};
1321 use tokio::spawn;
1322 use tokio::sync::mpsc::{UnboundedReceiver, unbounded_channel};
1323 use tokio::time::{Duration, timeout};
1324 use tokio_tungstenite::tungstenite::Message;
1325
1326 async fn setup() -> (
1327 Arc<WebsocketApi>,
1328 Arc<WebsocketConnection>,
1329 UnboundedReceiver<Message>,
1330 ) {
1331 let conn = WebsocketConnection::new("test-conn");
1332 let (tx, rx) = unbounded_channel::<Message>();
1333 {
1334 let mut conn_state = conn.state.lock().await;
1335 conn_state.ws_write_tx = Some(tx);
1336 }
1337
1338 let config = ConfigurationWebsocketApi::builder()
1339 .api_key("key")
1340 .api_secret("secret")
1341 .build()
1342 .expect("Failed to build configuration");
1343 let ws_api = WebsocketApi::new(config, vec![conn.clone()]);
1344 conn.set_handler(ws_api.clone() as Arc<dyn WebsocketHandler>)
1345 .await;
1346 ws_api.clone().connect().await.unwrap();
1347
1348 (ws_api, conn, rx)
1349 }
1350
1351 #[test]
1352 fn account_commission_success() {
1353 TOKIO_SHARED_RT.block_on(async {
1354 let (ws_api, conn, mut rx) = setup().await;
1355 let client = AccountApiClient::new(ws_api.clone());
1356
1357 let handle = spawn(async move {
1358 let params = AccountCommissionParams::builder("BNBUSDT".to_string(),).build().unwrap();
1359 client.account_commission(params).await
1360 });
1361
1362 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
1363 let Message::Text(text) = sent else { panic!() };
1364 let v: Value = serde_json::from_str(&text).unwrap();
1365 let id = v["id"].as_str().unwrap();
1366 assert_eq!(v["method"], "/account.commission".trim_start_matches('/'));
1367
1368 let mut resp_json: Value = serde_json::from_str(r#"{"id":"d3df8a61-98ea-4fe0-8f4e-0fcea5d418b0","status":200,"result":{"symbol":"BTCUSDT","standardCommission":{"maker":"0.00000010","taker":"0.00000020","buyer":"0.00000030","seller":"0.00000040"},"specialCommission":{"maker":"0.01000000","taker":"0.02000000","buyer":"0.03000000","seller":"0.04000000"},"taxCommission":{"maker":"0.00000112","taker":"0.00000114","buyer":"0.00000118","seller":"0.00000116"},"discount":{"enabledForAccount":true,"enabledForSymbol":true,"discountAsset":"BNB","discount":"0.75000000"}},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":20}]}"#).unwrap();
1369 resp_json["id"] = id.into();
1370
1371 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
1372 let expected_data: Box<models::AccountCommissionResponseResult> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
1373 let empty_array = Value::Array(vec![]);
1374 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
1375 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
1376 match raw_rate_limits.as_array() {
1377 Some(arr) if arr.is_empty() => None,
1378 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
1379 None => None,
1380 };
1381
1382 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
1383
1384 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
1385
1386
1387 let response_rate_limits = response.rate_limits.clone();
1388 let response_data = response.data().expect("deserialize data");
1389
1390 assert_eq!(response_rate_limits, expected_rate_limits);
1391 assert_eq!(response_data, expected_data);
1392 });
1393 }
1394
1395 #[test]
1396 fn account_commission_error_response() {
1397 TOKIO_SHARED_RT.block_on(async {
1398 let (ws_api, conn, mut rx) = setup().await;
1399 let client = AccountApiClient::new(ws_api.clone());
1400
1401 let handle = tokio::spawn(async move {
1402 let params = AccountCommissionParams::builder("BNBUSDT".to_string(),).build().unwrap();
1403 client.account_commission(params).await
1404 });
1405
1406 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
1407 let Message::Text(text) = sent else { panic!() };
1408 let v: Value = serde_json::from_str(&text).unwrap();
1409 let id = v["id"].as_str().unwrap().to_string();
1410
1411 let resp_json = json!({
1412 "id": id,
1413 "status": 400,
1414 "error": {
1415 "code": -2010,
1416 "msg": "Account has insufficient balance for requested action.",
1417 },
1418 "rateLimits": [
1419 {
1420 "rateLimitType": "ORDERS",
1421 "interval": "SECOND",
1422 "intervalNum": 10,
1423 "limit": 50,
1424 "count": 13
1425 },
1426 ],
1427 });
1428 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
1429
1430 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
1431 match join {
1432 Ok(Err(e)) => {
1433 let msg = e.to_string();
1434 assert!(
1435 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
1436 "Expected error msg to contain server error, got: {msg}"
1437 );
1438 }
1439 Ok(Ok(_)) => panic!("Expected error"),
1440 Err(_) => panic!("Task panicked"),
1441 }
1442 });
1443 }
1444
1445 #[test]
1446 fn account_commission_request_timeout() {
1447 TOKIO_SHARED_RT.block_on(async {
1448 let (ws_api, _conn, mut rx) = setup().await;
1449 let client = AccountApiClient::new(ws_api.clone());
1450
1451 let handle = spawn(async move {
1452 let params = AccountCommissionParams::builder("BNBUSDT".to_string())
1453 .build()
1454 .unwrap();
1455 client.account_commission(params).await
1456 });
1457
1458 let sent = timeout(Duration::from_secs(1), rx.recv())
1459 .await
1460 .expect("send should occur")
1461 .expect("channel closed");
1462 let Message::Text(text) = sent else {
1463 panic!("expected Message Text")
1464 };
1465
1466 let _: Value = serde_json::from_str(&text).unwrap();
1467
1468 let result = handle.await.expect("task completed");
1469 match result {
1470 Err(e) => {
1471 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
1472 assert!(matches!(inner, WebsocketError::Timeout));
1473 } else {
1474 panic!("Unexpected error type: {:?}", e);
1475 }
1476 }
1477 Ok(_) => panic!("Expected timeout error"),
1478 }
1479 });
1480 }
1481
1482 #[test]
1483 fn account_rate_limits_orders_success() {
1484 TOKIO_SHARED_RT.block_on(async {
1485 let (ws_api, conn, mut rx) = setup().await;
1486 let client = AccountApiClient::new(ws_api.clone());
1487
1488 let handle = spawn(async move {
1489 let params = AccountRateLimitsOrdersParams::builder().build().unwrap();
1490 client.account_rate_limits_orders(params).await
1491 });
1492
1493 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
1494 let Message::Text(text) = sent else { panic!() };
1495 let v: Value = serde_json::from_str(&text).unwrap();
1496 let id = v["id"].as_str().unwrap();
1497 assert_eq!(v["method"], "/account.rateLimits.orders".trim_start_matches('/'));
1498
1499 let mut resp_json: Value = serde_json::from_str(r#"{"id":"d3783d8d-f8d1-4d2c-b8a0-b7596af5a664","status":200,"result":[{"rateLimitType":"ORDERS","interval":"DAY","intervalNum":1,"limit":160000,"count":0},{"rateLimitType":"ORDERS","interval":"SECOND","intervalNum":10,"limit":50,"count":0}],"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":40}]}"#).unwrap();
1500 resp_json["id"] = id.into();
1501
1502 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
1503 let expected_data: Vec<models::AccountRateLimitsOrdersResponseResultInner> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
1504 let empty_array = Value::Array(vec![]);
1505 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
1506 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
1507 match raw_rate_limits.as_array() {
1508 Some(arr) if arr.is_empty() => None,
1509 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
1510 None => None,
1511 };
1512
1513 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
1514
1515 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
1516
1517
1518 let response_rate_limits = response.rate_limits.clone();
1519 let response_data = response.data().expect("deserialize data");
1520
1521 assert_eq!(response_rate_limits, expected_rate_limits);
1522 assert_eq!(response_data, expected_data);
1523 });
1524 }
1525
1526 #[test]
1527 fn account_rate_limits_orders_error_response() {
1528 TOKIO_SHARED_RT.block_on(async {
1529 let (ws_api, conn, mut rx) = setup().await;
1530 let client = AccountApiClient::new(ws_api.clone());
1531
1532 let handle = tokio::spawn(async move {
1533 let params = AccountRateLimitsOrdersParams::builder().build().unwrap();
1534 client.account_rate_limits_orders(params).await
1535 });
1536
1537 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
1538 let Message::Text(text) = sent else { panic!() };
1539 let v: Value = serde_json::from_str(&text).unwrap();
1540 let id = v["id"].as_str().unwrap().to_string();
1541
1542 let resp_json = json!({
1543 "id": id,
1544 "status": 400,
1545 "error": {
1546 "code": -2010,
1547 "msg": "Account has insufficient balance for requested action.",
1548 },
1549 "rateLimits": [
1550 {
1551 "rateLimitType": "ORDERS",
1552 "interval": "SECOND",
1553 "intervalNum": 10,
1554 "limit": 50,
1555 "count": 13
1556 },
1557 ],
1558 });
1559 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
1560
1561 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
1562 match join {
1563 Ok(Err(e)) => {
1564 let msg = e.to_string();
1565 assert!(
1566 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
1567 "Expected error msg to contain server error, got: {msg}"
1568 );
1569 }
1570 Ok(Ok(_)) => panic!("Expected error"),
1571 Err(_) => panic!("Task panicked"),
1572 }
1573 });
1574 }
1575
1576 #[test]
1577 fn account_rate_limits_orders_request_timeout() {
1578 TOKIO_SHARED_RT.block_on(async {
1579 let (ws_api, _conn, mut rx) = setup().await;
1580 let client = AccountApiClient::new(ws_api.clone());
1581
1582 let handle = spawn(async move {
1583 let params = AccountRateLimitsOrdersParams::builder().build().unwrap();
1584 client.account_rate_limits_orders(params).await
1585 });
1586
1587 let sent = timeout(Duration::from_secs(1), rx.recv())
1588 .await
1589 .expect("send should occur")
1590 .expect("channel closed");
1591 let Message::Text(text) = sent else {
1592 panic!("expected Message Text")
1593 };
1594
1595 let _: Value = serde_json::from_str(&text).unwrap();
1596
1597 let result = handle.await.expect("task completed");
1598 match result {
1599 Err(e) => {
1600 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
1601 assert!(matches!(inner, WebsocketError::Timeout));
1602 } else {
1603 panic!("Unexpected error type: {:?}", e);
1604 }
1605 }
1606 Ok(_) => panic!("Expected timeout error"),
1607 }
1608 });
1609 }
1610
1611 #[test]
1612 fn account_status_success() {
1613 TOKIO_SHARED_RT.block_on(async {
1614 let (ws_api, conn, mut rx) = setup().await;
1615 let client = AccountApiClient::new(ws_api.clone());
1616
1617 let handle = spawn(async move {
1618 let params = AccountStatusParams::builder().build().unwrap();
1619 client.account_status(params).await
1620 });
1621
1622 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
1623 let Message::Text(text) = sent else { panic!() };
1624 let v: Value = serde_json::from_str(&text).unwrap();
1625 let id = v["id"].as_str().unwrap();
1626 assert_eq!(v["method"], "/account.status".trim_start_matches('/'));
1627
1628 let mut resp_json: Value = serde_json::from_str(r#"{"id":"605a6d20-6588-4cb9-afa0-b0ab087507ba","status":200,"result":{"makerCommission":15,"takerCommission":15,"buyerCommission":0,"sellerCommission":0,"canTrade":true,"canWithdraw":true,"canDeposit":true,"commissionRates":{"maker":"0.00150000","taker":"0.00150000","buyer":"0.00000000","seller":"0.00000000"},"brokered":false,"requireSelfTradePrevention":false,"preventSor":false,"updateTime":1660801833000,"accountType":"SPOT","balances":[{"asset":"USDT","free":"1021.21000000","locked":"0.00000000"},{"asset":"BTC","free":"1.3447112","locked":"0.08600000"},{"asset":"BNB","free":"0.00000000","locked":"0.00000000"}],"permissions":["SPOT"],"uid":354937868},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":20}]}"#).unwrap();
1629 resp_json["id"] = id.into();
1630
1631 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
1632 let expected_data: Box<models::AccountStatusResponseResult> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
1633 let empty_array = Value::Array(vec![]);
1634 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
1635 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
1636 match raw_rate_limits.as_array() {
1637 Some(arr) if arr.is_empty() => None,
1638 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
1639 None => None,
1640 };
1641
1642 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
1643
1644 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
1645
1646
1647 let response_rate_limits = response.rate_limits.clone();
1648 let response_data = response.data().expect("deserialize data");
1649
1650 assert_eq!(response_rate_limits, expected_rate_limits);
1651 assert_eq!(response_data, expected_data);
1652 });
1653 }
1654
1655 #[test]
1656 fn account_status_error_response() {
1657 TOKIO_SHARED_RT.block_on(async {
1658 let (ws_api, conn, mut rx) = setup().await;
1659 let client = AccountApiClient::new(ws_api.clone());
1660
1661 let handle = tokio::spawn(async move {
1662 let params = AccountStatusParams::builder().build().unwrap();
1663 client.account_status(params).await
1664 });
1665
1666 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
1667 let Message::Text(text) = sent else { panic!() };
1668 let v: Value = serde_json::from_str(&text).unwrap();
1669 let id = v["id"].as_str().unwrap().to_string();
1670
1671 let resp_json = json!({
1672 "id": id,
1673 "status": 400,
1674 "error": {
1675 "code": -2010,
1676 "msg": "Account has insufficient balance for requested action.",
1677 },
1678 "rateLimits": [
1679 {
1680 "rateLimitType": "ORDERS",
1681 "interval": "SECOND",
1682 "intervalNum": 10,
1683 "limit": 50,
1684 "count": 13
1685 },
1686 ],
1687 });
1688 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
1689
1690 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
1691 match join {
1692 Ok(Err(e)) => {
1693 let msg = e.to_string();
1694 assert!(
1695 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
1696 "Expected error msg to contain server error, got: {msg}"
1697 );
1698 }
1699 Ok(Ok(_)) => panic!("Expected error"),
1700 Err(_) => panic!("Task panicked"),
1701 }
1702 });
1703 }
1704
1705 #[test]
1706 fn account_status_request_timeout() {
1707 TOKIO_SHARED_RT.block_on(async {
1708 let (ws_api, _conn, mut rx) = setup().await;
1709 let client = AccountApiClient::new(ws_api.clone());
1710
1711 let handle = spawn(async move {
1712 let params = AccountStatusParams::builder().build().unwrap();
1713 client.account_status(params).await
1714 });
1715
1716 let sent = timeout(Duration::from_secs(1), rx.recv())
1717 .await
1718 .expect("send should occur")
1719 .expect("channel closed");
1720 let Message::Text(text) = sent else {
1721 panic!("expected Message Text")
1722 };
1723
1724 let _: Value = serde_json::from_str(&text).unwrap();
1725
1726 let result = handle.await.expect("task completed");
1727 match result {
1728 Err(e) => {
1729 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
1730 assert!(matches!(inner, WebsocketError::Timeout));
1731 } else {
1732 panic!("Unexpected error type: {:?}", e);
1733 }
1734 }
1735 Ok(_) => panic!("Expected timeout error"),
1736 }
1737 });
1738 }
1739
1740 #[test]
1741 fn all_order_lists_success() {
1742 TOKIO_SHARED_RT.block_on(async {
1743 let (ws_api, conn, mut rx) = setup().await;
1744 let client = AccountApiClient::new(ws_api.clone());
1745
1746 let handle = spawn(async move {
1747 let params = AllOrderListsParams::builder().build().unwrap();
1748 client.all_order_lists(params).await
1749 });
1750
1751 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
1752 let Message::Text(text) = sent else { panic!() };
1753 let v: Value = serde_json::from_str(&text).unwrap();
1754 let id = v["id"].as_str().unwrap();
1755 assert_eq!(v["method"], "/allOrderLists".trim_start_matches('/'));
1756
1757 let mut resp_json: Value = serde_json::from_str(r#"{"id":"8617b7b3-1b3d-4dec-94cd-eefd929b8ceb","status":200,"result":[{"orderListId":1274512,"contingencyType":"OCO","listStatusType":"EXEC_STARTED","listOrderStatus":"EXECUTING","listClientOrderId":"08985fedd9ea2cf6b28996","transactionTime":1660801713793,"symbol":"BTCUSDT","orders":[{"symbol":"BTCUSDT","orderId":12569138902,"clientOrderId":"jLnZpj5enfMXTuhKB1d0us"},{"symbol":"BTCUSDT","orderId":12569138901,"clientOrderId":"BqtFCj5odMoWtSqGk2X9tU"}]}],"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":20}]}"#).unwrap();
1758 resp_json["id"] = id.into();
1759
1760 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
1761 let expected_data: Vec<models::AllOrderListsResponseResultInner> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
1762 let empty_array = Value::Array(vec![]);
1763 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
1764 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
1765 match raw_rate_limits.as_array() {
1766 Some(arr) if arr.is_empty() => None,
1767 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
1768 None => None,
1769 };
1770
1771 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
1772
1773 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
1774
1775
1776 let response_rate_limits = response.rate_limits.clone();
1777 let response_data = response.data().expect("deserialize data");
1778
1779 assert_eq!(response_rate_limits, expected_rate_limits);
1780 assert_eq!(response_data, expected_data);
1781 });
1782 }
1783
1784 #[test]
1785 fn all_order_lists_error_response() {
1786 TOKIO_SHARED_RT.block_on(async {
1787 let (ws_api, conn, mut rx) = setup().await;
1788 let client = AccountApiClient::new(ws_api.clone());
1789
1790 let handle = tokio::spawn(async move {
1791 let params = AllOrderListsParams::builder().build().unwrap();
1792 client.all_order_lists(params).await
1793 });
1794
1795 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
1796 let Message::Text(text) = sent else { panic!() };
1797 let v: Value = serde_json::from_str(&text).unwrap();
1798 let id = v["id"].as_str().unwrap().to_string();
1799
1800 let resp_json = json!({
1801 "id": id,
1802 "status": 400,
1803 "error": {
1804 "code": -2010,
1805 "msg": "Account has insufficient balance for requested action.",
1806 },
1807 "rateLimits": [
1808 {
1809 "rateLimitType": "ORDERS",
1810 "interval": "SECOND",
1811 "intervalNum": 10,
1812 "limit": 50,
1813 "count": 13
1814 },
1815 ],
1816 });
1817 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
1818
1819 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
1820 match join {
1821 Ok(Err(e)) => {
1822 let msg = e.to_string();
1823 assert!(
1824 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
1825 "Expected error msg to contain server error, got: {msg}"
1826 );
1827 }
1828 Ok(Ok(_)) => panic!("Expected error"),
1829 Err(_) => panic!("Task panicked"),
1830 }
1831 });
1832 }
1833
1834 #[test]
1835 fn all_order_lists_request_timeout() {
1836 TOKIO_SHARED_RT.block_on(async {
1837 let (ws_api, _conn, mut rx) = setup().await;
1838 let client = AccountApiClient::new(ws_api.clone());
1839
1840 let handle = spawn(async move {
1841 let params = AllOrderListsParams::builder().build().unwrap();
1842 client.all_order_lists(params).await
1843 });
1844
1845 let sent = timeout(Duration::from_secs(1), rx.recv())
1846 .await
1847 .expect("send should occur")
1848 .expect("channel closed");
1849 let Message::Text(text) = sent else {
1850 panic!("expected Message Text")
1851 };
1852
1853 let _: Value = serde_json::from_str(&text).unwrap();
1854
1855 let result = handle.await.expect("task completed");
1856 match result {
1857 Err(e) => {
1858 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
1859 assert!(matches!(inner, WebsocketError::Timeout));
1860 } else {
1861 panic!("Unexpected error type: {:?}", e);
1862 }
1863 }
1864 Ok(_) => panic!("Expected timeout error"),
1865 }
1866 });
1867 }
1868
1869 #[test]
1870 fn all_orders_success() {
1871 TOKIO_SHARED_RT.block_on(async {
1872 let (ws_api, conn, mut rx) = setup().await;
1873 let client = AccountApiClient::new(ws_api.clone());
1874
1875 let handle = spawn(async move {
1876 let params = AllOrdersParams::builder("BNBUSDT".to_string(),).build().unwrap();
1877 client.all_orders(params).await
1878 });
1879
1880 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
1881 let Message::Text(text) = sent else { panic!() };
1882 let v: Value = serde_json::from_str(&text).unwrap();
1883 let id = v["id"].as_str().unwrap();
1884 assert_eq!(v["method"], "/allOrders".trim_start_matches('/'));
1885
1886 let mut resp_json: Value = serde_json::from_str(r#"{"id":"734235c2-13d2-4574-be68-723e818c08f3","status":200,"result":[{"symbol":"BTCUSDT","orderId":12569099453,"orderListId":-1,"clientOrderId":"4d96324ff9d44481926157","price":"23416.10000000","origQty":"0.00847000","executedQty":"0.00847000","cummulativeQuoteQty":"198.33521500","status":"FILLED","timeInForce":"GTC","type":"LIMIT","side":"SELL","stopPrice":"0.00000000","icebergQty":"0.00000000","time":1660801715639,"updateTime":1660801717945,"isWorking":true,"workingTime":1660801715639,"origQuoteOrderQty":"0.00000000","selfTradePreventionMode":"NONE","preventedMatchId":0,"preventedQuantity":"1.200000"}],"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":20}]}"#).unwrap();
1887 resp_json["id"] = id.into();
1888
1889 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
1890 let expected_data: Vec<models::AllOrdersResponseResultInner> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
1891 let empty_array = Value::Array(vec![]);
1892 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
1893 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
1894 match raw_rate_limits.as_array() {
1895 Some(arr) if arr.is_empty() => None,
1896 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
1897 None => None,
1898 };
1899
1900 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
1901
1902 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
1903
1904
1905 let response_rate_limits = response.rate_limits.clone();
1906 let response_data = response.data().expect("deserialize data");
1907
1908 assert_eq!(response_rate_limits, expected_rate_limits);
1909 assert_eq!(response_data, expected_data);
1910 });
1911 }
1912
1913 #[test]
1914 fn all_orders_error_response() {
1915 TOKIO_SHARED_RT.block_on(async {
1916 let (ws_api, conn, mut rx) = setup().await;
1917 let client = AccountApiClient::new(ws_api.clone());
1918
1919 let handle = tokio::spawn(async move {
1920 let params = AllOrdersParams::builder("BNBUSDT".to_string(),).build().unwrap();
1921 client.all_orders(params).await
1922 });
1923
1924 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
1925 let Message::Text(text) = sent else { panic!() };
1926 let v: Value = serde_json::from_str(&text).unwrap();
1927 let id = v["id"].as_str().unwrap().to_string();
1928
1929 let resp_json = json!({
1930 "id": id,
1931 "status": 400,
1932 "error": {
1933 "code": -2010,
1934 "msg": "Account has insufficient balance for requested action.",
1935 },
1936 "rateLimits": [
1937 {
1938 "rateLimitType": "ORDERS",
1939 "interval": "SECOND",
1940 "intervalNum": 10,
1941 "limit": 50,
1942 "count": 13
1943 },
1944 ],
1945 });
1946 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
1947
1948 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
1949 match join {
1950 Ok(Err(e)) => {
1951 let msg = e.to_string();
1952 assert!(
1953 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
1954 "Expected error msg to contain server error, got: {msg}"
1955 );
1956 }
1957 Ok(Ok(_)) => panic!("Expected error"),
1958 Err(_) => panic!("Task panicked"),
1959 }
1960 });
1961 }
1962
1963 #[test]
1964 fn all_orders_request_timeout() {
1965 TOKIO_SHARED_RT.block_on(async {
1966 let (ws_api, _conn, mut rx) = setup().await;
1967 let client = AccountApiClient::new(ws_api.clone());
1968
1969 let handle = spawn(async move {
1970 let params = AllOrdersParams::builder("BNBUSDT".to_string())
1971 .build()
1972 .unwrap();
1973 client.all_orders(params).await
1974 });
1975
1976 let sent = timeout(Duration::from_secs(1), rx.recv())
1977 .await
1978 .expect("send should occur")
1979 .expect("channel closed");
1980 let Message::Text(text) = sent else {
1981 panic!("expected Message Text")
1982 };
1983
1984 let _: Value = serde_json::from_str(&text).unwrap();
1985
1986 let result = handle.await.expect("task completed");
1987 match result {
1988 Err(e) => {
1989 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
1990 assert!(matches!(inner, WebsocketError::Timeout));
1991 } else {
1992 panic!("Unexpected error type: {:?}", e);
1993 }
1994 }
1995 Ok(_) => panic!("Expected timeout error"),
1996 }
1997 });
1998 }
1999
2000 #[test]
2001 fn my_allocations_success() {
2002 TOKIO_SHARED_RT.block_on(async {
2003 let (ws_api, conn, mut rx) = setup().await;
2004 let client = AccountApiClient::new(ws_api.clone());
2005
2006 let handle = spawn(async move {
2007 let params = MyAllocationsParams::builder("BNBUSDT".to_string(),).build().unwrap();
2008 client.my_allocations(params).await
2009 });
2010
2011 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
2012 let Message::Text(text) = sent else { panic!() };
2013 let v: Value = serde_json::from_str(&text).unwrap();
2014 let id = v["id"].as_str().unwrap();
2015 assert_eq!(v["method"], "/myAllocations".trim_start_matches('/'));
2016
2017 let mut resp_json: Value = serde_json::from_str(r#"{"id":"g4ce6a53-a39d-4f71-823b-4ab5r391d6y8","status":200,"result":[{"symbol":"BTCUSDT","allocationId":0,"allocationType":"SOR","orderId":500,"orderListId":-1,"price":"1.00000000","qty":"0.10000000","quoteQty":"0.10000000","commission":"0.00000000","commissionAsset":"BTC","time":1687319487614,"isBuyer":false,"isMaker":false,"isAllocator":false}],"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":20}]}"#).unwrap();
2018 resp_json["id"] = id.into();
2019
2020 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
2021 let expected_data: Vec<models::MyAllocationsResponseResultInner> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
2022 let empty_array = Value::Array(vec![]);
2023 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
2024 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
2025 match raw_rate_limits.as_array() {
2026 Some(arr) if arr.is_empty() => None,
2027 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
2028 None => None,
2029 };
2030
2031 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2032
2033 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
2034
2035
2036 let response_rate_limits = response.rate_limits.clone();
2037 let response_data = response.data().expect("deserialize data");
2038
2039 assert_eq!(response_rate_limits, expected_rate_limits);
2040 assert_eq!(response_data, expected_data);
2041 });
2042 }
2043
2044 #[test]
2045 fn my_allocations_error_response() {
2046 TOKIO_SHARED_RT.block_on(async {
2047 let (ws_api, conn, mut rx) = setup().await;
2048 let client = AccountApiClient::new(ws_api.clone());
2049
2050 let handle = tokio::spawn(async move {
2051 let params = MyAllocationsParams::builder("BNBUSDT".to_string(),).build().unwrap();
2052 client.my_allocations(params).await
2053 });
2054
2055 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
2056 let Message::Text(text) = sent else { panic!() };
2057 let v: Value = serde_json::from_str(&text).unwrap();
2058 let id = v["id"].as_str().unwrap().to_string();
2059
2060 let resp_json = json!({
2061 "id": id,
2062 "status": 400,
2063 "error": {
2064 "code": -2010,
2065 "msg": "Account has insufficient balance for requested action.",
2066 },
2067 "rateLimits": [
2068 {
2069 "rateLimitType": "ORDERS",
2070 "interval": "SECOND",
2071 "intervalNum": 10,
2072 "limit": 50,
2073 "count": 13
2074 },
2075 ],
2076 });
2077 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2078
2079 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
2080 match join {
2081 Ok(Err(e)) => {
2082 let msg = e.to_string();
2083 assert!(
2084 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
2085 "Expected error msg to contain server error, got: {msg}"
2086 );
2087 }
2088 Ok(Ok(_)) => panic!("Expected error"),
2089 Err(_) => panic!("Task panicked"),
2090 }
2091 });
2092 }
2093
2094 #[test]
2095 fn my_allocations_request_timeout() {
2096 TOKIO_SHARED_RT.block_on(async {
2097 let (ws_api, _conn, mut rx) = setup().await;
2098 let client = AccountApiClient::new(ws_api.clone());
2099
2100 let handle = spawn(async move {
2101 let params = MyAllocationsParams::builder("BNBUSDT".to_string())
2102 .build()
2103 .unwrap();
2104 client.my_allocations(params).await
2105 });
2106
2107 let sent = timeout(Duration::from_secs(1), rx.recv())
2108 .await
2109 .expect("send should occur")
2110 .expect("channel closed");
2111 let Message::Text(text) = sent else {
2112 panic!("expected Message Text")
2113 };
2114
2115 let _: Value = serde_json::from_str(&text).unwrap();
2116
2117 let result = handle.await.expect("task completed");
2118 match result {
2119 Err(e) => {
2120 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
2121 assert!(matches!(inner, WebsocketError::Timeout));
2122 } else {
2123 panic!("Unexpected error type: {:?}", e);
2124 }
2125 }
2126 Ok(_) => panic!("Expected timeout error"),
2127 }
2128 });
2129 }
2130
2131 #[test]
2132 fn my_filters_success() {
2133 TOKIO_SHARED_RT.block_on(async {
2134 let (ws_api, conn, mut rx) = setup().await;
2135 let client = AccountApiClient::new(ws_api.clone());
2136
2137 let handle = spawn(async move {
2138 let params = MyFiltersParams::builder("BNBUSDT".to_string(),).build().unwrap();
2139 client.my_filters(params).await
2140 });
2141
2142 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
2143 let Message::Text(text) = sent else { panic!() };
2144 let v: Value = serde_json::from_str(&text).unwrap();
2145 let id = v["id"].as_str().unwrap();
2146 assert_eq!(v["method"], "/myFilters".trim_start_matches('/'));
2147
2148 let mut resp_json: Value = serde_json::from_str(r#"{"id":"1758009606869","status":200,"result":{"exchangeFilters":[{"filterType":"EXCHANGE_MAX_NUM_ORDERS","maxNumOrders":1000}],"symbolFilters":[{"filterType":"MAX_NUM_ORDER_LISTS","maxNumOrderLists":20}],"assetFilters":[{"filterType":"MAX_ASSET","asset":"JPY","limit":"1000000.00000000"}]},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000},{"rateLimitType":"ORDERS","interval":"DAY","intervalNum":1,"limit":160000},{"rateLimitType":"RAW_REQUESTS","interval":"MINUTE","intervalNum":5,"limit":61000}]}"#).unwrap();
2149 resp_json["id"] = id.into();
2150
2151 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
2152 let expected_data: Box<models::MyFiltersResponseResult> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
2153 let empty_array = Value::Array(vec![]);
2154 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
2155 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
2156 match raw_rate_limits.as_array() {
2157 Some(arr) if arr.is_empty() => None,
2158 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
2159 None => None,
2160 };
2161
2162 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2163
2164 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
2165
2166
2167 let response_rate_limits = response.rate_limits.clone();
2168 let response_data = response.data().expect("deserialize data");
2169
2170 assert_eq!(response_rate_limits, expected_rate_limits);
2171 assert_eq!(response_data, expected_data);
2172 });
2173 }
2174
2175 #[test]
2176 fn my_filters_error_response() {
2177 TOKIO_SHARED_RT.block_on(async {
2178 let (ws_api, conn, mut rx) = setup().await;
2179 let client = AccountApiClient::new(ws_api.clone());
2180
2181 let handle = tokio::spawn(async move {
2182 let params = MyFiltersParams::builder("BNBUSDT".to_string(),).build().unwrap();
2183 client.my_filters(params).await
2184 });
2185
2186 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
2187 let Message::Text(text) = sent else { panic!() };
2188 let v: Value = serde_json::from_str(&text).unwrap();
2189 let id = v["id"].as_str().unwrap().to_string();
2190
2191 let resp_json = json!({
2192 "id": id,
2193 "status": 400,
2194 "error": {
2195 "code": -2010,
2196 "msg": "Account has insufficient balance for requested action.",
2197 },
2198 "rateLimits": [
2199 {
2200 "rateLimitType": "ORDERS",
2201 "interval": "SECOND",
2202 "intervalNum": 10,
2203 "limit": 50,
2204 "count": 13
2205 },
2206 ],
2207 });
2208 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2209
2210 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
2211 match join {
2212 Ok(Err(e)) => {
2213 let msg = e.to_string();
2214 assert!(
2215 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
2216 "Expected error msg to contain server error, got: {msg}"
2217 );
2218 }
2219 Ok(Ok(_)) => panic!("Expected error"),
2220 Err(_) => panic!("Task panicked"),
2221 }
2222 });
2223 }
2224
2225 #[test]
2226 fn my_filters_request_timeout() {
2227 TOKIO_SHARED_RT.block_on(async {
2228 let (ws_api, _conn, mut rx) = setup().await;
2229 let client = AccountApiClient::new(ws_api.clone());
2230
2231 let handle = spawn(async move {
2232 let params = MyFiltersParams::builder("BNBUSDT".to_string())
2233 .build()
2234 .unwrap();
2235 client.my_filters(params).await
2236 });
2237
2238 let sent = timeout(Duration::from_secs(1), rx.recv())
2239 .await
2240 .expect("send should occur")
2241 .expect("channel closed");
2242 let Message::Text(text) = sent else {
2243 panic!("expected Message Text")
2244 };
2245
2246 let _: Value = serde_json::from_str(&text).unwrap();
2247
2248 let result = handle.await.expect("task completed");
2249 match result {
2250 Err(e) => {
2251 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
2252 assert!(matches!(inner, WebsocketError::Timeout));
2253 } else {
2254 panic!("Unexpected error type: {:?}", e);
2255 }
2256 }
2257 Ok(_) => panic!("Expected timeout error"),
2258 }
2259 });
2260 }
2261
2262 #[test]
2263 fn my_prevented_matches_success() {
2264 TOKIO_SHARED_RT.block_on(async {
2265 let (ws_api, conn, mut rx) = setup().await;
2266 let client = AccountApiClient::new(ws_api.clone());
2267
2268 let handle = spawn(async move {
2269 let params = MyPreventedMatchesParams::builder("BNBUSDT".to_string(),).build().unwrap();
2270 client.my_prevented_matches(params).await
2271 });
2272
2273 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
2274 let Message::Text(text) = sent else { panic!() };
2275 let v: Value = serde_json::from_str(&text).unwrap();
2276 let id = v["id"].as_str().unwrap();
2277 assert_eq!(v["method"], "/myPreventedMatches".trim_start_matches('/'));
2278
2279 let mut resp_json: Value = serde_json::from_str(r#"{"id":"g4ce6a53-a39d-4f71-823b-4ab5r391d6y8","status":200,"result":[{"symbol":"BTCUSDT","preventedMatchId":1,"takerOrderId":5,"makerSymbol":"BTCUSDT","makerOrderId":3,"tradeGroupId":1,"selfTradePreventionMode":"EXPIRE_MAKER","price":"1.100000","makerPreventedQuantity":"1.300000","transactTime":1669101687094}],"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":20}]}"#).unwrap();
2280 resp_json["id"] = id.into();
2281
2282 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
2283 let expected_data: Vec<models::MyPreventedMatchesResponseResultInner> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
2284 let empty_array = Value::Array(vec![]);
2285 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
2286 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
2287 match raw_rate_limits.as_array() {
2288 Some(arr) if arr.is_empty() => None,
2289 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
2290 None => None,
2291 };
2292
2293 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2294
2295 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
2296
2297
2298 let response_rate_limits = response.rate_limits.clone();
2299 let response_data = response.data().expect("deserialize data");
2300
2301 assert_eq!(response_rate_limits, expected_rate_limits);
2302 assert_eq!(response_data, expected_data);
2303 });
2304 }
2305
2306 #[test]
2307 fn my_prevented_matches_error_response() {
2308 TOKIO_SHARED_RT.block_on(async {
2309 let (ws_api, conn, mut rx) = setup().await;
2310 let client = AccountApiClient::new(ws_api.clone());
2311
2312 let handle = tokio::spawn(async move {
2313 let params = MyPreventedMatchesParams::builder("BNBUSDT".to_string(),).build().unwrap();
2314 client.my_prevented_matches(params).await
2315 });
2316
2317 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
2318 let Message::Text(text) = sent else { panic!() };
2319 let v: Value = serde_json::from_str(&text).unwrap();
2320 let id = v["id"].as_str().unwrap().to_string();
2321
2322 let resp_json = json!({
2323 "id": id,
2324 "status": 400,
2325 "error": {
2326 "code": -2010,
2327 "msg": "Account has insufficient balance for requested action.",
2328 },
2329 "rateLimits": [
2330 {
2331 "rateLimitType": "ORDERS",
2332 "interval": "SECOND",
2333 "intervalNum": 10,
2334 "limit": 50,
2335 "count": 13
2336 },
2337 ],
2338 });
2339 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2340
2341 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
2342 match join {
2343 Ok(Err(e)) => {
2344 let msg = e.to_string();
2345 assert!(
2346 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
2347 "Expected error msg to contain server error, got: {msg}"
2348 );
2349 }
2350 Ok(Ok(_)) => panic!("Expected error"),
2351 Err(_) => panic!("Task panicked"),
2352 }
2353 });
2354 }
2355
2356 #[test]
2357 fn my_prevented_matches_request_timeout() {
2358 TOKIO_SHARED_RT.block_on(async {
2359 let (ws_api, _conn, mut rx) = setup().await;
2360 let client = AccountApiClient::new(ws_api.clone());
2361
2362 let handle = spawn(async move {
2363 let params = MyPreventedMatchesParams::builder("BNBUSDT".to_string())
2364 .build()
2365 .unwrap();
2366 client.my_prevented_matches(params).await
2367 });
2368
2369 let sent = timeout(Duration::from_secs(1), rx.recv())
2370 .await
2371 .expect("send should occur")
2372 .expect("channel closed");
2373 let Message::Text(text) = sent else {
2374 panic!("expected Message Text")
2375 };
2376
2377 let _: Value = serde_json::from_str(&text).unwrap();
2378
2379 let result = handle.await.expect("task completed");
2380 match result {
2381 Err(e) => {
2382 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
2383 assert!(matches!(inner, WebsocketError::Timeout));
2384 } else {
2385 panic!("Unexpected error type: {:?}", e);
2386 }
2387 }
2388 Ok(_) => panic!("Expected timeout error"),
2389 }
2390 });
2391 }
2392
2393 #[test]
2394 fn my_trades_success() {
2395 TOKIO_SHARED_RT.block_on(async {
2396 let (ws_api, conn, mut rx) = setup().await;
2397 let client = AccountApiClient::new(ws_api.clone());
2398
2399 let handle = spawn(async move {
2400 let params = MyTradesParams::builder("BNBUSDT".to_string(),).build().unwrap();
2401 client.my_trades(params).await
2402 });
2403
2404 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
2405 let Message::Text(text) = sent else { panic!() };
2406 let v: Value = serde_json::from_str(&text).unwrap();
2407 let id = v["id"].as_str().unwrap();
2408 assert_eq!(v["method"], "/myTrades".trim_start_matches('/'));
2409
2410 let mut resp_json: Value = serde_json::from_str(r#"{"id":"f4ce6a53-a29d-4f70-823b-4ab59391d6e8","status":200,"result":[{"symbol":"BTCUSDT","id":1650422482,"orderId":12569099453,"orderListId":-1,"price":"23416.50000000","qty":"0.00212000","quoteQty":"49.64298000","commission":"0.00000000","commissionAsset":"BNB","time":1660801715793,"isBuyer":false,"isMaker":true,"isBestMatch":true},{"symbol":"BTCUSDT","id":1650422481,"orderId":12569099453,"orderListId":-1,"price":"23416.10000000","qty":"0.00635000","quoteQty":"148.69223500","commission":"0.00000000","commissionAsset":"BNB","time":1660801715793,"isBuyer":false,"isMaker":true,"isBestMatch":true}],"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":20}]}"#).unwrap();
2411 resp_json["id"] = id.into();
2412
2413 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
2414 let expected_data: Vec<models::MyTradesResponseResultInner> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
2415 let empty_array = Value::Array(vec![]);
2416 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
2417 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
2418 match raw_rate_limits.as_array() {
2419 Some(arr) if arr.is_empty() => None,
2420 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
2421 None => None,
2422 };
2423
2424 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2425
2426 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
2427
2428
2429 let response_rate_limits = response.rate_limits.clone();
2430 let response_data = response.data().expect("deserialize data");
2431
2432 assert_eq!(response_rate_limits, expected_rate_limits);
2433 assert_eq!(response_data, expected_data);
2434 });
2435 }
2436
2437 #[test]
2438 fn my_trades_error_response() {
2439 TOKIO_SHARED_RT.block_on(async {
2440 let (ws_api, conn, mut rx) = setup().await;
2441 let client = AccountApiClient::new(ws_api.clone());
2442
2443 let handle = tokio::spawn(async move {
2444 let params = MyTradesParams::builder("BNBUSDT".to_string(),).build().unwrap();
2445 client.my_trades(params).await
2446 });
2447
2448 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
2449 let Message::Text(text) = sent else { panic!() };
2450 let v: Value = serde_json::from_str(&text).unwrap();
2451 let id = v["id"].as_str().unwrap().to_string();
2452
2453 let resp_json = json!({
2454 "id": id,
2455 "status": 400,
2456 "error": {
2457 "code": -2010,
2458 "msg": "Account has insufficient balance for requested action.",
2459 },
2460 "rateLimits": [
2461 {
2462 "rateLimitType": "ORDERS",
2463 "interval": "SECOND",
2464 "intervalNum": 10,
2465 "limit": 50,
2466 "count": 13
2467 },
2468 ],
2469 });
2470 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2471
2472 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
2473 match join {
2474 Ok(Err(e)) => {
2475 let msg = e.to_string();
2476 assert!(
2477 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
2478 "Expected error msg to contain server error, got: {msg}"
2479 );
2480 }
2481 Ok(Ok(_)) => panic!("Expected error"),
2482 Err(_) => panic!("Task panicked"),
2483 }
2484 });
2485 }
2486
2487 #[test]
2488 fn my_trades_request_timeout() {
2489 TOKIO_SHARED_RT.block_on(async {
2490 let (ws_api, _conn, mut rx) = setup().await;
2491 let client = AccountApiClient::new(ws_api.clone());
2492
2493 let handle = spawn(async move {
2494 let params = MyTradesParams::builder("BNBUSDT".to_string())
2495 .build()
2496 .unwrap();
2497 client.my_trades(params).await
2498 });
2499
2500 let sent = timeout(Duration::from_secs(1), rx.recv())
2501 .await
2502 .expect("send should occur")
2503 .expect("channel closed");
2504 let Message::Text(text) = sent else {
2505 panic!("expected Message Text")
2506 };
2507
2508 let _: Value = serde_json::from_str(&text).unwrap();
2509
2510 let result = handle.await.expect("task completed");
2511 match result {
2512 Err(e) => {
2513 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
2514 assert!(matches!(inner, WebsocketError::Timeout));
2515 } else {
2516 panic!("Unexpected error type: {:?}", e);
2517 }
2518 }
2519 Ok(_) => panic!("Expected timeout error"),
2520 }
2521 });
2522 }
2523
2524 #[test]
2525 fn open_order_lists_status_success() {
2526 TOKIO_SHARED_RT.block_on(async {
2527 let (ws_api, conn, mut rx) = setup().await;
2528 let client = AccountApiClient::new(ws_api.clone());
2529
2530 let handle = spawn(async move {
2531 let params = OpenOrderListsStatusParams::builder().build().unwrap();
2532 client.open_order_lists_status(params).await
2533 });
2534
2535 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
2536 let Message::Text(text) = sent else { panic!() };
2537 let v: Value = serde_json::from_str(&text).unwrap();
2538 let id = v["id"].as_str().unwrap();
2539 assert_eq!(v["method"], "/openOrderLists.status".trim_start_matches('/'));
2540
2541 let mut resp_json: Value = serde_json::from_str(r#"{"id":"3a4437e2-41a3-4c19-897c-9cadc5dce8b6","status":200,"result":[{"orderListId":0,"contingencyType":"OCO","listStatusType":"EXEC_STARTED","listOrderStatus":"EXECUTING","listClientOrderId":"08985fedd9ea2cf6b28996","transactionTime":1660801713793,"symbol":"BTCUSDT","orders":[{"symbol":"BTCUSDT","orderId":5,"clientOrderId":"1ZqG7bBuYwaF4SU8CwnwHm"},{"symbol":"BTCUSDT","orderId":4,"clientOrderId":"CUhLgTXnX5n2c0gWiLpV4d"}]}],"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":6}]}"#).unwrap();
2542 resp_json["id"] = id.into();
2543
2544 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
2545 let expected_data: Vec<models::OpenOrderListsStatusResponseResultInner> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
2546 let empty_array = Value::Array(vec![]);
2547 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
2548 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
2549 match raw_rate_limits.as_array() {
2550 Some(arr) if arr.is_empty() => None,
2551 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
2552 None => None,
2553 };
2554
2555 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2556
2557 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
2558
2559
2560 let response_rate_limits = response.rate_limits.clone();
2561 let response_data = response.data().expect("deserialize data");
2562
2563 assert_eq!(response_rate_limits, expected_rate_limits);
2564 assert_eq!(response_data, expected_data);
2565 });
2566 }
2567
2568 #[test]
2569 fn open_order_lists_status_error_response() {
2570 TOKIO_SHARED_RT.block_on(async {
2571 let (ws_api, conn, mut rx) = setup().await;
2572 let client = AccountApiClient::new(ws_api.clone());
2573
2574 let handle = tokio::spawn(async move {
2575 let params = OpenOrderListsStatusParams::builder().build().unwrap();
2576 client.open_order_lists_status(params).await
2577 });
2578
2579 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
2580 let Message::Text(text) = sent else { panic!() };
2581 let v: Value = serde_json::from_str(&text).unwrap();
2582 let id = v["id"].as_str().unwrap().to_string();
2583
2584 let resp_json = json!({
2585 "id": id,
2586 "status": 400,
2587 "error": {
2588 "code": -2010,
2589 "msg": "Account has insufficient balance for requested action.",
2590 },
2591 "rateLimits": [
2592 {
2593 "rateLimitType": "ORDERS",
2594 "interval": "SECOND",
2595 "intervalNum": 10,
2596 "limit": 50,
2597 "count": 13
2598 },
2599 ],
2600 });
2601 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2602
2603 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
2604 match join {
2605 Ok(Err(e)) => {
2606 let msg = e.to_string();
2607 assert!(
2608 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
2609 "Expected error msg to contain server error, got: {msg}"
2610 );
2611 }
2612 Ok(Ok(_)) => panic!("Expected error"),
2613 Err(_) => panic!("Task panicked"),
2614 }
2615 });
2616 }
2617
2618 #[test]
2619 fn open_order_lists_status_request_timeout() {
2620 TOKIO_SHARED_RT.block_on(async {
2621 let (ws_api, _conn, mut rx) = setup().await;
2622 let client = AccountApiClient::new(ws_api.clone());
2623
2624 let handle = spawn(async move {
2625 let params = OpenOrderListsStatusParams::builder().build().unwrap();
2626 client.open_order_lists_status(params).await
2627 });
2628
2629 let sent = timeout(Duration::from_secs(1), rx.recv())
2630 .await
2631 .expect("send should occur")
2632 .expect("channel closed");
2633 let Message::Text(text) = sent else {
2634 panic!("expected Message Text")
2635 };
2636
2637 let _: Value = serde_json::from_str(&text).unwrap();
2638
2639 let result = handle.await.expect("task completed");
2640 match result {
2641 Err(e) => {
2642 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
2643 assert!(matches!(inner, WebsocketError::Timeout));
2644 } else {
2645 panic!("Unexpected error type: {:?}", e);
2646 }
2647 }
2648 Ok(_) => panic!("Expected timeout error"),
2649 }
2650 });
2651 }
2652
2653 #[test]
2654 fn open_orders_status_success() {
2655 TOKIO_SHARED_RT.block_on(async {
2656 let (ws_api, conn, mut rx) = setup().await;
2657 let client = AccountApiClient::new(ws_api.clone());
2658
2659 let handle = spawn(async move {
2660 let params = OpenOrdersStatusParams::builder().build().unwrap();
2661 client.open_orders_status(params).await
2662 });
2663
2664 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
2665 let Message::Text(text) = sent else { panic!() };
2666 let v: Value = serde_json::from_str(&text).unwrap();
2667 let id = v["id"].as_str().unwrap();
2668 assert_eq!(v["method"], "/openOrders.status".trim_start_matches('/'));
2669
2670 let mut resp_json: Value = serde_json::from_str(r#"{"id":"55f07876-4f6f-4c47-87dc-43e5fff3f2e7","status":200,"result":[{"symbol":"BTCUSDT","orderId":12569099453,"orderListId":-1,"clientOrderId":"4d96324ff9d44481926157","price":"23416.10000000","origQty":"0.00847000","executedQty":"0.00720000","origQuoteOrderQty":"0.00000000","cummulativeQuoteQty":"172.43931000","status":"PARTIALLY_FILLED","timeInForce":"GTC","type":"LIMIT","side":"SELL","stopPrice":"0.00000000","icebergQty":"0.00000000","time":1660801715639,"updateTime":1660801717945,"isWorking":true,"workingTime":1660801715639,"selfTradePreventionMode":"NONE"}],"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":6}]}"#).unwrap();
2671 resp_json["id"] = id.into();
2672
2673 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
2674 let expected_data: Vec<models::OpenOrdersStatusResponseResultInner> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
2675 let empty_array = Value::Array(vec![]);
2676 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
2677 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
2678 match raw_rate_limits.as_array() {
2679 Some(arr) if arr.is_empty() => None,
2680 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
2681 None => None,
2682 };
2683
2684 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2685
2686 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
2687
2688
2689 let response_rate_limits = response.rate_limits.clone();
2690 let response_data = response.data().expect("deserialize data");
2691
2692 assert_eq!(response_rate_limits, expected_rate_limits);
2693 assert_eq!(response_data, expected_data);
2694 });
2695 }
2696
2697 #[test]
2698 fn open_orders_status_error_response() {
2699 TOKIO_SHARED_RT.block_on(async {
2700 let (ws_api, conn, mut rx) = setup().await;
2701 let client = AccountApiClient::new(ws_api.clone());
2702
2703 let handle = tokio::spawn(async move {
2704 let params = OpenOrdersStatusParams::builder().build().unwrap();
2705 client.open_orders_status(params).await
2706 });
2707
2708 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
2709 let Message::Text(text) = sent else { panic!() };
2710 let v: Value = serde_json::from_str(&text).unwrap();
2711 let id = v["id"].as_str().unwrap().to_string();
2712
2713 let resp_json = json!({
2714 "id": id,
2715 "status": 400,
2716 "error": {
2717 "code": -2010,
2718 "msg": "Account has insufficient balance for requested action.",
2719 },
2720 "rateLimits": [
2721 {
2722 "rateLimitType": "ORDERS",
2723 "interval": "SECOND",
2724 "intervalNum": 10,
2725 "limit": 50,
2726 "count": 13
2727 },
2728 ],
2729 });
2730 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2731
2732 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
2733 match join {
2734 Ok(Err(e)) => {
2735 let msg = e.to_string();
2736 assert!(
2737 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
2738 "Expected error msg to contain server error, got: {msg}"
2739 );
2740 }
2741 Ok(Ok(_)) => panic!("Expected error"),
2742 Err(_) => panic!("Task panicked"),
2743 }
2744 });
2745 }
2746
2747 #[test]
2748 fn open_orders_status_request_timeout() {
2749 TOKIO_SHARED_RT.block_on(async {
2750 let (ws_api, _conn, mut rx) = setup().await;
2751 let client = AccountApiClient::new(ws_api.clone());
2752
2753 let handle = spawn(async move {
2754 let params = OpenOrdersStatusParams::builder().build().unwrap();
2755 client.open_orders_status(params).await
2756 });
2757
2758 let sent = timeout(Duration::from_secs(1), rx.recv())
2759 .await
2760 .expect("send should occur")
2761 .expect("channel closed");
2762 let Message::Text(text) = sent else {
2763 panic!("expected Message Text")
2764 };
2765
2766 let _: Value = serde_json::from_str(&text).unwrap();
2767
2768 let result = handle.await.expect("task completed");
2769 match result {
2770 Err(e) => {
2771 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
2772 assert!(matches!(inner, WebsocketError::Timeout));
2773 } else {
2774 panic!("Unexpected error type: {:?}", e);
2775 }
2776 }
2777 Ok(_) => panic!("Expected timeout error"),
2778 }
2779 });
2780 }
2781
2782 #[test]
2783 fn order_amendments_success() {
2784 TOKIO_SHARED_RT.block_on(async {
2785 let (ws_api, conn, mut rx) = setup().await;
2786 let client = AccountApiClient::new(ws_api.clone());
2787
2788 let handle = spawn(async move {
2789 let params = OrderAmendmentsParams::builder("BNBUSDT".to_string(),1,).build().unwrap();
2790 client.order_amendments(params).await
2791 });
2792
2793 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
2794 let Message::Text(text) = sent else { panic!() };
2795 let v: Value = serde_json::from_str(&text).unwrap();
2796 let id = v["id"].as_str().unwrap();
2797 assert_eq!(v["method"], "/order.amendments".trim_start_matches('/'));
2798
2799 let mut resp_json: Value = serde_json::from_str(r#"{"id":"6f5ebe91-01d9-43ac-be99-57cf062e0e30","status":200,"result":[{"symbol":"BTCUSDT","orderId":23,"executionId":60,"origClientOrderId":"my_pending_order","newClientOrderId":"xbxXh5SSwaHS7oUEOCI88B","origQty":"7.00000000","newQty":"5.00000000","time":1741924229819}],"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":4}]}"#).unwrap();
2800 resp_json["id"] = id.into();
2801
2802 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
2803 let expected_data: Vec<models::OrderAmendmentsResponseResultInner> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
2804 let empty_array = Value::Array(vec![]);
2805 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
2806 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
2807 match raw_rate_limits.as_array() {
2808 Some(arr) if arr.is_empty() => None,
2809 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
2810 None => None,
2811 };
2812
2813 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2814
2815 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
2816
2817
2818 let response_rate_limits = response.rate_limits.clone();
2819 let response_data = response.data().expect("deserialize data");
2820
2821 assert_eq!(response_rate_limits, expected_rate_limits);
2822 assert_eq!(response_data, expected_data);
2823 });
2824 }
2825
2826 #[test]
2827 fn order_amendments_error_response() {
2828 TOKIO_SHARED_RT.block_on(async {
2829 let (ws_api, conn, mut rx) = setup().await;
2830 let client = AccountApiClient::new(ws_api.clone());
2831
2832 let handle = tokio::spawn(async move {
2833 let params = OrderAmendmentsParams::builder("BNBUSDT".to_string(),1,).build().unwrap();
2834 client.order_amendments(params).await
2835 });
2836
2837 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
2838 let Message::Text(text) = sent else { panic!() };
2839 let v: Value = serde_json::from_str(&text).unwrap();
2840 let id = v["id"].as_str().unwrap().to_string();
2841
2842 let resp_json = json!({
2843 "id": id,
2844 "status": 400,
2845 "error": {
2846 "code": -2010,
2847 "msg": "Account has insufficient balance for requested action.",
2848 },
2849 "rateLimits": [
2850 {
2851 "rateLimitType": "ORDERS",
2852 "interval": "SECOND",
2853 "intervalNum": 10,
2854 "limit": 50,
2855 "count": 13
2856 },
2857 ],
2858 });
2859 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2860
2861 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
2862 match join {
2863 Ok(Err(e)) => {
2864 let msg = e.to_string();
2865 assert!(
2866 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
2867 "Expected error msg to contain server error, got: {msg}"
2868 );
2869 }
2870 Ok(Ok(_)) => panic!("Expected error"),
2871 Err(_) => panic!("Task panicked"),
2872 }
2873 });
2874 }
2875
2876 #[test]
2877 fn order_amendments_request_timeout() {
2878 TOKIO_SHARED_RT.block_on(async {
2879 let (ws_api, _conn, mut rx) = setup().await;
2880 let client = AccountApiClient::new(ws_api.clone());
2881
2882 let handle = spawn(async move {
2883 let params = OrderAmendmentsParams::builder("BNBUSDT".to_string(), 1)
2884 .build()
2885 .unwrap();
2886 client.order_amendments(params).await
2887 });
2888
2889 let sent = timeout(Duration::from_secs(1), rx.recv())
2890 .await
2891 .expect("send should occur")
2892 .expect("channel closed");
2893 let Message::Text(text) = sent else {
2894 panic!("expected Message Text")
2895 };
2896
2897 let _: Value = serde_json::from_str(&text).unwrap();
2898
2899 let result = handle.await.expect("task completed");
2900 match result {
2901 Err(e) => {
2902 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
2903 assert!(matches!(inner, WebsocketError::Timeout));
2904 } else {
2905 panic!("Unexpected error type: {:?}", e);
2906 }
2907 }
2908 Ok(_) => panic!("Expected timeout error"),
2909 }
2910 });
2911 }
2912
2913 #[test]
2914 fn order_list_status_success() {
2915 TOKIO_SHARED_RT.block_on(async {
2916 let (ws_api, conn, mut rx) = setup().await;
2917 let client = AccountApiClient::new(ws_api.clone());
2918
2919 let handle = spawn(async move {
2920 let params = OrderListStatusParams::builder().build().unwrap();
2921 client.order_list_status(params).await
2922 });
2923
2924 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
2925 let Message::Text(text) = sent else { panic!() };
2926 let v: Value = serde_json::from_str(&text).unwrap();
2927 let id = v["id"].as_str().unwrap();
2928 assert_eq!(v["method"], "/orderList.status".trim_start_matches('/'));
2929
2930 let mut resp_json: Value = serde_json::from_str(r#"{"id":"b53fd5ff-82c7-4a04-bd64-5f9dc42c2100","status":200,"result":{"orderListId":1274512,"contingencyType":"OCO","listStatusType":"EXEC_STARTED","listOrderStatus":"EXECUTING","listClientOrderId":"08985fedd9ea2cf6b28996","transactionTime":1660801713793,"symbol":"BTCUSDT","orders":[{"symbol":"BTCUSDT","orderId":12569138902,"clientOrderId":"jLnZpj5enfMXTuhKB1d0us"},{"symbol":"BTCUSDT","orderId":12569138901,"clientOrderId":"BqtFCj5odMoWtSqGk2X9tU"}]},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":4}]}"#).unwrap();
2931 resp_json["id"] = id.into();
2932
2933 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
2934 let expected_data: Box<models::AllOrderListsResponseResultInner> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
2935 let empty_array = Value::Array(vec![]);
2936 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
2937 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
2938 match raw_rate_limits.as_array() {
2939 Some(arr) if arr.is_empty() => None,
2940 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
2941 None => None,
2942 };
2943
2944 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2945
2946 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
2947
2948
2949 let response_rate_limits = response.rate_limits.clone();
2950 let response_data = response.data().expect("deserialize data");
2951
2952 assert_eq!(response_rate_limits, expected_rate_limits);
2953 assert_eq!(response_data, expected_data);
2954 });
2955 }
2956
2957 #[test]
2958 fn order_list_status_error_response() {
2959 TOKIO_SHARED_RT.block_on(async {
2960 let (ws_api, conn, mut rx) = setup().await;
2961 let client = AccountApiClient::new(ws_api.clone());
2962
2963 let handle = tokio::spawn(async move {
2964 let params = OrderListStatusParams::builder().build().unwrap();
2965 client.order_list_status(params).await
2966 });
2967
2968 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
2969 let Message::Text(text) = sent else { panic!() };
2970 let v: Value = serde_json::from_str(&text).unwrap();
2971 let id = v["id"].as_str().unwrap().to_string();
2972
2973 let resp_json = json!({
2974 "id": id,
2975 "status": 400,
2976 "error": {
2977 "code": -2010,
2978 "msg": "Account has insufficient balance for requested action.",
2979 },
2980 "rateLimits": [
2981 {
2982 "rateLimitType": "ORDERS",
2983 "interval": "SECOND",
2984 "intervalNum": 10,
2985 "limit": 50,
2986 "count": 13
2987 },
2988 ],
2989 });
2990 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
2991
2992 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
2993 match join {
2994 Ok(Err(e)) => {
2995 let msg = e.to_string();
2996 assert!(
2997 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
2998 "Expected error msg to contain server error, got: {msg}"
2999 );
3000 }
3001 Ok(Ok(_)) => panic!("Expected error"),
3002 Err(_) => panic!("Task panicked"),
3003 }
3004 });
3005 }
3006
3007 #[test]
3008 fn order_list_status_request_timeout() {
3009 TOKIO_SHARED_RT.block_on(async {
3010 let (ws_api, _conn, mut rx) = setup().await;
3011 let client = AccountApiClient::new(ws_api.clone());
3012
3013 let handle = spawn(async move {
3014 let params = OrderListStatusParams::builder().build().unwrap();
3015 client.order_list_status(params).await
3016 });
3017
3018 let sent = timeout(Duration::from_secs(1), rx.recv())
3019 .await
3020 .expect("send should occur")
3021 .expect("channel closed");
3022 let Message::Text(text) = sent else {
3023 panic!("expected Message Text")
3024 };
3025
3026 let _: Value = serde_json::from_str(&text).unwrap();
3027
3028 let result = handle.await.expect("task completed");
3029 match result {
3030 Err(e) => {
3031 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
3032 assert!(matches!(inner, WebsocketError::Timeout));
3033 } else {
3034 panic!("Unexpected error type: {:?}", e);
3035 }
3036 }
3037 Ok(_) => panic!("Expected timeout error"),
3038 }
3039 });
3040 }
3041
3042 #[test]
3043 fn order_status_success() {
3044 TOKIO_SHARED_RT.block_on(async {
3045 let (ws_api, conn, mut rx) = setup().await;
3046 let client = AccountApiClient::new(ws_api.clone());
3047
3048 let handle = spawn(async move {
3049 let params = OrderStatusParams::builder("BNBUSDT".to_string(),).build().unwrap();
3050 client.order_status(params).await
3051 });
3052
3053 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
3054 let Message::Text(text) = sent else { panic!() };
3055 let v: Value = serde_json::from_str(&text).unwrap();
3056 let id = v["id"].as_str().unwrap();
3057 assert_eq!(v["method"], "/order.status".trim_start_matches('/'));
3058
3059 let mut resp_json: Value = serde_json::from_str(r#"{"id":"aa62318a-5a97-4f3b-bdc7-640bbe33b291","status":200,"result":{"symbol":"BTCUSDT","orderId":12569099453,"orderListId":-1,"clientOrderId":"4d96324ff9d44481926157","price":"23416.10000000","origQty":"0.00847000","executedQty":"0.00847000","cummulativeQuoteQty":"198.33521500","status":"FILLED","timeInForce":"GTC","type":"LIMIT","side":"SELL","stopPrice":"0.00000000","trailingDelta":10,"trailingTime":-1,"icebergQty":"0.00000000","time":1660801715639,"updateTime":1660801717945,"isWorking":true,"workingTime":1660801715639,"origQuoteOrderQty":"0.00000000","strategyId":37463720,"strategyType":1000000,"selfTradePreventionMode":"NONE","preventedMatchId":0,"preventedQuantity":"1.200000"},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":4}]}"#).unwrap();
3060 resp_json["id"] = id.into();
3061
3062 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
3063 let expected_data: Box<models::OrderStatusResponseResult> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
3064 let empty_array = Value::Array(vec![]);
3065 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
3066 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
3067 match raw_rate_limits.as_array() {
3068 Some(arr) if arr.is_empty() => None,
3069 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
3070 None => None,
3071 };
3072
3073 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
3074
3075 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
3076
3077
3078 let response_rate_limits = response.rate_limits.clone();
3079 let response_data = response.data().expect("deserialize data");
3080
3081 assert_eq!(response_rate_limits, expected_rate_limits);
3082 assert_eq!(response_data, expected_data);
3083 });
3084 }
3085
3086 #[test]
3087 fn order_status_error_response() {
3088 TOKIO_SHARED_RT.block_on(async {
3089 let (ws_api, conn, mut rx) = setup().await;
3090 let client = AccountApiClient::new(ws_api.clone());
3091
3092 let handle = tokio::spawn(async move {
3093 let params = OrderStatusParams::builder("BNBUSDT".to_string(),).build().unwrap();
3094 client.order_status(params).await
3095 });
3096
3097 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
3098 let Message::Text(text) = sent else { panic!() };
3099 let v: Value = serde_json::from_str(&text).unwrap();
3100 let id = v["id"].as_str().unwrap().to_string();
3101
3102 let resp_json = json!({
3103 "id": id,
3104 "status": 400,
3105 "error": {
3106 "code": -2010,
3107 "msg": "Account has insufficient balance for requested action.",
3108 },
3109 "rateLimits": [
3110 {
3111 "rateLimitType": "ORDERS",
3112 "interval": "SECOND",
3113 "intervalNum": 10,
3114 "limit": 50,
3115 "count": 13
3116 },
3117 ],
3118 });
3119 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
3120
3121 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
3122 match join {
3123 Ok(Err(e)) => {
3124 let msg = e.to_string();
3125 assert!(
3126 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
3127 "Expected error msg to contain server error, got: {msg}"
3128 );
3129 }
3130 Ok(Ok(_)) => panic!("Expected error"),
3131 Err(_) => panic!("Task panicked"),
3132 }
3133 });
3134 }
3135
3136 #[test]
3137 fn order_status_request_timeout() {
3138 TOKIO_SHARED_RT.block_on(async {
3139 let (ws_api, _conn, mut rx) = setup().await;
3140 let client = AccountApiClient::new(ws_api.clone());
3141
3142 let handle = spawn(async move {
3143 let params = OrderStatusParams::builder("BNBUSDT".to_string())
3144 .build()
3145 .unwrap();
3146 client.order_status(params).await
3147 });
3148
3149 let sent = timeout(Duration::from_secs(1), rx.recv())
3150 .await
3151 .expect("send should occur")
3152 .expect("channel closed");
3153 let Message::Text(text) = sent else {
3154 panic!("expected Message Text")
3155 };
3156
3157 let _: Value = serde_json::from_str(&text).unwrap();
3158
3159 let result = handle.await.expect("task completed");
3160 match result {
3161 Err(e) => {
3162 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
3163 assert!(matches!(inner, WebsocketError::Timeout));
3164 } else {
3165 panic!("Unexpected error type: {:?}", e);
3166 }
3167 }
3168 Ok(_) => panic!("Expected timeout error"),
3169 }
3170 });
3171 }
3172}