1#![allow(unused_imports)]
15use async_trait::async_trait;
16use derive_builder::Builder;
17use reqwest;
18use rust_decimal::prelude::*;
19use serde::{Deserialize, Serialize};
20use serde_json::{Value, json};
21use std::collections::BTreeMap;
22
23use crate::common::{
24 config::ConfigurationRestApi,
25 models::{ParamBuildError, RestApiResponse},
26 utils::send_request,
27};
28use crate::margin_trading::rest_api::models;
29
30const HAS_TIME_UNIT: bool = false;
31
32#[async_trait]
33pub trait TransferApi: Send + Sync {
34 async fn get_cross_margin_transfer_history(
35 &self,
36 params: GetCrossMarginTransferHistoryParams,
37 ) -> anyhow::Result<RestApiResponse<models::GetCrossMarginTransferHistoryResponse>>;
38 async fn query_max_transfer_out_amount(
39 &self,
40 params: QueryMaxTransferOutAmountParams,
41 ) -> anyhow::Result<RestApiResponse<models::QueryMaxTransferOutAmountResponse>>;
42}
43
44#[derive(Debug, Clone)]
45pub struct TransferApiClient {
46 configuration: ConfigurationRestApi,
47}
48
49impl TransferApiClient {
50 pub fn new(configuration: ConfigurationRestApi) -> Self {
51 Self { configuration }
52 }
53}
54
55#[allow(non_camel_case_types)]
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub enum GetCrossMarginTransferHistoryTypeEnum {
58 #[serde(rename = "ROLL_IN")]
59 RollIn,
60 #[serde(rename = "ROLL_OUT")]
61 RollOut,
62}
63
64impl GetCrossMarginTransferHistoryTypeEnum {
65 #[must_use]
66 pub fn as_str(&self) -> &'static str {
67 match self {
68 Self::RollIn => "ROLL_IN",
69 Self::RollOut => "ROLL_OUT",
70 }
71 }
72}
73
74impl std::str::FromStr for GetCrossMarginTransferHistoryTypeEnum {
75 type Err = Box<dyn std::error::Error + Send + Sync>;
76
77 fn from_str(s: &str) -> Result<Self, Self::Err> {
78 match s {
79 "ROLL_IN" => Ok(Self::RollIn),
80 "ROLL_OUT" => Ok(Self::RollOut),
81 other => {
82 Err(format!("invalid GetCrossMarginTransferHistoryTypeEnum: {}", other).into())
83 }
84 }
85 }
86}
87
88#[derive(Clone, Debug, Builder, Deserialize, Default)]
93#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
94pub struct GetCrossMarginTransferHistoryParams {
95 #[builder(setter(into), default)]
100 #[serde(rename = "asset", default)]
101 pub asset: Option<String>,
102 #[builder(setter(into), default)]
107 #[serde(rename = "type", default)]
108 pub r#type: Option<GetCrossMarginTransferHistoryTypeEnum>,
109 #[builder(setter(into), default)]
114 #[serde(rename = "startTime", default)]
115 pub start_time: Option<i64>,
116 #[builder(setter(into), default)]
121 #[serde(rename = "endTime", default)]
122 pub end_time: Option<i64>,
123 #[builder(setter(into), default)]
128 #[serde(rename = "current", default)]
129 pub current: Option<i64>,
130 #[builder(setter(into), default)]
135 #[serde(rename = "size", default)]
136 pub size: Option<i64>,
137 #[builder(setter(into), default)]
142 #[serde(rename = "isolatedSymbol", default)]
143 pub isolated_symbol: Option<String>,
144 #[builder(setter(into), default)]
149 #[serde(rename = "recvWindow", default)]
150 pub recv_window: Option<i64>,
151}
152
153impl GetCrossMarginTransferHistoryParams {
154 #[must_use]
157 pub fn builder() -> GetCrossMarginTransferHistoryParamsBuilder {
158 GetCrossMarginTransferHistoryParamsBuilder::default()
159 }
160}
161#[derive(Clone, Debug, Builder, Deserialize)]
166#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
167pub struct QueryMaxTransferOutAmountParams {
168 #[builder(setter(into))]
173 #[serde(rename = "asset")]
174 pub asset: String,
175 #[builder(setter(into), default)]
180 #[serde(rename = "isolatedSymbol", default)]
181 pub isolated_symbol: Option<String>,
182 #[builder(setter(into), default)]
187 #[serde(rename = "recvWindow", default)]
188 pub recv_window: Option<i64>,
189}
190
191impl QueryMaxTransferOutAmountParams {
192 #[must_use]
199 pub fn builder(asset: String) -> QueryMaxTransferOutAmountParamsBuilder {
200 QueryMaxTransferOutAmountParamsBuilder::default().asset(asset)
201 }
202}
203
204#[async_trait]
205impl TransferApi for TransferApiClient {
206 async fn get_cross_margin_transfer_history(
207 &self,
208 params: GetCrossMarginTransferHistoryParams,
209 ) -> anyhow::Result<RestApiResponse<models::GetCrossMarginTransferHistoryResponse>> {
210 let GetCrossMarginTransferHistoryParams {
211 asset,
212 r#type,
213 start_time,
214 end_time,
215 current,
216 size,
217 isolated_symbol,
218 recv_window,
219 } = params;
220
221 let mut query_params = BTreeMap::new();
222 let body_params = BTreeMap::new();
223
224 if let Some(rw) = asset {
225 query_params.insert("asset".to_string(), json!(rw));
226 }
227
228 if let Some(rw) = r#type {
229 query_params.insert("type".to_string(), json!(rw));
230 }
231
232 if let Some(rw) = start_time {
233 query_params.insert("startTime".to_string(), json!(rw));
234 }
235
236 if let Some(rw) = end_time {
237 query_params.insert("endTime".to_string(), json!(rw));
238 }
239
240 if let Some(rw) = current {
241 query_params.insert("current".to_string(), json!(rw));
242 }
243
244 if let Some(rw) = size {
245 query_params.insert("size".to_string(), json!(rw));
246 }
247
248 if let Some(rw) = isolated_symbol {
249 query_params.insert("isolatedSymbol".to_string(), json!(rw));
250 }
251
252 if let Some(rw) = recv_window {
253 query_params.insert("recvWindow".to_string(), json!(rw));
254 }
255
256 send_request::<models::GetCrossMarginTransferHistoryResponse>(
257 &self.configuration,
258 "/sapi/v1/margin/transfer",
259 reqwest::Method::GET,
260 query_params,
261 body_params,
262 if HAS_TIME_UNIT {
263 self.configuration.time_unit
264 } else {
265 None
266 },
267 true,
268 )
269 .await
270 }
271
272 async fn query_max_transfer_out_amount(
273 &self,
274 params: QueryMaxTransferOutAmountParams,
275 ) -> anyhow::Result<RestApiResponse<models::QueryMaxTransferOutAmountResponse>> {
276 let QueryMaxTransferOutAmountParams {
277 asset,
278 isolated_symbol,
279 recv_window,
280 } = params;
281
282 let mut query_params = BTreeMap::new();
283 let body_params = BTreeMap::new();
284
285 query_params.insert("asset".to_string(), json!(asset));
286
287 if let Some(rw) = isolated_symbol {
288 query_params.insert("isolatedSymbol".to_string(), json!(rw));
289 }
290
291 if let Some(rw) = recv_window {
292 query_params.insert("recvWindow".to_string(), json!(rw));
293 }
294
295 send_request::<models::QueryMaxTransferOutAmountResponse>(
296 &self.configuration,
297 "/sapi/v1/margin/maxTransferable",
298 reqwest::Method::GET,
299 query_params,
300 body_params,
301 if HAS_TIME_UNIT {
302 self.configuration.time_unit
303 } else {
304 None
305 },
306 true,
307 )
308 .await
309 }
310}
311
312#[cfg(all(test, feature = "margin_trading"))]
313mod tests {
314 use super::*;
315 use crate::TOKIO_SHARED_RT;
316 use crate::{errors::ConnectorError, models::DataFuture, models::RestApiRateLimit};
317 use async_trait::async_trait;
318 use std::collections::HashMap;
319
320 struct DummyRestApiResponse<T> {
321 inner: Box<dyn FnOnce() -> DataFuture<Result<T, ConnectorError>> + Send + Sync>,
322 status: u16,
323 headers: HashMap<String, String>,
324 rate_limits: Option<Vec<RestApiRateLimit>>,
325 }
326
327 impl<T> From<DummyRestApiResponse<T>> for RestApiResponse<T> {
328 fn from(dummy: DummyRestApiResponse<T>) -> Self {
329 Self {
330 data_fn: dummy.inner,
331 status: dummy.status,
332 headers: dummy.headers,
333 rate_limits: dummy.rate_limits,
334 }
335 }
336 }
337
338 struct MockTransferApiClient {
339 force_error: bool,
340 }
341
342 #[async_trait]
343 impl TransferApi for MockTransferApiClient {
344 async fn get_cross_margin_transfer_history(
345 &self,
346 _params: GetCrossMarginTransferHistoryParams,
347 ) -> anyhow::Result<RestApiResponse<models::GetCrossMarginTransferHistoryResponse>>
348 {
349 if self.force_error {
350 return Err(ConnectorError::ConnectorClientError {
351 msg: "ResponseError".to_string(),
352 code: None,
353 }
354 .into());
355 }
356
357 let resp_json: Value = serde_json::from_str(r#"{"rows":[{"amount":"0.10000000","asset":"BNB","status":"CONFIRMED","timestamp":1566898617,"txId":5240372201,"type":"ROLL_IN","transFrom":"SPOT","transTo":"ISOLATED_MARGIN","fromSymbol":"BNBUSDT","toSymbol":"BTCUSDT"}],"total":3}"#).unwrap_or_else(|_| serde_json::json!({}));
358 let dummy_response: models::GetCrossMarginTransferHistoryResponse =
359 serde_json::from_value(resp_json.clone())
360 .expect("should parse into models::GetCrossMarginTransferHistoryResponse");
361
362 let dummy = DummyRestApiResponse {
363 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
364 status: 200,
365 headers: HashMap::new(),
366 rate_limits: None,
367 };
368
369 Ok(dummy.into())
370 }
371
372 async fn query_max_transfer_out_amount(
373 &self,
374 _params: QueryMaxTransferOutAmountParams,
375 ) -> anyhow::Result<RestApiResponse<models::QueryMaxTransferOutAmountResponse>> {
376 if self.force_error {
377 return Err(ConnectorError::ConnectorClientError {
378 msg: "ResponseError".to_string(),
379 code: None,
380 }
381 .into());
382 }
383
384 let resp_json: Value = serde_json::from_str(r#"{"amount":"3.59498107"}"#)
385 .unwrap_or_else(|_| serde_json::json!({}));
386 let dummy_response: models::QueryMaxTransferOutAmountResponse =
387 serde_json::from_value(resp_json.clone())
388 .expect("should parse into models::QueryMaxTransferOutAmountResponse");
389
390 let dummy = DummyRestApiResponse {
391 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
392 status: 200,
393 headers: HashMap::new(),
394 rate_limits: None,
395 };
396
397 Ok(dummy.into())
398 }
399 }
400
401 #[test]
402 fn get_cross_margin_transfer_history_required_params_success() {
403 TOKIO_SHARED_RT.block_on(async {
404 let client = MockTransferApiClient { force_error: false };
405
406 let params = GetCrossMarginTransferHistoryParams::builder().build().unwrap();
407
408 let resp_json: Value = serde_json::from_str(r#"{"rows":[{"amount":"0.10000000","asset":"BNB","status":"CONFIRMED","timestamp":1566898617,"txId":5240372201,"type":"ROLL_IN","transFrom":"SPOT","transTo":"ISOLATED_MARGIN","fromSymbol":"BNBUSDT","toSymbol":"BTCUSDT"}],"total":3}"#).unwrap_or_else(|_| serde_json::json!({}));
409 let expected_response : models::GetCrossMarginTransferHistoryResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::GetCrossMarginTransferHistoryResponse");
410
411 let resp = client.get_cross_margin_transfer_history(params).await.expect("Expected a response");
412 let data_future = resp.data();
413 let actual_response = data_future.await.unwrap();
414 assert_eq!(actual_response, expected_response);
415 });
416 }
417
418 #[test]
419 fn get_cross_margin_transfer_history_optional_params_success() {
420 TOKIO_SHARED_RT.block_on(async {
421 let client = MockTransferApiClient { force_error: false };
422
423 let params = GetCrossMarginTransferHistoryParams::builder().asset("BNB".to_string()).r#type(GetCrossMarginTransferHistoryTypeEnum::RollIn).start_time(1623319461670).end_time(1641782889000).current(1).size(10).isolated_symbol("BNBUSDT".to_string()).recv_window(5000).build().unwrap();
424
425 let resp_json: Value = serde_json::from_str(r#"{"rows":[{"amount":"0.10000000","asset":"BNB","status":"CONFIRMED","timestamp":1566898617,"txId":5240372201,"type":"ROLL_IN","transFrom":"SPOT","transTo":"ISOLATED_MARGIN","fromSymbol":"BNBUSDT","toSymbol":"BTCUSDT"}],"total":3}"#).unwrap_or_else(|_| serde_json::json!({}));
426 let expected_response : models::GetCrossMarginTransferHistoryResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::GetCrossMarginTransferHistoryResponse");
427
428 let resp = client.get_cross_margin_transfer_history(params).await.expect("Expected a response");
429 let data_future = resp.data();
430 let actual_response = data_future.await.unwrap();
431 assert_eq!(actual_response, expected_response);
432 });
433 }
434
435 #[test]
436 fn get_cross_margin_transfer_history_response_error() {
437 TOKIO_SHARED_RT.block_on(async {
438 let client = MockTransferApiClient { force_error: true };
439
440 let params = GetCrossMarginTransferHistoryParams::builder()
441 .build()
442 .unwrap();
443
444 match client.get_cross_margin_transfer_history(params).await {
445 Ok(_) => panic!("Expected an error"),
446 Err(err) => {
447 assert_eq!(err.to_string(), "Connector client error: ResponseError");
448 }
449 }
450 });
451 }
452
453 #[test]
454 fn query_max_transfer_out_amount_required_params_success() {
455 TOKIO_SHARED_RT.block_on(async {
456 let client = MockTransferApiClient { force_error: false };
457
458 let params = QueryMaxTransferOutAmountParams::builder("BTC".to_string())
459 .build()
460 .unwrap();
461
462 let resp_json: Value = serde_json::from_str(r#"{"amount":"3.59498107"}"#)
463 .unwrap_or_else(|_| serde_json::json!({}));
464 let expected_response: models::QueryMaxTransferOutAmountResponse =
465 serde_json::from_value(resp_json.clone())
466 .expect("should parse into models::QueryMaxTransferOutAmountResponse");
467
468 let resp = client
469 .query_max_transfer_out_amount(params)
470 .await
471 .expect("Expected a response");
472 let data_future = resp.data();
473 let actual_response = data_future.await.unwrap();
474 assert_eq!(actual_response, expected_response);
475 });
476 }
477
478 #[test]
479 fn query_max_transfer_out_amount_optional_params_success() {
480 TOKIO_SHARED_RT.block_on(async {
481 let client = MockTransferApiClient { force_error: false };
482
483 let params = QueryMaxTransferOutAmountParams::builder("BTC".to_string())
484 .isolated_symbol("BTCUSDT".to_string())
485 .recv_window(5000)
486 .build()
487 .unwrap();
488
489 let resp_json: Value = serde_json::from_str(r#"{"amount":"3.59498107"}"#)
490 .unwrap_or_else(|_| serde_json::json!({}));
491 let expected_response: models::QueryMaxTransferOutAmountResponse =
492 serde_json::from_value(resp_json.clone())
493 .expect("should parse into models::QueryMaxTransferOutAmountResponse");
494
495 let resp = client
496 .query_max_transfer_out_amount(params)
497 .await
498 .expect("Expected a response");
499 let data_future = resp.data();
500 let actual_response = data_future.await.unwrap();
501 assert_eq!(actual_response, expected_response);
502 });
503 }
504
505 #[test]
506 fn query_max_transfer_out_amount_response_error() {
507 TOKIO_SHARED_RT.block_on(async {
508 let client = MockTransferApiClient { force_error: true };
509
510 let params = QueryMaxTransferOutAmountParams::builder("BTC".to_string())
511 .build()
512 .unwrap();
513
514 match client.query_max_transfer_out_amount(params).await {
515 Ok(_) => panic!("Expected an error"),
516 Err(err) => {
517 assert_eq!(err.to_string(), "Connector client error: ResponseError");
518 }
519 }
520 });
521 }
522}