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    /// Get authenticator instance.
346    pub(crate) fn get_auth(&self) -> ccxt_core::Result<super::auth::BinanceAuth> {
347        let api_key = self
348            .base()
349            .config
350            .api_key
351            .as_ref()
352            .ok_or_else(|| ccxt_core::Error::authentication("API key is required"))?
353            .clone();
354
355        let secret = self
356            .base()
357            .config
358            .secret
359            .as_ref()
360            .ok_or_else(|| ccxt_core::Error::authentication("Secret is required"))?
361            .clone();
362
363        Ok(super::auth::BinanceAuth::new(
364            api_key.expose_secret(),
365            secret.expose_secret(),
366        ))
367    }
368
369    // ==================== Time Sync Helper Methods ====================
370
371    /// Gets the server timestamp for signing requests.
372    ///
373    /// This method uses the cached time offset if available, otherwise fetches
374    /// the server time directly. It also triggers a background resync if needed.
375    ///
376    /// # Optimization
377    ///
378    /// By caching the time offset between local and server time, this method
379    /// reduces the number of network round-trips for signed API requests from 2 to 1.
380    /// Instead of fetching server time before every signed request, we calculate
381    /// the server timestamp locally using: `server_timestamp = local_time + cached_offset`
382    ///
383    /// # Returns
384    ///
385    /// Returns the estimated server timestamp in milliseconds.
386    ///
387    /// # Errors
388    ///
389    /// Returns an error if:
390    /// - The time sync manager is not initialized and the server time fetch fails
391    /// - Network errors occur during time synchronization
392    ///
393    /// # Example
394    ///
395    /// ```no_run
396    /// # use ccxt_exchanges::binance::Binance;
397    /// # use ccxt_core::ExchangeConfig;
398    /// # async fn example() -> ccxt_core::Result<()> {
399    /// let binance = Binance::new(ExchangeConfig::default())?;
400    ///
401    /// // Get timestamp for signing (uses cached offset if available)
402    /// let timestamp = binance.get_signing_timestamp().await?;
403    /// println!("Server timestamp: {}", timestamp);
404    /// # Ok(())
405    /// # }
406    /// ```
407    ///
408    /// _Requirements: 1.2, 2.3, 6.4_
409    pub async fn get_signing_timestamp(&self) -> ccxt_core::Result<i64> {
410        // Check if we need to sync (not initialized or sync interval elapsed)
411        if self.time_sync.needs_resync() {
412            // Attempt to sync time
413            if let Err(e) = self.sync_time().await {
414                // If sync fails and we're not initialized, we must fail
415                if !self.time_sync.is_initialized() {
416                    return Err(e);
417                }
418                // If sync fails but we have a cached offset, log and continue
419                tracing::warn!(
420                    error = %e,
421                    "Time sync failed, using cached offset"
422                );
423            }
424        }
425
426        // If still not initialized after sync attempt, fall back to direct fetch
427        if !self.time_sync.is_initialized() {
428            return self.fetch_time_raw().await;
429        }
430
431        // Return the estimated server timestamp using cached offset
432        Ok(self.time_sync.get_server_timestamp())
433    }
434
435    /// Synchronizes local time with Binance server time.
436    ///
437    /// This method fetches the current server time from Binance and updates
438    /// the cached time offset. The offset is calculated as:
439    /// `offset = server_time - local_time`
440    ///
441    /// # When to Use
442    ///
443    /// - Called automatically by `get_signing_timestamp()` when resync is needed
444    /// - Can be called manually to force a time synchronization
445    /// - Useful after receiving timestamp-related errors from the API
446    ///
447    /// # Returns
448    ///
449    /// Returns `Ok(())` on successful synchronization.
450    ///
451    /// # Errors
452    ///
453    /// Returns an error if the server time fetch fails due to network issues.
454    ///
455    /// # Example
456    ///
457    /// ```no_run
458    /// # use ccxt_exchanges::binance::Binance;
459    /// # use ccxt_core::ExchangeConfig;
460    /// # async fn example() -> ccxt_core::Result<()> {
461    /// let binance = Binance::new(ExchangeConfig::default())?;
462    ///
463    /// // Manually sync time with server
464    /// binance.sync_time().await?;
465    ///
466    /// // Check the current offset
467    /// let offset = binance.time_sync().get_offset();
468    /// println!("Time offset: {}ms", offset);
469    /// # Ok(())
470    /// # }
471    /// ```
472    ///
473    /// _Requirements: 2.1, 2.2, 6.3_
474    pub async fn sync_time(&self) -> ccxt_core::Result<()> {
475        let server_time = self.fetch_time_raw().await?;
476        self.time_sync.update_offset(server_time);
477        tracing::debug!(
478            server_time = server_time,
479            offset = self.time_sync.get_offset(),
480            "Time synchronized with Binance server"
481        );
482        Ok(())
483    }
484
485    /// Checks if an error is related to timestamp validation.
486    ///
487    /// Binance returns specific error codes and messages when the request
488    /// timestamp is outside the acceptable window (recvWindow). This method
489    /// detects such errors to enable automatic retry with a fresh timestamp.
490    ///
491    /// # Binance Timestamp Error Codes
492    ///
493    /// | Error Code | Message | Meaning |
494    /// |------------|---------|---------|
495    /// | -1021 | "Timestamp for this request is outside of the recvWindow" | Timestamp too old or too new |
496    /// | -1022 | "Signature for this request is not valid" | May be caused by wrong timestamp |
497    ///
498    /// # Arguments
499    ///
500    /// * `error` - The error to check
501    ///
502    /// # Returns
503    ///
504    /// Returns `true` if the error is related to timestamp validation, `false` otherwise.
505    ///
506    /// # Example
507    ///
508    /// ```rust
509    /// use ccxt_exchanges::binance::Binance;
510    /// use ccxt_core::{ExchangeConfig, Error};
511    ///
512    /// let binance = Binance::new(ExchangeConfig::default()).unwrap();
513    ///
514    /// // Simulate a timestamp error
515    /// let err = Error::exchange("-1021", "Timestamp for this request is outside of the recvWindow");
516    /// assert!(binance.is_timestamp_error(&err));
517    ///
518    /// // Non-timestamp error
519    /// let err = Error::exchange("-1100", "Illegal characters found in parameter");
520    /// assert!(!binance.is_timestamp_error(&err));
521    /// ```
522    ///
523    /// _Requirements: 4.1_
524    pub fn is_timestamp_error(&self, error: &ccxt_core::Error) -> bool {
525        let error_str = error.to_string().to_lowercase();
526
527        // Check for timestamp-related keywords in the error message
528        let has_timestamp_keyword = error_str.contains("timestamp");
529        let has_recv_window = error_str.contains("recvwindow");
530        let has_ahead = error_str.contains("ahead");
531        let has_behind = error_str.contains("behind");
532
533        // Timestamp error if it mentions timestamp AND one of the time-related issues
534        if has_timestamp_keyword && (has_recv_window || has_ahead || has_behind) {
535            return true;
536        }
537
538        // Also check for specific Binance error codes
539        // -1021: Timestamp for this request is outside of the recvWindow
540        // -1022: Signature for this request is not valid (may be timestamp-related)
541        if error_str.contains("-1021") {
542            return true;
543        }
544
545        // Check for error code in Exchange variant
546        if let ccxt_core::Error::Exchange(details) = error {
547            if details.code == "-1021" {
548                return true;
549            }
550        }
551
552        false
553    }
554
555    /// Executes a signed request with automatic timestamp error recovery.
556    ///
557    /// This method wraps a signed request operation and automatically handles
558    /// timestamp-related errors by resyncing time and retrying the request once.
559    ///
560    /// # Error Recovery Flow
561    ///
562    /// 1. Execute the request with the current timestamp
563    /// 2. If the request fails with a timestamp error:
564    ///    a. Resync time with the server
565    ///    b. Retry the request with a fresh timestamp
566    /// 3. If the retry also fails, return the error
567    ///
568    /// # Retry Limit
569    ///
570    /// To prevent infinite retry loops, this method limits automatic retries to
571    /// exactly 1 retry per request. If the retry also fails, the error is returned.
572    ///
573    /// # Type Parameters
574    ///
575    /// * `T` - The return type of the request
576    /// * `F` - The async function that performs the signed request
577    ///
578    /// # Arguments
579    ///
580    /// * `request_fn` - An async function that takes a timestamp (i64) and returns
581    ///   a `Result<T>`. This function should perform the actual signed API request.
582    ///
583    /// # Returns
584    ///
585    /// Returns `Ok(T)` on success, or `Err` if the request fails after retry.
586    ///
587    /// # Example
588    ///
589    /// ```no_run
590    /// # use ccxt_exchanges::binance::Binance;
591    /// # use ccxt_core::ExchangeConfig;
592    /// # async fn example() -> ccxt_core::Result<()> {
593    /// let binance = Binance::new(ExchangeConfig::default())?;
594    ///
595    /// // Execute a signed request with automatic retry on timestamp error
596    /// let result = binance.execute_signed_request_with_retry(|timestamp| {
597    ///     Box::pin(async move {
598    ///         // Perform signed request using the provided timestamp
599    ///         // ... actual request logic here ...
600    ///         Ok(())
601    ///     })
602    /// }).await?;
603    /// # Ok(())
604    /// # }
605    /// ```
606    ///
607    /// _Requirements: 4.1, 4.2, 4.3, 4.4_
608    pub async fn execute_signed_request_with_retry<T, F, Fut>(
609        &self,
610        request_fn: F,
611    ) -> ccxt_core::Result<T>
612    where
613        F: Fn(i64) -> Fut,
614        Fut: std::future::Future<Output = ccxt_core::Result<T>>,
615    {
616        // Get the initial timestamp
617        let timestamp = self.get_signing_timestamp().await?;
618
619        // Execute the request
620        match request_fn(timestamp).await {
621            Ok(result) => Ok(result),
622            Err(e) if self.is_timestamp_error(&e) => {
623                // Log the timestamp error and retry
624                tracing::warn!(
625                    error = %e,
626                    "Timestamp error detected, resyncing time and retrying request"
627                );
628
629                // Force resync time with server
630                if let Err(sync_err) = self.sync_time().await {
631                    tracing::error!(
632                        error = %sync_err,
633                        "Failed to resync time after timestamp error"
634                    );
635                    // Return the original error if sync fails
636                    return Err(e);
637                }
638
639                // Get fresh timestamp after resync
640                let new_timestamp = self.time_sync.get_server_timestamp();
641
642                tracing::debug!(
643                    old_timestamp = timestamp,
644                    new_timestamp = new_timestamp,
645                    offset = self.time_sync.get_offset(),
646                    "Retrying request with fresh timestamp"
647                );
648
649                // Retry the request once with the new timestamp
650                request_fn(new_timestamp).await
651            }
652            Err(e) => Err(e),
653        }
654    }
655
656    /// Helper method to handle timestamp errors and trigger resync.
657    ///
658    /// This method checks if an error is a timestamp error and, if so,
659    /// triggers a time resync. It's useful for manual error handling
660    /// when you want more control over the retry logic.
661    ///
662    /// # Arguments
663    ///
664    /// * `error` - The error to check
665    ///
666    /// # Returns
667    ///
668    /// Returns `true` if the error was a timestamp error and resync was triggered,
669    /// `false` otherwise.
670    ///
671    /// # Example
672    ///
673    /// ```no_run
674    /// # use ccxt_exchanges::binance::Binance;
675    /// # use ccxt_core::ExchangeConfig;
676    /// # async fn example() -> ccxt_core::Result<()> {
677    /// let binance = Binance::new(ExchangeConfig::default())?;
678    ///
679    /// // Manual error handling with resync
680    /// let result = some_signed_request().await;
681    /// if let Err(ref e) = result {
682    ///     if binance.handle_timestamp_error_and_resync(e).await {
683    ///         // Timestamp error detected and resync triggered
684    ///         // You can now retry the request
685    ///     }
686    /// }
687    /// # async fn some_signed_request() -> ccxt_core::Result<()> { Ok(()) }
688    /// # Ok(())
689    /// # }
690    /// ```
691    ///
692    /// _Requirements: 4.1, 4.2_
693    pub async fn handle_timestamp_error_and_resync(&self, error: &ccxt_core::Error) -> bool {
694        if self.is_timestamp_error(error) {
695            tracing::warn!(
696                error = %error,
697                "Timestamp error detected, triggering time resync"
698            );
699
700            if let Err(sync_err) = self.sync_time().await {
701                tracing::error!(
702                    error = %sync_err,
703                    "Failed to resync time after timestamp error"
704                );
705                return false;
706            }
707
708            tracing::debug!(
709                offset = self.time_sync.get_offset(),
710                "Time resync completed after timestamp error"
711            );
712
713            return true;
714        }
715
716        false
717    }
718}
719
720// Implement HasHttpClient trait for generic SignedRequestBuilder support
721impl ccxt_core::signed_request::HasHttpClient for Binance {
722    fn http_client(&self) -> &ccxt_core::http_client::HttpClient {
723        &self.base().http_client
724    }
725
726    fn base_url(&self) -> &'static str {
727        // Return empty string since Binance uses full URLs in endpoints
728        ""
729    }
730}
731
732#[cfg(test)]
733mod tests {
734    #![allow(clippy::disallowed_methods)]
735    use super::*;
736    use ccxt_core::ExchangeConfig;
737
738    #[test]
739    fn test_binance_exchange_trait_metadata() {
740        let config = ExchangeConfig::default();
741        let binance = Binance::new(config).unwrap();
742
743        // Test metadata methods via Exchange trait
744        let exchange: &dyn Exchange = &binance;
745
746        assert_eq!(exchange.id(), "binance");
747        assert_eq!(exchange.name(), "Binance");
748        assert_eq!(exchange.version(), "v3");
749        assert!(exchange.certified());
750        assert!(exchange.has_websocket());
751    }
752
753    #[test]
754    fn test_binance_exchange_trait_capabilities() {
755        let config = ExchangeConfig::default();
756        let binance = Binance::new(config).unwrap();
757
758        let exchange: &dyn Exchange = &binance;
759        let caps = exchange.capabilities();
760
761        assert!(caps.fetch_markets());
762        assert!(caps.fetch_ticker());
763        assert!(caps.create_order());
764        assert!(caps.websocket());
765        assert!(!caps.edit_order()); // Binance doesn't support order editing
766    }
767
768    #[test]
769    fn test_binance_exchange_trait_timeframes() {
770        let config = ExchangeConfig::default();
771        let binance = Binance::new(config).unwrap();
772
773        let exchange: &dyn Exchange = &binance;
774        let timeframes = exchange.timeframes();
775
776        assert!(!timeframes.is_empty());
777        assert!(timeframes.contains(&Timeframe::M1));
778        assert!(timeframes.contains(&Timeframe::H1));
779        assert!(timeframes.contains(&Timeframe::D1));
780    }
781
782    #[test]
783    fn test_binance_exchange_trait_object_safety() {
784        let config = ExchangeConfig::default();
785        let binance = Binance::new(config).unwrap();
786
787        // Test that we can create a trait object
788        let exchange: Box<dyn Exchange> = Box::new(binance);
789
790        assert_eq!(exchange.id(), "binance");
791        assert_eq!(exchange.rate_limit(), 50);
792    }
793
794    #[test]
795    fn test_binance_exchange_ext_trait() {
796        let config = ExchangeConfig::default();
797        let binance = Binance::new(config).unwrap();
798
799        // Test ExchangeExt methods
800        let exchange: &dyn Exchange = &binance;
801
802        // Binance supports all capabilities
803        assert!(
804            exchange.supports_market_data(),
805            "Binance should support market data"
806        );
807        assert!(
808            exchange.supports_trading(),
809            "Binance should support trading"
810        );
811        assert!(
812            exchange.supports_account(),
813            "Binance should support account operations"
814        );
815        assert!(
816            exchange.supports_margin(),
817            "Binance should support margin operations"
818        );
819        assert!(
820            exchange.supports_funding(),
821            "Binance should support funding operations"
822        );
823    }
824
825    #[test]
826    fn test_binance_implements_both_exchange_and_public_exchange() {
827        let config = ExchangeConfig::default();
828        let binance = Binance::new(config).unwrap();
829
830        // Test that Binance can be used as both Exchange and PublicExchange
831        let exchange: &dyn Exchange = &binance;
832        let public_exchange: &dyn PublicExchange = &binance;
833
834        // Both should return the same values
835        assert_eq!(exchange.id(), public_exchange.id());
836        assert_eq!(exchange.name(), public_exchange.name());
837        assert_eq!(exchange.version(), public_exchange.version());
838        assert_eq!(exchange.certified(), public_exchange.certified());
839        assert_eq!(exchange.rate_limit(), public_exchange.rate_limit());
840        assert_eq!(exchange.has_websocket(), public_exchange.has_websocket());
841        assert_eq!(exchange.timeframes(), public_exchange.timeframes());
842    }
843
844    // ==================== Time Sync Helper Tests ====================
845
846    #[test]
847    fn test_is_timestamp_error_with_recv_window_message() {
848        let config = ExchangeConfig::default();
849        let binance = Binance::new(config).unwrap();
850
851        // Test with recvWindow error message
852        let err = ccxt_core::Error::exchange(
853            "-1021",
854            "Timestamp for this request is outside of the recvWindow",
855        );
856        assert!(
857            binance.is_timestamp_error(&err),
858            "Should detect recvWindow timestamp error"
859        );
860    }
861
862    #[test]
863    fn test_is_timestamp_error_with_ahead_message() {
864        let config = ExchangeConfig::default();
865        let binance = Binance::new(config).unwrap();
866
867        // Test with "ahead" error message
868        let err = ccxt_core::Error::exchange("-1021", "Timestamp is ahead of server time");
869        assert!(
870            binance.is_timestamp_error(&err),
871            "Should detect 'ahead' timestamp error"
872        );
873    }
874
875    #[test]
876    fn test_is_timestamp_error_with_behind_message() {
877        let config = ExchangeConfig::default();
878        let binance = Binance::new(config).unwrap();
879
880        // Test with "behind" error message
881        let err = ccxt_core::Error::exchange("-1021", "Timestamp is behind server time");
882        assert!(
883            binance.is_timestamp_error(&err),
884            "Should detect 'behind' timestamp error"
885        );
886    }
887
888    #[test]
889    fn test_is_timestamp_error_with_error_code_only() {
890        let config = ExchangeConfig::default();
891        let binance = Binance::new(config).unwrap();
892
893        // Test with error code -1021 in message
894        let err = ccxt_core::Error::exchange("-1021", "Some error message");
895        assert!(
896            binance.is_timestamp_error(&err),
897            "Should detect error code -1021"
898        );
899    }
900
901    #[test]
902    fn test_is_timestamp_error_non_timestamp_error() {
903        let config = ExchangeConfig::default();
904        let binance = Binance::new(config).unwrap();
905
906        // Test with non-timestamp error
907        let err = ccxt_core::Error::exchange("-1100", "Illegal characters found in parameter");
908        assert!(
909            !binance.is_timestamp_error(&err),
910            "Should not detect non-timestamp error"
911        );
912
913        // Test with authentication error
914        let err = ccxt_core::Error::authentication("Invalid API key");
915        assert!(
916            !binance.is_timestamp_error(&err),
917            "Should not detect authentication error as timestamp error"
918        );
919
920        // Test with network error
921        let err = ccxt_core::Error::network("Connection refused");
922        assert!(
923            !binance.is_timestamp_error(&err),
924            "Should not detect network error as timestamp error"
925        );
926    }
927
928    #[test]
929    fn test_is_timestamp_error_case_insensitive() {
930        let config = ExchangeConfig::default();
931        let binance = Binance::new(config).unwrap();
932
933        // Test case insensitivity
934        let err = ccxt_core::Error::exchange(
935            "-1021",
936            "TIMESTAMP for this request is outside of the RECVWINDOW",
937        );
938        assert!(
939            binance.is_timestamp_error(&err),
940            "Should detect timestamp error case-insensitively"
941        );
942    }
943
944    #[test]
945    fn test_time_sync_manager_accessible() {
946        let config = ExchangeConfig::default();
947        let binance = Binance::new(config).unwrap();
948
949        // Test that time_sync() returns a reference to the manager
950        let time_sync = binance.time_sync();
951        assert!(
952            !time_sync.is_initialized(),
953            "Time sync should not be initialized initially"
954        );
955        assert!(
956            time_sync.needs_resync(),
957            "Time sync should need resync initially"
958        );
959    }
960
961    #[test]
962    fn test_time_sync_manager_update_offset() {
963        let config = ExchangeConfig::default();
964        let binance = Binance::new(config).unwrap();
965
966        // Simulate updating the offset
967        let server_time = ccxt_core::time::TimestampUtils::now_ms() + 100;
968        binance.time_sync().update_offset(server_time);
969
970        assert!(
971            binance.time_sync().is_initialized(),
972            "Time sync should be initialized after update"
973        );
974        assert!(
975            !binance.time_sync().needs_resync(),
976            "Time sync should not need resync immediately after update"
977        );
978
979        // Offset should be approximately 100ms
980        let offset = binance.time_sync().get_offset();
981        assert!(
982            offset >= 90 && offset <= 110,
983            "Offset should be approximately 100ms, got {}",
984            offset
985        );
986    }
987
988    // ==================== Error Recovery Tests ====================
989
990    #[tokio::test]
991    async fn test_execute_signed_request_with_retry_success() {
992        let config = ExchangeConfig::default();
993        let binance = Binance::new(config).unwrap();
994
995        // Initialize time sync
996        let server_time = ccxt_core::time::TimestampUtils::now_ms();
997        binance.time_sync().update_offset(server_time);
998
999        // Test successful request (no retry needed)
1000        let result = binance
1001            .execute_signed_request_with_retry(|timestamp| async move {
1002                assert!(timestamp > 0, "Timestamp should be positive");
1003                Ok::<_, ccxt_core::Error>(42)
1004            })
1005            .await;
1006
1007        assert!(result.is_ok(), "Request should succeed");
1008        assert_eq!(result.unwrap(), 42);
1009    }
1010
1011    #[tokio::test]
1012    async fn test_execute_signed_request_with_retry_non_timestamp_error() {
1013        let config = ExchangeConfig::default();
1014        let binance = Binance::new(config).unwrap();
1015
1016        // Initialize time sync
1017        let server_time = ccxt_core::time::TimestampUtils::now_ms();
1018        binance.time_sync().update_offset(server_time);
1019
1020        // Test non-timestamp error (should not retry)
1021        let result = binance
1022            .execute_signed_request_with_retry(|_timestamp| async move {
1023                Err::<i32, _>(ccxt_core::Error::exchange("-1100", "Invalid parameter"))
1024            })
1025            .await;
1026
1027        assert!(result.is_err(), "Request should fail");
1028        let err = result.unwrap_err();
1029        assert!(
1030            err.to_string().contains("-1100"),
1031            "Error should contain original error code"
1032        );
1033    }
1034
1035    #[test]
1036    fn test_handle_timestamp_error_detection() {
1037        let config = ExchangeConfig::default();
1038        let binance = Binance::new(config).unwrap();
1039
1040        // Test various timestamp error formats
1041        let timestamp_errors = vec![
1042            ccxt_core::Error::exchange(
1043                "-1021",
1044                "Timestamp for this request is outside of the recvWindow",
1045            ),
1046            ccxt_core::Error::exchange("-1021", "Timestamp is ahead of server time"),
1047            ccxt_core::Error::exchange("-1021", "Timestamp is behind server time"),
1048            ccxt_core::Error::exchange("-1021", "Some error with timestamp and recvwindow"),
1049        ];
1050
1051        for err in timestamp_errors {
1052            assert!(
1053                binance.is_timestamp_error(&err),
1054                "Should detect timestamp error: {}",
1055                err
1056            );
1057        }
1058
1059        // Test non-timestamp errors
1060        let non_timestamp_errors = vec![
1061            ccxt_core::Error::exchange("-1100", "Invalid parameter"),
1062            ccxt_core::Error::exchange("-1000", "Unknown error"),
1063            ccxt_core::Error::authentication("Invalid API key"),
1064            ccxt_core::Error::network("Connection refused"),
1065            ccxt_core::Error::timeout("Request timed out"),
1066        ];
1067
1068        for err in non_timestamp_errors {
1069            assert!(
1070                !binance.is_timestamp_error(&err),
1071                "Should not detect as timestamp error: {}",
1072                err
1073            );
1074        }
1075    }
1076
1077    // ==================== Property-Based Tests ====================
1078
1079    mod property_tests {
1080        use super::*;
1081        use proptest::prelude::*;
1082
1083        // Strategy to generate various ExchangeConfig configurations
1084        fn arb_exchange_config() -> impl Strategy<Value = ExchangeConfig> {
1085            (
1086                prop::bool::ANY,                                                      // sandbox
1087                prop::option::of(any::<u64>().prop_map(|n| format!("key_{}", n))),    // api_key
1088                prop::option::of(any::<u64>().prop_map(|n| format!("secret_{}", n))), // secret
1089            )
1090                .prop_map(|(sandbox, api_key, secret)| ExchangeConfig {
1091                    sandbox,
1092                    api_key: api_key.map(ccxt_core::SecretString::new),
1093                    secret: secret.map(ccxt_core::SecretString::new),
1094                    ..Default::default()
1095                })
1096        }
1097
1098        proptest! {
1099            #![proptest_config(ProptestConfig::with_cases(100))]
1100
1101            /// **Feature: unified-exchange-trait, Property 8: Timeframes Non-Empty**
1102            ///
1103            /// *For any* exchange configuration, calling `timeframes()` should return
1104            /// a non-empty vector of valid `Timeframe` values.
1105            ///
1106            /// **Validates: Requirements 8.4**
1107            #[test]
1108            fn prop_timeframes_non_empty(config in arb_exchange_config()) {
1109                let binance = Binance::new(config).expect("Should create Binance instance");
1110                let exchange: &dyn Exchange = &binance;
1111
1112                let timeframes = exchange.timeframes();
1113
1114                // Property: timeframes should never be empty
1115                prop_assert!(!timeframes.is_empty(), "Timeframes should not be empty");
1116
1117                // Property: all timeframes should be valid (no duplicates)
1118                let mut seen = std::collections::HashSet::new();
1119                for tf in &timeframes {
1120                    prop_assert!(
1121                        seen.insert(*tf),
1122                        "Timeframes should not contain duplicates: {:?}",
1123                        tf
1124                    );
1125                }
1126
1127                // Property: should contain common timeframes
1128                prop_assert!(
1129                    timeframes.contains(&Timeframe::M1),
1130                    "Should contain 1-minute timeframe"
1131                );
1132                prop_assert!(
1133                    timeframes.contains(&Timeframe::H1),
1134                    "Should contain 1-hour timeframe"
1135                );
1136                prop_assert!(
1137                    timeframes.contains(&Timeframe::D1),
1138                    "Should contain 1-day timeframe"
1139                );
1140            }
1141
1142            /// **Feature: unified-exchange-trait, Property 7: Backward Compatibility**
1143            ///
1144            /// *For any* exchange configuration, metadata methods called through the Exchange trait
1145            /// should return the same values as calling them directly on Binance.
1146            ///
1147            /// **Validates: Requirements 3.2, 3.4**
1148            #[test]
1149            fn prop_backward_compatibility_metadata(config in arb_exchange_config()) {
1150                let binance = Binance::new(config).expect("Should create Binance instance");
1151
1152                // Get trait object reference
1153                let exchange: &dyn Exchange = &binance;
1154
1155                // Property: id() should be consistent
1156                prop_assert_eq!(
1157                    exchange.id(),
1158                    Binance::id(&binance),
1159                    "id() should be consistent between trait and direct call"
1160                );
1161
1162                // Property: name() should be consistent
1163                prop_assert_eq!(
1164                    exchange.name(),
1165                    Binance::name(&binance),
1166                    "name() should be consistent between trait and direct call"
1167                );
1168
1169                // Property: version() should be consistent
1170                prop_assert_eq!(
1171                    exchange.version(),
1172                    Binance::version(&binance),
1173                    "version() should be consistent between trait and direct call"
1174                );
1175
1176                // Property: certified() should be consistent
1177                prop_assert_eq!(
1178                    exchange.certified(),
1179                    Binance::certified(&binance),
1180                    "certified() should be consistent between trait and direct call"
1181                );
1182
1183                // Property: rate_limit() should be consistent
1184                prop_assert_eq!(
1185                    exchange.rate_limit(),
1186                    Binance::rate_limit(&binance),
1187                    "rate_limit() should be consistent between trait and direct call"
1188                );
1189
1190                // Property: capabilities should be consistent
1191                let trait_caps = exchange.capabilities();
1192                prop_assert!(trait_caps.fetch_markets(), "Should support fetch_markets");
1193                prop_assert!(trait_caps.fetch_ticker(), "Should support fetch_ticker");
1194                prop_assert!(trait_caps.websocket(), "Should support websocket");
1195            }
1196        }
1197    }
1198}