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::staking::rest_api::models;
29
30const HAS_TIME_UNIT: bool = false;
31
32#[async_trait]
33pub trait SoftStakingApi: Send + Sync {
34 async fn get_soft_staking_product_list(
35 &self,
36 params: GetSoftStakingProductListParams,
37 ) -> anyhow::Result<RestApiResponse<models::GetSoftStakingProductListResponse>>;
38 async fn get_soft_staking_rewards_history(
39 &self,
40 params: GetSoftStakingRewardsHistoryParams,
41 ) -> anyhow::Result<RestApiResponse<models::GetSoftStakingRewardsHistoryResponse>>;
42 async fn set_soft_staking(
43 &self,
44 params: SetSoftStakingParams,
45 ) -> anyhow::Result<RestApiResponse<models::SetSoftStakingResponse>>;
46}
47
48#[derive(Debug, Clone)]
49pub struct SoftStakingApiClient {
50 configuration: ConfigurationRestApi,
51}
52
53impl SoftStakingApiClient {
54 pub fn new(configuration: ConfigurationRestApi) -> Self {
55 Self { configuration }
56 }
57}
58
59#[derive(Clone, Debug, Builder, Deserialize, Default)]
64#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
65pub struct GetSoftStakingProductListParams {
66 #[builder(setter(into), default)]
71 #[serde(rename = "asset", default)]
72 pub asset: Option<String>,
73 #[builder(setter(into), default)]
77 #[serde(rename = "current", default)]
78 pub current: Option<i64>,
79 #[builder(setter(into), default)]
84 #[serde(rename = "size", default)]
85 pub size: Option<i64>,
86 #[builder(setter(into), default)]
90 #[serde(rename = "recvWindow", default)]
91 pub recv_window: Option<i64>,
92}
93
94impl GetSoftStakingProductListParams {
95 #[must_use]
98 pub fn builder() -> GetSoftStakingProductListParamsBuilder {
99 GetSoftStakingProductListParamsBuilder::default()
100 }
101}
102#[derive(Clone, Debug, Builder, Deserialize, Default)]
107#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
108pub struct GetSoftStakingRewardsHistoryParams {
109 #[builder(setter(into), default)]
114 #[serde(rename = "asset", default)]
115 pub asset: Option<String>,
116 #[builder(setter(into), default)]
121 #[serde(rename = "startTime", default)]
122 pub start_time: Option<i64>,
123 #[builder(setter(into), default)]
128 #[serde(rename = "endTime", default)]
129 pub end_time: Option<i64>,
130 #[builder(setter(into), default)]
134 #[serde(rename = "current", default)]
135 pub current: Option<i64>,
136 #[builder(setter(into), default)]
141 #[serde(rename = "size", default)]
142 pub size: Option<i64>,
143 #[builder(setter(into), default)]
147 #[serde(rename = "recvWindow", default)]
148 pub recv_window: Option<i64>,
149}
150
151impl GetSoftStakingRewardsHistoryParams {
152 #[must_use]
155 pub fn builder() -> GetSoftStakingRewardsHistoryParamsBuilder {
156 GetSoftStakingRewardsHistoryParamsBuilder::default()
157 }
158}
159#[derive(Clone, Debug, Builder, Deserialize)]
164#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
165pub struct SetSoftStakingParams {
166 #[builder(setter(into))]
171 #[serde(rename = "softStaking")]
172 pub soft_staking: bool,
173 #[builder(setter(into), default)]
177 #[serde(rename = "recvWindow", default)]
178 pub recv_window: Option<i64>,
179}
180
181impl SetSoftStakingParams {
182 #[must_use]
189 pub fn builder(soft_staking: bool) -> SetSoftStakingParamsBuilder {
190 SetSoftStakingParamsBuilder::default().soft_staking(soft_staking)
191 }
192}
193
194#[async_trait]
195impl SoftStakingApi for SoftStakingApiClient {
196 async fn get_soft_staking_product_list(
197 &self,
198 params: GetSoftStakingProductListParams,
199 ) -> anyhow::Result<RestApiResponse<models::GetSoftStakingProductListResponse>> {
200 let GetSoftStakingProductListParams {
201 asset,
202 current,
203 size,
204 recv_window,
205 } = params;
206
207 let mut query_params = BTreeMap::new();
208 let body_params = BTreeMap::new();
209
210 if let Some(rw) = asset {
211 query_params.insert("asset".to_string(), json!(rw));
212 }
213
214 if let Some(rw) = current {
215 query_params.insert("current".to_string(), json!(rw));
216 }
217
218 if let Some(rw) = size {
219 query_params.insert("size".to_string(), json!(rw));
220 }
221
222 if let Some(rw) = recv_window {
223 query_params.insert("recvWindow".to_string(), json!(rw));
224 }
225
226 send_request::<models::GetSoftStakingProductListResponse>(
227 &self.configuration,
228 "/sapi/v1/soft-staking/list",
229 reqwest::Method::GET,
230 query_params,
231 body_params,
232 if HAS_TIME_UNIT {
233 self.configuration.time_unit
234 } else {
235 None
236 },
237 true,
238 )
239 .await
240 }
241
242 async fn get_soft_staking_rewards_history(
243 &self,
244 params: GetSoftStakingRewardsHistoryParams,
245 ) -> anyhow::Result<RestApiResponse<models::GetSoftStakingRewardsHistoryResponse>> {
246 let GetSoftStakingRewardsHistoryParams {
247 asset,
248 start_time,
249 end_time,
250 current,
251 size,
252 recv_window,
253 } = params;
254
255 let mut query_params = BTreeMap::new();
256 let body_params = BTreeMap::new();
257
258 if let Some(rw) = asset {
259 query_params.insert("asset".to_string(), json!(rw));
260 }
261
262 if let Some(rw) = start_time {
263 query_params.insert("startTime".to_string(), json!(rw));
264 }
265
266 if let Some(rw) = end_time {
267 query_params.insert("endTime".to_string(), json!(rw));
268 }
269
270 if let Some(rw) = current {
271 query_params.insert("current".to_string(), json!(rw));
272 }
273
274 if let Some(rw) = size {
275 query_params.insert("size".to_string(), json!(rw));
276 }
277
278 if let Some(rw) = recv_window {
279 query_params.insert("recvWindow".to_string(), json!(rw));
280 }
281
282 send_request::<models::GetSoftStakingRewardsHistoryResponse>(
283 &self.configuration,
284 "/sapi/v1/soft-staking/history/rewardsRecord",
285 reqwest::Method::GET,
286 query_params,
287 body_params,
288 if HAS_TIME_UNIT {
289 self.configuration.time_unit
290 } else {
291 None
292 },
293 true,
294 )
295 .await
296 }
297
298 async fn set_soft_staking(
299 &self,
300 params: SetSoftStakingParams,
301 ) -> anyhow::Result<RestApiResponse<models::SetSoftStakingResponse>> {
302 let SetSoftStakingParams {
303 soft_staking,
304 recv_window,
305 } = params;
306
307 let mut query_params = BTreeMap::new();
308 let body_params = BTreeMap::new();
309
310 query_params.insert("softStaking".to_string(), json!(soft_staking));
311
312 if let Some(rw) = recv_window {
313 query_params.insert("recvWindow".to_string(), json!(rw));
314 }
315
316 send_request::<models::SetSoftStakingResponse>(
317 &self.configuration,
318 "/sapi/v1/soft-staking/set",
319 reqwest::Method::GET,
320 query_params,
321 body_params,
322 if HAS_TIME_UNIT {
323 self.configuration.time_unit
324 } else {
325 None
326 },
327 true,
328 )
329 .await
330 }
331}
332
333#[cfg(all(test, feature = "staking"))]
334mod tests {
335 use super::*;
336 use crate::TOKIO_SHARED_RT;
337 use crate::{errors::ConnectorError, models::DataFuture, models::RestApiRateLimit};
338 use async_trait::async_trait;
339 use std::collections::HashMap;
340
341 struct DummyRestApiResponse<T> {
342 inner: Box<dyn FnOnce() -> DataFuture<Result<T, ConnectorError>> + Send + Sync>,
343 status: u16,
344 headers: HashMap<String, String>,
345 rate_limits: Option<Vec<RestApiRateLimit>>,
346 }
347
348 impl<T> From<DummyRestApiResponse<T>> for RestApiResponse<T> {
349 fn from(dummy: DummyRestApiResponse<T>) -> Self {
350 Self {
351 data_fn: dummy.inner,
352 status: dummy.status,
353 headers: dummy.headers,
354 rate_limits: dummy.rate_limits,
355 }
356 }
357 }
358
359 struct MockSoftStakingApiClient {
360 force_error: bool,
361 }
362
363 #[async_trait]
364 impl SoftStakingApi for MockSoftStakingApiClient {
365 async fn get_soft_staking_product_list(
366 &self,
367 _params: GetSoftStakingProductListParams,
368 ) -> anyhow::Result<RestApiResponse<models::GetSoftStakingProductListResponse>> {
369 if self.force_error {
370 return Err(ConnectorError::ConnectorClientError {
371 msg: "ResponseError".to_string(),
372 code: None,
373 }
374 .into());
375 }
376
377 let resp_json: Value = serde_json::from_str(r#"{"status":true,"totalRewardsUsdt":"3.09827182","rows":[{"asset":"BNB","minAmount":"0.5","maxCap":"1000","apr":"0.0015","stakedAmount":"2.14","totalProfit":"0.00171234"}],"total":2}"#).unwrap_or_else(|_| serde_json::json!({}));
378 let dummy_response: models::GetSoftStakingProductListResponse =
379 serde_json::from_value(resp_json.clone())
380 .expect("should parse into models::GetSoftStakingProductListResponse");
381
382 let dummy = DummyRestApiResponse {
383 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
384 status: 200,
385 headers: HashMap::new(),
386 rate_limits: None,
387 };
388
389 Ok(dummy.into())
390 }
391
392 async fn get_soft_staking_rewards_history(
393 &self,
394 _params: GetSoftStakingRewardsHistoryParams,
395 ) -> anyhow::Result<RestApiResponse<models::GetSoftStakingRewardsHistoryResponse>> {
396 if self.force_error {
397 return Err(ConnectorError::ConnectorClientError {
398 msg: "ResponseError".to_string(),
399 code: None,
400 }
401 .into());
402 }
403
404 let resp_json: Value = serde_json::from_str(r#"{"rows":[{"asset":"BNB","rewards":"0.00000557","rewardAsset":"BNB","avgAmount":"2.14","time":1754007978000}],"total":2}"#).unwrap_or_else(|_| serde_json::json!({}));
405 let dummy_response: models::GetSoftStakingRewardsHistoryResponse =
406 serde_json::from_value(resp_json.clone())
407 .expect("should parse into models::GetSoftStakingRewardsHistoryResponse");
408
409 let dummy = DummyRestApiResponse {
410 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
411 status: 200,
412 headers: HashMap::new(),
413 rate_limits: None,
414 };
415
416 Ok(dummy.into())
417 }
418
419 async fn set_soft_staking(
420 &self,
421 _params: SetSoftStakingParams,
422 ) -> anyhow::Result<RestApiResponse<models::SetSoftStakingResponse>> {
423 if self.force_error {
424 return Err(ConnectorError::ConnectorClientError {
425 msg: "ResponseError".to_string(),
426 code: None,
427 }
428 .into());
429 }
430
431 let resp_json: Value = serde_json::from_str(r#"{"success":true}"#)
432 .unwrap_or_else(|_| serde_json::json!({}));
433 let dummy_response: models::SetSoftStakingResponse =
434 serde_json::from_value(resp_json.clone())
435 .expect("should parse into models::SetSoftStakingResponse");
436
437 let dummy = DummyRestApiResponse {
438 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
439 status: 200,
440 headers: HashMap::new(),
441 rate_limits: None,
442 };
443
444 Ok(dummy.into())
445 }
446 }
447
448 #[test]
449 fn get_soft_staking_product_list_required_params_success() {
450 TOKIO_SHARED_RT.block_on(async {
451 let client = MockSoftStakingApiClient { force_error: false };
452
453 let params = GetSoftStakingProductListParams::builder().build().unwrap();
454
455 let resp_json: Value = serde_json::from_str(r#"{"status":true,"totalRewardsUsdt":"3.09827182","rows":[{"asset":"BNB","minAmount":"0.5","maxCap":"1000","apr":"0.0015","stakedAmount":"2.14","totalProfit":"0.00171234"}],"total":2}"#).unwrap_or_else(|_| serde_json::json!({}));
456 let expected_response : models::GetSoftStakingProductListResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::GetSoftStakingProductListResponse");
457
458 let resp = client.get_soft_staking_product_list(params).await.expect("Expected a response");
459 let data_future = resp.data();
460 let actual_response = data_future.await.unwrap();
461 assert_eq!(actual_response, expected_response);
462 });
463 }
464
465 #[test]
466 fn get_soft_staking_product_list_optional_params_success() {
467 TOKIO_SHARED_RT.block_on(async {
468 let client = MockSoftStakingApiClient { force_error: false };
469
470 let params = GetSoftStakingProductListParams::builder().asset("BTC".to_string()).current(1).size(10).recv_window(5000).build().unwrap();
471
472 let resp_json: Value = serde_json::from_str(r#"{"status":true,"totalRewardsUsdt":"3.09827182","rows":[{"asset":"BNB","minAmount":"0.5","maxCap":"1000","apr":"0.0015","stakedAmount":"2.14","totalProfit":"0.00171234"}],"total":2}"#).unwrap_or_else(|_| serde_json::json!({}));
473 let expected_response : models::GetSoftStakingProductListResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::GetSoftStakingProductListResponse");
474
475 let resp = client.get_soft_staking_product_list(params).await.expect("Expected a response");
476 let data_future = resp.data();
477 let actual_response = data_future.await.unwrap();
478 assert_eq!(actual_response, expected_response);
479 });
480 }
481
482 #[test]
483 fn get_soft_staking_product_list_response_error() {
484 TOKIO_SHARED_RT.block_on(async {
485 let client = MockSoftStakingApiClient { force_error: true };
486
487 let params = GetSoftStakingProductListParams::builder().build().unwrap();
488
489 match client.get_soft_staking_product_list(params).await {
490 Ok(_) => panic!("Expected an error"),
491 Err(err) => {
492 assert_eq!(err.to_string(), "Connector client error: ResponseError");
493 }
494 }
495 });
496 }
497
498 #[test]
499 fn get_soft_staking_rewards_history_required_params_success() {
500 TOKIO_SHARED_RT.block_on(async {
501 let client = MockSoftStakingApiClient { force_error: false };
502
503 let params = GetSoftStakingRewardsHistoryParams::builder().build().unwrap();
504
505 let resp_json: Value = serde_json::from_str(r#"{"rows":[{"asset":"BNB","rewards":"0.00000557","rewardAsset":"BNB","avgAmount":"2.14","time":1754007978000}],"total":2}"#).unwrap_or_else(|_| serde_json::json!({}));
506 let expected_response : models::GetSoftStakingRewardsHistoryResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::GetSoftStakingRewardsHistoryResponse");
507
508 let resp = client.get_soft_staking_rewards_history(params).await.expect("Expected a response");
509 let data_future = resp.data();
510 let actual_response = data_future.await.unwrap();
511 assert_eq!(actual_response, expected_response);
512 });
513 }
514
515 #[test]
516 fn get_soft_staking_rewards_history_optional_params_success() {
517 TOKIO_SHARED_RT.block_on(async {
518 let client = MockSoftStakingApiClient { force_error: false };
519
520 let params = GetSoftStakingRewardsHistoryParams::builder().asset("BTC".to_string()).start_time(1623319461670).end_time(1641782889000).current(1).size(10).recv_window(5000).build().unwrap();
521
522 let resp_json: Value = serde_json::from_str(r#"{"rows":[{"asset":"BNB","rewards":"0.00000557","rewardAsset":"BNB","avgAmount":"2.14","time":1754007978000}],"total":2}"#).unwrap_or_else(|_| serde_json::json!({}));
523 let expected_response : models::GetSoftStakingRewardsHistoryResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::GetSoftStakingRewardsHistoryResponse");
524
525 let resp = client.get_soft_staking_rewards_history(params).await.expect("Expected a response");
526 let data_future = resp.data();
527 let actual_response = data_future.await.unwrap();
528 assert_eq!(actual_response, expected_response);
529 });
530 }
531
532 #[test]
533 fn get_soft_staking_rewards_history_response_error() {
534 TOKIO_SHARED_RT.block_on(async {
535 let client = MockSoftStakingApiClient { force_error: true };
536
537 let params = GetSoftStakingRewardsHistoryParams::builder()
538 .build()
539 .unwrap();
540
541 match client.get_soft_staking_rewards_history(params).await {
542 Ok(_) => panic!("Expected an error"),
543 Err(err) => {
544 assert_eq!(err.to_string(), "Connector client error: ResponseError");
545 }
546 }
547 });
548 }
549
550 #[test]
551 fn set_soft_staking_required_params_success() {
552 TOKIO_SHARED_RT.block_on(async {
553 let client = MockSoftStakingApiClient { force_error: false };
554
555 let params = SetSoftStakingParams::builder(true).build().unwrap();
556
557 let resp_json: Value = serde_json::from_str(r#"{"success":true}"#)
558 .unwrap_or_else(|_| serde_json::json!({}));
559 let expected_response: models::SetSoftStakingResponse =
560 serde_json::from_value(resp_json.clone())
561 .expect("should parse into models::SetSoftStakingResponse");
562
563 let resp = client
564 .set_soft_staking(params)
565 .await
566 .expect("Expected a response");
567 let data_future = resp.data();
568 let actual_response = data_future.await.unwrap();
569 assert_eq!(actual_response, expected_response);
570 });
571 }
572
573 #[test]
574 fn set_soft_staking_optional_params_success() {
575 TOKIO_SHARED_RT.block_on(async {
576 let client = MockSoftStakingApiClient { force_error: false };
577
578 let params = SetSoftStakingParams::builder(true)
579 .recv_window(5000)
580 .build()
581 .unwrap();
582
583 let resp_json: Value = serde_json::from_str(r#"{"success":true}"#)
584 .unwrap_or_else(|_| serde_json::json!({}));
585 let expected_response: models::SetSoftStakingResponse =
586 serde_json::from_value(resp_json.clone())
587 .expect("should parse into models::SetSoftStakingResponse");
588
589 let resp = client
590 .set_soft_staking(params)
591 .await
592 .expect("Expected a response");
593 let data_future = resp.data();
594 let actual_response = data_future.await.unwrap();
595 assert_eq!(actual_response, expected_response);
596 });
597 }
598
599 #[test]
600 fn set_soft_staking_response_error() {
601 TOKIO_SHARED_RT.block_on(async {
602 let client = MockSoftStakingApiClient { force_error: true };
603
604 let params = SetSoftStakingParams::builder(true).build().unwrap();
605
606 match client.set_soft_staking(params).await {
607 Ok(_) => panic!("Expected an error"),
608 Err(err) => {
609 assert_eq!(err.to_string(), "Connector client error: ResponseError");
610 }
611 }
612 });
613 }
614}