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 get_exchange_info(
39 &self,
40 ) -> anyhow::Result<RestApiResponse<models::GetExchangeInfoResponse>>;
41 async fn klines(
42 &self,
43 params: KlinesParams,
44 ) -> anyhow::Result<RestApiResponse<models::KlinesResponse>>;
45 async fn ticker(
46 &self,
47 params: TickerParams,
48 ) -> anyhow::Result<RestApiResponse<models::TickerResponse>>;
49 async fn token_list(&self) -> anyhow::Result<RestApiResponse<models::TokenListResponse>>;
50}
51
52#[derive(Debug, Clone)]
53pub struct MarketDataApiClient {
54 configuration: ConfigurationRestApi,
55}
56
57impl MarketDataApiClient {
58 pub fn new(configuration: ConfigurationRestApi) -> Self {
59 Self { configuration }
60 }
61}
62
63#[derive(Clone, Debug, Builder)]
68#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
69pub struct AggregatedTradesParams {
70 #[builder(setter(into))]
74 pub symbol: String,
75 #[builder(setter(into), default)]
79 pub from_id: Option<i64>,
80 #[builder(setter(into), default)]
84 pub start_time: Option<i64>,
85 #[builder(setter(into), default)]
89 pub end_time: Option<i64>,
90 #[builder(setter(into), default)]
94 pub limit: Option<i64>,
95}
96
97impl AggregatedTradesParams {
98 #[must_use]
105 pub fn builder(symbol: String) -> AggregatedTradesParamsBuilder {
106 AggregatedTradesParamsBuilder::default().symbol(symbol)
107 }
108}
109#[derive(Clone, Debug, Builder)]
114#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
115pub struct KlinesParams {
116 #[builder(setter(into))]
120 pub symbol: String,
121 #[builder(setter(into))]
125 pub interval: String,
126 #[builder(setter(into), default)]
130 pub limit: Option<i64>,
131 #[builder(setter(into), default)]
135 pub start_time: Option<i64>,
136 #[builder(setter(into), default)]
140 pub end_time: Option<i64>,
141}
142
143impl KlinesParams {
144 #[must_use]
152 pub fn builder(symbol: String, interval: String) -> KlinesParamsBuilder {
153 KlinesParamsBuilder::default()
154 .symbol(symbol)
155 .interval(interval)
156 }
157}
158#[derive(Clone, Debug, Builder)]
163#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
164pub struct TickerParams {
165 #[builder(setter(into))]
169 pub symbol: String,
170}
171
172impl TickerParams {
173 #[must_use]
180 pub fn builder(symbol: String) -> TickerParamsBuilder {
181 TickerParamsBuilder::default().symbol(symbol)
182 }
183}
184
185#[async_trait]
186impl MarketDataApi for MarketDataApiClient {
187 async fn aggregated_trades(
188 &self,
189 params: AggregatedTradesParams,
190 ) -> anyhow::Result<RestApiResponse<models::AggregatedTradesResponse>> {
191 let AggregatedTradesParams {
192 symbol,
193 from_id,
194 start_time,
195 end_time,
196 limit,
197 } = params;
198
199 let mut query_params = BTreeMap::new();
200 let body_params = BTreeMap::new();
201
202 query_params.insert("symbol".to_string(), json!(symbol));
203
204 if let Some(rw) = from_id {
205 query_params.insert("fromId".to_string(), json!(rw));
206 }
207
208 if let Some(rw) = start_time {
209 query_params.insert("startTime".to_string(), json!(rw));
210 }
211
212 if let Some(rw) = end_time {
213 query_params.insert("endTime".to_string(), json!(rw));
214 }
215
216 if let Some(rw) = limit {
217 query_params.insert("limit".to_string(), json!(rw));
218 }
219
220 send_request::<models::AggregatedTradesResponse>(
221 &self.configuration,
222 "/bapi/defi/v1/public/alpha-trade/agg-trades",
223 reqwest::Method::GET,
224 query_params,
225 body_params,
226 if HAS_TIME_UNIT {
227 self.configuration.time_unit
228 } else {
229 None
230 },
231 false,
232 )
233 .await
234 }
235
236 async fn get_exchange_info(
237 &self,
238 ) -> anyhow::Result<RestApiResponse<models::GetExchangeInfoResponse>> {
239 let query_params = BTreeMap::new();
240 let body_params = BTreeMap::new();
241
242 send_request::<models::GetExchangeInfoResponse>(
243 &self.configuration,
244 "/bapi/defi/v1/public/alpha-trade/get-exchange-info",
245 reqwest::Method::GET,
246 query_params,
247 body_params,
248 if HAS_TIME_UNIT {
249 self.configuration.time_unit
250 } else {
251 None
252 },
253 false,
254 )
255 .await
256 }
257
258 async fn klines(
259 &self,
260 params: KlinesParams,
261 ) -> anyhow::Result<RestApiResponse<models::KlinesResponse>> {
262 let KlinesParams {
263 symbol,
264 interval,
265 limit,
266 start_time,
267 end_time,
268 } = params;
269
270 let mut query_params = BTreeMap::new();
271 let body_params = BTreeMap::new();
272
273 query_params.insert("symbol".to_string(), json!(symbol));
274
275 query_params.insert("interval".to_string(), json!(interval));
276
277 if let Some(rw) = limit {
278 query_params.insert("limit".to_string(), json!(rw));
279 }
280
281 if let Some(rw) = start_time {
282 query_params.insert("startTime".to_string(), json!(rw));
283 }
284
285 if let Some(rw) = end_time {
286 query_params.insert("endTime".to_string(), json!(rw));
287 }
288
289 send_request::<models::KlinesResponse>(
290 &self.configuration,
291 "/bapi/defi/v1/public/alpha-trade/klines",
292 reqwest::Method::GET,
293 query_params,
294 body_params,
295 if HAS_TIME_UNIT {
296 self.configuration.time_unit
297 } else {
298 None
299 },
300 false,
301 )
302 .await
303 }
304
305 async fn ticker(
306 &self,
307 params: TickerParams,
308 ) -> anyhow::Result<RestApiResponse<models::TickerResponse>> {
309 let TickerParams { symbol } = params;
310
311 let mut query_params = BTreeMap::new();
312 let body_params = BTreeMap::new();
313
314 query_params.insert("symbol".to_string(), json!(symbol));
315
316 send_request::<models::TickerResponse>(
317 &self.configuration,
318 "/bapi/defi/v1/public/alpha-trade/ticker",
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 false,
328 )
329 .await
330 }
331
332 async fn token_list(&self) -> anyhow::Result<RestApiResponse<models::TokenListResponse>> {
333 let query_params = BTreeMap::new();
334 let body_params = BTreeMap::new();
335
336 send_request::<models::TokenListResponse>(
337 &self.configuration,
338 "/bapi/defi/v1/public/wallet-direct/buw/wallet/cex/alpha/all/token/list",
339 reqwest::Method::GET,
340 query_params,
341 body_params,
342 if HAS_TIME_UNIT {
343 self.configuration.time_unit
344 } else {
345 None
346 },
347 false,
348 )
349 .await
350 }
351}
352
353#[cfg(all(test, feature = "alpha"))]
354mod tests {
355 use super::*;
356 use crate::TOKIO_SHARED_RT;
357 use crate::{errors::ConnectorError, models::DataFuture, models::RestApiRateLimit};
358 use async_trait::async_trait;
359 use std::collections::HashMap;
360
361 struct DummyRestApiResponse<T> {
362 inner: Box<dyn FnOnce() -> DataFuture<Result<T, ConnectorError>> + Send + Sync>,
363 status: u16,
364 headers: HashMap<String, String>,
365 rate_limits: Option<Vec<RestApiRateLimit>>,
366 }
367
368 impl<T> From<DummyRestApiResponse<T>> for RestApiResponse<T> {
369 fn from(dummy: DummyRestApiResponse<T>) -> Self {
370 Self {
371 data_fn: dummy.inner,
372 status: dummy.status,
373 headers: dummy.headers,
374 rate_limits: dummy.rate_limits,
375 }
376 }
377 }
378
379 struct MockMarketDataApiClient {
380 force_error: bool,
381 }
382
383 #[async_trait]
384 impl MarketDataApi for MockMarketDataApiClient {
385 async fn aggregated_trades(
386 &self,
387 _params: AggregatedTradesParams,
388 ) -> anyhow::Result<RestApiResponse<models::AggregatedTradesResponse>> {
389 if self.force_error {
390 return Err(ConnectorError::ConnectorClientError {
391 msg: "ResponseError".to_string(),
392 code: None,
393 }
394 .into());
395 }
396
397 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();
398 let dummy_response: models::AggregatedTradesResponse =
399 serde_json::from_value(resp_json.clone())
400 .expect("should parse into models::AggregatedTradesResponse");
401
402 let dummy = DummyRestApiResponse {
403 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
404 status: 200,
405 headers: HashMap::new(),
406 rate_limits: None,
407 };
408
409 Ok(dummy.into())
410 }
411
412 async fn get_exchange_info(
413 &self,
414 ) -> anyhow::Result<RestApiResponse<models::GetExchangeInfoResponse>> {
415 if self.force_error {
416 return Err(ConnectorError::ConnectorClientError {
417 msg: "ResponseError".to_string(),
418 code: None,
419 }
420 .into());
421 }
422
423 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"},{"filterType":"LOT_SIZE","stepSize":"0.01000000","maxQty":"277778","minQty":"0.01000000"},{"filterType":"MAX_NUM_ORDERS","limit":200},{"filterType":"MIN_NOTIONAL","minNotional":"0.1"},{"filterType":"MAX_NOTIONAL","maxNotional":"1000000"},{"filterType":"NOTIONAL","minNotional":"0.1","maxNotional":"1000000"},{"filterType":"PERCENT_PRICE","multiplierDown":"0.20000","multiplierUp":"5"},{"filterType":"PERCENT_PRICE_BY_SIDE","bidMultiplierUp":"5","askMultiplierUp":"5","bidMultiplierDown":"0.20000","askMultiplierDown":"0.20000"}],"orderTypes":["LIMIT"]}],"orderTypes":""}}"#).unwrap();
424 let dummy_response: models::GetExchangeInfoResponse =
425 serde_json::from_value(resp_json.clone())
426 .expect("should parse into models::GetExchangeInfoResponse");
427
428 let dummy = DummyRestApiResponse {
429 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
430 status: 200,
431 headers: HashMap::new(),
432 rate_limits: None,
433 };
434
435 Ok(dummy.into())
436 }
437
438 async fn klines(
439 &self,
440 _params: KlinesParams,
441 ) -> anyhow::Result<RestApiResponse<models::KlinesResponse>> {
442 if self.force_error {
443 return Err(ConnectorError::ConnectorClientError {
444 msg: "ResponseError".to_string(),
445 code: None,
446 }
447 .into());
448 }
449
450 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"","messageDetail":"","success":true,"data":[["1752642000000","0.00171473","0.00172515","0.00171473","0.00172515","1771.86000000","1752645599999","3.05093481","2","1771.86000000","3.05093481",0]]}"#).unwrap();
451 let dummy_response: models::KlinesResponse = serde_json::from_value(resp_json.clone())
452 .expect("should parse into models::KlinesResponse");
453
454 let dummy = DummyRestApiResponse {
455 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
456 status: 200,
457 headers: HashMap::new(),
458 rate_limits: None,
459 };
460
461 Ok(dummy.into())
462 }
463
464 async fn ticker(
465 &self,
466 _params: TickerParams,
467 ) -> anyhow::Result<RestApiResponse<models::TickerResponse>> {
468 if self.force_error {
469 return Err(ConnectorError::ConnectorClientError {
470 msg: "ResponseError".to_string(),
471 code: None,
472 }
473 .into());
474 }
475
476 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();
477 let dummy_response: models::TickerResponse = serde_json::from_value(resp_json.clone())
478 .expect("should parse into models::TickerResponse");
479
480 let dummy = DummyRestApiResponse {
481 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
482 status: 200,
483 headers: HashMap::new(),
484 rate_limits: None,
485 };
486
487 Ok(dummy.into())
488 }
489
490 async fn token_list(&self) -> anyhow::Result<RestApiResponse<models::TokenListResponse>> {
491 if self.force_error {
492 return Err(ConnectorError::ConnectorClientError {
493 msg: "ResponseError".to_string(),
494 code: None,
495 }
496 .into());
497 }
498
499 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();
500 let dummy_response: models::TokenListResponse =
501 serde_json::from_value(resp_json.clone())
502 .expect("should parse into models::TokenListResponse");
503
504 let dummy = DummyRestApiResponse {
505 inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
506 status: 200,
507 headers: HashMap::new(),
508 rate_limits: None,
509 };
510
511 Ok(dummy.into())
512 }
513 }
514
515 #[test]
516 fn aggregated_trades_required_params_success() {
517 TOKIO_SHARED_RT.block_on(async {
518 let client = MockMarketDataApiClient { force_error: false };
519
520 let params = AggregatedTradesParams::builder("symbol_example".to_string(),).build().unwrap();
521
522 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();
523 let expected_response : models::AggregatedTradesResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::AggregatedTradesResponse");
524
525 let resp = client.aggregated_trades(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 aggregated_trades_optional_params_success() {
534 TOKIO_SHARED_RT.block_on(async {
535 let client = MockMarketDataApiClient { force_error: false };
536
537 let params = AggregatedTradesParams::builder("symbol_example".to_string(),).from_id(1).start_time(1623319461670).end_time(1641782889000).limit(500).build().unwrap();
538
539 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();
540 let expected_response : models::AggregatedTradesResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::AggregatedTradesResponse");
541
542 let resp = client.aggregated_trades(params).await.expect("Expected a response");
543 let data_future = resp.data();
544 let actual_response = data_future.await.unwrap();
545 assert_eq!(actual_response, expected_response);
546 });
547 }
548
549 #[test]
550 fn aggregated_trades_response_error() {
551 TOKIO_SHARED_RT.block_on(async {
552 let client = MockMarketDataApiClient { force_error: true };
553
554 let params = AggregatedTradesParams::builder("symbol_example".to_string())
555 .build()
556 .unwrap();
557
558 match client.aggregated_trades(params).await {
559 Ok(_) => panic!("Expected an error"),
560 Err(err) => {
561 assert_eq!(err.to_string(), "Connector client error: ResponseError");
562 }
563 }
564 });
565 }
566
567 #[test]
568 fn get_exchange_info_required_params_success() {
569 TOKIO_SHARED_RT.block_on(async {
570 let client = MockMarketDataApiClient { force_error: false };
571
572
573 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"},{"filterType":"LOT_SIZE","stepSize":"0.01000000","maxQty":"277778","minQty":"0.01000000"},{"filterType":"MAX_NUM_ORDERS","limit":200},{"filterType":"MIN_NOTIONAL","minNotional":"0.1"},{"filterType":"MAX_NOTIONAL","maxNotional":"1000000"},{"filterType":"NOTIONAL","minNotional":"0.1","maxNotional":"1000000"},{"filterType":"PERCENT_PRICE","multiplierDown":"0.20000","multiplierUp":"5"},{"filterType":"PERCENT_PRICE_BY_SIDE","bidMultiplierUp":"5","askMultiplierUp":"5","bidMultiplierDown":"0.20000","askMultiplierDown":"0.20000"}],"orderTypes":["LIMIT"]}],"orderTypes":""}}"#).unwrap();
574 let expected_response : models::GetExchangeInfoResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::GetExchangeInfoResponse");
575
576 let resp = client.get_exchange_info().await.expect("Expected a response");
577 let data_future = resp.data();
578 let actual_response = data_future.await.unwrap();
579 assert_eq!(actual_response, expected_response);
580 });
581 }
582
583 #[test]
584 fn get_exchange_info_optional_params_success() {
585 TOKIO_SHARED_RT.block_on(async {
586 let client = MockMarketDataApiClient { force_error: false };
587
588
589 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"},{"filterType":"LOT_SIZE","stepSize":"0.01000000","maxQty":"277778","minQty":"0.01000000"},{"filterType":"MAX_NUM_ORDERS","limit":200},{"filterType":"MIN_NOTIONAL","minNotional":"0.1"},{"filterType":"MAX_NOTIONAL","maxNotional":"1000000"},{"filterType":"NOTIONAL","minNotional":"0.1","maxNotional":"1000000"},{"filterType":"PERCENT_PRICE","multiplierDown":"0.20000","multiplierUp":"5"},{"filterType":"PERCENT_PRICE_BY_SIDE","bidMultiplierUp":"5","askMultiplierUp":"5","bidMultiplierDown":"0.20000","askMultiplierDown":"0.20000"}],"orderTypes":["LIMIT"]}],"orderTypes":""}}"#).unwrap();
590 let expected_response : models::GetExchangeInfoResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::GetExchangeInfoResponse");
591
592 let resp = client.get_exchange_info().await.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 get_exchange_info_response_error() {
601 TOKIO_SHARED_RT.block_on(async {
602 let client = MockMarketDataApiClient { force_error: true };
603
604 match client.get_exchange_info().await {
605 Ok(_) => panic!("Expected an error"),
606 Err(err) => {
607 assert_eq!(err.to_string(), "Connector client error: ResponseError");
608 }
609 }
610 });
611 }
612
613 #[test]
614 fn klines_required_params_success() {
615 TOKIO_SHARED_RT.block_on(async {
616 let client = MockMarketDataApiClient { force_error: false };
617
618 let params = KlinesParams::builder("symbol_example".to_string(),"interval_example".to_string(),).build().unwrap();
619
620 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"","messageDetail":"","success":true,"data":[["1752642000000","0.00171473","0.00172515","0.00171473","0.00172515","1771.86000000","1752645599999","3.05093481","2","1771.86000000","3.05093481",0]]}"#).unwrap();
621 let expected_response : models::KlinesResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::KlinesResponse");
622
623 let resp = client.klines(params).await.expect("Expected a response");
624 let data_future = resp.data();
625 let actual_response = data_future.await.unwrap();
626 assert_eq!(actual_response, expected_response);
627 });
628 }
629
630 #[test]
631 fn klines_optional_params_success() {
632 TOKIO_SHARED_RT.block_on(async {
633 let client = MockMarketDataApiClient { force_error: false };
634
635 let params = KlinesParams::builder("symbol_example".to_string(),"interval_example".to_string(),).limit(500).start_time(1623319461670).end_time(1641782889000).build().unwrap();
636
637 let resp_json: Value = serde_json::from_str(r#"{"code":"000000","message":"","messageDetail":"","success":true,"data":[["1752642000000","0.00171473","0.00172515","0.00171473","0.00172515","1771.86000000","1752645599999","3.05093481","2","1771.86000000","3.05093481",0]]}"#).unwrap();
638 let expected_response : models::KlinesResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::KlinesResponse");
639
640 let resp = client.klines(params).await.expect("Expected a response");
641 let data_future = resp.data();
642 let actual_response = data_future.await.unwrap();
643 assert_eq!(actual_response, expected_response);
644 });
645 }
646
647 #[test]
648 fn klines_response_error() {
649 TOKIO_SHARED_RT.block_on(async {
650 let client = MockMarketDataApiClient { force_error: true };
651
652 let params =
653 KlinesParams::builder("symbol_example".to_string(), "interval_example".to_string())
654 .build()
655 .unwrap();
656
657 match client.klines(params).await {
658 Ok(_) => panic!("Expected an error"),
659 Err(err) => {
660 assert_eq!(err.to_string(), "Connector client error: ResponseError");
661 }
662 }
663 });
664 }
665
666 #[test]
667 fn ticker_required_params_success() {
668 TOKIO_SHARED_RT.block_on(async {
669 let client = MockMarketDataApiClient { force_error: false };
670
671 let params = TickerParams::builder("symbol_example".to_string()).build().unwrap();
672
673 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();
674 let expected_response : models::TickerResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::TickerResponse");
675
676 let resp = client.ticker(params).await.expect("Expected a response");
677 let data_future = resp.data();
678 let actual_response = data_future.await.unwrap();
679 assert_eq!(actual_response, expected_response);
680 });
681 }
682
683 #[test]
684 fn ticker_optional_params_success() {
685 TOKIO_SHARED_RT.block_on(async {
686 let client = MockMarketDataApiClient { force_error: false };
687
688 let params = TickerParams::builder("symbol_example".to_string()).build().unwrap();
689
690 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();
691 let expected_response : models::TickerResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::TickerResponse");
692
693 let resp = client.ticker(params).await.expect("Expected a response");
694 let data_future = resp.data();
695 let actual_response = data_future.await.unwrap();
696 assert_eq!(actual_response, expected_response);
697 });
698 }
699
700 #[test]
701 fn ticker_response_error() {
702 TOKIO_SHARED_RT.block_on(async {
703 let client = MockMarketDataApiClient { force_error: true };
704
705 let params = TickerParams::builder("symbol_example".to_string())
706 .build()
707 .unwrap();
708
709 match client.ticker(params).await {
710 Ok(_) => panic!("Expected an error"),
711 Err(err) => {
712 assert_eq!(err.to_string(), "Connector client error: ResponseError");
713 }
714 }
715 });
716 }
717
718 #[test]
719 fn token_list_required_params_success() {
720 TOKIO_SHARED_RT.block_on(async {
721 let client = MockMarketDataApiClient { force_error: false };
722
723
724 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();
725 let expected_response : models::TokenListResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::TokenListResponse");
726
727 let resp = client.token_list().await.expect("Expected a response");
728 let data_future = resp.data();
729 let actual_response = data_future.await.unwrap();
730 assert_eq!(actual_response, expected_response);
731 });
732 }
733
734 #[test]
735 fn token_list_optional_params_success() {
736 TOKIO_SHARED_RT.block_on(async {
737 let client = MockMarketDataApiClient { force_error: false };
738
739
740 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();
741 let expected_response : models::TokenListResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::TokenListResponse");
742
743 let resp = client.token_list().await.expect("Expected a response");
744 let data_future = resp.data();
745 let actual_response = data_future.await.unwrap();
746 assert_eq!(actual_response, expected_response);
747 });
748 }
749
750 #[test]
751 fn token_list_response_error() {
752 TOKIO_SHARED_RT.block_on(async {
753 let client = MockMarketDataApiClient { force_error: true };
754
755 match client.token_list().await {
756 Ok(_) => panic!("Expected an error"),
757 Err(err) => {
758 assert_eq!(err.to_string(), "Connector client error: ResponseError");
759 }
760 }
761 });
762 }
763}