1#![allow(unused_imports)]
15use anyhow::Context;
16use async_trait::async_trait;
17use derive_builder::Builder;
18use rust_decimal::prelude::*;
19use serde::{Deserialize, Serialize};
20use serde_json::Value;
21use std::{collections::BTreeMap, sync::Arc};
22
23use crate::common::{
24 errors::WebsocketError,
25 models::{ParamBuildError, WebsocketApiResponse},
26 utils::remove_empty_value,
27 websocket::{WebsocketApi, WebsocketMessageSendOptions},
28};
29use crate::spot::websocket_api::models;
30
31#[async_trait]
32pub trait GeneralApi: Send + Sync {
33 async fn exchange_info(
34 &self,
35 params: ExchangeInfoParams,
36 ) -> anyhow::Result<WebsocketApiResponse<models::ExchangeInfoResponse>>;
37 async fn execution_rules(
38 &self,
39 params: ExecutionRulesParams,
40 ) -> anyhow::Result<WebsocketApiResponse<Box<models::ExecutionRulesResponseResult>>>;
41 async fn ping(
42 &self,
43 params: PingParams,
44 ) -> anyhow::Result<WebsocketApiResponse<serde_json::Value>>;
45 async fn time(
46 &self,
47 params: TimeParams,
48 ) -> anyhow::Result<WebsocketApiResponse<Box<models::TimeResponseResult>>>;
49}
50
51#[derive(Clone)]
52pub struct GeneralApiClient {
53 websocket_api_base: Arc<WebsocketApi>,
54}
55
56impl GeneralApiClient {
57 pub fn new(websocket_api_base: Arc<WebsocketApi>) -> Self {
58 Self { websocket_api_base }
59 }
60}
61
62#[allow(non_camel_case_types)]
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub enum ExchangeInfoSymbolStatusEnum {
65 #[serde(rename = "TRADING")]
66 Trading,
67 #[serde(rename = "HALT")]
68 Halt,
69 #[serde(rename = "BREAK")]
70 Break,
71}
72
73impl ExchangeInfoSymbolStatusEnum {
74 #[must_use]
75 pub fn as_str(&self) -> &'static str {
76 match self {
77 Self::Trading => "TRADING",
78 Self::Halt => "HALT",
79 Self::Break => "BREAK",
80 }
81 }
82}
83
84impl std::str::FromStr for ExchangeInfoSymbolStatusEnum {
85 type Err = Box<dyn std::error::Error + Send + Sync>;
86
87 fn from_str(s: &str) -> Result<Self, Self::Err> {
88 match s {
89 "TRADING" => Ok(Self::Trading),
90 "HALT" => Ok(Self::Halt),
91 "BREAK" => Ok(Self::Break),
92 other => Err(format!("invalid ExchangeInfoSymbolStatusEnum: {}", other).into()),
93 }
94 }
95}
96
97#[allow(non_camel_case_types)]
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub enum ExecutionRulesSymbolStatusEnum {
100 #[serde(rename = "TRADING")]
101 Trading,
102 #[serde(rename = "HALT")]
103 Halt,
104 #[serde(rename = "BREAK")]
105 Break,
106}
107
108impl ExecutionRulesSymbolStatusEnum {
109 #[must_use]
110 pub fn as_str(&self) -> &'static str {
111 match self {
112 Self::Trading => "TRADING",
113 Self::Halt => "HALT",
114 Self::Break => "BREAK",
115 }
116 }
117}
118
119impl std::str::FromStr for ExecutionRulesSymbolStatusEnum {
120 type Err = Box<dyn std::error::Error + Send + Sync>;
121
122 fn from_str(s: &str) -> Result<Self, Self::Err> {
123 match s {
124 "TRADING" => Ok(Self::Trading),
125 "HALT" => Ok(Self::Halt),
126 "BREAK" => Ok(Self::Break),
127 other => Err(format!("invalid ExecutionRulesSymbolStatusEnum: {}", other).into()),
128 }
129 }
130}
131
132#[derive(Clone, Debug, Builder, Deserialize, Default)]
137#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
138pub struct ExchangeInfoParams {
139 #[builder(setter(into), default)]
143 #[serde(rename = "id", default)]
144 pub id: Option<String>,
145 #[builder(setter(into), default)]
149 #[serde(rename = "symbol", default)]
150 pub symbol: Option<String>,
151 #[builder(setter(into), default)]
155 #[serde(rename = "symbols", default)]
156 pub symbols: Option<Vec<String>>,
157 #[builder(setter(into), default)]
161 #[serde(rename = "permissions", default)]
162 pub permissions: Option<Vec<String>>,
163 #[builder(setter(into), default)]
167 #[serde(rename = "showPermissionSets", default)]
168 pub show_permission_sets: Option<bool>,
169 #[builder(setter(into), default)]
173 #[serde(rename = "symbolStatus", default)]
174 pub symbol_status: Option<ExchangeInfoSymbolStatusEnum>,
175}
176
177impl ExchangeInfoParams {
178 #[must_use]
181 pub fn builder() -> ExchangeInfoParamsBuilder {
182 ExchangeInfoParamsBuilder::default()
183 }
184}
185#[derive(Clone, Debug, Builder, Deserialize, Default)]
190#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
191pub struct ExecutionRulesParams {
192 #[builder(setter(into), default)]
196 #[serde(rename = "id", default)]
197 pub id: Option<String>,
198 #[builder(setter(into), default)]
202 #[serde(rename = "symbol", default)]
203 pub symbol: Option<String>,
204 #[builder(setter(into), default)]
208 #[serde(rename = "symbols", default)]
209 pub symbols: Option<Vec<String>>,
210 #[builder(setter(into), default)]
214 #[serde(rename = "symbolStatus", default)]
215 pub symbol_status: Option<ExecutionRulesSymbolStatusEnum>,
216}
217
218impl ExecutionRulesParams {
219 #[must_use]
222 pub fn builder() -> ExecutionRulesParamsBuilder {
223 ExecutionRulesParamsBuilder::default()
224 }
225}
226#[derive(Clone, Debug, Builder, Deserialize, Default)]
231#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
232pub struct PingParams {
233 #[builder(setter(into), default)]
237 #[serde(rename = "id", default)]
238 pub id: Option<String>,
239}
240
241impl PingParams {
242 #[must_use]
245 pub fn builder() -> PingParamsBuilder {
246 PingParamsBuilder::default()
247 }
248}
249#[derive(Clone, Debug, Builder, Deserialize, Default)]
254#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
255pub struct TimeParams {
256 #[builder(setter(into), default)]
260 #[serde(rename = "id", default)]
261 pub id: Option<String>,
262}
263
264impl TimeParams {
265 #[must_use]
268 pub fn builder() -> TimeParamsBuilder {
269 TimeParamsBuilder::default()
270 }
271}
272
273#[async_trait]
274impl GeneralApi for GeneralApiClient {
275 async fn exchange_info(
276 &self,
277 params: ExchangeInfoParams,
278 ) -> anyhow::Result<WebsocketApiResponse<models::ExchangeInfoResponse>> {
279 let ExchangeInfoParams {
280 id,
281 symbol,
282 symbols,
283 permissions,
284 show_permission_sets,
285 symbol_status,
286 } = params;
287
288 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
289 if let Some(value) = id {
290 payload.insert("id".to_string(), serde_json::json!(value));
291 }
292 if let Some(value) = symbol {
293 payload.insert("symbol".to_string(), serde_json::json!(value));
294 }
295 if let Some(value) = symbols {
296 payload.insert("symbols".to_string(), serde_json::json!(value));
297 }
298 if let Some(value) = permissions {
299 payload.insert("permissions".to_string(), serde_json::json!(value));
300 }
301 if let Some(value) = show_permission_sets {
302 payload.insert("showPermissionSets".to_string(), serde_json::json!(value));
303 }
304 if let Some(value) = symbol_status {
305 payload.insert("symbolStatus".to_string(), serde_json::json!(value));
306 }
307 let payload = remove_empty_value(payload);
308
309 self.websocket_api_base
310 .send_message::<models::ExchangeInfoResponse>(
311 "/exchangeInfo".trim_start_matches('/'),
312 payload,
313 WebsocketMessageSendOptions::new(),
314 )
315 .await
316 .map_err(anyhow::Error::from)?
317 .into_iter()
318 .next()
319 .ok_or(WebsocketError::NoResponse)
320 .map_err(anyhow::Error::from)
321 }
322
323 async fn execution_rules(
324 &self,
325 params: ExecutionRulesParams,
326 ) -> anyhow::Result<WebsocketApiResponse<Box<models::ExecutionRulesResponseResult>>> {
327 let ExecutionRulesParams {
328 id,
329 symbol,
330 symbols,
331 symbol_status,
332 } = params;
333
334 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
335 if let Some(value) = id {
336 payload.insert("id".to_string(), serde_json::json!(value));
337 }
338 if let Some(value) = symbol {
339 payload.insert("symbol".to_string(), serde_json::json!(value));
340 }
341 if let Some(value) = symbols {
342 payload.insert("symbols".to_string(), serde_json::json!(value));
343 }
344 if let Some(value) = symbol_status {
345 payload.insert("symbolStatus".to_string(), serde_json::json!(value));
346 }
347 let payload = remove_empty_value(payload);
348
349 self.websocket_api_base
350 .send_message::<Box<models::ExecutionRulesResponseResult>>(
351 "/executionRules".trim_start_matches('/'),
352 payload,
353 WebsocketMessageSendOptions::new(),
354 )
355 .await
356 .map_err(anyhow::Error::from)?
357 .into_iter()
358 .next()
359 .ok_or(WebsocketError::NoResponse)
360 .map_err(anyhow::Error::from)
361 }
362
363 async fn ping(
364 &self,
365 params: PingParams,
366 ) -> anyhow::Result<WebsocketApiResponse<serde_json::Value>> {
367 let PingParams { id } = params;
368
369 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
370 if let Some(value) = id {
371 payload.insert("id".to_string(), serde_json::json!(value));
372 }
373 let payload = remove_empty_value(payload);
374
375 self.websocket_api_base
376 .send_message::<serde_json::Value>(
377 "/ping".trim_start_matches('/'),
378 payload,
379 WebsocketMessageSendOptions::new(),
380 )
381 .await
382 .map_err(anyhow::Error::from)?
383 .into_iter()
384 .next()
385 .ok_or(WebsocketError::NoResponse)
386 .map_err(anyhow::Error::from)
387 }
388
389 async fn time(
390 &self,
391 params: TimeParams,
392 ) -> anyhow::Result<WebsocketApiResponse<Box<models::TimeResponseResult>>> {
393 let TimeParams { id } = params;
394
395 let mut payload: BTreeMap<String, Value> = BTreeMap::new();
396 if let Some(value) = id {
397 payload.insert("id".to_string(), serde_json::json!(value));
398 }
399 let payload = remove_empty_value(payload);
400
401 self.websocket_api_base
402 .send_message::<Box<models::TimeResponseResult>>(
403 "/time".trim_start_matches('/'),
404 payload,
405 WebsocketMessageSendOptions::new(),
406 )
407 .await
408 .map_err(anyhow::Error::from)?
409 .into_iter()
410 .next()
411 .ok_or(WebsocketError::NoResponse)
412 .map_err(anyhow::Error::from)
413 }
414}
415
416#[cfg(all(test, feature = "spot"))]
417mod tests {
418 use super::*;
419 use crate::TOKIO_SHARED_RT;
420 use crate::common::websocket::{WebsocketApi, WebsocketConnection, WebsocketHandler};
421 use crate::config::ConfigurationWebsocketApi;
422 use crate::errors::WebsocketError;
423 use crate::models::WebsocketApiRateLimit;
424 use serde_json::{Value, json};
425 use tokio::spawn;
426 use tokio::sync::mpsc::{UnboundedReceiver, unbounded_channel};
427 use tokio::time::{Duration, timeout};
428 use tokio_tungstenite::tungstenite::Message;
429
430 async fn setup() -> (
431 Arc<WebsocketApi>,
432 Arc<WebsocketConnection>,
433 UnboundedReceiver<Message>,
434 ) {
435 let conn = WebsocketConnection::new("test-conn");
436 let (tx, rx) = unbounded_channel::<Message>();
437 {
438 let mut conn_state = conn.state.lock().await;
439 conn_state.ws_write_tx = Some(tx);
440 }
441
442 let config = ConfigurationWebsocketApi::builder()
443 .api_key("key")
444 .api_secret("secret")
445 .build()
446 .expect("Failed to build configuration");
447 let ws_api = WebsocketApi::new(config, vec![conn.clone()]);
448 conn.set_handler(ws_api.clone() as Arc<dyn WebsocketHandler>)
449 .await;
450 ws_api.clone().connect().await.unwrap();
451
452 (ws_api, conn, rx)
453 }
454
455 #[test]
456 fn exchange_info_success() {
457 TOKIO_SHARED_RT.block_on(async {
458 let (ws_api, conn, mut rx) = setup().await;
459 let client = GeneralApiClient::new(ws_api.clone());
460
461 let handle = spawn(async move {
462 let params = ExchangeInfoParams::builder().build().unwrap();
463 client.exchange_info(params).await
464 });
465
466 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
467 let Message::Text(text) = sent else { panic!() };
468 let v: Value = serde_json::from_str(&text).unwrap();
469 let id = v["id"].as_str().unwrap();
470 assert_eq!(v["method"], "/exchangeInfo".trim_start_matches('/'));
471 let mut resp_json: Value = serde_json::from_str(r#"{"id":"5494febb-d167-46a2-996d-70533eb4d976","status":200,"result":{"timezone":"UTC","serverTime":1655969291181,"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":321}],"exchangeFilters":[{"filterType":"EXCHANGE_MAX_NUM_ORDERS","maxNumOrders":1000}],"symbols":[{"symbol":"BNBBTC","status":"TRADING","baseAsset":"BNB","baseAssetPrecision":8,"quoteAsset":"BTC","quotePrecision":8,"quoteAssetPrecision":8,"baseCommissionPrecision":8,"quoteCommissionPrecision":8,"orderTypes":["LIMIT"],"icebergAllowed":true,"ocoAllowed":true,"otoAllowed":true,"opoAllowed":true,"quoteOrderQtyMarketAllowed":true,"allowTrailingStop":true,"cancelReplaceAllowed":true,"amendAllowed":false,"pegInstructionsAllowed":true,"isSpotTradingAllowed":true,"isMarginTradingAllowed":true,"filters":[{"filterType":"PRICE_FILTER","priceExponent":8,"minPrice":"0.00000100","maxPrice":"100000.00000000","tickSize":"0.00000100"}],"permissions":["SPOT"],"permissionSets":[["SPOT"]],"defaultSelfTradePreventionMode":"NONE","allowedSelfTradePreventionModes":["NONE"]}],"sors":[{"baseAsset":"BTC","symbols":["BTCUSDT"]}]}}"#).unwrap_or_else(|_| serde_json::json!({}));
472 resp_json["id"] = id.into();
473
474 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
475 let expected_data: models::ExchangeInfoResponse = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
476 let empty_array = Value::Array(vec![]);
477 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
478 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
479 match raw_rate_limits.as_array() {
480 Some(arr) if arr.is_empty() => None,
481 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
482 None => None,
483 };
484
485 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
486
487 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
488
489
490 let response_rate_limits = response.rate_limits.clone();
491 let response_data = response.data().expect("deserialize data");
492
493 assert_eq!(response_rate_limits, expected_rate_limits);
494 assert_eq!(response_data, expected_data);
495 });
496 }
497
498 #[test]
499 fn exchange_info_error_response() {
500 TOKIO_SHARED_RT.block_on(async {
501 let (ws_api, conn, mut rx) = setup().await;
502 let client = GeneralApiClient::new(ws_api.clone());
503
504 let handle = tokio::spawn(async move {
505 let params = ExchangeInfoParams::builder().build().unwrap();
506 client.exchange_info(params).await
507 });
508
509 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
510 let Message::Text(text) = sent else { panic!() };
511 let v: Value = serde_json::from_str(&text).unwrap();
512 let id = v["id"].as_str().unwrap().to_string();
513
514 let resp_json = json!({
515 "id": id,
516 "status": 400,
517 "error": {
518 "code": -2010,
519 "msg": "Account has insufficient balance for requested action.",
520 },
521 "rateLimits": [
522 {
523 "rateLimitType": "ORDERS",
524 "interval": "SECOND",
525 "intervalNum": 10,
526 "limit": 50,
527 "count": 13
528 },
529 ],
530 });
531 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
532
533 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
534 match join {
535 Ok(Err(e)) => {
536 let msg = e.to_string();
537 assert!(
538 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
539 "Expected error msg to contain server error, got: {msg}"
540 );
541 }
542 Ok(Ok(_)) => panic!("Expected error"),
543 Err(_) => panic!("Task panicked"),
544 }
545 });
546 }
547
548 #[test]
549 fn exchange_info_request_timeout() {
550 TOKIO_SHARED_RT.block_on(async {
551 let (ws_api, _conn, mut rx) = setup().await;
552 let client = GeneralApiClient::new(ws_api.clone());
553
554 let handle = spawn(async move {
555 let params = ExchangeInfoParams::builder().build().unwrap();
556 client.exchange_info(params).await
557 });
558
559 let sent = timeout(Duration::from_secs(1), rx.recv())
560 .await
561 .expect("send should occur")
562 .expect("channel closed");
563 let Message::Text(text) = sent else {
564 panic!("expected Message Text")
565 };
566
567 let _: Value = serde_json::from_str(&text).unwrap();
568
569 let result = handle.await.expect("task completed");
570 match result {
571 Err(e) => {
572 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
573 assert!(matches!(inner, WebsocketError::Timeout));
574 } else {
575 panic!("Unexpected error type: {:?}", e);
576 }
577 }
578 Ok(_) => panic!("Expected timeout error"),
579 }
580 });
581 }
582
583 #[test]
584 fn execution_rules_success() {
585 TOKIO_SHARED_RT.block_on(async {
586 let (ws_api, conn, mut rx) = setup().await;
587 let client = GeneralApiClient::new(ws_api.clone());
588
589 let handle = spawn(async move {
590 let params = ExecutionRulesParams::builder().build().unwrap();
591 client.execution_rules(params).await
592 });
593
594 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
595 let Message::Text(text) = sent else { panic!() };
596 let v: Value = serde_json::from_str(&text).unwrap();
597 let id = v["id"].as_str().unwrap();
598 assert_eq!(v["method"], "/executionRules".trim_start_matches('/'));
599 let mut resp_json: Value = serde_json::from_str(r#"{"id":"5162affb-0aba-4821-b475-f2625006eb43","status":200,"result":{"symbolRules":[{"symbol":"BAZUSD","rules":[{"ruleType":"PRICE_RANGE","bidLimitMultUp":"1.0001","bidLimitMultDown":"0.9999","askLimitMultUp":"1.0001","askLimitMultDown":"0.9999"}]}]}}"#).unwrap_or_else(|_| serde_json::json!({}));
600 resp_json["id"] = id.into();
601
602 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
603 let expected_data: Box<models::ExecutionRulesResponseResult> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
604 let empty_array = Value::Array(vec![]);
605 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
606 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
607 match raw_rate_limits.as_array() {
608 Some(arr) if arr.is_empty() => None,
609 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
610 None => None,
611 };
612
613 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
614
615 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
616
617
618 let response_rate_limits = response.rate_limits.clone();
619 let response_data = response.data().expect("deserialize data");
620
621 assert_eq!(response_rate_limits, expected_rate_limits);
622 assert_eq!(response_data, expected_data);
623 });
624 }
625
626 #[test]
627 fn execution_rules_error_response() {
628 TOKIO_SHARED_RT.block_on(async {
629 let (ws_api, conn, mut rx) = setup().await;
630 let client = GeneralApiClient::new(ws_api.clone());
631
632 let handle = tokio::spawn(async move {
633 let params = ExecutionRulesParams::builder().build().unwrap();
634 client.execution_rules(params).await
635 });
636
637 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
638 let Message::Text(text) = sent else { panic!() };
639 let v: Value = serde_json::from_str(&text).unwrap();
640 let id = v["id"].as_str().unwrap().to_string();
641
642 let resp_json = json!({
643 "id": id,
644 "status": 400,
645 "error": {
646 "code": -2010,
647 "msg": "Account has insufficient balance for requested action.",
648 },
649 "rateLimits": [
650 {
651 "rateLimitType": "ORDERS",
652 "interval": "SECOND",
653 "intervalNum": 10,
654 "limit": 50,
655 "count": 13
656 },
657 ],
658 });
659 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
660
661 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
662 match join {
663 Ok(Err(e)) => {
664 let msg = e.to_string();
665 assert!(
666 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
667 "Expected error msg to contain server error, got: {msg}"
668 );
669 }
670 Ok(Ok(_)) => panic!("Expected error"),
671 Err(_) => panic!("Task panicked"),
672 }
673 });
674 }
675
676 #[test]
677 fn execution_rules_request_timeout() {
678 TOKIO_SHARED_RT.block_on(async {
679 let (ws_api, _conn, mut rx) = setup().await;
680 let client = GeneralApiClient::new(ws_api.clone());
681
682 let handle = spawn(async move {
683 let params = ExecutionRulesParams::builder().build().unwrap();
684 client.execution_rules(params).await
685 });
686
687 let sent = timeout(Duration::from_secs(1), rx.recv())
688 .await
689 .expect("send should occur")
690 .expect("channel closed");
691 let Message::Text(text) = sent else {
692 panic!("expected Message Text")
693 };
694
695 let _: Value = serde_json::from_str(&text).unwrap();
696
697 let result = handle.await.expect("task completed");
698 match result {
699 Err(e) => {
700 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
701 assert!(matches!(inner, WebsocketError::Timeout));
702 } else {
703 panic!("Unexpected error type: {:?}", e);
704 }
705 }
706 Ok(_) => panic!("Expected timeout error"),
707 }
708 });
709 }
710
711 #[test]
712 fn ping_success() {
713 TOKIO_SHARED_RT.block_on(async {
714 let (ws_api, conn, mut rx) = setup().await;
715 let client = GeneralApiClient::new(ws_api.clone());
716
717 let handle = spawn(async move {
718 let params = PingParams::builder().build().unwrap();
719 client.ping(params).await
720 });
721
722 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
723 let Message::Text(text) = sent else { panic!() };
724 let v: Value = serde_json::from_str(&text).unwrap();
725 let id = v["id"].as_str().unwrap();
726 assert_eq!(v["method"], "/ping".trim_start_matches('/'));
727 let mut resp_json: Value = serde_json::from_str(r#"{"id":"922bcc6e-9de8-440d-9e84-7c80933a8d0d","status":200,"result":{},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":321}]}"#).unwrap_or_else(|_| serde_json::json!({}));
728 resp_json["id"] = id.into();
729
730 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
731 let expected_data: serde_json::Value = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
732 let empty_array = Value::Array(vec![]);
733 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
734 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
735 match raw_rate_limits.as_array() {
736 Some(arr) if arr.is_empty() => None,
737 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
738 None => None,
739 };
740
741 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
742
743 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
744
745
746 let response_rate_limits = response.rate_limits.clone();
747 let response_data = response.data().expect("deserialize data");
748
749 assert_eq!(response_rate_limits, expected_rate_limits);
750 assert_eq!(response_data, expected_data);
751 });
752 }
753
754 #[test]
755 fn ping_error_response() {
756 TOKIO_SHARED_RT.block_on(async {
757 let (ws_api, conn, mut rx) = setup().await;
758 let client = GeneralApiClient::new(ws_api.clone());
759
760 let handle = tokio::spawn(async move {
761 let params = PingParams::builder().build().unwrap();
762 client.ping(params).await
763 });
764
765 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
766 let Message::Text(text) = sent else { panic!() };
767 let v: Value = serde_json::from_str(&text).unwrap();
768 let id = v["id"].as_str().unwrap().to_string();
769
770 let resp_json = json!({
771 "id": id,
772 "status": 400,
773 "error": {
774 "code": -2010,
775 "msg": "Account has insufficient balance for requested action.",
776 },
777 "rateLimits": [
778 {
779 "rateLimitType": "ORDERS",
780 "interval": "SECOND",
781 "intervalNum": 10,
782 "limit": 50,
783 "count": 13
784 },
785 ],
786 });
787 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
788
789 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
790 match join {
791 Ok(Err(e)) => {
792 let msg = e.to_string();
793 assert!(
794 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
795 "Expected error msg to contain server error, got: {msg}"
796 );
797 }
798 Ok(Ok(_)) => panic!("Expected error"),
799 Err(_) => panic!("Task panicked"),
800 }
801 });
802 }
803
804 #[test]
805 fn ping_request_timeout() {
806 TOKIO_SHARED_RT.block_on(async {
807 let (ws_api, _conn, mut rx) = setup().await;
808 let client = GeneralApiClient::new(ws_api.clone());
809
810 let handle = spawn(async move {
811 let params = PingParams::builder().build().unwrap();
812 client.ping(params).await
813 });
814
815 let sent = timeout(Duration::from_secs(1), rx.recv())
816 .await
817 .expect("send should occur")
818 .expect("channel closed");
819 let Message::Text(text) = sent else {
820 panic!("expected Message Text")
821 };
822
823 let _: Value = serde_json::from_str(&text).unwrap();
824
825 let result = handle.await.expect("task completed");
826 match result {
827 Err(e) => {
828 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
829 assert!(matches!(inner, WebsocketError::Timeout));
830 } else {
831 panic!("Unexpected error type: {:?}", e);
832 }
833 }
834 Ok(_) => panic!("Expected timeout error"),
835 }
836 });
837 }
838
839 #[test]
840 fn time_success() {
841 TOKIO_SHARED_RT.block_on(async {
842 let (ws_api, conn, mut rx) = setup().await;
843 let client = GeneralApiClient::new(ws_api.clone());
844
845 let handle = spawn(async move {
846 let params = TimeParams::builder().build().unwrap();
847 client.time(params).await
848 });
849
850 let sent = timeout(Duration::from_secs(1), rx.recv()).await.expect("send should occur").expect("channel closed");
851 let Message::Text(text) = sent else { panic!() };
852 let v: Value = serde_json::from_str(&text).unwrap();
853 let id = v["id"].as_str().unwrap();
854 assert_eq!(v["method"], "/time".trim_start_matches('/'));
855 let mut resp_json: Value = serde_json::from_str(r#"{"id":"187d3cb2-942d-484c-8271-4e2141bbadb1","status":200,"result":{"serverTime":1656400526260},"rateLimits":[{"rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":6000,"count":321}]}"#).unwrap_or_else(|_| serde_json::json!({}));
856 resp_json["id"] = id.into();
857
858 let raw_data = resp_json.get("result").or_else(|| resp_json.get("response")).expect("no response in JSON");
859 let expected_data: Box<models::TimeResponseResult> = serde_json::from_value(raw_data.clone()).expect("should parse raw response");
860 let empty_array = Value::Array(vec![]);
861 let raw_rate_limits = resp_json.get("rateLimits").unwrap_or(&empty_array);
862 let expected_rate_limits: Option<Vec<WebsocketApiRateLimit>> =
863 match raw_rate_limits.as_array() {
864 Some(arr) if arr.is_empty() => None,
865 Some(_) => Some(serde_json::from_value(raw_rate_limits.clone()).expect("should parse rateLimits array")),
866 None => None,
867 };
868
869 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
870
871 let response = timeout(Duration::from_secs(1), handle).await.expect("task done").expect("no panic").expect("no error");
872
873
874 let response_rate_limits = response.rate_limits.clone();
875 let response_data = response.data().expect("deserialize data");
876
877 assert_eq!(response_rate_limits, expected_rate_limits);
878 assert_eq!(response_data, expected_data);
879 });
880 }
881
882 #[test]
883 fn time_error_response() {
884 TOKIO_SHARED_RT.block_on(async {
885 let (ws_api, conn, mut rx) = setup().await;
886 let client = GeneralApiClient::new(ws_api.clone());
887
888 let handle = tokio::spawn(async move {
889 let params = TimeParams::builder().build().unwrap();
890 client.time(params).await
891 });
892
893 let sent = timeout(Duration::from_secs(1), rx.recv()).await.unwrap().unwrap();
894 let Message::Text(text) = sent else { panic!() };
895 let v: Value = serde_json::from_str(&text).unwrap();
896 let id = v["id"].as_str().unwrap().to_string();
897
898 let resp_json = json!({
899 "id": id,
900 "status": 400,
901 "error": {
902 "code": -2010,
903 "msg": "Account has insufficient balance for requested action.",
904 },
905 "rateLimits": [
906 {
907 "rateLimitType": "ORDERS",
908 "interval": "SECOND",
909 "intervalNum": 10,
910 "limit": 50,
911 "count": 13
912 },
913 ],
914 });
915 WebsocketHandler::on_message(&*ws_api, resp_json.to_string(), conn.clone()).await;
916
917 let join = timeout(Duration::from_secs(1), handle).await.unwrap();
918 match join {
919 Ok(Err(e)) => {
920 let msg = e.to_string();
921 assert!(
922 msg.contains("Server‐side response error (code -2010): Account has insufficient balance for requested action."),
923 "Expected error msg to contain server error, got: {msg}"
924 );
925 }
926 Ok(Ok(_)) => panic!("Expected error"),
927 Err(_) => panic!("Task panicked"),
928 }
929 });
930 }
931
932 #[test]
933 fn time_request_timeout() {
934 TOKIO_SHARED_RT.block_on(async {
935 let (ws_api, _conn, mut rx) = setup().await;
936 let client = GeneralApiClient::new(ws_api.clone());
937
938 let handle = spawn(async move {
939 let params = TimeParams::builder().build().unwrap();
940 client.time(params).await
941 });
942
943 let sent = timeout(Duration::from_secs(1), rx.recv())
944 .await
945 .expect("send should occur")
946 .expect("channel closed");
947 let Message::Text(text) = sent else {
948 panic!("expected Message Text")
949 };
950
951 let _: Value = serde_json::from_str(&text).unwrap();
952
953 let result = handle.await.expect("task completed");
954 match result {
955 Err(e) => {
956 if let Some(inner) = e.downcast_ref::<WebsocketError>() {
957 assert!(matches!(inner, WebsocketError::Timeout));
958 } else {
959 panic!("Unexpected error type: {:?}", e);
960 }
961 }
962 Ok(_) => panic!("Expected timeout error"),
963 }
964 });
965 }
966}