Skip to main content

ccxt_exchanges/binance/
exchange_impl.rs

1//! Exchange trait implementation for Binance
2//!
3//! This module implements the unified `Exchange` trait from `ccxt-core` for Binance,
4//! as well as the new decomposed traits (PublicExchange, MarketData, Trading, Account, Margin, Funding).
5//!
6//! # Backward Compatibility
7//!
8//! Binance implements both the legacy `Exchange` trait and the new modular traits.
9//! This ensures that existing code using `dyn Exchange` continues to work, while
10//! new code can use the more granular trait interfaces.
11//!
12//! # Trait Implementations
13//!
14//! - `Exchange`: Unified interface (backward compatible)
15//! - `PublicExchange`: Metadata and capabilities
16//! - `MarketData`: Public market data (via traits module)
17//! - `Trading`: Order management (via traits module)
18//! - `Account`: Balance and trade history (via traits module)
19//! - `Margin`: Positions, leverage, funding (via traits module)
20//! - `Funding`: Deposits, withdrawals, transfers (via traits module)
21
22use async_trait::async_trait;
23use ccxt_core::{
24    Result,
25    exchange::{Capability, Exchange, ExchangeCapabilities},
26    traits::PublicExchange,
27    types::{
28        Amount, Balance, Market, Ohlcv, Order, OrderBook, OrderSide, OrderType, Price, Ticker,
29        Timeframe, Trade,
30    },
31};
32use rust_decimal::Decimal;
33use std::collections::HashMap;
34use std::sync::Arc;
35
36use super::Binance;
37
38// Re-export ExchangeExt for use in tests
39#[cfg(test)]
40use ccxt_core::exchange::ExchangeExt;
41
42#[async_trait]
43impl Exchange for Binance {
44    // ==================== Metadata ====================
45
46    fn id(&self) -> &'static str {
47        "binance"
48    }
49
50    fn name(&self) -> &'static str {
51        "Binance"
52    }
53
54    fn version(&self) -> &'static str {
55        "v3"
56    }
57
58    fn certified(&self) -> bool {
59        true
60    }
61
62    fn has_websocket(&self) -> bool {
63        true
64    }
65
66    fn capabilities(&self) -> ExchangeCapabilities {
67        // Binance supports almost all capabilities except:
68        // - EditOrder: Binance doesn't support order editing
69        // - FetchCanceledOrders: Binance doesn't have a separate API for canceled orders
70        ExchangeCapabilities::builder()
71            .all()
72            .without_capability(Capability::EditOrder)
73            .without_capability(Capability::FetchCanceledOrders)
74            .build()
75    }
76
77    fn timeframes(&self) -> Vec<Timeframe> {
78        vec![
79            Timeframe::M1,
80            Timeframe::M3,
81            Timeframe::M5,
82            Timeframe::M15,
83            Timeframe::M30,
84            Timeframe::H1,
85            Timeframe::H2,
86            Timeframe::H4,
87            Timeframe::H6,
88            Timeframe::H8,
89            Timeframe::H12,
90            Timeframe::D1,
91            Timeframe::D3,
92            Timeframe::W1,
93            Timeframe::Mon1,
94        ]
95    }
96
97    fn rate_limit(&self) -> u32 {
98        self.options.rate_limit
99    }
100
101    // ==================== Market Data (Public API) ====================
102
103    async fn fetch_markets(&self) -> Result<Vec<Market>> {
104        let arc_markets = Binance::fetch_markets(self).await?;
105        Ok(arc_markets.values().map(|v| (**v).clone()).collect())
106    }
107
108    async fn load_markets(&self, reload: bool) -> Result<Arc<HashMap<String, Arc<Market>>>> {
109        Binance::load_markets(self, reload).await
110    }
111
112    async fn fetch_ticker(&self, symbol: &str) -> Result<Ticker> {
113        // Delegate to existing implementation using default parameters
114        Binance::fetch_ticker(self, symbol, ()).await
115    }
116
117    async fn fetch_tickers(&self, symbols: Option<&[String]>) -> Result<Vec<Ticker>> {
118        // Convert slice to Vec for existing implementation
119        let symbols_vec = symbols.map(<[String]>::to_vec);
120        Binance::fetch_tickers(self, symbols_vec).await
121    }
122
123    async fn fetch_order_book(&self, symbol: &str, limit: Option<u32>) -> Result<OrderBook> {
124        // Delegate to existing implementation
125        Binance::fetch_order_book(self, symbol, limit).await
126    }
127
128    async fn fetch_trades(&self, symbol: &str, limit: Option<u32>) -> Result<Vec<Trade>> {
129        // Delegate to existing implementation
130        Binance::fetch_trades(self, symbol, limit).await
131    }
132
133    async fn fetch_ohlcv(
134        &self,
135        symbol: &str,
136        timeframe: Timeframe,
137        since: Option<i64>,
138        limit: Option<u32>,
139    ) -> Result<Vec<Ohlcv>> {
140        use ccxt_core::types::{Amount, Price};
141
142        // Convert Timeframe enum to string for existing implementation
143        let timeframe_str = timeframe.to_string();
144        // Use i64 directly for the updated method signature
145        #[allow(deprecated)]
146        let ohlcv_data =
147            Binance::fetch_ohlcv(self, symbol, &timeframe_str, since, limit, None).await?;
148
149        // Convert OHLCV to Ohlcv with proper type conversions
150        Ok(ohlcv_data
151            .into_iter()
152            .map(|o| Ohlcv {
153                timestamp: o.timestamp,
154                open: Price::from(Decimal::try_from(o.open).unwrap_or_default()),
155                high: Price::from(Decimal::try_from(o.high).unwrap_or_default()),
156                low: Price::from(Decimal::try_from(o.low).unwrap_or_default()),
157                close: Price::from(Decimal::try_from(o.close).unwrap_or_default()),
158                volume: Amount::from(Decimal::try_from(o.volume).unwrap_or_default()),
159            })
160            .collect())
161    }
162
163    // ==================== Trading (Private API) ====================
164
165    async fn create_order(
166        &self,
167        symbol: &str,
168        order_type: OrderType,
169        side: OrderSide,
170        amount: Amount,
171        price: Option<Price>,
172    ) -> Result<Order> {
173        // Direct delegation - no type conversion needed
174        #[allow(deprecated)]
175        Binance::create_order(self, symbol, order_type, side, amount, price, None).await
176    }
177
178    async fn cancel_order(&self, id: &str, symbol: Option<&str>) -> Result<Order> {
179        // Delegate to existing implementation
180        // Note: Binance requires symbol for cancel_order
181        let symbol_str = symbol.ok_or_else(|| {
182            ccxt_core::Error::invalid_request("Symbol is required for cancel_order on Binance")
183        })?;
184        Binance::cancel_order(self, id, symbol_str).await
185    }
186
187    async fn cancel_all_orders(&self, symbol: Option<&str>) -> Result<Vec<Order>> {
188        // Delegate to existing implementation
189        // Note: Binance requires symbol for cancel_all_orders
190        let symbol_str = symbol.ok_or_else(|| {
191            ccxt_core::Error::invalid_request("Symbol is required for cancel_all_orders on Binance")
192        })?;
193        Binance::cancel_all_orders(self, symbol_str).await
194    }
195
196    async fn fetch_order(&self, id: &str, symbol: Option<&str>) -> Result<Order> {
197        // Delegate to existing implementation
198        // Note: Binance requires symbol for fetch_order
199        let symbol_str = symbol.ok_or_else(|| {
200            ccxt_core::Error::invalid_request("Symbol is required for fetch_order on Binance")
201        })?;
202        Binance::fetch_order(self, id, symbol_str).await
203    }
204
205    async fn fetch_open_orders(
206        &self,
207        symbol: Option<&str>,
208        _since: Option<i64>,
209        _limit: Option<u32>,
210    ) -> Result<Vec<Order>> {
211        // Delegate to existing implementation
212        // Note: Binance's fetch_open_orders doesn't support since/limit parameters
213        Binance::fetch_open_orders(self, symbol).await
214    }
215
216    async fn fetch_closed_orders(
217        &self,
218        symbol: Option<&str>,
219        since: Option<i64>,
220        limit: Option<u32>,
221    ) -> Result<Vec<Order>> {
222        // Delegate to existing implementation
223        // Use i64 directly for since parameter
224        Binance::fetch_closed_orders(self, symbol, since, limit).await
225    }
226
227    // ==================== Account (Private API) ====================
228
229    async fn fetch_balance(&self) -> Result<Balance> {
230        // Delegate to existing implementation
231        Binance::fetch_balance(self, None).await
232    }
233
234    async fn fetch_my_trades(
235        &self,
236        symbol: Option<&str>,
237        since: Option<i64>,
238        limit: Option<u32>,
239    ) -> Result<Vec<Trade>> {
240        // Delegate to existing implementation
241        // Note: Binance's fetch_my_trades requires a symbol
242        let symbol_str = symbol.ok_or_else(|| {
243            ccxt_core::Error::invalid_request("Symbol is required for fetch_my_trades on Binance")
244        })?;
245        // Use i64 directly for the updated method signature
246        Binance::fetch_my_trades(self, symbol_str, since, limit).await
247    }
248
249    // ==================== Helper Methods ====================
250
251    async fn market(&self, symbol: &str) -> Result<Arc<Market>> {
252        // Use async read for async method
253        let cache = self.base().market_cache.read().await;
254
255        if !cache.is_loaded() {
256            return Err(ccxt_core::Error::exchange(
257                "-1",
258                "Markets not loaded. Call load_markets() first.",
259            ));
260        }
261
262        cache
263            .get_market(symbol)
264            .ok_or_else(|| ccxt_core::Error::bad_symbol(format!("Market {} not found", symbol)))
265    }
266
267    async fn markets(&self) -> Arc<HashMap<String, Arc<Market>>> {
268        let cache = self.base().market_cache.read().await;
269        cache.markets()
270    }
271}
272
273// ==================== PublicExchange Trait Implementation ====================
274
275#[async_trait]
276impl PublicExchange for Binance {
277    fn id(&self) -> &'static str {
278        "binance"
279    }
280
281    fn name(&self) -> &'static str {
282        "Binance"
283    }
284
285    fn version(&self) -> &'static str {
286        "v3"
287    }
288
289    fn certified(&self) -> bool {
290        true
291    }
292
293    fn capabilities(&self) -> ExchangeCapabilities {
294        // Binance supports almost all capabilities except:
295        // - EditOrder: Binance doesn't support order editing
296        // - FetchCanceledOrders: Binance doesn't have a separate API for canceled orders
297        ExchangeCapabilities::builder()
298            .all()
299            .without_capability(Capability::EditOrder)
300            .without_capability(Capability::FetchCanceledOrders)
301            .build()
302    }
303
304    fn timeframes(&self) -> Vec<Timeframe> {
305        vec![
306            Timeframe::M1,
307            Timeframe::M3,
308            Timeframe::M5,
309            Timeframe::M15,
310            Timeframe::M30,
311            Timeframe::H1,
312            Timeframe::H2,
313            Timeframe::H4,
314            Timeframe::H6,
315            Timeframe::H8,
316            Timeframe::H12,
317            Timeframe::D1,
318            Timeframe::D3,
319            Timeframe::W1,
320            Timeframe::Mon1,
321        ]
322    }
323
324    fn rate_limit(&self) -> u32 {
325        self.options.rate_limit
326    }
327
328    fn has_websocket(&self) -> bool {
329        true
330    }
331}
332
333// Helper methods for REST API operations
334impl Binance {
335    /// Check required authentication credentials.
336    pub(crate) fn check_required_credentials(&self) -> ccxt_core::Result<()> {
337        if self.base().config.api_key.is_none() || self.base().config.secret.is_none() {
338            return Err(ccxt_core::Error::authentication(
339                "API key and secret are required",
340            ));
341        }
342        Ok(())
343    }
344
345    /// Check that API key is available (for endpoints that only need X-MBX-APIKEY header).
346    pub(crate) fn check_api_key(&self) -> ccxt_core::Result<()> {
347        if self.base().config.api_key.is_none() {
348            return Err(ccxt_core::Error::authentication("API key is required"));
349        }
350        Ok(())
351    }
352
353    /// Get authenticator instance.
354    pub(crate) fn get_auth(&self) -> ccxt_core::Result<super::auth::BinanceAuth> {
355        let api_key = self
356            .base()
357            .config
358            .api_key
359            .as_ref()
360            .ok_or_else(|| ccxt_core::Error::authentication("API key is required"))?
361            .clone();
362
363        let secret = self
364            .base()
365            .config
366            .secret
367            .as_ref()
368            .ok_or_else(|| ccxt_core::Error::authentication("Secret is required"))?
369            .clone();
370
371        Ok(super::auth::BinanceAuth::new(
372            api_key.expose_secret(),
373            secret.expose_secret(),
374        ))
375    }
376
377    // ==================== Time Sync Helper Methods ====================
378
379    /// Gets the server timestamp for signing requests.
380    ///
381    /// This method uses the cached time offset if available, otherwise fetches
382    /// the server time directly. It also triggers a background resync if needed.
383    ///
384    /// # Optimization
385    ///
386    /// By caching the time offset between local and server time, this method
387    /// reduces the number of network round-trips for signed API requests from 2 to 1.
388    /// Instead of fetching server time before every signed request, we calculate
389    /// the server timestamp locally using: `server_timestamp = local_time + cached_offset`
390    ///
391    /// # Returns
392    ///
393    /// Returns the estimated server timestamp in milliseconds.
394    ///
395    /// # Errors
396    ///
397    /// Returns an error if:
398    /// - The time sync manager is not initialized and the server time fetch fails
399    /// - Network errors occur during time synchronization
400    ///
401    /// # Example
402    ///
403    /// ```no_run
404    /// # use ccxt_exchanges::binance::Binance;
405    /// # use ccxt_core::ExchangeConfig;
406    /// # async fn example() -> ccxt_core::Result<()> {
407    /// let binance = Binance::new(ExchangeConfig::default())?;
408    ///
409    /// // Get timestamp for signing (uses cached offset if available)
410    /// let timestamp = binance.get_signing_timestamp().await?;
411    /// println!("Server timestamp: {}", timestamp);
412    /// # Ok(())
413    /// # }
414    /// ```
415    ///
416    /// _Requirements: 1.2, 2.3, 6.4_
417    pub async fn get_signing_timestamp(&self) -> ccxt_core::Result<i64> {
418        // Check if we need to sync (not initialized or sync interval elapsed)
419        if self.time_sync.needs_resync() {
420            // Attempt to sync time
421            if let Err(e) = self.sync_time().await {
422                // If sync fails and we're not initialized, we must fail
423                if !self.time_sync.is_initialized() {
424                    return Err(e);
425                }
426                // If sync fails but we have a cached offset, log and continue
427                tracing::warn!(
428                    error = %e,
429                    "Time sync failed, using cached offset"
430                );
431            }
432        }
433
434        // If still not initialized after sync attempt, fall back to direct fetch
435        if !self.time_sync.is_initialized() {
436            return self.fetch_time_raw().await;
437        }
438
439        // Return the estimated server timestamp using cached offset
440        Ok(self.time_sync.get_server_timestamp())
441    }
442
443    /// Synchronizes local time with Binance server time.
444    ///
445    /// This method fetches the current server time from Binance and updates
446    /// the cached time offset. The offset is calculated as:
447    /// `offset = server_time - local_time`
448    ///
449    /// # When to Use
450    ///
451    /// - Called automatically by `get_signing_timestamp()` when resync is needed
452    /// - Can be called manually to force a time synchronization
453    /// - Useful after receiving timestamp-related errors from the API
454    ///
455    /// # Returns
456    ///
457    /// Returns `Ok(())` on successful synchronization.
458    ///
459    /// # Errors
460    ///
461    /// Returns an error if the server time fetch fails due to network issues.
462    ///
463    /// # Example
464    ///
465    /// ```no_run
466    /// # use ccxt_exchanges::binance::Binance;
467    /// # use ccxt_core::ExchangeConfig;
468    /// # async fn example() -> ccxt_core::Result<()> {
469    /// let binance = Binance::new(ExchangeConfig::default())?;
470    ///
471    /// // Manually sync time with server
472    /// binance.sync_time().await?;
473    ///
474    /// // Check the current offset
475    /// let offset = binance.time_sync().get_offset();
476    /// println!("Time offset: {}ms", offset);
477    /// # Ok(())
478    /// # }
479    /// ```
480    ///
481    /// _Requirements: 2.1, 2.2, 6.3_
482    pub async fn sync_time(&self) -> ccxt_core::Result<()> {
483        let server_time = self.fetch_time_raw().await?;
484        self.time_sync.update_offset(server_time);
485        tracing::debug!(
486            server_time = server_time,
487            offset = self.time_sync.get_offset(),
488            "Time synchronized with Binance server"
489        );
490        Ok(())
491    }
492
493    /// Checks if an error is related to timestamp validation.
494    ///
495    /// Binance returns specific error codes and messages when the request
496    /// timestamp is outside the acceptable window (recvWindow). This method
497    /// detects such errors to enable automatic retry with a fresh timestamp.
498    ///
499    /// # Binance Timestamp Error Codes
500    ///
501    /// | Error Code | Message | Meaning |
502    /// |------------|---------|---------|
503    /// | -1021 | "Timestamp for this request is outside of the recvWindow" | Timestamp too old or too new |
504    /// | -1022 | "Signature for this request is not valid" | May be caused by wrong timestamp |
505    ///
506    /// # Arguments
507    ///
508    /// * `error` - The error to check
509    ///
510    /// # Returns
511    ///
512    /// Returns `true` if the error is related to timestamp validation, `false` otherwise.
513    ///
514    /// # Example
515    ///
516    /// ```rust
517    /// use ccxt_exchanges::binance::Binance;
518    /// use ccxt_core::{ExchangeConfig, Error};
519    ///
520    /// let binance = Binance::new(ExchangeConfig::default()).unwrap();
521    ///
522    /// // Simulate a timestamp error
523    /// let err = Error::exchange("-1021", "Timestamp for this request is outside of the recvWindow");
524    /// assert!(binance.is_timestamp_error(&err));
525    ///
526    /// // Non-timestamp error
527    /// let err = Error::exchange("-1100", "Illegal characters found in parameter");
528    /// assert!(!binance.is_timestamp_error(&err));
529    /// ```
530    ///
531    /// _Requirements: 4.1_
532    pub fn is_timestamp_error(&self, error: &ccxt_core::Error) -> bool {
533        let error_str = error.to_string().to_lowercase();
534
535        // Check for timestamp-related keywords in the error message
536        let has_timestamp_keyword = error_str.contains("timestamp");
537        let has_recv_window = error_str.contains("recvwindow");
538        let has_ahead = error_str.contains("ahead");
539        let has_behind = error_str.contains("behind");
540
541        // Timestamp error if it mentions timestamp AND one of the time-related issues
542        if has_timestamp_keyword && (has_recv_window || has_ahead || has_behind) {
543            return true;
544        }
545
546        // Also check for specific Binance error codes
547        // -1021: Timestamp for this request is outside of the recvWindow
548        // -1022: Signature for this request is not valid (may be timestamp-related)
549        if error_str.contains("-1021") {
550            return true;
551        }
552
553        // Check for error code in Exchange variant
554        if let ccxt_core::Error::Exchange(details) = error {
555            if details.code == "-1021" {
556                return true;
557            }
558        }
559
560        false
561    }
562
563    /// Executes a signed request with automatic timestamp error recovery.
564    ///
565    /// This method wraps a signed request operation and automatically handles
566    /// timestamp-related errors by resyncing time and retrying the request once.
567    ///
568    /// # Error Recovery Flow
569    ///
570    /// 1. Execute the request with the current timestamp
571    /// 2. If the request fails with a timestamp error:
572    ///    a. Resync time with the server
573    ///    b. Retry the request with a fresh timestamp
574    /// 3. If the retry also fails, return the error
575    ///
576    /// # Retry Limit
577    ///
578    /// To prevent infinite retry loops, this method limits automatic retries to
579    /// exactly 1 retry per request. If the retry also fails, the error is returned.
580    ///
581    /// # Type Parameters
582    ///
583    /// * `T` - The return type of the request
584    /// * `F` - The async function that performs the signed request
585    ///
586    /// # Arguments
587    ///
588    /// * `request_fn` - An async function that takes a timestamp (i64) and returns
589    ///   a `Result<T>`. This function should perform the actual signed API request.
590    ///
591    /// # Returns
592    ///
593    /// Returns `Ok(T)` on success, or `Err` if the request fails after retry.
594    ///
595    /// # Example
596    ///
597    /// ```no_run
598    /// # use ccxt_exchanges::binance::Binance;
599    /// # use ccxt_core::ExchangeConfig;
600    /// # async fn example() -> ccxt_core::Result<()> {
601    /// let binance = Binance::new(ExchangeConfig::default())?;
602    ///
603    /// // Execute a signed request with automatic retry on timestamp error
604    /// let result = binance.execute_signed_request_with_retry(|timestamp| {
605    ///     Box::pin(async move {
606    ///         // Perform signed request using the provided timestamp
607    ///         // ... actual request logic here ...
608    ///         Ok(())
609    ///     })
610    /// }).await?;
611    /// # Ok(())
612    /// # }
613    /// ```
614    ///
615    /// _Requirements: 4.1, 4.2, 4.3, 4.4_
616    pub async fn execute_signed_request_with_retry<T, F, Fut>(
617        &self,
618        request_fn: F,
619    ) -> ccxt_core::Result<T>
620    where
621        F: Fn(i64) -> Fut,
622        Fut: std::future::Future<Output = ccxt_core::Result<T>>,
623    {
624        // Get the initial timestamp
625        let timestamp = self.get_signing_timestamp().await?;
626
627        // Execute the request
628        match request_fn(timestamp).await {
629            Ok(result) => Ok(result),
630            Err(e) if self.is_timestamp_error(&e) => {
631                // Log the timestamp error and retry
632                tracing::warn!(
633                    error = %e,
634                    "Timestamp error detected, resyncing time and retrying request"
635                );
636
637                // Force resync time with server
638                if let Err(sync_err) = self.sync_time().await {
639                    tracing::error!(
640                        error = %sync_err,
641                        "Failed to resync time after timestamp error"
642                    );
643                    // Return the original error if sync fails
644                    return Err(e);
645                }
646
647                // Get fresh timestamp after resync
648                let new_timestamp = self.time_sync.get_server_timestamp();
649
650                tracing::debug!(
651                    old_timestamp = timestamp,
652                    new_timestamp = new_timestamp,
653                    offset = self.time_sync.get_offset(),
654                    "Retrying request with fresh timestamp"
655                );
656
657                // Retry the request once with the new timestamp
658                request_fn(new_timestamp).await
659            }
660            Err(e) => Err(e),
661        }
662    }
663
664    /// Helper method to handle timestamp errors and trigger resync.
665    ///
666    /// This method checks if an error is a timestamp error and, if so,
667    /// triggers a time resync. It's useful for manual error handling
668    /// when you want more control over the retry logic.
669    ///
670    /// # Arguments
671    ///
672    /// * `error` - The error to check
673    ///
674    /// # Returns
675    ///
676    /// Returns `true` if the error was a timestamp error and resync was triggered,
677    /// `false` otherwise.
678    ///
679    /// # Example
680    ///
681    /// ```no_run
682    /// # use ccxt_exchanges::binance::Binance;
683    /// # use ccxt_core::ExchangeConfig;
684    /// # async fn example() -> ccxt_core::Result<()> {
685    /// let binance = Binance::new(ExchangeConfig::default())?;
686    ///
687    /// // Manual error handling with resync
688    /// let result = some_signed_request().await;
689    /// if let Err(ref e) = result {
690    ///     if binance.handle_timestamp_error_and_resync(e).await {
691    ///         // Timestamp error detected and resync triggered
692    ///         // You can now retry the request
693    ///     }
694    /// }
695    /// # async fn some_signed_request() -> ccxt_core::Result<()> { Ok(()) }
696    /// # Ok(())
697    /// # }
698    /// ```
699    ///
700    /// _Requirements: 4.1, 4.2_
701    pub async fn handle_timestamp_error_and_resync(&self, error: &ccxt_core::Error) -> bool {
702        if self.is_timestamp_error(error) {
703            tracing::warn!(
704                error = %error,
705                "Timestamp error detected, triggering time resync"
706            );
707
708            if let Err(sync_err) = self.sync_time().await {
709                tracing::error!(
710                    error = %sync_err,
711                    "Failed to resync time after timestamp error"
712                );
713                return false;
714            }
715
716            tracing::debug!(
717                offset = self.time_sync.get_offset(),
718                "Time resync completed after timestamp error"
719            );
720
721            return true;
722        }
723
724        false
725    }
726}
727
728// Implement HasHttpClient trait for generic SignedRequestBuilder support
729impl ccxt_core::signed_request::HasHttpClient for Binance {
730    fn http_client(&self) -> &ccxt_core::http_client::HttpClient {
731        &self.base().http_client
732    }
733
734    fn base_url(&self) -> &'static str {
735        // Return empty string since Binance uses full URLs in endpoints
736        ""
737    }
738}
739
740#[cfg(test)]
741mod tests {
742    #![allow(clippy::disallowed_methods)]
743    use super::*;
744    use ccxt_core::ExchangeConfig;
745
746    #[test]
747    fn test_binance_exchange_trait_metadata() {
748        let config = ExchangeConfig::default();
749        let binance = Binance::new(config).unwrap();
750
751        // Test metadata methods via Exchange trait
752        let exchange: &dyn Exchange = &binance;
753
754        assert_eq!(exchange.id(), "binance");
755        assert_eq!(exchange.name(), "Binance");
756        assert_eq!(exchange.version(), "v3");
757        assert!(exchange.certified());
758        assert!(exchange.has_websocket());
759    }
760
761    #[test]
762    fn test_binance_exchange_trait_capabilities() {
763        let config = ExchangeConfig::default();
764        let binance = Binance::new(config).unwrap();
765
766        let exchange: &dyn Exchange = &binance;
767        let caps = exchange.capabilities();
768
769        assert!(caps.fetch_markets());
770        assert!(caps.fetch_ticker());
771        assert!(caps.create_order());
772        assert!(caps.websocket());
773        assert!(!caps.edit_order()); // Binance doesn't support order editing
774    }
775
776    #[test]
777    fn test_binance_exchange_trait_timeframes() {
778        let config = ExchangeConfig::default();
779        let binance = Binance::new(config).unwrap();
780
781        let exchange: &dyn Exchange = &binance;
782        let timeframes = exchange.timeframes();
783
784        assert!(!timeframes.is_empty());
785        assert!(timeframes.contains(&Timeframe::M1));
786        assert!(timeframes.contains(&Timeframe::H1));
787        assert!(timeframes.contains(&Timeframe::D1));
788    }
789
790    #[test]
791    fn test_binance_exchange_trait_object_safety() {
792        let config = ExchangeConfig::default();
793        let binance = Binance::new(config).unwrap();
794
795        // Test that we can create a trait object
796        let exchange: Box<dyn Exchange> = Box::new(binance);
797
798        assert_eq!(exchange.id(), "binance");
799        assert_eq!(exchange.rate_limit(), 50);
800    }
801
802    #[test]
803    fn test_binance_exchange_ext_trait() {
804        let config = ExchangeConfig::default();
805        let binance = Binance::new(config).unwrap();
806
807        // Test ExchangeExt methods
808        let exchange: &dyn Exchange = &binance;
809
810        // Binance supports all capabilities
811        assert!(
812            exchange.supports_market_data(),
813            "Binance should support market data"
814        );
815        assert!(
816            exchange.supports_trading(),
817            "Binance should support trading"
818        );
819        assert!(
820            exchange.supports_account(),
821            "Binance should support account operations"
822        );
823        assert!(
824            exchange.supports_margin(),
825            "Binance should support margin operations"
826        );
827        assert!(
828            exchange.supports_funding(),
829            "Binance should support funding operations"
830        );
831    }
832
833    #[test]
834    fn test_binance_implements_both_exchange_and_public_exchange() {
835        let config = ExchangeConfig::default();
836        let binance = Binance::new(config).unwrap();
837
838        // Test that Binance can be used as both Exchange and PublicExchange
839        let exchange: &dyn Exchange = &binance;
840        let public_exchange: &dyn PublicExchange = &binance;
841
842        // Both should return the same values
843        assert_eq!(exchange.id(), public_exchange.id());
844        assert_eq!(exchange.name(), public_exchange.name());
845        assert_eq!(exchange.version(), public_exchange.version());
846        assert_eq!(exchange.certified(), public_exchange.certified());
847        assert_eq!(exchange.rate_limit(), public_exchange.rate_limit());
848        assert_eq!(exchange.has_websocket(), public_exchange.has_websocket());
849        assert_eq!(exchange.timeframes(), public_exchange.timeframes());
850    }
851
852    // ==================== Time Sync Helper Tests ====================
853
854    #[test]
855    fn test_is_timestamp_error_with_recv_window_message() {
856        let config = ExchangeConfig::default();
857        let binance = Binance::new(config).unwrap();
858
859        // Test with recvWindow error message
860        let err = ccxt_core::Error::exchange(
861            "-1021",
862            "Timestamp for this request is outside of the recvWindow",
863        );
864        assert!(
865            binance.is_timestamp_error(&err),
866            "Should detect recvWindow timestamp error"
867        );
868    }
869
870    #[test]
871    fn test_is_timestamp_error_with_ahead_message() {
872        let config = ExchangeConfig::default();
873        let binance = Binance::new(config).unwrap();
874
875        // Test with "ahead" error message
876        let err = ccxt_core::Error::exchange("-1021", "Timestamp is ahead of server time");
877        assert!(
878            binance.is_timestamp_error(&err),
879            "Should detect 'ahead' timestamp error"
880        );
881    }
882
883    #[test]
884    fn test_is_timestamp_error_with_behind_message() {
885        let config = ExchangeConfig::default();
886        let binance = Binance::new(config).unwrap();
887
888        // Test with "behind" error message
889        let err = ccxt_core::Error::exchange("-1021", "Timestamp is behind server time");
890        assert!(
891            binance.is_timestamp_error(&err),
892            "Should detect 'behind' timestamp error"
893        );
894    }
895
896    #[test]
897    fn test_is_timestamp_error_with_error_code_only() {
898        let config = ExchangeConfig::default();
899        let binance = Binance::new(config).unwrap();
900
901        // Test with error code -1021 in message
902        let err = ccxt_core::Error::exchange("-1021", "Some error message");
903        assert!(
904            binance.is_timestamp_error(&err),
905            "Should detect error code -1021"
906        );
907    }
908
909    #[test]
910    fn test_is_timestamp_error_non_timestamp_error() {
911        let config = ExchangeConfig::default();
912        let binance = Binance::new(config).unwrap();
913
914        // Test with non-timestamp error
915        let err = ccxt_core::Error::exchange("-1100", "Illegal characters found in parameter");
916        assert!(
917            !binance.is_timestamp_error(&err),
918            "Should not detect non-timestamp error"
919        );
920
921        // Test with authentication error
922        let err = ccxt_core::Error::authentication("Invalid API key");
923        assert!(
924            !binance.is_timestamp_error(&err),
925            "Should not detect authentication error as timestamp error"
926        );
927
928        // Test with network error
929        let err = ccxt_core::Error::network("Connection refused");
930        assert!(
931            !binance.is_timestamp_error(&err),
932            "Should not detect network error as timestamp error"
933        );
934    }
935
936    #[test]
937    fn test_is_timestamp_error_case_insensitive() {
938        let config = ExchangeConfig::default();
939        let binance = Binance::new(config).unwrap();
940
941        // Test case insensitivity
942        let err = ccxt_core::Error::exchange(
943            "-1021",
944            "TIMESTAMP for this request is outside of the RECVWINDOW",
945        );
946        assert!(
947            binance.is_timestamp_error(&err),
948            "Should detect timestamp error case-insensitively"
949        );
950    }
951
952    #[test]
953    fn test_time_sync_manager_accessible() {
954        let config = ExchangeConfig::default();
955        let binance = Binance::new(config).unwrap();
956
957        // Test that time_sync() returns a reference to the manager
958        let time_sync = binance.time_sync();
959        assert!(
960            !time_sync.is_initialized(),
961            "Time sync should not be initialized initially"
962        );
963        assert!(
964            time_sync.needs_resync(),
965            "Time sync should need resync initially"
966        );
967    }
968
969    #[test]
970    fn test_time_sync_manager_update_offset() {
971        let config = ExchangeConfig::default();
972        let binance = Binance::new(config).unwrap();
973
974        // Simulate updating the offset
975        let server_time = ccxt_core::time::TimestampUtils::now_ms() + 100;
976        binance.time_sync().update_offset(server_time);
977
978        assert!(
979            binance.time_sync().is_initialized(),
980            "Time sync should be initialized after update"
981        );
982        assert!(
983            !binance.time_sync().needs_resync(),
984            "Time sync should not need resync immediately after update"
985        );
986
987        // Offset should be approximately 100ms
988        let offset = binance.time_sync().get_offset();
989        assert!(
990            offset >= 90 && offset <= 110,
991            "Offset should be approximately 100ms, got {}",
992            offset
993        );
994    }
995
996    // ==================== Error Recovery Tests ====================
997
998    #[tokio::test]
999    async fn test_execute_signed_request_with_retry_success() {
1000        let config = ExchangeConfig::default();
1001        let binance = Binance::new(config).unwrap();
1002
1003        // Initialize time sync
1004        let server_time = ccxt_core::time::TimestampUtils::now_ms();
1005        binance.time_sync().update_offset(server_time);
1006
1007        // Test successful request (no retry needed)
1008        let result = binance
1009            .execute_signed_request_with_retry(|timestamp| async move {
1010                assert!(timestamp > 0, "Timestamp should be positive");
1011                Ok::<_, ccxt_core::Error>(42)
1012            })
1013            .await;
1014
1015        assert!(result.is_ok(), "Request should succeed");
1016        assert_eq!(result.unwrap(), 42);
1017    }
1018
1019    #[tokio::test]
1020    async fn test_execute_signed_request_with_retry_non_timestamp_error() {
1021        let config = ExchangeConfig::default();
1022        let binance = Binance::new(config).unwrap();
1023
1024        // Initialize time sync
1025        let server_time = ccxt_core::time::TimestampUtils::now_ms();
1026        binance.time_sync().update_offset(server_time);
1027
1028        // Test non-timestamp error (should not retry)
1029        let result = binance
1030            .execute_signed_request_with_retry(|_timestamp| async move {
1031                Err::<i32, _>(ccxt_core::Error::exchange("-1100", "Invalid parameter"))
1032            })
1033            .await;
1034
1035        assert!(result.is_err(), "Request should fail");
1036        let err = result.unwrap_err();
1037        assert!(
1038            err.to_string().contains("-1100"),
1039            "Error should contain original error code"
1040        );
1041    }
1042
1043    #[test]
1044    fn test_handle_timestamp_error_detection() {
1045        let config = ExchangeConfig::default();
1046        let binance = Binance::new(config).unwrap();
1047
1048        // Test various timestamp error formats
1049        let timestamp_errors = vec![
1050            ccxt_core::Error::exchange(
1051                "-1021",
1052                "Timestamp for this request is outside of the recvWindow",
1053            ),
1054            ccxt_core::Error::exchange("-1021", "Timestamp is ahead of server time"),
1055            ccxt_core::Error::exchange("-1021", "Timestamp is behind server time"),
1056            ccxt_core::Error::exchange("-1021", "Some error with timestamp and recvwindow"),
1057        ];
1058
1059        for err in timestamp_errors {
1060            assert!(
1061                binance.is_timestamp_error(&err),
1062                "Should detect timestamp error: {}",
1063                err
1064            );
1065        }
1066
1067        // Test non-timestamp errors
1068        let non_timestamp_errors = vec![
1069            ccxt_core::Error::exchange("-1100", "Invalid parameter"),
1070            ccxt_core::Error::exchange("-1000", "Unknown error"),
1071            ccxt_core::Error::authentication("Invalid API key"),
1072            ccxt_core::Error::network("Connection refused"),
1073            ccxt_core::Error::timeout("Request timed out"),
1074        ];
1075
1076        for err in non_timestamp_errors {
1077            assert!(
1078                !binance.is_timestamp_error(&err),
1079                "Should not detect as timestamp error: {}",
1080                err
1081            );
1082        }
1083    }
1084
1085    // ==================== Property-Based Tests ====================
1086
1087    mod property_tests {
1088        use super::*;
1089        use proptest::prelude::*;
1090
1091        // Strategy to generate various ExchangeConfig configurations
1092        fn arb_exchange_config() -> impl Strategy<Value = ExchangeConfig> {
1093            (
1094                prop::bool::ANY,                                                      // sandbox
1095                prop::option::of(any::<u64>().prop_map(|n| format!("key_{}", n))),    // api_key
1096                prop::option::of(any::<u64>().prop_map(|n| format!("secret_{}", n))), // secret
1097            )
1098                .prop_map(|(sandbox, api_key, secret)| ExchangeConfig {
1099                    sandbox,
1100                    api_key: api_key.map(ccxt_core::SecretString::new),
1101                    secret: secret.map(ccxt_core::SecretString::new),
1102                    ..Default::default()
1103                })
1104        }
1105
1106        proptest! {
1107            #![proptest_config(ProptestConfig::with_cases(100))]
1108
1109            /// **Feature: unified-exchange-trait, Property 8: Timeframes Non-Empty**
1110            ///
1111            /// *For any* exchange configuration, calling `timeframes()` should return
1112            /// a non-empty vector of valid `Timeframe` values.
1113            ///
1114            /// **Validates: Requirements 8.4**
1115            #[test]
1116            fn prop_timeframes_non_empty(config in arb_exchange_config()) {
1117                let binance = Binance::new(config).expect("Should create Binance instance");
1118                let exchange: &dyn Exchange = &binance;
1119
1120                let timeframes = exchange.timeframes();
1121
1122                // Property: timeframes should never be empty
1123                prop_assert!(!timeframes.is_empty(), "Timeframes should not be empty");
1124
1125                // Property: all timeframes should be valid (no duplicates)
1126                let mut seen = std::collections::HashSet::new();
1127                for tf in &timeframes {
1128                    prop_assert!(
1129                        seen.insert(*tf),
1130                        "Timeframes should not contain duplicates: {:?}",
1131                        tf
1132                    );
1133                }
1134
1135                // Property: should contain common timeframes
1136                prop_assert!(
1137                    timeframes.contains(&Timeframe::M1),
1138                    "Should contain 1-minute timeframe"
1139                );
1140                prop_assert!(
1141                    timeframes.contains(&Timeframe::H1),
1142                    "Should contain 1-hour timeframe"
1143                );
1144                prop_assert!(
1145                    timeframes.contains(&Timeframe::D1),
1146                    "Should contain 1-day timeframe"
1147                );
1148            }
1149
1150            /// **Feature: unified-exchange-trait, Property 7: Backward Compatibility**
1151            ///
1152            /// *For any* exchange configuration, metadata methods called through the Exchange trait
1153            /// should return the same values as calling them directly on Binance.
1154            ///
1155            /// **Validates: Requirements 3.2, 3.4**
1156            #[test]
1157            fn prop_backward_compatibility_metadata(config in arb_exchange_config()) {
1158                let binance = Binance::new(config).expect("Should create Binance instance");
1159
1160                // Get trait object reference
1161                let exchange: &dyn Exchange = &binance;
1162
1163                // Property: id() should be consistent
1164                prop_assert_eq!(
1165                    exchange.id(),
1166                    Binance::id(&binance),
1167                    "id() should be consistent between trait and direct call"
1168                );
1169
1170                // Property: name() should be consistent
1171                prop_assert_eq!(
1172                    exchange.name(),
1173                    Binance::name(&binance),
1174                    "name() should be consistent between trait and direct call"
1175                );
1176
1177                // Property: version() should be consistent
1178                prop_assert_eq!(
1179                    exchange.version(),
1180                    Binance::version(&binance),
1181                    "version() should be consistent between trait and direct call"
1182                );
1183
1184                // Property: certified() should be consistent
1185                prop_assert_eq!(
1186                    exchange.certified(),
1187                    Binance::certified(&binance),
1188                    "certified() should be consistent between trait and direct call"
1189                );
1190
1191                // Property: rate_limit() should be consistent
1192                prop_assert_eq!(
1193                    exchange.rate_limit(),
1194                    Binance::rate_limit(&binance),
1195                    "rate_limit() should be consistent between trait and direct call"
1196                );
1197
1198                // Property: capabilities should be consistent
1199                let trait_caps = exchange.capabilities();
1200                prop_assert!(trait_caps.fetch_markets(), "Should support fetch_markets");
1201                prop_assert!(trait_caps.fetch_ticker(), "Should support fetch_ticker");
1202                prop_assert!(trait_caps.websocket(), "Should support websocket");
1203            }
1204        }
1205    }
1206}