Skip to main content

ccxt_exchanges/binance/
mod.rs

1//! Binance exchange implementation.
2//!
3//! Supports spot trading, futures trading, and options trading with complete REST API and WebSocket support.
4
5use ccxt_core::types::EndpointType;
6use ccxt_core::types::MarketType;
7use ccxt_core::types::default_type::{DefaultSubType, DefaultType};
8use ccxt_core::{BaseExchange, ExchangeConfig, Result};
9use std::sync::Arc;
10use std::time::Duration;
11use tokio::sync::RwLock;
12
13pub mod auth;
14pub mod builder;
15pub mod constants;
16pub mod endpoint_router;
17pub mod error;
18mod exchange_impl;
19pub mod options;
20pub mod parser;
21pub mod rate_limiter;
22pub mod rest;
23pub mod signed_request;
24pub mod signing_strategy;
25pub mod symbol;
26pub mod time_sync;
27pub mod urls;
28pub mod ws;
29mod ws_exchange_impl;
30
31pub use builder::BinanceBuilder;
32pub use endpoint_router::BinanceEndpointRouter;
33pub use error::BinanceApiError;
34pub use options::BinanceOptions;
35pub use signed_request::{HttpMethod, SignedRequestBuilder};
36pub use time_sync::{TimeSyncConfig, TimeSyncManager};
37pub use urls::BinanceUrls;
38
39use rate_limiter::WeightRateLimiter;
40
41/// Binance exchange structure.
42#[derive(Debug, Clone)]
43pub struct Binance {
44    /// Base exchange instance.
45    base: BaseExchange,
46    /// Binance-specific options.
47    options: BinanceOptions,
48    /// Time synchronization manager for caching server time offset.
49    time_sync: Arc<TimeSyncManager>,
50    /// Persistent WebSocket connection reference for state tracking.
51    #[deprecated(note = "Use connection_manager instead")]
52    ws_connection: Arc<RwLock<Option<ws::BinanceWs>>>,
53    /// Centralized WebSocket connection manager
54    pub connection_manager: Arc<ws::BinanceConnectionManager>,
55    /// Rate limiter for API requests.
56    rate_limiter: Arc<WeightRateLimiter>,
57}
58
59#[allow(deprecated)]
60impl Binance {
61    /// Creates a new Binance instance using the builder pattern.
62    ///
63    /// This is the recommended way to create a Binance instance.
64    ///
65    /// # Example
66    ///
67    /// ```no_run
68    /// use ccxt_exchanges::binance::Binance;
69    ///
70    /// let binance = Binance::builder()
71    ///     .api_key("your-api-key")
72    ///     .secret("your-secret")
73    ///     .sandbox(true)
74    ///     .build()
75    ///     .unwrap();
76    /// ```
77    pub fn builder() -> BinanceBuilder {
78        BinanceBuilder::new()
79    }
80
81    /// Creates a new Binance instance.
82    ///
83    /// # Arguments
84    ///
85    /// * `config` - Exchange configuration.
86    ///
87    /// # Example
88    ///
89    /// ```no_run
90    /// use ccxt_exchanges::binance::Binance;
91    /// use ccxt_core::ExchangeConfig;
92    ///
93    /// let config = ExchangeConfig {
94    ///     id: "binance".to_string(),
95    ///     name: "Binance".to_string(),
96    ///     api_key: Some("your-api-key".to_string()),
97    ///     secret: Some("your-secret".to_string()),
98    ///     ..Default::default()
99    /// };
100    ///
101    /// let binance = Binance::new(config).unwrap();
102    /// ```
103    pub fn new(config: ExchangeConfig) -> Result<Self> {
104        let base = BaseExchange::new(config)?;
105        let options = BinanceOptions::default();
106        let time_sync = Arc::new(TimeSyncManager::new());
107
108        // Determine URLs for ConnectionManager
109        let mut urls = if base.config.sandbox {
110            BinanceUrls::testnet()
111        } else {
112            BinanceUrls::production()
113        };
114        // Apply URL overrides
115        if let Some(ws_url) = base.config.url_overrides.get("ws") {
116            urls.ws.clone_from(ws_url);
117        }
118        if let Some(ws_fapi_url) = base.config.url_overrides.get("wsFapi") {
119            urls.ws_fapi.clone_from(ws_fapi_url);
120        }
121        if let Some(ws_dapi_url) = base.config.url_overrides.get("wsDapi") {
122            urls.ws_dapi.clone_from(ws_dapi_url);
123        }
124        if let Some(ws_eapi_url) = base.config.url_overrides.get("wsEapi") {
125            urls.ws_eapi.clone_from(ws_eapi_url);
126        }
127
128        let connection_manager =
129            Arc::new(ws::BinanceConnectionManager::new(urls, base.config.sandbox));
130        let rate_limiter = Arc::new(WeightRateLimiter::new());
131
132        Ok(Self {
133            base,
134            options,
135            time_sync,
136            ws_connection: Arc::new(RwLock::new(None)),
137            connection_manager,
138            rate_limiter,
139        })
140    }
141
142    /// Creates a new Binance instance with custom options.
143    ///
144    /// This is used internally by the builder pattern.
145    ///
146    /// # Arguments
147    ///
148    /// * `config` - Exchange configuration.
149    /// * `options` - Binance-specific options.
150    ///
151    /// # Example
152    ///
153    /// ```no_run
154    /// use ccxt_exchanges::binance::{Binance, BinanceOptions};
155    /// use ccxt_core::ExchangeConfig;
156    /// use ccxt_core::types::default_type::DefaultType;
157    ///
158    /// let config = ExchangeConfig::default();
159    /// let options = BinanceOptions {
160    ///     default_type: DefaultType::Swap,
161    ///     ..Default::default()
162    /// };
163    ///
164    /// let binance = Binance::new_with_options(config, options).unwrap();
165    /// ```
166    pub fn new_with_options(config: ExchangeConfig, options: BinanceOptions) -> Result<Self> {
167        let base = BaseExchange::new(config)?;
168
169        // Create TimeSyncManager with configuration from options
170        let time_sync_config = TimeSyncConfig {
171            sync_interval: Duration::from_secs(options.time_sync_interval_secs),
172            auto_sync: options.auto_time_sync,
173            max_offset_drift: options.recv_window as i64,
174        };
175        let time_sync = Arc::new(TimeSyncManager::with_config(time_sync_config));
176
177        // Determine WS URLs
178        let mut urls = if base.config.sandbox {
179            BinanceUrls::testnet()
180        } else {
181            BinanceUrls::production()
182        };
183
184        // Apply overrides
185        if let Some(ws_url) = base.config.url_overrides.get("ws") {
186            urls.ws.clone_from(ws_url);
187        }
188        if let Some(ws_fapi_url) = base.config.url_overrides.get("wsFapi") {
189            urls.ws_fapi.clone_from(ws_fapi_url);
190        }
191        if let Some(ws_dapi_url) = base.config.url_overrides.get("wsDapi") {
192            urls.ws_dapi.clone_from(ws_dapi_url);
193        }
194        if let Some(ws_eapi_url) = base.config.url_overrides.get("wsEapi") {
195            urls.ws_eapi.clone_from(ws_eapi_url);
196        }
197
198        let connection_manager =
199            Arc::new(ws::BinanceConnectionManager::new(urls, base.config.sandbox));
200        let rate_limiter = Arc::new(WeightRateLimiter::new());
201
202        Ok(Self {
203            base,
204            options,
205            time_sync,
206            ws_connection: Arc::new(RwLock::new(None)),
207            connection_manager,
208            rate_limiter,
209        })
210    }
211
212    /// Creates a new Binance futures instance for perpetual contracts.
213    ///
214    /// # Arguments
215    ///
216    /// * `config` - Exchange configuration.
217    ///
218    /// # Example
219    ///
220    /// ```no_run
221    /// use ccxt_exchanges::binance::Binance;
222    /// use ccxt_core::ExchangeConfig;
223    ///
224    /// let config = ExchangeConfig::default();
225    /// let futures = Binance::new_swap(config).unwrap();
226    /// ```
227    pub fn new_swap(config: ExchangeConfig) -> Result<Self> {
228        let base = BaseExchange::new(config)?;
229        let options = BinanceOptions {
230            default_type: DefaultType::Swap, // Perpetual futures
231            ..Default::default()
232        };
233
234        // Create TimeSyncManager with configuration from options
235        let time_sync_config = TimeSyncConfig {
236            sync_interval: Duration::from_secs(options.time_sync_interval_secs),
237            auto_sync: options.auto_time_sync,
238            max_offset_drift: options.recv_window as i64,
239        };
240        let time_sync = Arc::new(TimeSyncManager::with_config(time_sync_config));
241
242        // Determine WS URLs (Futures/Swap)
243        let mut urls = if base.config.sandbox {
244            BinanceUrls::testnet()
245        } else {
246            BinanceUrls::production()
247        };
248
249        if let Some(ws_fapi_url) = base.config.url_overrides.get("wsFapi") {
250            urls.ws_fapi.clone_from(ws_fapi_url);
251        }
252
253        let connection_manager =
254            Arc::new(ws::BinanceConnectionManager::new(urls, base.config.sandbox));
255        let rate_limiter = Arc::new(WeightRateLimiter::new());
256
257        Ok(Self {
258            base,
259            options,
260            time_sync,
261            ws_connection: Arc::new(RwLock::new(None)),
262            connection_manager,
263            rate_limiter,
264        })
265    }
266
267    /// Returns a reference to the base exchange.
268    pub fn base(&self) -> &BaseExchange {
269        &self.base
270    }
271
272    /// Returns a mutable reference to the base exchange.
273    pub fn base_mut(&mut self) -> &mut BaseExchange {
274        &mut self.base
275    }
276
277    /// Returns the Binance options.
278    pub fn options(&self) -> &BinanceOptions {
279        &self.options
280    }
281
282    /// Sets the Binance options.
283    pub fn set_options(&mut self, options: BinanceOptions) {
284        self.options = options;
285    }
286
287    /// Returns a reference to the persistent WebSocket connection.
288    ///
289    /// This allows access to the stored WebSocket instance for state tracking.
290    /// The connection is stored when `ws_connect()` is called and cleared
291    /// when `ws_disconnect()` is called.
292    ///
293    /// # Example
294    ///
295    /// ```no_run
296    /// use ccxt_exchanges::binance::Binance;
297    /// use ccxt_core::ExchangeConfig;
298    ///
299    /// # async fn example() -> ccxt_core::Result<()> {
300    /// let binance = Binance::new(ExchangeConfig::default())?;
301    /// let ws_conn = binance.ws_connection();
302    /// let guard = ws_conn.read().await;
303    /// if guard.is_some() {
304    ///     println!("WebSocket is connected");
305    /// }
306    /// # Ok(())
307    /// # }
308    /// ```
309    pub fn ws_connection(&self) -> &Arc<RwLock<Option<ws::BinanceWs>>> {
310        &self.ws_connection
311    }
312
313    /// Returns a reference to the time synchronization manager.
314    ///
315    /// The `TimeSyncManager` caches the time offset between local system time
316    /// and Binance server time, reducing network round-trips for signed requests.
317    ///
318    /// # Example
319    ///
320    /// ```no_run
321    /// use ccxt_exchanges::binance::Binance;
322    /// use ccxt_core::ExchangeConfig;
323    ///
324    /// let binance = Binance::new(ExchangeConfig::default()).unwrap();
325    /// let time_sync = binance.time_sync();
326    /// println!("Time sync initialized: {}", time_sync.is_initialized());
327    /// ```
328    pub fn time_sync(&self) -> &Arc<TimeSyncManager> {
329        &self.time_sync
330    }
331
332    /// Returns a reference to the rate limiter.
333    ///
334    /// The `WeightRateLimiter` tracks API usage based on response headers
335    /// and provides throttling recommendations to avoid hitting rate limits.
336    ///
337    /// # Example
338    ///
339    /// ```no_run
340    /// use ccxt_exchanges::binance::Binance;
341    /// use ccxt_core::ExchangeConfig;
342    ///
343    /// let binance = Binance::new(ExchangeConfig::default()).unwrap();
344    /// let rate_limiter = binance.rate_limiter();
345    /// println!("Current weight usage: {}%", rate_limiter.weight_usage_ratio() * 100.0);
346    /// ```
347    pub fn rate_limiter(&self) -> &Arc<WeightRateLimiter> {
348        &self.rate_limiter
349    }
350
351    /// Creates a new signed request builder for the given endpoint.
352    ///
353    /// This is the recommended way to make authenticated API requests.
354    /// The builder handles credential validation, timestamp generation,
355    /// parameter signing, and request execution.
356    ///
357    /// # Arguments
358    ///
359    /// * `endpoint` - Full API endpoint URL
360    ///
361    /// # Example
362    ///
363    /// ```no_run
364    /// use ccxt_exchanges::binance::{Binance, HttpMethod};
365    /// use ccxt_core::ExchangeConfig;
366    ///
367    /// # async fn example() -> ccxt_core::Result<()> {
368    /// let binance = Binance::new(ExchangeConfig::default())?;
369    ///
370    /// // Simple GET request
371    /// let response = binance.signed_request("https://api.binance.com/api/v3/account")
372    ///     .execute()
373    ///     .await?;
374    ///
375    /// // POST request with parameters
376    /// let response = binance.signed_request("https://api.binance.com/api/v3/order")
377    ///     .method(HttpMethod::Post)
378    ///     .param("symbol", "BTCUSDT")
379    ///     .param("side", "BUY")
380    ///     .execute()
381    ///     .await?;
382    /// # Ok(())
383    /// # }
384    /// ```
385    pub fn signed_request(&self, endpoint: impl Into<String>) -> SignedRequestBuilder<'_> {
386        SignedRequestBuilder::new(self, endpoint)
387    }
388
389    /// Creates an API-key-only request builder (no signature).
390    ///
391    /// Used for endpoints that require only the `X-MBX-APIKEY` header,
392    /// such as listen key operations.
393    pub(crate) fn api_key_request(
394        &self,
395        endpoint: impl Into<String>,
396    ) -> signed_request::ApiKeyRequestBuilder<'_> {
397        signed_request::ApiKeyRequestBuilder::new(self, endpoint)
398    }
399
400    /// Returns the exchange ID.
401    pub fn id(&self) -> &'static str {
402        "binance"
403    }
404
405    /// Returns the exchange name.
406    pub fn name(&self) -> &'static str {
407        "Binance"
408    }
409
410    /// Returns the API version.
411    pub fn version(&self) -> &'static str {
412        "v3"
413    }
414
415    /// Returns `true` if the exchange is CCXT-certified.
416    pub fn certified(&self) -> bool {
417        true
418    }
419
420    /// Returns `true` if Pro version (WebSocket) is supported.
421    pub fn pro(&self) -> bool {
422        true
423    }
424
425    /// Returns the rate limit in requests per second.
426    pub fn rate_limit(&self) -> u32 {
427        50
428    }
429
430    /// Returns `true` if sandbox/testnet mode is enabled.
431    ///
432    /// Sandbox mode is enabled when either:
433    /// - `config.sandbox` is set to `true`
434    /// - `options.test` is set to `true`
435    ///
436    /// # Returns
437    ///
438    /// `true` if sandbox mode is enabled, `false` otherwise.
439    ///
440    /// # Example
441    ///
442    /// ```no_run
443    /// use ccxt_exchanges::binance::Binance;
444    /// use ccxt_core::ExchangeConfig;
445    ///
446    /// let config = ExchangeConfig {
447    ///     sandbox: true,
448    ///     ..Default::default()
449    /// };
450    /// let binance = Binance::new(config).unwrap();
451    /// assert!(binance.is_sandbox());
452    /// ```
453    pub fn is_sandbox(&self) -> bool {
454        self.base().config.sandbox || self.options.test
455    }
456
457    /// Returns the supported timeframes.
458    pub fn timeframes(&self) -> std::collections::HashMap<String, String> {
459        constants::timeframes()
460    }
461
462    /// Returns the API URLs.
463    pub fn urls(&self) -> BinanceUrls {
464        let mut urls = if self.base().config.sandbox {
465            BinanceUrls::testnet()
466        } else {
467            BinanceUrls::production()
468        };
469
470        // Apply URL overrides if present
471        if let Some(public_url) = self.base().config.url_overrides.get("public") {
472            urls.public.clone_from(public_url);
473        }
474        if let Some(private_url) = self.base().config.url_overrides.get("private") {
475            urls.private.clone_from(private_url);
476        }
477        if let Some(fapi_public_url) = self.base().config.url_overrides.get("fapiPublic") {
478            urls.fapi_public.clone_from(fapi_public_url);
479        }
480        if let Some(fapi_private_url) = self.base().config.url_overrides.get("fapiPrivate") {
481            urls.fapi_private.clone_from(fapi_private_url);
482        }
483        if let Some(dapi_public_url) = self.base().config.url_overrides.get("dapiPublic") {
484            urls.dapi_public.clone_from(dapi_public_url);
485        }
486        if let Some(dapi_private_url) = self.base().config.url_overrides.get("dapiPrivate") {
487            urls.dapi_private.clone_from(dapi_private_url);
488        }
489        // WebSocket URL overrides
490        if let Some(ws_url) = self.base().config.url_overrides.get("ws") {
491            urls.ws.clone_from(ws_url);
492        }
493        if let Some(ws_fapi_url) = self.base().config.url_overrides.get("wsFapi") {
494            urls.ws_fapi.clone_from(ws_fapi_url);
495        }
496        if let Some(ws_dapi_url) = self.base().config.url_overrides.get("wsDapi") {
497            urls.ws_dapi.clone_from(ws_dapi_url);
498        }
499        if let Some(ws_eapi_url) = self.base().config.url_overrides.get("wsEapi") {
500            urls.ws_eapi.clone_from(ws_eapi_url);
501        }
502
503        urls
504    }
505
506    /// Determines the WebSocket URL based on default_type and default_sub_type.
507    ///
508    /// This method implements the endpoint routing logic according to:
509    /// - Spot/Margin: Uses the standard WebSocket endpoint
510    /// - Swap/Futures with Linear sub-type: Uses FAPI WebSocket endpoint
511    /// - Swap/Futures with Inverse sub-type: Uses DAPI WebSocket endpoint
512    /// - Option: Uses EAPI WebSocket endpoint
513    ///
514    /// # Returns
515    ///
516    /// The appropriate WebSocket URL string.
517    ///
518    /// # Note
519    ///
520    /// This method delegates to `BinanceEndpointRouter::default_ws_endpoint()`.
521    /// The routing logic is centralized in the trait implementation.
522    pub fn get_ws_url(&self) -> String {
523        BinanceEndpointRouter::default_ws_endpoint(self)
524    }
525
526    /// Returns the public REST API base URL based on default_type and default_sub_type.
527    ///
528    /// This method implements the endpoint routing logic for public REST API calls:
529    /// - Spot: Uses the public API endpoint (api.binance.com)
530    /// - Margin: Uses the SAPI endpoint (api.binance.com/sapi)
531    /// - Swap/Futures with Linear sub-type: Uses FAPI endpoint (fapi.binance.com)
532    /// - Swap/Futures with Inverse sub-type: Uses DAPI endpoint (dapi.binance.com)
533    /// - Option: Uses EAPI endpoint (eapi.binance.com)
534    ///
535    /// # Returns
536    ///
537    /// The appropriate REST API base URL string.
538    ///
539    /// # Note
540    ///
541    /// This method delegates to `BinanceEndpointRouter::default_rest_endpoint(EndpointType::Public)`.
542    /// The routing logic is centralized in the trait implementation.
543    ///
544    /// # Example
545    ///
546    /// ```no_run
547    /// use ccxt_exchanges::binance::{Binance, BinanceOptions};
548    /// use ccxt_core::ExchangeConfig;
549    /// use ccxt_core::types::default_type::{DefaultType, DefaultSubType};
550    ///
551    /// let options = BinanceOptions {
552    ///     default_type: DefaultType::Swap,
553    ///     default_sub_type: Some(DefaultSubType::Linear),
554    ///     ..Default::default()
555    /// };
556    /// let binance = Binance::new_with_options(ExchangeConfig::default(), options).unwrap();
557    /// let url = binance.get_rest_url_public();
558    /// assert!(url.contains("fapi.binance.com"));
559    /// ```
560    pub fn get_rest_url_public(&self) -> String {
561        BinanceEndpointRouter::default_rest_endpoint(self, EndpointType::Public)
562    }
563
564    /// Returns the private REST API base URL based on default_type and default_sub_type.
565    ///
566    /// This method implements the endpoint routing logic for private REST API calls:
567    /// - Spot: Uses the private API endpoint (api.binance.com)
568    /// - Margin: Uses the SAPI endpoint (api.binance.com/sapi)
569    /// - Swap/Futures with Linear sub-type: Uses FAPI endpoint (fapi.binance.com)
570    /// - Swap/Futures with Inverse sub-type: Uses DAPI endpoint (dapi.binance.com)
571    /// - Option: Uses EAPI endpoint (eapi.binance.com)
572    ///
573    /// # Returns
574    ///
575    /// The appropriate REST API base URL string.
576    ///
577    /// # Note
578    ///
579    /// This method delegates to `BinanceEndpointRouter::default_rest_endpoint(EndpointType::Private)`.
580    /// The routing logic is centralized in the trait implementation.
581    ///
582    /// # Example
583    ///
584    /// ```no_run
585    /// use ccxt_exchanges::binance::{Binance, BinanceOptions};
586    /// use ccxt_core::ExchangeConfig;
587    /// use ccxt_core::types::default_type::{DefaultType, DefaultSubType};
588    ///
589    /// let options = BinanceOptions {
590    ///     default_type: DefaultType::Swap,
591    ///     default_sub_type: Some(DefaultSubType::Inverse),
592    ///     ..Default::default()
593    /// };
594    /// let binance = Binance::new_with_options(ExchangeConfig::default(), options).unwrap();
595    /// let url = binance.get_rest_url_private();
596    /// assert!(url.contains("dapi.binance.com"));
597    /// ```
598    pub fn get_rest_url_private(&self) -> String {
599        BinanceEndpointRouter::default_rest_endpoint(self, EndpointType::Private)
600    }
601
602    /// Checks if the current default_type is a contract type (Swap, Futures, or Option).
603    ///
604    /// This is useful for determining whether contract-specific API endpoints should be used.
605    ///
606    /// # Returns
607    ///
608    /// `true` if the default_type is Swap, Futures, or Option; `false` otherwise.
609    pub fn is_contract_type(&self) -> bool {
610        self.options.default_type.is_contract()
611    }
612
613    /// Checks if the current configuration uses inverse (coin-margined) contracts.
614    ///
615    /// # Returns
616    ///
617    /// `true` if default_sub_type is Inverse; `false` otherwise.
618    pub fn is_inverse(&self) -> bool {
619        matches!(self.options.default_sub_type, Some(DefaultSubType::Inverse))
620    }
621
622    /// Checks if the current configuration uses linear (USDT-margined) contracts.
623    ///
624    /// # Returns
625    ///
626    /// `true` if default_sub_type is Linear or not specified (defaults to Linear); `false` otherwise.
627    pub fn is_linear(&self) -> bool {
628        !self.is_inverse()
629    }
630
631    /// Creates a WebSocket client for public data streams.
632    ///
633    /// Used for subscribing to public data streams (ticker, orderbook, trades, etc.).
634    /// The WebSocket endpoint is automatically selected based on `default_type` and `default_sub_type`:
635    /// - Spot/Margin: `wss://stream.binance.com:9443/ws`
636    /// - Swap/Futures (Linear): `wss://fstream.binance.com/ws`
637    /// - Swap/Futures (Inverse): `wss://dstream.binance.com/ws`
638    /// - Option: `wss://nbstream.binance.com/eoptions/ws`
639    ///
640    /// # Returns
641    ///
642    /// Returns a `BinanceWs` instance.
643    ///
644    /// # Example
645    /// ```no_run
646    /// use ccxt_exchanges::binance::Binance;
647    /// use ccxt_core::ExchangeConfig;
648    ///
649    /// # async fn example() -> ccxt_core::error::Result<()> {
650    /// let binance = Binance::new(ExchangeConfig::default())?;
651    /// let ws = binance.create_ws();
652    /// ws.connect().await?;
653    /// # Ok(())
654    /// # }
655    /// ```
656    pub fn create_ws(&self) -> ws::BinanceWs {
657        let ws_url = self.get_ws_url();
658        ws::BinanceWs::new(ws_url)
659    }
660
661    /// Creates an authenticated WebSocket client for user data streams.
662    ///
663    /// Used for subscribing to private data streams (account balance, order updates, trade history, etc.).
664    /// Requires API key configuration.
665    /// The WebSocket endpoint is automatically selected based on `default_type` and `default_sub_type`.
666    ///
667    /// # Returns
668    ///
669    /// Returns a `BinanceWs` instance with listen key manager.
670    ///
671    /// # Example
672    /// ```no_run
673    /// use ccxt_exchanges::binance::Binance;
674    /// use ccxt_core::ExchangeConfig;
675    /// use std::sync::Arc;
676    ///
677    /// # async fn example() -> ccxt_core::error::Result<()> {
678    /// let config = ExchangeConfig {
679    ///     api_key: Some("your-api-key".to_string()),
680    ///     secret: Some("your-secret".to_string()),
681    ///     ..Default::default()
682    /// };
683    /// let binance = Arc::new(Binance::new(config)?);
684    /// let ws = binance.create_authenticated_ws();
685    /// ws.connect_user_stream().await?;
686    /// # Ok(())
687    /// # }
688    /// ```
689    pub fn create_authenticated_ws(self: &std::sync::Arc<Self>) -> ws::BinanceWs {
690        let ws_url = self.get_ws_url();
691        let market_type = MarketType::from(self.options.default_type);
692        ws::BinanceWs::new_with_auth(ws_url, self.clone(), market_type)
693    }
694
695    /// Checks a JSON response for Binance API errors and converts them to `Result`.
696    ///
697    /// Binance API may return errors in the response body even with HTTP 200 status.
698    /// The error format is: `{"code": -1121, "msg": "Invalid symbol."}`
699    ///
700    /// This method should be called after receiving a response from public API endpoints
701    /// to ensure proper error handling.
702    ///
703    /// # Arguments
704    ///
705    /// * `response` - The JSON response from Binance API
706    ///
707    /// # Returns
708    ///
709    /// Returns `Ok(response)` if no error is found, or `Err(CoreError)` if the response
710    /// contains a Binance API error.
711    ///
712    /// # Example
713    ///
714    /// ```no_run
715    /// use ccxt_exchanges::binance::Binance;
716    /// use ccxt_core::ExchangeConfig;
717    ///
718    /// # async fn example() -> ccxt_core::Result<()> {
719    /// let binance = Binance::new(ExchangeConfig::default())?;
720    /// let response = binance.base().http_client.get("https://api.binance.com/api/v3/ticker/price", None).await?;
721    /// let validated = binance.check_response(response)?;
722    /// # Ok(())
723    /// # }
724    /// ```
725    pub fn check_response(&self, response: serde_json::Value) -> Result<serde_json::Value> {
726        if let Some(api_error) = BinanceApiError::from_json(&response) {
727            return Err(api_error.into());
728        }
729        Ok(response)
730    }
731
732    /// Executes a public GET request with unified error checking and rate limiter updates.
733    ///
734    /// This method wraps `http_client.get()` to provide:
735    /// - Rate limiter updates from response headers
736    /// - API error checking (Binance may return errors in the body with HTTP 200)
737    ///
738    /// All public REST API methods should use this instead of calling `http_client.get()` directly.
739    pub(crate) async fn public_get(
740        &self,
741        url: &str,
742        headers: Option<reqwest::header::HeaderMap>,
743    ) -> Result<serde_json::Value> {
744        // Apply rate limiting before making the request
745        if let Some(wait) = self.rate_limiter().wait_duration() {
746            tokio::time::sleep(wait).await;
747        }
748
749        let data = self.base().http_client.get(url, headers).await?;
750
751        // Update rate limiter from response headers
752        if let Some(resp_headers) = data.get("responseHeaders") {
753            let rate_info = rate_limiter::RateLimitInfo::from_headers(resp_headers);
754            if rate_info.has_data() {
755                self.rate_limiter().update(rate_info);
756            }
757        }
758
759        // Check for API errors in response body
760        self.check_response(data)
761    }
762}
763
764#[cfg(test)]
765mod tests {
766    #![allow(clippy::disallowed_methods)]
767    use super::*;
768
769    #[test]
770    fn test_binance_creation() {
771        let config = ExchangeConfig {
772            id: "binance".to_string(),
773            name: "Binance".to_string(),
774            ..Default::default()
775        };
776
777        let binance = Binance::new(config);
778        assert!(binance.is_ok());
779
780        let binance = binance.unwrap();
781        assert_eq!(binance.id(), "binance");
782        assert_eq!(binance.name(), "Binance");
783        assert_eq!(binance.version(), "v3");
784        assert!(binance.certified());
785        assert!(binance.pro());
786    }
787
788    #[test]
789    fn test_timeframes() {
790        let config = ExchangeConfig::default();
791        let binance = Binance::new(config).unwrap();
792        let timeframes = binance.timeframes();
793
794        assert!(timeframes.contains_key("1m"));
795        assert!(timeframes.contains_key("1h"));
796        assert!(timeframes.contains_key("1d"));
797        assert_eq!(timeframes.len(), 16);
798    }
799
800    #[test]
801    fn test_urls() {
802        let config = ExchangeConfig::default();
803        let binance = Binance::new(config).unwrap();
804        let urls = binance.urls();
805
806        assert!(urls.public.contains("api.binance.com"));
807        assert!(urls.ws.contains("stream.binance.com"));
808    }
809
810    #[test]
811    fn test_sandbox_urls() {
812        let config = ExchangeConfig {
813            sandbox: true,
814            ..Default::default()
815        };
816        let binance = Binance::new(config).unwrap();
817        let urls = binance.urls();
818
819        assert!(urls.public.contains("testnet"));
820    }
821
822    #[test]
823    fn test_is_sandbox_with_config_sandbox() {
824        let config = ExchangeConfig {
825            sandbox: true,
826            ..Default::default()
827        };
828        let binance = Binance::new(config).unwrap();
829        assert!(binance.is_sandbox());
830    }
831
832    #[test]
833    fn test_is_sandbox_with_options_test() {
834        let config = ExchangeConfig::default();
835        let options = BinanceOptions {
836            test: true,
837            ..Default::default()
838        };
839        let binance = Binance::new_with_options(config, options).unwrap();
840        assert!(binance.is_sandbox());
841    }
842
843    #[test]
844    fn test_is_sandbox_false_by_default() {
845        let config = ExchangeConfig::default();
846        let binance = Binance::new(config).unwrap();
847        assert!(!binance.is_sandbox());
848    }
849
850    #[test]
851    fn test_binance_options_default() {
852        let options = BinanceOptions::default();
853        assert_eq!(options.default_type, DefaultType::Spot);
854        assert_eq!(options.default_sub_type, None);
855        assert!(!options.adjust_for_time_difference);
856        assert_eq!(options.recv_window, 5000);
857        assert!(!options.test);
858        assert_eq!(options.time_sync_interval_secs, 30);
859        assert!(options.auto_time_sync);
860    }
861
862    #[test]
863    fn test_binance_options_with_default_type() {
864        let options = BinanceOptions {
865            default_type: DefaultType::Swap,
866            default_sub_type: Some(DefaultSubType::Linear),
867            ..Default::default()
868        };
869        assert_eq!(options.default_type, DefaultType::Swap);
870        assert_eq!(options.default_sub_type, Some(DefaultSubType::Linear));
871    }
872
873    #[test]
874    fn test_binance_options_serialization() {
875        let options = BinanceOptions {
876            default_type: DefaultType::Swap,
877            default_sub_type: Some(DefaultSubType::Linear),
878            ..Default::default()
879        };
880        let json = serde_json::to_string(&options).unwrap();
881        assert!(json.contains("\"default_type\":\"swap\""));
882        assert!(json.contains("\"default_sub_type\":\"linear\""));
883        assert!(json.contains("\"time_sync_interval_secs\":30"));
884        assert!(json.contains("\"auto_time_sync\":true"));
885    }
886
887    #[test]
888    fn test_binance_options_deserialization_with_enum() {
889        let json = r#"{
890            "adjust_for_time_difference": false,
891            "recv_window": 5000,
892            "default_type": "swap",
893            "default_sub_type": "linear",
894            "test": false,
895            "time_sync_interval_secs": 60,
896            "auto_time_sync": false
897        }"#;
898        let options: BinanceOptions = serde_json::from_str(json).unwrap();
899        assert_eq!(options.default_type, DefaultType::Swap);
900        assert_eq!(options.default_sub_type, Some(DefaultSubType::Linear));
901        assert_eq!(options.time_sync_interval_secs, 60);
902        assert!(!options.auto_time_sync);
903    }
904
905    #[test]
906    fn test_binance_options_deserialization_legacy_future() {
907        // Test backward compatibility with legacy "future" value
908        let json = r#"{
909            "adjust_for_time_difference": false,
910            "recv_window": 5000,
911            "default_type": "future",
912            "test": false
913        }"#;
914        let options: BinanceOptions = serde_json::from_str(json).unwrap();
915        assert_eq!(options.default_type, DefaultType::Swap);
916        // Verify defaults are applied for missing fields
917        assert_eq!(options.time_sync_interval_secs, 30);
918        assert!(options.auto_time_sync);
919    }
920
921    #[test]
922    fn test_binance_options_deserialization_legacy_delivery() {
923        // Test backward compatibility with legacy "delivery" value
924        let json = r#"{
925            "adjust_for_time_difference": false,
926            "recv_window": 5000,
927            "default_type": "delivery",
928            "test": false
929        }"#;
930        let options: BinanceOptions = serde_json::from_str(json).unwrap();
931        assert_eq!(options.default_type, DefaultType::Futures);
932    }
933
934    #[test]
935    fn test_binance_options_deserialization_without_sub_type() {
936        let json = r#"{
937            "adjust_for_time_difference": false,
938            "recv_window": 5000,
939            "default_type": "spot",
940            "test": false
941        }"#;
942        let options: BinanceOptions = serde_json::from_str(json).unwrap();
943        assert_eq!(options.default_type, DefaultType::Spot);
944        assert_eq!(options.default_sub_type, None);
945    }
946
947    #[test]
948    fn test_binance_options_deserialization_case_insensitive() {
949        // Test case-insensitive deserialization
950        let json = r#"{
951            "adjust_for_time_difference": false,
952            "recv_window": 5000,
953            "default_type": "SWAP",
954            "test": false
955        }"#;
956        let options: BinanceOptions = serde_json::from_str(json).unwrap();
957        assert_eq!(options.default_type, DefaultType::Swap);
958
959        // Test mixed case
960        let json = r#"{
961            "adjust_for_time_difference": false,
962            "recv_window": 5000,
963            "default_type": "FuTuReS",
964            "test": false
965        }"#;
966        let options: BinanceOptions = serde_json::from_str(json).unwrap();
967        assert_eq!(options.default_type, DefaultType::Futures);
968    }
969
970    #[test]
971    fn test_new_futures_uses_swap_type() {
972        let config = ExchangeConfig::default();
973        let binance = Binance::new_swap(config).unwrap();
974        assert_eq!(binance.options().default_type, DefaultType::Swap);
975    }
976
977    #[test]
978    fn test_get_ws_url_spot() {
979        let config = ExchangeConfig::default();
980        let options = BinanceOptions {
981            default_type: DefaultType::Spot,
982            ..Default::default()
983        };
984        let binance = Binance::new_with_options(config, options).unwrap();
985        let ws_url = binance.get_ws_url();
986        assert!(ws_url.contains("stream.binance.com"));
987    }
988
989    #[test]
990    fn test_get_ws_url_margin() {
991        let config = ExchangeConfig::default();
992        let options = BinanceOptions {
993            default_type: DefaultType::Margin,
994            ..Default::default()
995        };
996        let binance = Binance::new_with_options(config, options).unwrap();
997        let ws_url = binance.get_ws_url();
998        // Margin uses the same WebSocket as Spot
999        assert!(ws_url.contains("stream.binance.com"));
1000    }
1001
1002    #[test]
1003    fn test_get_ws_url_swap_linear() {
1004        let config = ExchangeConfig::default();
1005        let options = BinanceOptions {
1006            default_type: DefaultType::Swap,
1007            default_sub_type: Some(DefaultSubType::Linear),
1008            ..Default::default()
1009        };
1010        let binance = Binance::new_with_options(config, options).unwrap();
1011        let ws_url = binance.get_ws_url();
1012        assert!(ws_url.contains("fstream.binance.com"));
1013    }
1014
1015    #[test]
1016    fn test_get_ws_url_swap_inverse() {
1017        let config = ExchangeConfig::default();
1018        let options = BinanceOptions {
1019            default_type: DefaultType::Swap,
1020            default_sub_type: Some(DefaultSubType::Inverse),
1021            ..Default::default()
1022        };
1023        let binance = Binance::new_with_options(config, options).unwrap();
1024        let ws_url = binance.get_ws_url();
1025        assert!(ws_url.contains("dstream.binance.com"));
1026    }
1027
1028    #[test]
1029    fn test_get_ws_url_swap_default_sub_type() {
1030        // When sub_type is not specified, should default to Linear (FAPI)
1031        let config = ExchangeConfig::default();
1032        let options = BinanceOptions {
1033            default_type: DefaultType::Swap,
1034            default_sub_type: None,
1035            ..Default::default()
1036        };
1037        let binance = Binance::new_with_options(config, options).unwrap();
1038        let ws_url = binance.get_ws_url();
1039        assert!(ws_url.contains("fstream.binance.com"));
1040    }
1041
1042    #[test]
1043    fn test_get_ws_url_futures_linear() {
1044        let config = ExchangeConfig::default();
1045        let options = BinanceOptions {
1046            default_type: DefaultType::Futures,
1047            default_sub_type: Some(DefaultSubType::Linear),
1048            ..Default::default()
1049        };
1050        let binance = Binance::new_with_options(config, options).unwrap();
1051        let ws_url = binance.get_ws_url();
1052        assert!(ws_url.contains("fstream.binance.com"));
1053    }
1054
1055    #[test]
1056    fn test_get_ws_url_futures_inverse() {
1057        let config = ExchangeConfig::default();
1058        let options = BinanceOptions {
1059            default_type: DefaultType::Futures,
1060            default_sub_type: Some(DefaultSubType::Inverse),
1061            ..Default::default()
1062        };
1063        let binance = Binance::new_with_options(config, options).unwrap();
1064        let ws_url = binance.get_ws_url();
1065        assert!(ws_url.contains("dstream.binance.com"));
1066    }
1067
1068    #[test]
1069    fn test_get_ws_url_option() {
1070        let config = ExchangeConfig::default();
1071        let options = BinanceOptions {
1072            default_type: DefaultType::Option,
1073            ..Default::default()
1074        };
1075        let binance = Binance::new_with_options(config, options).unwrap();
1076        let ws_url = binance.get_ws_url();
1077        assert!(ws_url.contains("nbstream.binance.com") || ws_url.contains("eoptions"));
1078    }
1079
1080    #[test]
1081    fn test_binance_urls_has_all_ws_endpoints() {
1082        let urls = BinanceUrls::production();
1083        assert!(!urls.ws.is_empty());
1084        assert!(!urls.ws_fapi.is_empty());
1085        assert!(!urls.ws_dapi.is_empty());
1086        assert!(!urls.ws_eapi.is_empty());
1087    }
1088
1089    #[test]
1090    fn test_binance_urls_testnet_has_all_ws_endpoints() {
1091        let urls = BinanceUrls::testnet();
1092        assert!(!urls.ws.is_empty());
1093        assert!(!urls.ws_fapi.is_empty());
1094        assert!(!urls.ws_dapi.is_empty());
1095        assert!(!urls.ws_eapi.is_empty());
1096    }
1097
1098    #[test]
1099    fn test_binance_urls_demo_has_all_ws_endpoints() {
1100        let urls = BinanceUrls::demo();
1101        assert!(!urls.ws.is_empty());
1102        assert!(!urls.ws_fapi.is_empty());
1103        assert!(!urls.ws_dapi.is_empty());
1104        assert!(!urls.ws_eapi.is_empty());
1105    }
1106
1107    #[test]
1108    fn test_get_rest_url_public_spot() {
1109        let config = ExchangeConfig::default();
1110        let options = BinanceOptions {
1111            default_type: DefaultType::Spot,
1112            ..Default::default()
1113        };
1114        let binance = Binance::new_with_options(config, options).unwrap();
1115        let url = binance.get_rest_url_public();
1116        assert!(url.contains("api.binance.com"));
1117        assert!(url.contains("/api/v3"));
1118    }
1119
1120    #[test]
1121    fn test_get_rest_url_public_margin() {
1122        let config = ExchangeConfig::default();
1123        let options = BinanceOptions {
1124            default_type: DefaultType::Margin,
1125            ..Default::default()
1126        };
1127        let binance = Binance::new_with_options(config, options).unwrap();
1128        let url = binance.get_rest_url_public();
1129        assert!(url.contains("api.binance.com"));
1130        assert!(url.contains("/sapi/"));
1131    }
1132
1133    #[test]
1134    fn test_get_rest_url_public_swap_linear() {
1135        let config = ExchangeConfig::default();
1136        let options = BinanceOptions {
1137            default_type: DefaultType::Swap,
1138            default_sub_type: Some(DefaultSubType::Linear),
1139            ..Default::default()
1140        };
1141        let binance = Binance::new_with_options(config, options).unwrap();
1142        let url = binance.get_rest_url_public();
1143        assert!(url.contains("fapi.binance.com"));
1144    }
1145
1146    #[test]
1147    fn test_get_rest_url_public_swap_inverse() {
1148        let config = ExchangeConfig::default();
1149        let options = BinanceOptions {
1150            default_type: DefaultType::Swap,
1151            default_sub_type: Some(DefaultSubType::Inverse),
1152            ..Default::default()
1153        };
1154        let binance = Binance::new_with_options(config, options).unwrap();
1155        let url = binance.get_rest_url_public();
1156        assert!(url.contains("dapi.binance.com"));
1157    }
1158
1159    #[test]
1160    fn test_get_rest_url_public_futures_linear() {
1161        let config = ExchangeConfig::default();
1162        let options = BinanceOptions {
1163            default_type: DefaultType::Futures,
1164            default_sub_type: Some(DefaultSubType::Linear),
1165            ..Default::default()
1166        };
1167        let binance = Binance::new_with_options(config, options).unwrap();
1168        let url = binance.get_rest_url_public();
1169        assert!(url.contains("fapi.binance.com"));
1170    }
1171
1172    #[test]
1173    fn test_get_rest_url_public_futures_inverse() {
1174        let config = ExchangeConfig::default();
1175        let options = BinanceOptions {
1176            default_type: DefaultType::Futures,
1177            default_sub_type: Some(DefaultSubType::Inverse),
1178            ..Default::default()
1179        };
1180        let binance = Binance::new_with_options(config, options).unwrap();
1181        let url = binance.get_rest_url_public();
1182        assert!(url.contains("dapi.binance.com"));
1183    }
1184
1185    #[test]
1186    fn test_get_rest_url_public_option() {
1187        let config = ExchangeConfig::default();
1188        let options = BinanceOptions {
1189            default_type: DefaultType::Option,
1190            ..Default::default()
1191        };
1192        let binance = Binance::new_with_options(config, options).unwrap();
1193        let url = binance.get_rest_url_public();
1194        assert!(url.contains("eapi.binance.com"));
1195    }
1196
1197    #[test]
1198    fn test_get_rest_url_private_swap_linear() {
1199        let config = ExchangeConfig::default();
1200        let options = BinanceOptions {
1201            default_type: DefaultType::Swap,
1202            default_sub_type: Some(DefaultSubType::Linear),
1203            ..Default::default()
1204        };
1205        let binance = Binance::new_with_options(config, options).unwrap();
1206        let url = binance.get_rest_url_private();
1207        assert!(url.contains("fapi.binance.com"));
1208    }
1209
1210    #[test]
1211    fn test_get_rest_url_private_swap_inverse() {
1212        let config = ExchangeConfig::default();
1213        let options = BinanceOptions {
1214            default_type: DefaultType::Swap,
1215            default_sub_type: Some(DefaultSubType::Inverse),
1216            ..Default::default()
1217        };
1218        let binance = Binance::new_with_options(config, options).unwrap();
1219        let url = binance.get_rest_url_private();
1220        assert!(url.contains("dapi.binance.com"));
1221    }
1222
1223    #[test]
1224    fn test_is_contract_type() {
1225        let config = ExchangeConfig::default();
1226
1227        // Spot is not a contract type
1228        let options = BinanceOptions {
1229            default_type: DefaultType::Spot,
1230            ..Default::default()
1231        };
1232        let binance = Binance::new_with_options(config.clone(), options).unwrap();
1233        assert!(!binance.is_contract_type());
1234
1235        // Margin is not a contract type
1236        let options = BinanceOptions {
1237            default_type: DefaultType::Margin,
1238            ..Default::default()
1239        };
1240        let binance = Binance::new_with_options(config.clone(), options).unwrap();
1241        assert!(!binance.is_contract_type());
1242
1243        // Swap is a contract type
1244        let options = BinanceOptions {
1245            default_type: DefaultType::Swap,
1246            ..Default::default()
1247        };
1248        let binance = Binance::new_with_options(config.clone(), options).unwrap();
1249        assert!(binance.is_contract_type());
1250
1251        // Futures is a contract type
1252        let options = BinanceOptions {
1253            default_type: DefaultType::Futures,
1254            ..Default::default()
1255        };
1256        let binance = Binance::new_with_options(config.clone(), options).unwrap();
1257        assert!(binance.is_contract_type());
1258
1259        // Option is a contract type
1260        let options = BinanceOptions {
1261            default_type: DefaultType::Option,
1262            ..Default::default()
1263        };
1264        let binance = Binance::new_with_options(config, options).unwrap();
1265        assert!(binance.is_contract_type());
1266    }
1267
1268    #[test]
1269    fn test_is_linear_and_is_inverse() {
1270        let config = ExchangeConfig::default();
1271
1272        // No sub-type specified defaults to linear
1273        let options = BinanceOptions {
1274            default_type: DefaultType::Swap,
1275            default_sub_type: None,
1276            ..Default::default()
1277        };
1278        let binance = Binance::new_with_options(config.clone(), options).unwrap();
1279        assert!(binance.is_linear());
1280        assert!(!binance.is_inverse());
1281
1282        // Explicit linear
1283        let options = BinanceOptions {
1284            default_type: DefaultType::Swap,
1285            default_sub_type: Some(DefaultSubType::Linear),
1286            ..Default::default()
1287        };
1288        let binance = Binance::new_with_options(config.clone(), options).unwrap();
1289        assert!(binance.is_linear());
1290        assert!(!binance.is_inverse());
1291
1292        // Explicit inverse
1293        let options = BinanceOptions {
1294            default_type: DefaultType::Swap,
1295            default_sub_type: Some(DefaultSubType::Inverse),
1296            ..Default::default()
1297        };
1298        let binance = Binance::new_with_options(config, options).unwrap();
1299        assert!(!binance.is_linear());
1300        assert!(binance.is_inverse());
1301    }
1302
1303    // ============================================================
1304    // Sandbox Mode Market Type URL Selection Tests
1305    // ============================================================
1306
1307    #[test]
1308    fn test_sandbox_market_type_spot() {
1309        let config = ExchangeConfig {
1310            sandbox: true,
1311            ..Default::default()
1312        };
1313        let options = BinanceOptions {
1314            default_type: DefaultType::Spot,
1315            ..Default::default()
1316        };
1317        let binance = Binance::new_with_options(config, options).unwrap();
1318
1319        assert!(binance.is_sandbox());
1320        let url = binance.get_rest_url_public();
1321        assert!(
1322            url.contains("testnet.binance.vision"),
1323            "Spot sandbox URL should contain testnet.binance.vision, got: {}",
1324            url
1325        );
1326        assert!(
1327            url.contains("/api/v3"),
1328            "Spot sandbox URL should contain /api/v3, got: {}",
1329            url
1330        );
1331    }
1332
1333    #[test]
1334    fn test_sandbox_market_type_swap_linear() {
1335        let config = ExchangeConfig {
1336            sandbox: true,
1337            ..Default::default()
1338        };
1339        let options = BinanceOptions {
1340            default_type: DefaultType::Swap,
1341            default_sub_type: Some(DefaultSubType::Linear),
1342            ..Default::default()
1343        };
1344        let binance = Binance::new_with_options(config, options).unwrap();
1345
1346        assert!(binance.is_sandbox());
1347        let url = binance.get_rest_url_public();
1348        assert!(
1349            url.contains("testnet.binancefuture.com"),
1350            "Linear sandbox URL should contain testnet.binancefuture.com, got: {}",
1351            url
1352        );
1353        assert!(
1354            url.contains("/fapi/"),
1355            "Linear sandbox URL should contain /fapi/, got: {}",
1356            url
1357        );
1358    }
1359
1360    #[test]
1361    fn test_sandbox_market_type_swap_inverse() {
1362        let config = ExchangeConfig {
1363            sandbox: true,
1364            ..Default::default()
1365        };
1366        let options = BinanceOptions {
1367            default_type: DefaultType::Swap,
1368            default_sub_type: Some(DefaultSubType::Inverse),
1369            ..Default::default()
1370        };
1371        let binance = Binance::new_with_options(config, options).unwrap();
1372
1373        assert!(binance.is_sandbox());
1374        let url = binance.get_rest_url_public();
1375        assert!(
1376            url.contains("testnet.binancefuture.com"),
1377            "Inverse sandbox URL should contain testnet.binancefuture.com, got: {}",
1378            url
1379        );
1380        assert!(
1381            url.contains("/dapi/"),
1382            "Inverse sandbox URL should contain /dapi/, got: {}",
1383            url
1384        );
1385    }
1386
1387    #[test]
1388    fn test_sandbox_market_type_option() {
1389        let config = ExchangeConfig {
1390            sandbox: true,
1391            ..Default::default()
1392        };
1393        let options = BinanceOptions {
1394            default_type: DefaultType::Option,
1395            ..Default::default()
1396        };
1397        let binance = Binance::new_with_options(config, options).unwrap();
1398
1399        assert!(binance.is_sandbox());
1400        let url = binance.get_rest_url_public();
1401        assert!(
1402            url.contains("testnet.binanceops.com"),
1403            "Option sandbox URL should contain testnet.binanceops.com, got: {}",
1404            url
1405        );
1406        assert!(
1407            url.contains("/eapi/"),
1408            "Option sandbox URL should contain /eapi/, got: {}",
1409            url
1410        );
1411    }
1412
1413    #[test]
1414    fn test_sandbox_market_type_futures_linear() {
1415        let config = ExchangeConfig {
1416            sandbox: true,
1417            ..Default::default()
1418        };
1419        let options = BinanceOptions {
1420            default_type: DefaultType::Futures,
1421            default_sub_type: Some(DefaultSubType::Linear),
1422            ..Default::default()
1423        };
1424        let binance = Binance::new_with_options(config, options).unwrap();
1425
1426        assert!(binance.is_sandbox());
1427        let url = binance.get_rest_url_public();
1428        assert!(
1429            url.contains("testnet.binancefuture.com"),
1430            "Futures Linear sandbox URL should contain testnet.binancefuture.com, got: {}",
1431            url
1432        );
1433        assert!(
1434            url.contains("/fapi/"),
1435            "Futures Linear sandbox URL should contain /fapi/, got: {}",
1436            url
1437        );
1438    }
1439
1440    #[test]
1441    fn test_sandbox_market_type_futures_inverse() {
1442        let config = ExchangeConfig {
1443            sandbox: true,
1444            ..Default::default()
1445        };
1446        let options = BinanceOptions {
1447            default_type: DefaultType::Futures,
1448            default_sub_type: Some(DefaultSubType::Inverse),
1449            ..Default::default()
1450        };
1451        let binance = Binance::new_with_options(config, options).unwrap();
1452
1453        assert!(binance.is_sandbox());
1454        let url = binance.get_rest_url_public();
1455        assert!(
1456            url.contains("testnet.binancefuture.com"),
1457            "Futures Inverse sandbox URL should contain testnet.binancefuture.com, got: {}",
1458            url
1459        );
1460        assert!(
1461            url.contains("/dapi/"),
1462            "Futures Inverse sandbox URL should contain /dapi/, got: {}",
1463            url
1464        );
1465    }
1466
1467    #[test]
1468    fn test_sandbox_websocket_url_spot() {
1469        // Verify WebSocket URL selection in sandbox mode for Spot
1470        let config = ExchangeConfig {
1471            sandbox: true,
1472            ..Default::default()
1473        };
1474        let options = BinanceOptions {
1475            default_type: DefaultType::Spot,
1476            ..Default::default()
1477        };
1478        let binance = Binance::new_with_options(config, options).unwrap();
1479
1480        let urls = binance.urls();
1481        assert!(
1482            urls.ws.contains("testnet.binance.vision"),
1483            "Spot WS sandbox URL should contain testnet.binance.vision, got: {}",
1484            urls.ws
1485        );
1486    }
1487
1488    #[test]
1489    fn test_sandbox_websocket_url_fapi() {
1490        // Verify WebSocket URL selection in sandbox mode for FAPI (Linear)
1491        let config = ExchangeConfig {
1492            sandbox: true,
1493            ..Default::default()
1494        };
1495        let options = BinanceOptions {
1496            default_type: DefaultType::Swap,
1497            default_sub_type: Some(DefaultSubType::Linear),
1498            ..Default::default()
1499        };
1500        let binance = Binance::new_with_options(config, options).unwrap();
1501
1502        let urls = binance.urls();
1503        assert!(
1504            urls.ws_fapi.contains("binancefuture.com"),
1505            "FAPI WS sandbox URL should contain binancefuture.com, got: {}",
1506            urls.ws_fapi
1507        );
1508    }
1509
1510    #[test]
1511    fn test_sandbox_websocket_url_dapi() {
1512        // Verify WebSocket URL selection in sandbox mode for DAPI (Inverse)
1513        let config = ExchangeConfig {
1514            sandbox: true,
1515            ..Default::default()
1516        };
1517        let options = BinanceOptions {
1518            default_type: DefaultType::Swap,
1519            default_sub_type: Some(DefaultSubType::Inverse),
1520            ..Default::default()
1521        };
1522        let binance = Binance::new_with_options(config, options).unwrap();
1523
1524        let urls = binance.urls();
1525        assert!(
1526            urls.ws_dapi.contains("dstream.binancefuture.com"),
1527            "DAPI WS sandbox URL should contain dstream.binancefuture.com, got: {}",
1528            urls.ws_dapi
1529        );
1530    }
1531
1532    #[test]
1533    fn test_sandbox_websocket_url_eapi() {
1534        // Verify WebSocket URL selection in sandbox mode for EAPI (Options)
1535        let config = ExchangeConfig {
1536            sandbox: true,
1537            ..Default::default()
1538        };
1539        let options = BinanceOptions {
1540            default_type: DefaultType::Option,
1541            ..Default::default()
1542        };
1543        let binance = Binance::new_with_options(config, options).unwrap();
1544
1545        let urls = binance.urls();
1546        assert!(
1547            urls.ws_eapi.contains("testnet.binanceops.com"),
1548            "EAPI WS sandbox URL should contain testnet.binanceops.com, got: {}",
1549            urls.ws_eapi
1550        );
1551    }
1552
1553    // ============================================================
1554    // check_response Tests
1555    // ============================================================
1556
1557    #[test]
1558    fn test_check_response_success() {
1559        let config = ExchangeConfig::default();
1560        let binance = Binance::new(config).unwrap();
1561
1562        // Valid response without error
1563        let response = serde_json::json!({
1564            "symbol": "BTCUSDT",
1565            "price": "50000.00"
1566        });
1567
1568        let result = binance.check_response(response.clone());
1569        assert!(result.is_ok());
1570        assert_eq!(result.unwrap(), response);
1571    }
1572
1573    #[test]
1574    fn test_check_response_with_binance_error() {
1575        let config = ExchangeConfig::default();
1576        let binance = Binance::new(config).unwrap();
1577
1578        // Response with Binance API error
1579        let response = serde_json::json!({
1580            "code": -1121,
1581            "msg": "Invalid symbol."
1582        });
1583
1584        let result = binance.check_response(response);
1585        assert!(result.is_err());
1586        let err = result.unwrap_err();
1587        assert!(err.to_string().contains("Invalid symbol"));
1588    }
1589
1590    #[test]
1591    fn test_check_response_with_rate_limit_error() {
1592        let config = ExchangeConfig::default();
1593        let binance = Binance::new(config).unwrap();
1594
1595        // Response with rate limit error
1596        let response = serde_json::json!({
1597            "code": -1003,
1598            "msg": "Too many requests; please use the websocket for live updates."
1599        });
1600
1601        let result = binance.check_response(response);
1602        assert!(result.is_err());
1603        let err = result.unwrap_err();
1604        assert!(matches!(err, ccxt_core::error::Error::RateLimit { .. }));
1605    }
1606
1607    #[test]
1608    fn test_check_response_with_auth_error() {
1609        let config = ExchangeConfig::default();
1610        let binance = Binance::new(config).unwrap();
1611
1612        // Response with authentication error
1613        let response = serde_json::json!({
1614            "code": -2015,
1615            "msg": "Invalid API-key, IP, or permissions for action."
1616        });
1617
1618        let result = binance.check_response(response);
1619        assert!(result.is_err());
1620        let err = result.unwrap_err();
1621        assert!(matches!(err, ccxt_core::error::Error::Authentication(_)));
1622    }
1623
1624    #[test]
1625    fn test_check_response_array_response() {
1626        let config = ExchangeConfig::default();
1627        let binance = Binance::new(config).unwrap();
1628
1629        // Array response (common for list endpoints)
1630        let response = serde_json::json!([
1631            {"symbol": "BTCUSDT", "price": "50000.00"},
1632            {"symbol": "ETHUSDT", "price": "3000.00"}
1633        ]);
1634
1635        let result = binance.check_response(response.clone());
1636        assert!(result.is_ok());
1637        assert_eq!(result.unwrap(), response);
1638    }
1639}