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::alpha::rest_api::models;
24use crate::common::{
25 config::ConfigurationRestApi,
26 models::{ParamBuildError, RestApiResponse},
27 utils::send_request,
28};
29
30const HAS_TIME_UNIT: bool = false;
31
32#[async_trait]
33pub trait MarketDataApi: Send + Sync {
34 async fn aggregated_trades(
35 &self,
36 params: AggregatedTradesParams,
37 ) -> anyhow::Result<RestApiResponse<models::AggregatedTradesResponse>>;
38 async fn full_depth(
39 &self,
40 params: FullDepthParams,
41 ) -> anyhow::Result<RestApiResponse<models::FullDepthResponse>>;
42 async fn get_exchange_info(
43 &self,
44 ) -> anyhow::Result<RestApiResponse<models::GetExchangeInfoResponse>>;
45 async fn klines(
46 &self,
47 params: KlinesParams,
48 ) -> anyhow::Result<RestApiResponse<models::KlinesResponse>>;
49 async fn ticker(
50 &self,
51 params: TickerParams,
52 ) -> anyhow::Result<RestApiResponse<models::TickerResponse>>;
53 async fn token_list(&self) -> anyhow::Result<RestApiResponse<models::TokenListResponse>>;
54}
55
56#[derive(Debug, Clone)]
57pub struct MarketDataApiClient {
58 configuration: ConfigurationRestApi,
59}
60
61impl MarketDataApiClient {
62 pub fn new(configuration: ConfigurationRestApi) -> Self {
63 Self { configuration }
64 }
65}
66
67#[allow(non_camel_case_types)]
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub enum FullDepthLimitEnum {
70 #[serde(rename = "5")]
71 Limit5,
72 #[serde(rename = "10")]
73 Limit10,
74 #[serde(rename = "20")]
75 Limit20,
76 #[serde(rename = "50")]
77 Limit50,
78 #[serde(rename = "100")]
79 Limit100,
80 #[serde(rename = "500")]
81 Limit500,
82 #[serde(rename = "1000")]
83 Limit1000,
84}
85
86impl FullDepthLimitEnum {
87 #[must_use]
88 pub fn as_str(&self) -> &'static str {
89 match self {
90 Self::Limit5 => "5",
91 Self::Limit10 => "10",
92 Self::Limit20 => "20",
93 Self::Limit50 => "50",
94 Self::Limit100 => "100",
95 Self::Limit500 => "500",
96 Self::Limit1000 => "1000",
97 }
98 }
99}
100
101impl std::str::FromStr for FullDepthLimitEnum {
102 type Err = Box<dyn std::error::Error + Send + Sync>;
103
104 fn from_str(s: &str) -> Result<Self, Self::Err> {
105 match s {
106 "5" => Ok(Self::Limit5),
107 "10" => Ok(Self::Limit10),
108 "20" => Ok(Self::Limit20),
109 "50" => Ok(Self::Limit50),
110 "100" => Ok(Self::Limit100),
111 "500" => Ok(Self::Limit500),
112 "1000" => Ok(Self::Limit1000),
113 other => Err(format!("invalid FullDepthLimitEnum: {}", other).into()),
114 }
115 }
116}
117
118#[allow(non_camel_case_types)]
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub enum KlinesIntervalEnum {
121 #[serde(rename = "1s")]
122 Interval1s,
123 #[serde(rename = "15s")]
124 Interval15s,
125 #[serde(rename = "1m")]
126 Interval1m,
127 #[serde(rename = "3m")]
128 Interval3m,
129 #[serde(rename = "5m")]
130 Interval5m,
131 #[serde(rename = "15m")]
132 Interval15m,
133 #[serde(rename = "30m")]
134 Interval30m,
135 #[serde(rename = "1h")]
136 Interval1h,
137 #[serde(rename = "2h")]
138 Interval2h,
139 #[serde(rename = "4h")]
140 Interval4h,
141 #[serde(rename = "6h")]
142 Interval6h,
143 #[serde(rename = "8h")]
144 Interval8h,
145 #[serde(rename = "12h")]
146 Interval12h,
147 #[serde(rename = "1d")]
148 Interval1d,
149 #[serde(rename = "3d")]
150 Interval3d,
151 #[serde(rename = "1w")]
152 Interval1w,
153 #[serde(rename = "1M")]
154 Interval1M,
155}
156
157impl KlinesIntervalEnum {
158 #[must_use]
159 pub fn as_str(&self) -> &'static str {
160 match self {
161 Self::Interval1s => "1s",
162 Self::Interval15s => "15s",
163 Self::Interval1m => "1m",
164 Self::Interval3m => "3m",
165 Self::Interval5m => "5m",
166 Self::Interval15m => "15m",
167 Self::Interval30m => "30m",
168 Self::Interval1h => "1h",
169 Self::Interval2h => "2h",
170 Self::Interval4h => "4h",
171 Self::Interval6h => "6h",
172 Self::Interval8h => "8h",
173 Self::Interval12h => "12h",
174 Self::Interval1d => "1d",
175 Self::Interval3d => "3d",
176 Self::Interval1w => "1w",
177 Self::Interval1M => "1M",
178 }
179 }
180}
181
182impl std::str::FromStr for KlinesIntervalEnum {
183 type Err = Box<dyn std::error::Error + Send + Sync>;
184
185 fn from_str(s: &str) -> Result<Self, Self::Err> {
186 match s {
187 "1s" => Ok(Self::Interval1s),
188 "15s" => Ok(Self::Interval15s),
189 "1m" => Ok(Self::Interval1m),
190 "3m" => Ok(Self::Interval3m),
191 "5m" => Ok(Self::Interval5m),
192 "15m" => Ok(Self::Interval15m),
193 "30m" => Ok(Self::Interval30m),
194 "1h" => Ok(Self::Interval1h),
195 "2h" => Ok(Self::Interval2h),
196 "4h" => Ok(Self::Interval4h),
197 "6h" => Ok(Self::Interval6h),
198 "8h" => Ok(Self::Interval8h),
199 "12h" => Ok(Self::Interval12h),
200 "1d" => Ok(Self::Interval1d),
201 "3d" => Ok(Self::Interval3d),
202 "1w" => Ok(Self::Interval1w),
203 "1M" => Ok(Self::Interval1M),
204 other => Err(format!("invalid KlinesIntervalEnum: {}", other).into()),
205 }
206 }
207}
208
209#[derive(Clone, Debug, Builder, Deserialize)]
214#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
215pub struct AggregatedTradesParams {
216 #[builder(setter(into))]
220 #[serde(rename = "symbol")]
221 pub symbol: String,
222 #[builder(setter(into), default)]
226 #[serde(rename = "fromId", default)]
227 pub from_id: Option<i64>,
228 #[builder(setter(into), default)]
232 #[serde(rename = "startTime", default)]
233 pub start_time: Option<i64>,
234 #[builder(setter(into), default)]
238 #[serde(rename = "endTime", default)]
239 pub end_time: Option<i64>,
240 #[builder(setter(into), default)]
244 #[serde(rename = "limit", default)]
245 pub limit: Option<i64>,
246}
247
248impl AggregatedTradesParams {
249 #[must_use]
256 pub fn builder(symbol: String) -> AggregatedTradesParamsBuilder {
257 AggregatedTradesParamsBuilder::default().symbol(symbol)
258 }
259}
260#[derive(Clone, Debug, Builder, Deserialize)]
265#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
266pub struct FullDepthParams {
267 #[builder(setter(into))]
271 #[serde(rename = "symbol")]
272 pub symbol: String,
273 #[builder(setter(into), default)]
277 #[serde(rename = "limit", default)]
278 pub limit: Option<FullDepthLimitEnum>,
279}
280
281impl FullDepthParams {
282 #[must_use]
289 pub fn builder(symbol: String) -> FullDepthParamsBuilder {
290 FullDepthParamsBuilder::default().symbol(symbol)
291 }
292}
293#[derive(Clone, Debug, Builder, Deserialize)]
298#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
299pub struct KlinesParams {
300 #[builder(setter(into))]
304 #[serde(rename = "symbol")]
305 pub symbol: String,
306 #[builder(setter(into))]
310 #[serde(rename = "interval")]
311 pub interval: KlinesIntervalEnum,
312 #[builder(setter(into), default)]
316 #[serde(rename = "limit", default)]
317 pub limit: Option<i64>,
318 #[builder(setter(into), default)]
322 #[serde(rename = "startTime", default)]
323 pub start_time: Option<i64>,
324 #[builder(setter(into), default)]
328 #[serde(rename = "endTime", default)]
329 pub end_time: Option<i64>,
330}
331
332impl KlinesParams {
333 #[must_use]
341 pub fn builder(symbol: String, interval: KlinesIntervalEnum) -> KlinesParamsBuilder {
342 KlinesParamsBuilder::default()
343 .symbol(symbol)
344 .interval(interval)
345 }
346}
347#[derive(Clone, Debug, Builder, Deserialize)]
352#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
353pub struct TickerParams {
354 #[builder(setter(into))]
358 #[serde(rename = "symbol")]
359 pub symbol: String,
360}
361
362impl TickerParams {
363 #[must_use]
370 pub fn builder(symbol: String) -> TickerParamsBuilder {
371 TickerParamsBuilder::default().symbol(symbol)
372 }
373}
374
375#[async_trait]
376impl MarketDataApi for MarketDataApiClient {
377 async fn aggregated_trades(
378 &self,
379 params: AggregatedTradesParams,
380 ) -> anyhow::Result<RestApiResponse<models::AggregatedTradesResponse>> {
381 let AggregatedTradesParams {
382 symbol,
383 from_id,
384 start_time,
385 end_time,
386 limit,
387 } = params;
388
389 let mut query_params = BTreeMap::new();
390 let body_params = BTreeMap::new();
391
392 query_params.insert("symbol".to_string(), json!(symbol));
393
394 if let Some(rw) = from_id {
395 query_params.insert("fromId".to_string(), json!(rw));
396 }
397
398 if let Some(rw) = start_time {
399 query_params.insert("startTime".to_string(), json!(rw));
400 }
401
402 if let Some(rw) = end_time {
403 query_params.insert("endTime".to_string(), json!(rw));
404 }
405
406 if let Some(rw) = limit {
407 query_params.insert("limit".to_string(), json!(rw));
408 }
409
410 send_request::<models::AggregatedTradesResponse>(
411 &self.configuration,
412 "/bapi/defi/v1/public/alpha-trade/agg-trades",
413 reqwest::Method::GET,
414 query_params,
415 body_params,
416 if HAS_TIME_UNIT {
417 self.configuration.time_unit
418 } else {
419 None
420 },
421 false,
422 )
423 .await
424 }
425
426 async fn full_depth(
427 &self,
428 params: FullDepthParams,
429 ) -> anyhow::Result<RestApiResponse<models::FullDepthResponse>> {
430 let FullDepthParams { symbol, limit } = params;
431
432 let mut query_params = BTreeMap::new();
433 let body_params = BTreeMap::new();
434
435 query_params.insert("symbol".to_string(), json!(symbol));
436
437 if let Some(rw) = limit {
438 query_params.insert("limit".to_string(), json!(rw));
439 }
440
441 send_request::<models::FullDepthResponse>(
442 &self.configuration,
443 "/bapi/defi/v1/public/alpha-trade/fullDepth",
444 reqwest::Method::GET,
445 query_params,
446 body_params,
447 if HAS_TIME_UNIT {
448 self.configuration.time_unit
449 } else {
450 None
451 },
452 false,
453 )
454 .await
455 }
456
457 async fn get_exchange_info(
458 &self,
459 ) -> anyhow::Result<RestApiResponse<models::GetExchangeInfoResponse>> {
460 let query_params = BTreeMap::new();
461 let body_params = BTreeMap::new();
462
463 send_request::<models::GetExchangeInfoResponse>(
464 &self.configuration,
465 "/bapi/defi/v1/public/alpha-trade/get-exchange-info",
466 reqwest::Method::GET,
467 query_params,
468 body_params,
469 if HAS_TIME_UNIT {
470 self.configuration.time_unit
471 } else {
472 None
473 },
474 false,
475 )
476 .await
477 }
478
479 async fn klines(
480 &self,
481 params: KlinesParams,
482 ) -> anyhow::Result<RestApiResponse<models::KlinesResponse>> {
483 let KlinesParams {
484 symbol,
485 interval,
486 limit,
487 start_time,
488 end_time,
489 } = params;
490
491 let mut query_params = BTreeMap::new();
492 let body_params = BTreeMap::new();
493
494 query_params.insert("symbol".to_string(), json!(symbol));
495
496 query_params.insert("interval".to_string(), json!(interval));
497
498 if let Some(rw) = limit {
499 query_params.insert("limit".to_string(), json!(rw));
500 }
501
502 if let Some(rw) = start_time {
503 query_params.insert("startTime".to_string(), json!(rw));
504 }
505
506 if let Some(rw) = end_time {
507 query_params.insert("endTime".to_string(), json!(rw));
508 }
509
510 send_request::<models::KlinesResponse>(
511 &self.configuration,
512 "/bapi/defi/v1/public/alpha-trade/klines",
513 reqwest::Method::GET,
514 query_params,
515 body_params,
516 if HAS_TIME_UNIT {
517 self.configuration.time_unit
518 } else {
519 None
520 },
521 false,
522 )
523 .await
524 }
525
526 async fn ticker(
527 &self,
528 params: TickerParams,
529 ) -> anyhow::Result<RestApiResponse<models::TickerResponse>> {
530 let TickerParams { symbol } = params;
531
532 let mut query_params = BTreeMap::new();
533 let body_params = BTreeMap::new();
534
535 query_params.insert("symbol".to_string(), json!(symbol));
536
537 send_request::<models::TickerResponse>(
538 &self.configuration,
539 "/bapi/defi/v1/public/alpha-trade/ticker",
540 reqwest::Method::GET,
541 query_params,
542 body_params,
543 if HAS_TIME_UNIT {
544 self.configuration.time_unit
545 } else {
546 None
547 },
548 false,
549 )
550 .await
551 }
552
553 async fn token_list(&self) -> anyhow::Result<RestApiResponse<models::TokenListResponse>> {
554 let query_params = BTreeMap::new();
555 let body_params = BTreeMap::new();
556
557 send_request::<models::TokenListResponse>(
558 &self.configuration,
559 "/bapi/defi/v1/public/wallet-direct/buw/wallet/cex/alpha/all/token/list",
560 reqwest::Method::GET,
561 query_params,
562 body_params,
563 if HAS_TIME_UNIT {
564 self.configuration.time_unit
565 } else {
566 None
567 },
568 false,
569 )
570 .await
571 }
572}
573
574#[cfg(all(test, feature = "alpha"))]
575mod tests {
576 use super::*;
577 use crate::TOKIO_SHARED_RT;
578 use crate::{errors::ConnectorError, models::DataFuture, models::RestApiRateLimit};
579 use async_trait::async_trait;
580 use std::collections::HashMap;
581
582 struct DummyRestApiResponse<T> {
583 inner: Box<dyn FnOnce() -> DataFuture<Result<T, ConnectorError>> + Send + Sync>,
584 status: u16,
585 headers: HashMap<String, String>,
586 rate_limits: Option<Vec<RestApiRateLimit>>,
587 }
588
589 impl<T> From<DummyRestApiResponse<T>> for RestApiResponse<T> {
590 fn from(dummy: DummyRestApiResponse<T>) -> Self {
591 Self {
592 data_fn: dummy.inner,
593 status: dummy.status,
594 headers: dummy.headers,
595 rate_limits: dummy.rate_limits,
596 }
597 }
598 }
599
600 struct MockMarketDataApiClient {
601 force_error: bool,
602 }
603
604 #[async_trait]
605 impl MarketDataApi for MockMarketDataApiClient {
606 async fn aggregated_trades(
607 &self,
608 _params: AggregatedTradesParams,
609 ) -> anyhow::Result<RestApiResponse<models::AggregatedTradesResponse>> {
610 if self.force_error {
611 return Err(ConnectorError::ConnectorClientError {
612 msg: "ResponseError".to_string(),
613 code: None,
614 }
615 .into());
616 }
617
618 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"","messageDetail":"","data":[{"a":58470,"p":"1.00","q":"1.00","f":58470,"l":58665,"T":1752568680000,"m":true}]}"#).unwrap_or_else(|_| serde_json::json!({}));
619 let dummy_response: models::AggregatedTradesResponse =
620 serde_json::from_value(resp_json.clone())
621 .expect("should parse into models::AggregatedTradesResponse");
622
623 let dummy = DummyRestApiResponse {
624 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
625 status: 200,
626 headers: HashMap::new(),
627 rate_limits: None,
628 };
629
630 Ok(dummy.into())
631 }
632
633 async fn full_depth(
634 &self,
635 _params: FullDepthParams,
636 ) -> anyhow::Result<RestApiResponse<models::FullDepthResponse>> {
637 if self.force_error {
638 return Err(ConnectorError::ConnectorClientError {
639 msg: "ResponseError".to_string(),
640 code: None,
641 }
642 .into());
643 }
644
645 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"message","messageDetail":"messageDetail","success":true,"data":{"lastUpdateId":47534656223,"symbol":"ALPHA_175USDT","bids":[["0.00040161","365980.85000000"]],"asks":[["0.00046996","61994.70000000"]],"E":1775027836086,"T":1775027836072}}"#).unwrap_or_else(|_| serde_json::json!({}));
646 let dummy_response: models::FullDepthResponse =
647 serde_json::from_value(resp_json.clone())
648 .expect("should parse into models::FullDepthResponse");
649
650 let dummy = DummyRestApiResponse {
651 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
652 status: 200,
653 headers: HashMap::new(),
654 rate_limits: None,
655 };
656
657 Ok(dummy.into())
658 }
659
660 async fn get_exchange_info(
661 &self,
662 ) -> anyhow::Result<RestApiResponse<models::GetExchangeInfoResponse>> {
663 if self.force_error {
664 return Err(ConnectorError::ConnectorClientError {
665 msg: "ResponseError".to_string(),
666 code: None,
667 }
668 .into());
669 }
670
671 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"","messageDetail":"","success":true,"data":{"timezone":"UTC","assets":[{"asset":"USDT"}],"symbols":[{"symbol":"ALPHA_105USDT","status":"TRADING","baseAsset":"ALPHA_105","quoteAsset":"USDT","pricePrecision":8,"quantityPrecision":8,"baseAssetPrecision":8,"quotePrecision":8,"filters":[{"filterType":"PRICE_FILTER","minPrice":"0.00000001","maxPrice":"1000","tickSize":"0.00000001","stepSize":"0.01000000","maxQty":"277778","minQty":"0.01000000","limit":200,"minNotional":"0.1","maxNotional":"1000000","multiplierDown":"0.20000","multiplierUp":"5","bidMultiplierUp":"5","askMultiplierUp":"5","bidMultiplierDown":"0.20000","askMultiplierDown":"0.20000"}],"orderTypes":["LIMIT"]}],"orderTypes":""}}"#).unwrap_or_else(|_| serde_json::json!({}));
672 let dummy_response: models::GetExchangeInfoResponse =
673 serde_json::from_value(resp_json.clone())
674 .expect("should parse into models::GetExchangeInfoResponse");
675
676 let dummy = DummyRestApiResponse {
677 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
678 status: 200,
679 headers: HashMap::new(),
680 rate_limits: None,
681 };
682
683 Ok(dummy.into())
684 }
685
686 async fn klines(
687 &self,
688 _params: KlinesParams,
689 ) -> anyhow::Result<RestApiResponse<models::KlinesResponse>> {
690 if self.force_error {
691 return Err(ConnectorError::ConnectorClientError {
692 msg: "ResponseError".to_string(),
693 code: None,
694 }
695 .into());
696 }
697
698 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"","messageDetail":"","success":true,"data":[["1752642000000"]]}"#).unwrap_or_else(|_| serde_json::json!({}));
699 let dummy_response: models::KlinesResponse = serde_json::from_value(resp_json.clone())
700 .expect("should parse into models::KlinesResponse");
701
702 let dummy = DummyRestApiResponse {
703 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
704 status: 200,
705 headers: HashMap::new(),
706 rate_limits: None,
707 };
708
709 Ok(dummy.into())
710 }
711
712 async fn ticker(
713 &self,
714 _params: TickerParams,
715 ) -> anyhow::Result<RestApiResponse<models::TickerResponse>> {
716 if self.force_error {
717 return Err(ConnectorError::ConnectorClientError {
718 msg: "ResponseError".to_string(),
719 code: None,
720 }
721 .into());
722 }
723
724 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"","messageDetail":"","data":{"symbol":"ALPHA_175USDT","priceChange":"-0.00007172","priceChangePercent":"-8.841","weightedAvgPrice":"0.00079608","lastPrice":"0.00073954","lastQty":"12600.43000000","openPrice":"0.00081126","highPrice":"0.00081126","lowPrice":"0.00073954","volume":"1204754.30000000","quoteVolume":"959.07729927","openTime":1768808100000,"closeTime":1768893244772,"firstId":93742,"lastId":93768,"count":38},"success":true}"#).unwrap_or_else(|_| serde_json::json!({}));
725 let dummy_response: models::TickerResponse = serde_json::from_value(resp_json.clone())
726 .expect("should parse into models::TickerResponse");
727
728 let dummy = DummyRestApiResponse {
729 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
730 status: 200,
731 headers: HashMap::new(),
732 rate_limits: None,
733 };
734
735 Ok(dummy.into())
736 }
737
738 async fn token_list(&self) -> anyhow::Result<RestApiResponse<models::TokenListResponse>> {
739 if self.force_error {
740 return Err(ConnectorError::ConnectorClientError {
741 msg: "ResponseError".to_string(),
742 code: None,
743 }
744 .into());
745 }
746
747 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"","messageDetail":"","success":true,"data":[{"tokenId":"3F350C8B3621770A673159B7A19BC034","chainId":"56","chainIconUrl":"https://bin.bnbstatic.com/image/admin_mgs_image_upload/20250228/d0216ce4-a3e9-4bda-8937-4a6aa943ccf2.png","chainName":"BSC","contractAddress":"0xcf640fdf9b3d9e45cbd69fda91d7e22579c14444","name":"gorilla","symbol":"gorilla","iconUrl":"https://bin.bnbstatic.com/images/web3-data/public/token/logos/248b5406f88a4ee28913a29107875339.png","price":"0.00080595003978242023","percentChange24h":"-7.42","volume24h":"14847.711222633409558029675","marketCap":"805950.03978242","fdv":"805950.03978242","liquidity":"263192.72813121626034","totalSupply":"1000000000","circulatingSupply":"1000000000","holders":"7120","decimals":18,"listingCex":false,"hotTag":false,"cexCoinName":"","canTransfer":false,"denomination":1,"offline":false,"tradeDecimal":8,"alphaId":"ALPHA_175","offsell":false,"priceHigh24h":"0.00088873864475645879","priceLow24h":"0.0008020805165487083","count24h":"166","onlineTge":false,"onlineAirdrop":false,"score":1,"cexOffDisplay":false,"stockState":false,"listingTime":1746686700000,"mulPoint":1,"bnExclusiveState":false}]}"#).unwrap_or_else(|_| serde_json::json!({}));
748 let dummy_response: models::TokenListResponse =
749 serde_json::from_value(resp_json.clone())
750 .expect("should parse into models::TokenListResponse");
751
752 let dummy = DummyRestApiResponse {
753 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
754 status: 200,
755 headers: HashMap::new(),
756 rate_limits: None,
757 };
758
759 Ok(dummy.into())
760 }
761 }
762
763 #[test]
764 fn aggregated_trades_required_params_success() {
765 TOKIO_SHARED_RT.block_on(async {
766 let client = MockMarketDataApiClient { force_error: false };
767
768 let params = AggregatedTradesParams::builder("ALPHA_118USDC".to_string(),).build().unwrap();
769
770 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"","messageDetail":"","data":[{"a":58470,"p":"1.00","q":"1.00","f":58470,"l":58665,"T":1752568680000,"m":true}]}"#).unwrap_or_else(|_| serde_json::json!({}));
771 let expected_response : models::AggregatedTradesResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::AggregatedTradesResponse");
772
773 let resp = client.aggregated_trades(params).await.expect("Expected a response");
774 let data_future = resp.data();
775 let actual_response = data_future.await.unwrap();
776 assert_eq!(actual_response, expected_response);
777 });
778 }
779
780 #[test]
781 fn aggregated_trades_optional_params_success() {
782 TOKIO_SHARED_RT.block_on(async {
783 let client = MockMarketDataApiClient { force_error: false };
784
785 let params = AggregatedTradesParams::builder("ALPHA_118USDC".to_string(),).from_id(58470).start_time(1752568680000).end_time(1752572280000).limit(500).build().unwrap();
786
787 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"","messageDetail":"","data":[{"a":58470,"p":"1.00","q":"1.00","f":58470,"l":58665,"T":1752568680000,"m":true}]}"#).unwrap_or_else(|_| serde_json::json!({}));
788 let expected_response : models::AggregatedTradesResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::AggregatedTradesResponse");
789
790 let resp = client.aggregated_trades(params).await.expect("Expected a response");
791 let data_future = resp.data();
792 let actual_response = data_future.await.unwrap();
793 assert_eq!(actual_response, expected_response);
794 });
795 }
796
797 #[test]
798 fn aggregated_trades_response_error() {
799 TOKIO_SHARED_RT.block_on(async {
800 let client = MockMarketDataApiClient { force_error: true };
801
802 let params = AggregatedTradesParams::builder("ALPHA_118USDC".to_string())
803 .build()
804 .unwrap();
805
806 match client.aggregated_trades(params).await {
807 Ok(_) => panic!("Expected an error"),
808 Err(err) => {
809 assert_eq!(err.to_string(), "Connector client error: ResponseError");
810 }
811 }
812 });
813 }
814
815 #[test]
816 fn full_depth_required_params_success() {
817 TOKIO_SHARED_RT.block_on(async {
818 let client = MockMarketDataApiClient { force_error: false };
819
820 let params = FullDepthParams::builder("ALPHA_175USDT".to_string(),).build().unwrap();
821
822 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"message","messageDetail":"messageDetail","success":true,"data":{"lastUpdateId":47534656223,"symbol":"ALPHA_175USDT","bids":[["0.00040161","365980.85000000"]],"asks":[["0.00046996","61994.70000000"]],"E":1775027836086,"T":1775027836072}}"#).unwrap_or_else(|_| serde_json::json!({}));
823 let expected_response : models::FullDepthResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::FullDepthResponse");
824
825 let resp = client.full_depth(params).await.expect("Expected a response");
826 let data_future = resp.data();
827 let actual_response = data_future.await.unwrap();
828 assert_eq!(actual_response, expected_response);
829 });
830 }
831
832 #[test]
833 fn full_depth_optional_params_success() {
834 TOKIO_SHARED_RT.block_on(async {
835 let client = MockMarketDataApiClient { force_error: false };
836
837 let params = FullDepthParams::builder("ALPHA_175USDT".to_string(),).limit(FullDepthLimitEnum::Limit5).build().unwrap();
838
839 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"message","messageDetail":"messageDetail","success":true,"data":{"lastUpdateId":47534656223,"symbol":"ALPHA_175USDT","bids":[["0.00040161","365980.85000000"]],"asks":[["0.00046996","61994.70000000"]],"E":1775027836086,"T":1775027836072}}"#).unwrap_or_else(|_| serde_json::json!({}));
840 let expected_response : models::FullDepthResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::FullDepthResponse");
841
842 let resp = client.full_depth(params).await.expect("Expected a response");
843 let data_future = resp.data();
844 let actual_response = data_future.await.unwrap();
845 assert_eq!(actual_response, expected_response);
846 });
847 }
848
849 #[test]
850 fn full_depth_response_error() {
851 TOKIO_SHARED_RT.block_on(async {
852 let client = MockMarketDataApiClient { force_error: true };
853
854 let params = FullDepthParams::builder("ALPHA_175USDT".to_string())
855 .build()
856 .unwrap();
857
858 match client.full_depth(params).await {
859 Ok(_) => panic!("Expected an error"),
860 Err(err) => {
861 assert_eq!(err.to_string(), "Connector client error: ResponseError");
862 }
863 }
864 });
865 }
866
867 #[test]
868 fn get_exchange_info_required_params_success() {
869 TOKIO_SHARED_RT.block_on(async {
870 let client = MockMarketDataApiClient { force_error: false };
871
872
873 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"","messageDetail":"","success":true,"data":{"timezone":"UTC","assets":[{"asset":"USDT"}],"symbols":[{"symbol":"ALPHA_105USDT","status":"TRADING","baseAsset":"ALPHA_105","quoteAsset":"USDT","pricePrecision":8,"quantityPrecision":8,"baseAssetPrecision":8,"quotePrecision":8,"filters":[{"filterType":"PRICE_FILTER","minPrice":"0.00000001","maxPrice":"1000","tickSize":"0.00000001","stepSize":"0.01000000","maxQty":"277778","minQty":"0.01000000","limit":200,"minNotional":"0.1","maxNotional":"1000000","multiplierDown":"0.20000","multiplierUp":"5","bidMultiplierUp":"5","askMultiplierUp":"5","bidMultiplierDown":"0.20000","askMultiplierDown":"0.20000"}],"orderTypes":["LIMIT"]}],"orderTypes":""}}"#).unwrap_or_else(|_| serde_json::json!({}));
874 let expected_response : models::GetExchangeInfoResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::GetExchangeInfoResponse");
875
876 let resp = client.get_exchange_info().await.expect("Expected a response");
877 let data_future = resp.data();
878 let actual_response = data_future.await.unwrap();
879 assert_eq!(actual_response, expected_response);
880 });
881 }
882
883 #[test]
884 fn get_exchange_info_optional_params_success() {
885 TOKIO_SHARED_RT.block_on(async {
886 let client = MockMarketDataApiClient { force_error: false };
887
888
889 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"","messageDetail":"","success":true,"data":{"timezone":"UTC","assets":[{"asset":"USDT"}],"symbols":[{"symbol":"ALPHA_105USDT","status":"TRADING","baseAsset":"ALPHA_105","quoteAsset":"USDT","pricePrecision":8,"quantityPrecision":8,"baseAssetPrecision":8,"quotePrecision":8,"filters":[{"filterType":"PRICE_FILTER","minPrice":"0.00000001","maxPrice":"1000","tickSize":"0.00000001","stepSize":"0.01000000","maxQty":"277778","minQty":"0.01000000","limit":200,"minNotional":"0.1","maxNotional":"1000000","multiplierDown":"0.20000","multiplierUp":"5","bidMultiplierUp":"5","askMultiplierUp":"5","bidMultiplierDown":"0.20000","askMultiplierDown":"0.20000"}],"orderTypes":["LIMIT"]}],"orderTypes":""}}"#).unwrap_or_else(|_| serde_json::json!({}));
890 let expected_response : models::GetExchangeInfoResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::GetExchangeInfoResponse");
891
892 let resp = client.get_exchange_info().await.expect("Expected a response");
893 let data_future = resp.data();
894 let actual_response = data_future.await.unwrap();
895 assert_eq!(actual_response, expected_response);
896 });
897 }
898
899 #[test]
900 fn get_exchange_info_response_error() {
901 TOKIO_SHARED_RT.block_on(async {
902 let client = MockMarketDataApiClient { force_error: true };
903
904 match client.get_exchange_info().await {
905 Ok(_) => panic!("Expected an error"),
906 Err(err) => {
907 assert_eq!(err.to_string(), "Connector client error: ResponseError");
908 }
909 }
910 });
911 }
912
913 #[test]
914 fn klines_required_params_success() {
915 TOKIO_SHARED_RT.block_on(async {
916 let client = MockMarketDataApiClient { force_error: false };
917
918 let params = KlinesParams::builder("ALPHA_175USDT".to_string(),KlinesIntervalEnum::Interval1s,).build().unwrap();
919
920 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"","messageDetail":"","success":true,"data":[["1752642000000"]]}"#).unwrap_or_else(|_| serde_json::json!({}));
921 let expected_response : models::KlinesResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::KlinesResponse");
922
923 let resp = client.klines(params).await.expect("Expected a response");
924 let data_future = resp.data();
925 let actual_response = data_future.await.unwrap();
926 assert_eq!(actual_response, expected_response);
927 });
928 }
929
930 #[test]
931 fn klines_optional_params_success() {
932 TOKIO_SHARED_RT.block_on(async {
933 let client = MockMarketDataApiClient { force_error: false };
934
935 let params = KlinesParams::builder("ALPHA_175USDT".to_string(),KlinesIntervalEnum::Interval1s,).limit(500).start_time(1752642000000).end_time(1752645599999).build().unwrap();
936
937 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"","messageDetail":"","success":true,"data":[["1752642000000"]]}"#).unwrap_or_else(|_| serde_json::json!({}));
938 let expected_response : models::KlinesResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::KlinesResponse");
939
940 let resp = client.klines(params).await.expect("Expected a response");
941 let data_future = resp.data();
942 let actual_response = data_future.await.unwrap();
943 assert_eq!(actual_response, expected_response);
944 });
945 }
946
947 #[test]
948 fn klines_response_error() {
949 TOKIO_SHARED_RT.block_on(async {
950 let client = MockMarketDataApiClient { force_error: true };
951
952 let params =
953 KlinesParams::builder("ALPHA_175USDT".to_string(), KlinesIntervalEnum::Interval1s)
954 .build()
955 .unwrap();
956
957 match client.klines(params).await {
958 Ok(_) => panic!("Expected an error"),
959 Err(err) => {
960 assert_eq!(err.to_string(), "Connector client error: ResponseError");
961 }
962 }
963 });
964 }
965
966 #[test]
967 fn ticker_required_params_success() {
968 TOKIO_SHARED_RT.block_on(async {
969 let client = MockMarketDataApiClient { force_error: false };
970
971 let params = TickerParams::builder("ALPHA_175USDT".to_string()).build().unwrap();
972
973 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"","messageDetail":"","data":{"symbol":"ALPHA_175USDT","priceChange":"-0.00007172","priceChangePercent":"-8.841","weightedAvgPrice":"0.00079608","lastPrice":"0.00073954","lastQty":"12600.43000000","openPrice":"0.00081126","highPrice":"0.00081126","lowPrice":"0.00073954","volume":"1204754.30000000","quoteVolume":"959.07729927","openTime":1768808100000,"closeTime":1768893244772,"firstId":93742,"lastId":93768,"count":38},"success":true}"#).unwrap_or_else(|_| serde_json::json!({}));
974 let expected_response : models::TickerResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::TickerResponse");
975
976 let resp = client.ticker(params).await.expect("Expected a response");
977 let data_future = resp.data();
978 let actual_response = data_future.await.unwrap();
979 assert_eq!(actual_response, expected_response);
980 });
981 }
982
983 #[test]
984 fn ticker_optional_params_success() {
985 TOKIO_SHARED_RT.block_on(async {
986 let client = MockMarketDataApiClient { force_error: false };
987
988 let params = TickerParams::builder("ALPHA_175USDT".to_string()).build().unwrap();
989
990 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"","messageDetail":"","data":{"symbol":"ALPHA_175USDT","priceChange":"-0.00007172","priceChangePercent":"-8.841","weightedAvgPrice":"0.00079608","lastPrice":"0.00073954","lastQty":"12600.43000000","openPrice":"0.00081126","highPrice":"0.00081126","lowPrice":"0.00073954","volume":"1204754.30000000","quoteVolume":"959.07729927","openTime":1768808100000,"closeTime":1768893244772,"firstId":93742,"lastId":93768,"count":38},"success":true}"#).unwrap_or_else(|_| serde_json::json!({}));
991 let expected_response : models::TickerResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::TickerResponse");
992
993 let resp = client.ticker(params).await.expect("Expected a response");
994 let data_future = resp.data();
995 let actual_response = data_future.await.unwrap();
996 assert_eq!(actual_response, expected_response);
997 });
998 }
999
1000 #[test]
1001 fn ticker_response_error() {
1002 TOKIO_SHARED_RT.block_on(async {
1003 let client = MockMarketDataApiClient { force_error: true };
1004
1005 let params = TickerParams::builder("ALPHA_175USDT".to_string())
1006 .build()
1007 .unwrap();
1008
1009 match client.ticker(params).await {
1010 Ok(_) => panic!("Expected an error"),
1011 Err(err) => {
1012 assert_eq!(err.to_string(), "Connector client error: ResponseError");
1013 }
1014 }
1015 });
1016 }
1017
1018 #[test]
1019 fn token_list_required_params_success() {
1020 TOKIO_SHARED_RT.block_on(async {
1021 let client = MockMarketDataApiClient { force_error: false };
1022
1023
1024 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"","messageDetail":"","success":true,"data":[{"tokenId":"3F350C8B3621770A673159B7A19BC034","chainId":"56","chainIconUrl":"https://bin.bnbstatic.com/image/admin_mgs_image_upload/20250228/d0216ce4-a3e9-4bda-8937-4a6aa943ccf2.png","chainName":"BSC","contractAddress":"0xcf640fdf9b3d9e45cbd69fda91d7e22579c14444","name":"gorilla","symbol":"gorilla","iconUrl":"https://bin.bnbstatic.com/images/web3-data/public/token/logos/248b5406f88a4ee28913a29107875339.png","price":"0.00080595003978242023","percentChange24h":"-7.42","volume24h":"14847.711222633409558029675","marketCap":"805950.03978242","fdv":"805950.03978242","liquidity":"263192.72813121626034","totalSupply":"1000000000","circulatingSupply":"1000000000","holders":"7120","decimals":18,"listingCex":false,"hotTag":false,"cexCoinName":"","canTransfer":false,"denomination":1,"offline":false,"tradeDecimal":8,"alphaId":"ALPHA_175","offsell":false,"priceHigh24h":"0.00088873864475645879","priceLow24h":"0.0008020805165487083","count24h":"166","onlineTge":false,"onlineAirdrop":false,"score":1,"cexOffDisplay":false,"stockState":false,"listingTime":1746686700000,"mulPoint":1,"bnExclusiveState":false}]}"#).unwrap_or_else(|_| serde_json::json!({}));
1025 let expected_response : models::TokenListResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::TokenListResponse");
1026
1027 let resp = client.token_list().await.expect("Expected a response");
1028 let data_future = resp.data();
1029 let actual_response = data_future.await.unwrap();
1030 assert_eq!(actual_response, expected_response);
1031 });
1032 }
1033
1034 #[test]
1035 fn token_list_optional_params_success() {
1036 TOKIO_SHARED_RT.block_on(async {
1037 let client = MockMarketDataApiClient { force_error: false };
1038
1039
1040 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"","messageDetail":"","success":true,"data":[{"tokenId":"3F350C8B3621770A673159B7A19BC034","chainId":"56","chainIconUrl":"https://bin.bnbstatic.com/image/admin_mgs_image_upload/20250228/d0216ce4-a3e9-4bda-8937-4a6aa943ccf2.png","chainName":"BSC","contractAddress":"0xcf640fdf9b3d9e45cbd69fda91d7e22579c14444","name":"gorilla","symbol":"gorilla","iconUrl":"https://bin.bnbstatic.com/images/web3-data/public/token/logos/248b5406f88a4ee28913a29107875339.png","price":"0.00080595003978242023","percentChange24h":"-7.42","volume24h":"14847.711222633409558029675","marketCap":"805950.03978242","fdv":"805950.03978242","liquidity":"263192.72813121626034","totalSupply":"1000000000","circulatingSupply":"1000000000","holders":"7120","decimals":18,"listingCex":false,"hotTag":false,"cexCoinName":"","canTransfer":false,"denomination":1,"offline":false,"tradeDecimal":8,"alphaId":"ALPHA_175","offsell":false,"priceHigh24h":"0.00088873864475645879","priceLow24h":"0.0008020805165487083","count24h":"166","onlineTge":false,"onlineAirdrop":false,"score":1,"cexOffDisplay":false,"stockState":false,"listingTime":1746686700000,"mulPoint":1,"bnExclusiveState":false}]}"#).unwrap_or_else(|_| serde_json::json!({}));
1041 let expected_response : models::TokenListResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::TokenListResponse");
1042
1043 let resp = client.token_list().await.expect("Expected a response");
1044 let data_future = resp.data();
1045 let actual_response = data_future.await.unwrap();
1046 assert_eq!(actual_response, expected_response);
1047 });
1048 }
1049
1050 #[test]
1051 fn token_list_response_error() {
1052 TOKIO_SHARED_RT.block_on(async {
1053 let client = MockMarketDataApiClient { force_error: true };
1054
1055 match client.token_list().await {
1056 Ok(_) => panic!("Expected an error"),
1057 Err(err) => {
1058 assert_eq!(err.to_string(), "Connector client error: ResponseError");
1059 }
1060 }
1061 });
1062 }
1063}