Skip to main content

binance_sdk/spot/websocket_api/apis/
general_api.rs

1/*
2 * Spot WebSocket API
3 *
4 * Access market data, manage accounts, and trade on Binance Spot.
5 *
6 * The version of the OpenAPI document: 1.0.0
7 *
8 *
9 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
10 * https://openapi-generator.tech
11 * Do not edit the class manually.
12 */
13
14#![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/// Request parameters for the [`exchange_info`] operation.
133///
134/// This struct holds all of the inputs you can pass when calling
135/// [`exchange_info`](#method.exchange_info).
136#[derive(Clone, Debug, Builder, Deserialize, Default)]
137#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
138pub struct ExchangeInfoParams {
139    /// Client-generated request identifier.
140    ///
141    /// This field is **optional.
142    #[builder(setter(into), default)]
143    #[serde(rename = "id", default)]
144    pub id: Option<String>,
145    /// Describe a single symbol
146    ///
147    /// This field is **optional.
148    #[builder(setter(into), default)]
149    #[serde(rename = "symbol", default)]
150    pub symbol: Option<String>,
151    /// Describe multiple symbols
152    ///
153    /// This field is **optional.
154    #[builder(setter(into), default)]
155    #[serde(rename = "symbols", default)]
156    pub symbols: Option<Vec<String>>,
157    /// Filter symbols by permissions
158    ///
159    /// This field is **optional.
160    #[builder(setter(into), default)]
161    #[serde(rename = "permissions", default)]
162    pub permissions: Option<Vec<String>>,
163    /// Controls whether the content of the `permissionSets` field is populated or not. Defaults to `true`.
164    ///
165    /// This field is **optional.
166    #[builder(setter(into), default)]
167    #[serde(rename = "showPermissionSets", default)]
168    pub show_permission_sets: Option<bool>,
169    /// Filters for symbols that have this `tradingStatus`. Valid values: `TRADING`, `HALT`, `BREAK`. Cannot be used in combination with `symbol` or `symbols`.
170    ///
171    /// This field is **optional.
172    #[builder(setter(into), default)]
173    #[serde(rename = "symbolStatus", default)]
174    pub symbol_status: Option<ExchangeInfoSymbolStatusEnum>,
175}
176
177impl ExchangeInfoParams {
178    /// Create a builder for [`exchange_info`].
179    ///
180    #[must_use]
181    pub fn builder() -> ExchangeInfoParamsBuilder {
182        ExchangeInfoParamsBuilder::default()
183    }
184}
185/// Request parameters for the [`execution_rules`] operation.
186///
187/// This struct holds all of the inputs you can pass when calling
188/// [`execution_rules`](#method.execution_rules).
189#[derive(Clone, Debug, Builder, Deserialize, Default)]
190#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
191pub struct ExecutionRulesParams {
192    /// Client-generated request identifier.
193    ///
194    /// This field is **optional.
195    #[builder(setter(into), default)]
196    #[serde(rename = "id", default)]
197    pub id: Option<String>,
198    /// Query for specified symbol.
199    ///
200    /// This field is **optional.
201    #[builder(setter(into), default)]
202    #[serde(rename = "symbol", default)]
203    pub symbol: Option<String>,
204    /// Query for multiple symbols.
205    ///
206    /// This field is **optional.
207    #[builder(setter(into), default)]
208    #[serde(rename = "symbols", default)]
209    pub symbols: Option<Vec<String>>,
210    /// Query for all symbols with the specified status. Supported values: `TRADING`, `HALT`, `BREAK`
211    ///
212    /// This field is **optional.
213    #[builder(setter(into), default)]
214    #[serde(rename = "symbolStatus", default)]
215    pub symbol_status: Option<ExecutionRulesSymbolStatusEnum>,
216}
217
218impl ExecutionRulesParams {
219    /// Create a builder for [`execution_rules`].
220    ///
221    #[must_use]
222    pub fn builder() -> ExecutionRulesParamsBuilder {
223        ExecutionRulesParamsBuilder::default()
224    }
225}
226/// Request parameters for the [`ping`] operation.
227///
228/// This struct holds all of the inputs you can pass when calling
229/// [`ping`](#method.ping).
230#[derive(Clone, Debug, Builder, Deserialize, Default)]
231#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
232pub struct PingParams {
233    /// Client-generated request identifier.
234    ///
235    /// This field is **optional.
236    #[builder(setter(into), default)]
237    #[serde(rename = "id", default)]
238    pub id: Option<String>,
239}
240
241impl PingParams {
242    /// Create a builder for [`ping`].
243    ///
244    #[must_use]
245    pub fn builder() -> PingParamsBuilder {
246        PingParamsBuilder::default()
247    }
248}
249/// Request parameters for the [`time`] operation.
250///
251/// This struct holds all of the inputs you can pass when calling
252/// [`time`](#method.time).
253#[derive(Clone, Debug, Builder, Deserialize, Default)]
254#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
255pub struct TimeParams {
256    /// Client-generated request identifier.
257    ///
258    /// This field is **optional.
259    #[builder(setter(into), default)]
260    #[serde(rename = "id", default)]
261    pub id: Option<String>,
262}
263
264impl TimeParams {
265    /// Create a builder for [`time`].
266    ///
267    #[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}