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