Skip to main content

ccxt_exchanges/binance/
error.rs

1//! Binance-specific error types.
2//!
3//! This module provides module-specific error types for Binance operations.
4//! These errors can be converted to the core `Error` type while preserving
5//! structured information for downcast capability.
6//!
7//! # Error Types
8//!
9//! - [`BinanceApiError`]: REST API errors with Binance error codes
10//! - [`BinanceWsError`]: WebSocket-specific errors
11//!
12//! # Example
13//!
14//! ```rust
15//! use ccxt_exchanges::binance::error::{BinanceApiError, BinanceWsError};
16//! use ccxt_core::error::Error as CoreError;
17//!
18//! // Create a Binance API error
19//! let api_err = BinanceApiError::new(-1121, "Invalid symbol.");
20//! let core_err: CoreError = api_err.into();
21//!
22//! // Create a Binance WebSocket error
23//! let ws_err = BinanceWsError::subscription_failed("btcusdt@ticker", "Invalid stream");
24//! let core_err: CoreError = ws_err.into();
25//! ```
26
27use ccxt_core::error::Error as CoreError;
28use thiserror::Error;
29
30#[cfg(test)]
31use std::error::Error as StdError;
32
33/// Binance REST API error.
34///
35/// Represents errors returned by the Binance REST API in the format:
36/// ```json
37/// {"code": -1121, "msg": "Invalid symbol."}
38/// ```
39///
40/// This error type preserves the original Binance error code and message,
41/// and can be converted to the appropriate `CoreError` variant based on
42/// the error code.
43#[derive(Error, Debug, Clone)]
44#[error("Binance API error {code}: {msg}")]
45pub struct BinanceApiError {
46    /// Binance error code (negative integer)
47    pub code: i32,
48    /// Error message from Binance
49    pub msg: String,
50}
51
52impl BinanceApiError {
53    /// Creates a new Binance API error.
54    ///
55    /// # Arguments
56    ///
57    /// * `code` - The Binance error code (typically negative)
58    /// * `msg` - The error message from Binance
59    ///
60    /// # Example
61    ///
62    /// ```rust
63    /// use ccxt_exchanges::binance::error::BinanceApiError;
64    ///
65    /// let err = BinanceApiError::new(-1121, "Invalid symbol.");
66    /// assert_eq!(err.code, -1121);
67    /// ```
68    pub fn new(code: i32, msg: impl Into<String>) -> Self {
69        Self {
70            code,
71            msg: msg.into(),
72        }
73    }
74
75    /// Parses a Binance API error from JSON response.
76    ///
77    /// # Arguments
78    ///
79    /// * `json` - The JSON response from Binance API
80    ///
81    /// # Returns
82    ///
83    /// Returns `Some(BinanceApiError)` if the JSON contains `code` and `msg` fields,
84    /// `None` otherwise.
85    ///
86    /// # Example
87    ///
88    /// ```rust
89    /// use ccxt_exchanges::binance::error::BinanceApiError;
90    /// use serde_json::json;
91    ///
92    /// let response = json!({"code": -1121, "msg": "Invalid symbol."});
93    /// let err = BinanceApiError::from_json(&response);
94    /// assert!(err.is_some());
95    /// assert_eq!(err.unwrap().code, -1121);
96    /// ```
97    pub fn from_json(json: &serde_json::Value) -> Option<Self> {
98        let code = json.get("code")?.as_i64()? as i32;
99        let msg = json.get("msg")?.as_str()?.to_string();
100        Some(Self { code, msg })
101    }
102
103    /// Returns true if this is a rate limit error.
104    ///
105    /// Rate limit errors have codes:
106    /// - -1003: Too many requests
107    /// - -1015: Too many orders
108    pub fn is_rate_limit(&self) -> bool {
109        matches!(self.code, -1003 | -1015)
110    }
111
112    /// Returns true if this is an IP ban error.
113    ///
114    /// IP ban errors have code -1003 with specific messages or HTTP 418 status.
115    pub fn is_ip_banned(&self) -> bool {
116        self.code == -1003 && self.msg.contains("banned")
117    }
118
119    /// Returns true if this is an authentication error.
120    pub fn is_auth_error(&self) -> bool {
121        matches!(self.code, -2014 | -2015 | -1022)
122    }
123
124    /// Returns true if this is an invalid symbol error.
125    pub fn is_invalid_symbol(&self) -> bool {
126        self.code == -1121
127    }
128
129    /// Returns true if this is an insufficient balance error.
130    pub fn is_insufficient_balance(&self) -> bool {
131        matches!(self.code, -2010 | -2011)
132    }
133
134    /// Returns true if this is an order not found error.
135    pub fn is_order_not_found(&self) -> bool {
136        self.code == -2013
137    }
138
139    /// Converts this error to the appropriate `CoreError` variant.
140    ///
141    /// The mapping follows these rules:
142    /// - Authentication errors (-2014, -2015, -1022) → `CoreError::Authentication`
143    /// - Rate limit errors (-1003, -1015) → `CoreError::RateLimit`
144    /// - Invalid symbol (-1121) → `CoreError::BadSymbol`
145    /// - Insufficient balance (-2010, -2011) → `CoreError::InsufficientBalance`
146    /// - Order not found (-2013) → `CoreError::OrderNotFound`
147    /// - Invalid order errors → `CoreError::InvalidOrder`
148    /// - Network errors (-1001) → `CoreError::Network`
149    /// - Other errors → `CoreError::Exchange`
150    pub fn to_core_error(&self) -> CoreError {
151        use std::borrow::Cow;
152
153        match self.code {
154            // Authentication errors
155            -2014 | -2015 | -1022 => CoreError::authentication(self.msg.clone()),
156            // Rate limit errors
157            -1003 | -1015 => CoreError::rate_limit(self.msg.clone(), None),
158            // Invalid symbol
159            -1121 => CoreError::bad_symbol(&self.msg),
160            // Insufficient balance
161            -2010 | -2011 => CoreError::insufficient_balance(self.msg.clone()),
162            // Order not found
163            -2013 => CoreError::OrderNotFound(Cow::Owned(self.msg.clone())),
164            // Invalid order (various)
165            -1102 | -1106 | -1111 | -1112 | -1114 | -1115 | -1116 | -1117 | -1118 => {
166                CoreError::InvalidOrder(Cow::Owned(self.msg.clone()))
167            }
168            // Network/timeout errors
169            -1001 => CoreError::network(&self.msg),
170            // All other errors
171            _ => CoreError::exchange(self.code.to_string(), &self.msg),
172        }
173    }
174}
175
176impl From<BinanceApiError> for CoreError {
177    fn from(e: BinanceApiError) -> Self {
178        e.to_core_error()
179    }
180}
181
182/// Binance WebSocket specific errors.
183///
184/// Implements `StdError + Send + Sync` for use with `CoreError::WebSocket`.
185/// Uses `#[non_exhaustive]` to allow adding variants in future versions.
186///
187/// # Variants
188///
189/// - `MissingStream`: Stream name missing in WebSocket message
190/// - `UnsupportedEvent`: Received an unsupported event type
191/// - `SubscriptionFailed`: Stream subscription failed
192/// - `Connection`: WebSocket connection error
193/// - `Core`: Passthrough for core errors
194#[derive(Error, Debug)]
195#[non_exhaustive]
196pub enum BinanceWsError {
197    /// Stream name missing in message.
198    ///
199    /// This occurs when a WebSocket message doesn't contain the expected
200    /// stream identifier, making it impossible to route the message.
201    #[error("Stream name missing in message: {raw}")]
202    MissingStream {
203        /// The raw message content (truncated for display)
204        raw: String,
205    },
206
207    /// Unsupported event type.
208    ///
209    /// This occurs when the WebSocket receives an event type that
210    /// is not handled by the current implementation.
211    #[error("Unsupported event type: {event}")]
212    UnsupportedEvent {
213        /// The event type that was not recognized
214        event: String,
215    },
216
217    /// Subscription failed.
218    ///
219    /// This occurs when a stream subscription request is rejected
220    /// by the Binance WebSocket server.
221    #[error("Subscription failed for {stream}: {reason}")]
222    SubscriptionFailed {
223        /// The stream name that failed to subscribe
224        stream: String,
225        /// The reason for the failure
226        reason: String,
227    },
228
229    /// WebSocket connection error.
230    ///
231    /// General connection-related errors that don't fit other categories.
232    #[error("WebSocket connection error: {0}")]
233    Connection(String),
234
235    /// Core error passthrough.
236    ///
237    /// Allows wrapping core errors within Binance-specific error handling
238    /// while maintaining the ability to extract the original error.
239    #[error(transparent)]
240    Core(#[from] CoreError),
241}
242
243impl BinanceWsError {
244    /// Creates a new `MissingStream` error.
245    ///
246    /// # Arguments
247    ///
248    /// * `raw` - The raw message content (will be truncated if too long)
249    ///
250    /// # Example
251    ///
252    /// ```rust
253    /// use ccxt_exchanges::binance::error::BinanceWsError;
254    ///
255    /// let err = BinanceWsError::missing_stream(r#"{"data": "unknown"}"#);
256    /// ```
257    pub fn missing_stream(raw: impl Into<String>) -> Self {
258        let mut raw_str = raw.into();
259        // Truncate long messages for display
260        if raw_str.len() > 200 {
261            raw_str.truncate(200);
262            raw_str.push_str("...");
263        }
264        Self::MissingStream { raw: raw_str }
265    }
266
267    /// Creates a new `UnsupportedEvent` error.
268    ///
269    /// # Arguments
270    ///
271    /// * `event` - The unsupported event type
272    ///
273    /// # Example
274    ///
275    /// ```rust
276    /// use ccxt_exchanges::binance::error::BinanceWsError;
277    ///
278    /// let err = BinanceWsError::unsupported_event("unknownEvent");
279    /// ```
280    pub fn unsupported_event(event: impl Into<String>) -> Self {
281        Self::UnsupportedEvent {
282            event: event.into(),
283        }
284    }
285
286    /// Creates a new `SubscriptionFailed` error.
287    ///
288    /// # Arguments
289    ///
290    /// * `stream` - The stream name that failed
291    /// * `reason` - The reason for the failure
292    ///
293    /// # Example
294    ///
295    /// ```rust
296    /// use ccxt_exchanges::binance::error::BinanceWsError;
297    ///
298    /// let err = BinanceWsError::subscription_failed("btcusdt@ticker", "Invalid symbol");
299    /// ```
300    pub fn subscription_failed(stream: impl Into<String>, reason: impl Into<String>) -> Self {
301        Self::SubscriptionFailed {
302            stream: stream.into(),
303            reason: reason.into(),
304        }
305    }
306
307    /// Creates a new `Connection` error.
308    ///
309    /// # Arguments
310    ///
311    /// * `message` - The connection error message
312    ///
313    /// # Example
314    ///
315    /// ```rust
316    /// use ccxt_exchanges::binance::error::BinanceWsError;
317    ///
318    /// let err = BinanceWsError::connection("Failed to establish connection");
319    /// ```
320    pub fn connection(message: impl Into<String>) -> Self {
321        Self::Connection(message.into())
322    }
323
324    /// Returns the stream name if this error is related to a specific stream.
325    ///
326    /// # Returns
327    ///
328    /// - `Some(&str)` - The stream name for `SubscriptionFailed` variant
329    /// - `None` - For all other variants
330    ///
331    /// # Example
332    ///
333    /// ```rust
334    /// use ccxt_exchanges::binance::error::BinanceWsError;
335    ///
336    /// let err = BinanceWsError::subscription_failed("btcusdt@ticker", "Invalid");
337    /// assert_eq!(err.stream_name(), Some("btcusdt@ticker"));
338    ///
339    /// let err = BinanceWsError::connection("Failed");
340    /// assert_eq!(err.stream_name(), None);
341    /// ```
342    pub fn stream_name(&self) -> Option<&str> {
343        match self {
344            Self::MissingStream { .. }
345            | Self::UnsupportedEvent { .. }
346            | Self::Connection(_)
347            | Self::Core(_) => None,
348            Self::SubscriptionFailed { stream, .. } => Some(stream),
349        }
350    }
351
352    /// Returns the event type if this is an `UnsupportedEvent` error.
353    ///
354    /// # Returns
355    ///
356    /// - `Some(&str)` - The event type for `UnsupportedEvent` variant
357    /// - `None` - For all other variants
358    pub fn event_type(&self) -> Option<&str> {
359        match self {
360            Self::UnsupportedEvent { event } => Some(event),
361            _ => None,
362        }
363    }
364
365    /// Returns the raw message if this is a `MissingStream` error.
366    ///
367    /// # Returns
368    ///
369    /// - `Some(&str)` - The raw message for `MissingStream` variant
370    /// - `None` - For all other variants
371    pub fn raw_message(&self) -> Option<&str> {
372        match self {
373            Self::MissingStream { raw } => Some(raw),
374            _ => None,
375        }
376    }
377}
378
379/// Conversion from `BinanceWsError` to `CoreError`.
380///
381/// This implementation allows Binance-specific errors to be returned from
382/// public API methods that return `CoreError`. The conversion preserves
383/// the original error for downcast capability.
384///
385/// # Conversion Rules
386///
387/// - `BinanceWsError::Core(core)` → Returns the inner `CoreError` directly
388/// - All other variants → Boxed into `CoreError::WebSocket` for downcast
389impl From<BinanceWsError> for CoreError {
390    fn from(e: BinanceWsError) -> Self {
391        match e {
392            // Passthrough: extract the inner CoreError
393            BinanceWsError::Core(core) => core,
394            // Preserve the original error for downcast capability
395            other => CoreError::WebSocket(Box::new(other)),
396        }
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403
404    #[test]
405    fn test_missing_stream_error() {
406        let err = BinanceWsError::missing_stream(r#"{"data": "test"}"#);
407        assert!(matches!(err, BinanceWsError::MissingStream { .. }));
408        assert!(err.to_string().contains("Stream name missing"));
409        assert_eq!(err.stream_name(), None);
410        assert!(err.raw_message().is_some());
411    }
412
413    #[test]
414    fn test_missing_stream_truncation() {
415        let long_message = "x".repeat(300);
416        let err = BinanceWsError::missing_stream(long_message);
417        if let BinanceWsError::MissingStream { raw } = &err {
418            assert!(raw.len() <= 203); // 200 + "..."
419            assert!(raw.ends_with("..."));
420        } else {
421            panic!("Expected MissingStream variant");
422        }
423    }
424
425    #[test]
426    fn test_unsupported_event_error() {
427        let err = BinanceWsError::unsupported_event("unknownEvent");
428        assert!(matches!(err, BinanceWsError::UnsupportedEvent { .. }));
429        assert!(err.to_string().contains("unknownEvent"));
430        assert_eq!(err.event_type(), Some("unknownEvent"));
431        assert_eq!(err.stream_name(), None);
432    }
433
434    #[test]
435    fn test_subscription_failed_error() {
436        let err = BinanceWsError::subscription_failed("btcusdt@ticker", "Invalid symbol");
437        assert!(matches!(err, BinanceWsError::SubscriptionFailed { .. }));
438        assert!(err.to_string().contains("btcusdt@ticker"));
439        assert!(err.to_string().contains("Invalid symbol"));
440        assert_eq!(err.stream_name(), Some("btcusdt@ticker"));
441    }
442
443    #[test]
444    fn test_connection_error() {
445        let err = BinanceWsError::connection("Connection refused");
446        assert!(matches!(err, BinanceWsError::Connection(_)));
447        assert!(err.to_string().contains("Connection refused"));
448        assert_eq!(err.stream_name(), None);
449    }
450
451    #[test]
452    fn test_core_passthrough() {
453        let core_err = CoreError::authentication("Invalid API key");
454        let ws_err = BinanceWsError::Core(core_err);
455        assert!(matches!(ws_err, BinanceWsError::Core(_)));
456    }
457
458    #[test]
459    fn test_conversion_to_core_error() {
460        // Test non-Core variant conversion
461        let ws_err = BinanceWsError::subscription_failed("btcusdt@ticker", "Invalid");
462        let core_err: CoreError = ws_err.into();
463        assert!(matches!(core_err, CoreError::WebSocket(_)));
464
465        // Verify downcast works
466        let downcast = core_err.downcast_websocket::<BinanceWsError>();
467        assert!(downcast.is_some());
468        if let Some(binance_err) = downcast {
469            assert_eq!(binance_err.stream_name(), Some("btcusdt@ticker"));
470        }
471    }
472
473    #[test]
474    fn test_core_passthrough_conversion() {
475        // Test Core variant passthrough
476        let original = CoreError::authentication("Invalid API key");
477        let ws_err = BinanceWsError::Core(original);
478        let core_err: CoreError = ws_err.into();
479
480        // Should be Authentication, not WebSocket
481        assert!(matches!(core_err, CoreError::Authentication(_)));
482        assert!(core_err.to_string().contains("Invalid API key"));
483    }
484
485    #[test]
486    fn test_error_is_send_sync() {
487        fn assert_send_sync<T: Send + Sync>() {}
488        assert_send_sync::<BinanceWsError>();
489        assert_send_sync::<BinanceApiError>();
490    }
491
492    #[test]
493    fn test_error_is_std_error() {
494        fn assert_std_error<T: StdError>() {}
495        assert_std_error::<BinanceWsError>();
496        assert_std_error::<BinanceApiError>();
497    }
498
499    // ==================== BinanceApiError Tests ====================
500
501    #[test]
502    fn test_binance_api_error_new() {
503        let err = BinanceApiError::new(-1121, "Invalid symbol.");
504        assert_eq!(err.code, -1121);
505        assert_eq!(err.msg, "Invalid symbol.");
506        assert!(err.to_string().contains("-1121"));
507        assert!(err.to_string().contains("Invalid symbol."));
508    }
509
510    #[test]
511    fn test_binance_api_error_from_json() {
512        let json = serde_json::json!({"code": -1121, "msg": "Invalid symbol."});
513        let err = BinanceApiError::from_json(&json);
514        assert!(err.is_some());
515        let err = err.unwrap();
516        assert_eq!(err.code, -1121);
517        assert_eq!(err.msg, "Invalid symbol.");
518    }
519
520    #[test]
521    fn test_binance_api_error_from_json_missing_fields() {
522        // Missing code
523        let json = serde_json::json!({"msg": "Invalid symbol."});
524        assert!(BinanceApiError::from_json(&json).is_none());
525
526        // Missing msg
527        let json = serde_json::json!({"code": -1121});
528        assert!(BinanceApiError::from_json(&json).is_none());
529
530        // Empty object
531        let json = serde_json::json!({});
532        assert!(BinanceApiError::from_json(&json).is_none());
533    }
534
535    #[test]
536    fn test_binance_api_error_is_rate_limit() {
537        let err = BinanceApiError::new(-1003, "Too many requests");
538        assert!(err.is_rate_limit());
539
540        let err = BinanceApiError::new(-1015, "Too many orders");
541        assert!(err.is_rate_limit());
542
543        let err = BinanceApiError::new(-1121, "Invalid symbol");
544        assert!(!err.is_rate_limit());
545    }
546
547    #[test]
548    fn test_binance_api_error_is_auth_error() {
549        let err = BinanceApiError::new(-2014, "API-key format invalid");
550        assert!(err.is_auth_error());
551
552        let err = BinanceApiError::new(-2015, "Invalid API-key, IP, or permissions");
553        assert!(err.is_auth_error());
554
555        let err = BinanceApiError::new(-1022, "Signature for this request is not valid");
556        assert!(err.is_auth_error());
557
558        let err = BinanceApiError::new(-1121, "Invalid symbol");
559        assert!(!err.is_auth_error());
560    }
561
562    #[test]
563    fn test_binance_api_error_is_invalid_symbol() {
564        let err = BinanceApiError::new(-1121, "Invalid symbol.");
565        assert!(err.is_invalid_symbol());
566
567        let err = BinanceApiError::new(-1003, "Too many requests");
568        assert!(!err.is_invalid_symbol());
569    }
570
571    #[test]
572    fn test_binance_api_error_is_insufficient_balance() {
573        let err = BinanceApiError::new(-2010, "Account has insufficient balance");
574        assert!(err.is_insufficient_balance());
575
576        let err = BinanceApiError::new(-2011, "Unknown order sent");
577        assert!(err.is_insufficient_balance());
578
579        let err = BinanceApiError::new(-1121, "Invalid symbol");
580        assert!(!err.is_insufficient_balance());
581    }
582
583    #[test]
584    fn test_binance_api_error_is_order_not_found() {
585        let err = BinanceApiError::new(-2013, "Order does not exist");
586        assert!(err.is_order_not_found());
587
588        let err = BinanceApiError::new(-1121, "Invalid symbol");
589        assert!(!err.is_order_not_found());
590    }
591
592    #[test]
593    fn test_binance_api_error_to_core_error_auth() {
594        let err = BinanceApiError::new(-2015, "Invalid API-key");
595        let core_err: CoreError = err.into();
596        assert!(matches!(core_err, CoreError::Authentication(_)));
597    }
598
599    #[test]
600    fn test_binance_api_error_to_core_error_rate_limit() {
601        let err = BinanceApiError::new(-1003, "Too many requests");
602        let core_err: CoreError = err.into();
603        assert!(matches!(core_err, CoreError::RateLimit { .. }));
604    }
605
606    #[test]
607    fn test_binance_api_error_to_core_error_invalid_symbol() {
608        let err = BinanceApiError::new(-1121, "Invalid symbol.");
609        let core_err: CoreError = err.into();
610        // bad_symbol returns InvalidRequest variant
611        assert!(matches!(core_err, CoreError::InvalidRequest(_)));
612    }
613
614    #[test]
615    fn test_binance_api_error_to_core_error_insufficient_funds() {
616        let err = BinanceApiError::new(-2010, "Account has insufficient balance");
617        let core_err: CoreError = err.into();
618        assert!(matches!(core_err, CoreError::InsufficientBalance(_)));
619    }
620
621    #[test]
622    fn test_binance_api_error_to_core_error_order_not_found() {
623        let err = BinanceApiError::new(-2013, "Order does not exist");
624        let core_err: CoreError = err.into();
625        assert!(matches!(core_err, CoreError::OrderNotFound(_)));
626    }
627
628    #[test]
629    fn test_binance_api_error_to_core_error_invalid_order() {
630        let err = BinanceApiError::new(-1102, "Mandatory parameter was not sent");
631        let core_err: CoreError = err.into();
632        assert!(matches!(core_err, CoreError::InvalidOrder(_)));
633    }
634
635    #[test]
636    fn test_binance_api_error_to_core_error_network() {
637        let err = BinanceApiError::new(-1001, "Internal error; unable to process your request");
638        let core_err: CoreError = err.into();
639        assert!(matches!(core_err, CoreError::Network(_)));
640    }
641
642    #[test]
643    fn test_binance_api_error_to_core_error_exchange() {
644        let err = BinanceApiError::new(-9999, "Unknown error");
645        let core_err: CoreError = err.into();
646        assert!(matches!(core_err, CoreError::Exchange(_)));
647    }
648}