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#[derive(Clone, Debug, Builder, Default)]
60#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
61pub struct GetCrossMarginTransferHistoryParams {
62 #[builder(setter(into), default)]
67 pub asset: Option<String>,
68 #[builder(setter(into), default)]
72 pub r#type: Option<String>,
73 #[builder(setter(into), default)]
77 pub start_time: Option<i64>,
78 #[builder(setter(into), default)]
83 pub end_time: Option<i64>,
84 #[builder(setter(into), default)]
88 pub current: Option<i64>,
89 #[builder(setter(into), default)]
93 pub size: Option<i64>,
94 #[builder(setter(into), default)]
98 pub isolated_symbol: Option<String>,
99 #[builder(setter(into), default)]
103 pub recv_window: Option<i64>,
104}
105
106impl GetCrossMarginTransferHistoryParams {
107 #[must_use]
110 pub fn builder() -> GetCrossMarginTransferHistoryParamsBuilder {
111 GetCrossMarginTransferHistoryParamsBuilder::default()
112 }
113}
114#[derive(Clone, Debug, Builder)]
119#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
120pub struct QueryMaxTransferOutAmountParams {
121 #[builder(setter(into))]
126 pub asset: String,
127 #[builder(setter(into), default)]
131 pub isolated_symbol: Option<String>,
132 #[builder(setter(into), default)]
136 pub recv_window: Option<i64>,
137}
138
139impl QueryMaxTransferOutAmountParams {
140 #[must_use]
147 pub fn builder(asset: String) -> QueryMaxTransferOutAmountParamsBuilder {
148 QueryMaxTransferOutAmountParamsBuilder::default().asset(asset)
149 }
150}
151
152#[async_trait]
153impl TransferApi for TransferApiClient {
154 async fn get_cross_margin_transfer_history(
155 &self,
156 params: GetCrossMarginTransferHistoryParams,
157 ) -> anyhow::Result<RestApiResponse<models::GetCrossMarginTransferHistoryResponse>> {
158 let GetCrossMarginTransferHistoryParams {
159 asset,
160 r#type,
161 start_time,
162 end_time,
163 current,
164 size,
165 isolated_symbol,
166 recv_window,
167 } = params;
168
169 let mut query_params = BTreeMap::new();
170 let body_params = BTreeMap::new();
171
172 if let Some(rw) = asset {
173 query_params.insert("asset".to_string(), json!(rw));
174 }
175
176 if let Some(rw) = r#type {
177 query_params.insert("type".to_string(), json!(rw));
178 }
179
180 if let Some(rw) = start_time {
181 query_params.insert("startTime".to_string(), json!(rw));
182 }
183
184 if let Some(rw) = end_time {
185 query_params.insert("endTime".to_string(), json!(rw));
186 }
187
188 if let Some(rw) = current {
189 query_params.insert("current".to_string(), json!(rw));
190 }
191
192 if let Some(rw) = size {
193 query_params.insert("size".to_string(), json!(rw));
194 }
195
196 if let Some(rw) = isolated_symbol {
197 query_params.insert("isolatedSymbol".to_string(), json!(rw));
198 }
199
200 if let Some(rw) = recv_window {
201 query_params.insert("recvWindow".to_string(), json!(rw));
202 }
203
204 send_request::<models::GetCrossMarginTransferHistoryResponse>(
205 &self.configuration,
206 "/sapi/v1/margin/transfer",
207 reqwest::Method::GET,
208 query_params,
209 body_params,
210 if HAS_TIME_UNIT {
211 self.configuration.time_unit
212 } else {
213 None
214 },
215 true,
216 )
217 .await
218 }
219
220 async fn query_max_transfer_out_amount(
221 &self,
222 params: QueryMaxTransferOutAmountParams,
223 ) -> anyhow::Result<RestApiResponse<models::QueryMaxTransferOutAmountResponse>> {
224 let QueryMaxTransferOutAmountParams {
225 asset,
226 isolated_symbol,
227 recv_window,
228 } = params;
229
230 let mut query_params = BTreeMap::new();
231 let body_params = BTreeMap::new();
232
233 query_params.insert("asset".to_string(), json!(asset));
234
235 if let Some(rw) = isolated_symbol {
236 query_params.insert("isolatedSymbol".to_string(), json!(rw));
237 }
238
239 if let Some(rw) = recv_window {
240 query_params.insert("recvWindow".to_string(), json!(rw));
241 }
242
243 send_request::<models::QueryMaxTransferOutAmountResponse>(
244 &self.configuration,
245 "/sapi/v1/margin/maxTransferable",
246 reqwest::Method::GET,
247 query_params,
248 body_params,
249 if HAS_TIME_UNIT {
250 self.configuration.time_unit
251 } else {
252 None
253 },
254 true,
255 )
256 .await
257 }
258}
259
260#[cfg(all(test, feature = "margin_trading"))]
261mod tests {
262 use super::*;
263 use crate::TOKIO_SHARED_RT;
264 use crate::{errors::ConnectorError, models::DataFuture, models::RestApiRateLimit};
265 use async_trait::async_trait;
266 use std::collections::HashMap;
267
268 struct DummyRestApiResponse<T> {
269 inner: Box<dyn FnOnce() -> DataFuture<Result<T, ConnectorError>> + Send + Sync>,
270 status: u16,
271 headers: HashMap<String, String>,
272 rate_limits: Option<Vec<RestApiRateLimit>>,
273 }
274
275 impl<T> From<DummyRestApiResponse<T>> for RestApiResponse<T> {
276 fn from(dummy: DummyRestApiResponse<T>) -> Self {
277 Self {
278 data_fn: dummy.inner,
279 status: dummy.status,
280 headers: dummy.headers,
281 rate_limits: dummy.rate_limits,
282 }
283 }
284 }
285
286 struct MockTransferApiClient {
287 force_error: bool,
288 }
289
290 #[async_trait]
291 impl TransferApi for MockTransferApiClient {
292 async fn get_cross_margin_transfer_history(
293 &self,
294 _params: GetCrossMarginTransferHistoryParams,
295 ) -> anyhow::Result<RestApiResponse<models::GetCrossMarginTransferHistoryResponse>>
296 {
297 if self.force_error {
298 return Err(ConnectorError::ConnectorClientError {
299 msg: "ResponseError".to_string(),
300 code: None,
301 }
302 .into());
303 }
304
305 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"},{"amount":"5.00000000","asset":"USDT","status":"CONFIRMED","timestamp":1566888436,"txId":5239810406,"type":"ROLL_OUT","transFrom":"ISOLATED_MARGIN","transTo":"ISOLATED_MARGIN","fromSymbol":"BNBUSDT","toSymbol":"BTCUSDT"},{"amount":"1.00000000","asset":"EOS","status":"CONFIRMED","timestamp":1566888403,"txId":5239808703,"type":"ROLL_IN"}],"total":3}"#).unwrap();
306 let dummy_response: models::GetCrossMarginTransferHistoryResponse =
307 serde_json::from_value(resp_json.clone())
308 .expect("should parse into models::GetCrossMarginTransferHistoryResponse");
309
310 let dummy = DummyRestApiResponse {
311 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
312 status: 200,
313 headers: HashMap::new(),
314 rate_limits: None,
315 };
316
317 Ok(dummy.into())
318 }
319
320 async fn query_max_transfer_out_amount(
321 &self,
322 _params: QueryMaxTransferOutAmountParams,
323 ) -> anyhow::Result<RestApiResponse<models::QueryMaxTransferOutAmountResponse>> {
324 if self.force_error {
325 return Err(ConnectorError::ConnectorClientError {
326 msg: "ResponseError".to_string(),
327 code: None,
328 }
329 .into());
330 }
331
332 let resp_json: Value = serde_json::from_str(r#"{"amount":"3.59498107"}"#).unwrap();
333 let dummy_response: models::QueryMaxTransferOutAmountResponse =
334 serde_json::from_value(resp_json.clone())
335 .expect("should parse into models::QueryMaxTransferOutAmountResponse");
336
337 let dummy = DummyRestApiResponse {
338 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
339 status: 200,
340 headers: HashMap::new(),
341 rate_limits: None,
342 };
343
344 Ok(dummy.into())
345 }
346 }
347
348 #[test]
349 fn get_cross_margin_transfer_history_required_params_success() {
350 TOKIO_SHARED_RT.block_on(async {
351 let client = MockTransferApiClient { force_error: false };
352
353 let params = GetCrossMarginTransferHistoryParams::builder().build().unwrap();
354
355 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"},{"amount":"5.00000000","asset":"USDT","status":"CONFIRMED","timestamp":1566888436,"txId":5239810406,"type":"ROLL_OUT","transFrom":"ISOLATED_MARGIN","transTo":"ISOLATED_MARGIN","fromSymbol":"BNBUSDT","toSymbol":"BTCUSDT"},{"amount":"1.00000000","asset":"EOS","status":"CONFIRMED","timestamp":1566888403,"txId":5239808703,"type":"ROLL_IN"}],"total":3}"#).unwrap();
356 let expected_response : models::GetCrossMarginTransferHistoryResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::GetCrossMarginTransferHistoryResponse");
357
358 let resp = client.get_cross_margin_transfer_history(params).await.expect("Expected a response");
359 let data_future = resp.data();
360 let actual_response = data_future.await.unwrap();
361 assert_eq!(actual_response, expected_response);
362 });
363 }
364
365 #[test]
366 fn get_cross_margin_transfer_history_optional_params_success() {
367 TOKIO_SHARED_RT.block_on(async {
368 let client = MockTransferApiClient { force_error: false };
369
370 let params = GetCrossMarginTransferHistoryParams::builder().asset("asset_example".to_string()).r#type("r#type_example".to_string()).start_time(1623319461670).end_time(1641782889000).current(1).size(10).isolated_symbol("isolated_symbol_example".to_string()).recv_window(5000).build().unwrap();
371
372 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"},{"amount":"5.00000000","asset":"USDT","status":"CONFIRMED","timestamp":1566888436,"txId":5239810406,"type":"ROLL_OUT","transFrom":"ISOLATED_MARGIN","transTo":"ISOLATED_MARGIN","fromSymbol":"BNBUSDT","toSymbol":"BTCUSDT"},{"amount":"1.00000000","asset":"EOS","status":"CONFIRMED","timestamp":1566888403,"txId":5239808703,"type":"ROLL_IN"}],"total":3}"#).unwrap();
373 let expected_response : models::GetCrossMarginTransferHistoryResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::GetCrossMarginTransferHistoryResponse");
374
375 let resp = client.get_cross_margin_transfer_history(params).await.expect("Expected a response");
376 let data_future = resp.data();
377 let actual_response = data_future.await.unwrap();
378 assert_eq!(actual_response, expected_response);
379 });
380 }
381
382 #[test]
383 fn get_cross_margin_transfer_history_response_error() {
384 TOKIO_SHARED_RT.block_on(async {
385 let client = MockTransferApiClient { force_error: true };
386
387 let params = GetCrossMarginTransferHistoryParams::builder()
388 .build()
389 .unwrap();
390
391 match client.get_cross_margin_transfer_history(params).await {
392 Ok(_) => panic!("Expected an error"),
393 Err(err) => {
394 assert_eq!(err.to_string(), "Connector client error: ResponseError");
395 }
396 }
397 });
398 }
399
400 #[test]
401 fn query_max_transfer_out_amount_required_params_success() {
402 TOKIO_SHARED_RT.block_on(async {
403 let client = MockTransferApiClient { force_error: false };
404
405 let params = QueryMaxTransferOutAmountParams::builder("asset_example".to_string())
406 .build()
407 .unwrap();
408
409 let resp_json: Value = serde_json::from_str(r#"{"amount":"3.59498107"}"#).unwrap();
410 let expected_response: models::QueryMaxTransferOutAmountResponse =
411 serde_json::from_value(resp_json.clone())
412 .expect("should parse into models::QueryMaxTransferOutAmountResponse");
413
414 let resp = client
415 .query_max_transfer_out_amount(params)
416 .await
417 .expect("Expected a response");
418 let data_future = resp.data();
419 let actual_response = data_future.await.unwrap();
420 assert_eq!(actual_response, expected_response);
421 });
422 }
423
424 #[test]
425 fn query_max_transfer_out_amount_optional_params_success() {
426 TOKIO_SHARED_RT.block_on(async {
427 let client = MockTransferApiClient { force_error: false };
428
429 let params = QueryMaxTransferOutAmountParams::builder("asset_example".to_string())
430 .isolated_symbol("isolated_symbol_example".to_string())
431 .recv_window(5000)
432 .build()
433 .unwrap();
434
435 let resp_json: Value = serde_json::from_str(r#"{"amount":"3.59498107"}"#).unwrap();
436 let expected_response: models::QueryMaxTransferOutAmountResponse =
437 serde_json::from_value(resp_json.clone())
438 .expect("should parse into models::QueryMaxTransferOutAmountResponse");
439
440 let resp = client
441 .query_max_transfer_out_amount(params)
442 .await
443 .expect("Expected a response");
444 let data_future = resp.data();
445 let actual_response = data_future.await.unwrap();
446 assert_eq!(actual_response, expected_response);
447 });
448 }
449
450 #[test]
451 fn query_max_transfer_out_amount_response_error() {
452 TOKIO_SHARED_RT.block_on(async {
453 let client = MockTransferApiClient { force_error: true };
454
455 let params = QueryMaxTransferOutAmountParams::builder("asset_example".to_string())
456 .build()
457 .unwrap();
458
459 match client.query_max_transfer_out_amount(params).await {
460 Ok(_) => panic!("Expected an error"),
461 Err(err) => {
462 assert_eq!(err.to_string(), "Connector client error: ResponseError");
463 }
464 }
465 });
466 }
467}