use ccxt_core::error::Error as CoreError;
use thiserror::Error;
#[cfg(test)]
use std::error::Error as StdError;
#[derive(Error, Debug, Clone)]
#[error("Binance API error {code}: {msg}")]
pub struct BinanceApiError {
pub code: i32,
pub msg: String,
}
impl BinanceApiError {
pub fn new(code: i32, msg: impl Into<String>) -> Self {
Self {
code,
msg: msg.into(),
}
}
pub fn from_json(json: &serde_json::Value) -> Option<Self> {
let code = json.get("code")?.as_i64()? as i32;
let msg = json.get("msg")?.as_str()?.to_string();
Some(Self { code, msg })
}
pub fn is_rate_limit(&self) -> bool {
matches!(self.code, -1003 | -1015)
}
pub fn is_ip_banned(&self) -> bool {
self.code == -1003 && self.msg.contains("banned")
}
pub fn is_auth_error(&self) -> bool {
matches!(self.code, -2014 | -2015 | -1022)
}
pub fn is_invalid_symbol(&self) -> bool {
self.code == -1121
}
pub fn is_insufficient_balance(&self) -> bool {
matches!(self.code, -2010 | -2011)
}
pub fn is_order_not_found(&self) -> bool {
self.code == -2013
}
pub fn to_core_error(&self) -> CoreError {
use std::borrow::Cow;
match self.code {
-2014 | -2015 | -1022 => CoreError::authentication(self.msg.clone()),
-1003 | -1015 => CoreError::rate_limit(self.msg.clone(), None),
-1121 => CoreError::bad_symbol(&self.msg),
-2010 | -2011 => CoreError::insufficient_balance(self.msg.clone()),
-2013 => CoreError::OrderNotFound(Cow::Owned(self.msg.clone())),
-1102 | -1106 | -1111 | -1112 | -1114 | -1115 | -1116 | -1117 | -1118 => {
CoreError::InvalidOrder(Cow::Owned(self.msg.clone()))
}
-1001 => CoreError::network(&self.msg),
_ => CoreError::exchange(self.code.to_string(), &self.msg),
}
}
}
impl From<BinanceApiError> for CoreError {
fn from(e: BinanceApiError) -> Self {
e.to_core_error()
}
}
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum BinanceWsError {
#[error("Stream name missing in message: {raw}")]
MissingStream {
raw: String,
},
#[error("Unsupported event type: {event}")]
UnsupportedEvent {
event: String,
},
#[error("Subscription failed for {stream}: {reason}")]
SubscriptionFailed {
stream: String,
reason: String,
},
#[error("WebSocket connection error: {0}")]
Connection(String),
#[error(transparent)]
Core(#[from] CoreError),
}
impl BinanceWsError {
pub fn missing_stream(raw: impl Into<String>) -> Self {
let mut raw_str = raw.into();
if raw_str.len() > 200 {
raw_str.truncate(200);
raw_str.push_str("...");
}
Self::MissingStream { raw: raw_str }
}
pub fn unsupported_event(event: impl Into<String>) -> Self {
Self::UnsupportedEvent {
event: event.into(),
}
}
pub fn subscription_failed(stream: impl Into<String>, reason: impl Into<String>) -> Self {
Self::SubscriptionFailed {
stream: stream.into(),
reason: reason.into(),
}
}
pub fn connection(message: impl Into<String>) -> Self {
Self::Connection(message.into())
}
pub fn stream_name(&self) -> Option<&str> {
match self {
Self::MissingStream { .. }
| Self::UnsupportedEvent { .. }
| Self::Connection(_)
| Self::Core(_) => None,
Self::SubscriptionFailed { stream, .. } => Some(stream),
}
}
pub fn event_type(&self) -> Option<&str> {
match self {
Self::UnsupportedEvent { event } => Some(event),
_ => None,
}
}
pub fn raw_message(&self) -> Option<&str> {
match self {
Self::MissingStream { raw } => Some(raw),
_ => None,
}
}
}
impl From<BinanceWsError> for CoreError {
fn from(e: BinanceWsError) -> Self {
match e {
BinanceWsError::Core(core) => core,
other => CoreError::WebSocket(Box::new(other)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_missing_stream_error() {
let err = BinanceWsError::missing_stream(r#"{"data": "test"}"#);
assert!(matches!(err, BinanceWsError::MissingStream { .. }));
assert!(err.to_string().contains("Stream name missing"));
assert_eq!(err.stream_name(), None);
assert!(err.raw_message().is_some());
}
#[test]
fn test_missing_stream_truncation() {
let long_message = "x".repeat(300);
let err = BinanceWsError::missing_stream(long_message);
if let BinanceWsError::MissingStream { raw } = &err {
assert!(raw.len() <= 203); assert!(raw.ends_with("..."));
} else {
panic!("Expected MissingStream variant");
}
}
#[test]
fn test_unsupported_event_error() {
let err = BinanceWsError::unsupported_event("unknownEvent");
assert!(matches!(err, BinanceWsError::UnsupportedEvent { .. }));
assert!(err.to_string().contains("unknownEvent"));
assert_eq!(err.event_type(), Some("unknownEvent"));
assert_eq!(err.stream_name(), None);
}
#[test]
fn test_subscription_failed_error() {
let err = BinanceWsError::subscription_failed("btcusdt@ticker", "Invalid symbol");
assert!(matches!(err, BinanceWsError::SubscriptionFailed { .. }));
assert!(err.to_string().contains("btcusdt@ticker"));
assert!(err.to_string().contains("Invalid symbol"));
assert_eq!(err.stream_name(), Some("btcusdt@ticker"));
}
#[test]
fn test_connection_error() {
let err = BinanceWsError::connection("Connection refused");
assert!(matches!(err, BinanceWsError::Connection(_)));
assert!(err.to_string().contains("Connection refused"));
assert_eq!(err.stream_name(), None);
}
#[test]
fn test_core_passthrough() {
let core_err = CoreError::authentication("Invalid API key");
let ws_err = BinanceWsError::Core(core_err);
assert!(matches!(ws_err, BinanceWsError::Core(_)));
}
#[test]
fn test_conversion_to_core_error() {
let ws_err = BinanceWsError::subscription_failed("btcusdt@ticker", "Invalid");
let core_err: CoreError = ws_err.into();
assert!(matches!(core_err, CoreError::WebSocket(_)));
let downcast = core_err.downcast_websocket::<BinanceWsError>();
assert!(downcast.is_some());
if let Some(binance_err) = downcast {
assert_eq!(binance_err.stream_name(), Some("btcusdt@ticker"));
}
}
#[test]
fn test_core_passthrough_conversion() {
let original = CoreError::authentication("Invalid API key");
let ws_err = BinanceWsError::Core(original);
let core_err: CoreError = ws_err.into();
assert!(matches!(core_err, CoreError::Authentication(_)));
assert!(core_err.to_string().contains("Invalid API key"));
}
#[test]
fn test_error_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<BinanceWsError>();
assert_send_sync::<BinanceApiError>();
}
#[test]
fn test_error_is_std_error() {
fn assert_std_error<T: StdError>() {}
assert_std_error::<BinanceWsError>();
assert_std_error::<BinanceApiError>();
}
#[test]
fn test_binance_api_error_new() {
let err = BinanceApiError::new(-1121, "Invalid symbol.");
assert_eq!(err.code, -1121);
assert_eq!(err.msg, "Invalid symbol.");
assert!(err.to_string().contains("-1121"));
assert!(err.to_string().contains("Invalid symbol."));
}
#[test]
fn test_binance_api_error_from_json() {
let json = serde_json::json!({"code": -1121, "msg": "Invalid symbol."});
let err = BinanceApiError::from_json(&json);
assert!(err.is_some());
let err = err.unwrap();
assert_eq!(err.code, -1121);
assert_eq!(err.msg, "Invalid symbol.");
}
#[test]
fn test_binance_api_error_from_json_missing_fields() {
let json = serde_json::json!({"msg": "Invalid symbol."});
assert!(BinanceApiError::from_json(&json).is_none());
let json = serde_json::json!({"code": -1121});
assert!(BinanceApiError::from_json(&json).is_none());
let json = serde_json::json!({});
assert!(BinanceApiError::from_json(&json).is_none());
}
#[test]
fn test_binance_api_error_is_rate_limit() {
let err = BinanceApiError::new(-1003, "Too many requests");
assert!(err.is_rate_limit());
let err = BinanceApiError::new(-1015, "Too many orders");
assert!(err.is_rate_limit());
let err = BinanceApiError::new(-1121, "Invalid symbol");
assert!(!err.is_rate_limit());
}
#[test]
fn test_binance_api_error_is_auth_error() {
let err = BinanceApiError::new(-2014, "API-key format invalid");
assert!(err.is_auth_error());
let err = BinanceApiError::new(-2015, "Invalid API-key, IP, or permissions");
assert!(err.is_auth_error());
let err = BinanceApiError::new(-1022, "Signature for this request is not valid");
assert!(err.is_auth_error());
let err = BinanceApiError::new(-1121, "Invalid symbol");
assert!(!err.is_auth_error());
}
#[test]
fn test_binance_api_error_is_invalid_symbol() {
let err = BinanceApiError::new(-1121, "Invalid symbol.");
assert!(err.is_invalid_symbol());
let err = BinanceApiError::new(-1003, "Too many requests");
assert!(!err.is_invalid_symbol());
}
#[test]
fn test_binance_api_error_is_insufficient_balance() {
let err = BinanceApiError::new(-2010, "Account has insufficient balance");
assert!(err.is_insufficient_balance());
let err = BinanceApiError::new(-2011, "Unknown order sent");
assert!(err.is_insufficient_balance());
let err = BinanceApiError::new(-1121, "Invalid symbol");
assert!(!err.is_insufficient_balance());
}
#[test]
fn test_binance_api_error_is_order_not_found() {
let err = BinanceApiError::new(-2013, "Order does not exist");
assert!(err.is_order_not_found());
let err = BinanceApiError::new(-1121, "Invalid symbol");
assert!(!err.is_order_not_found());
}
#[test]
fn test_binance_api_error_to_core_error_auth() {
let err = BinanceApiError::new(-2015, "Invalid API-key");
let core_err: CoreError = err.into();
assert!(matches!(core_err, CoreError::Authentication(_)));
}
#[test]
fn test_binance_api_error_to_core_error_rate_limit() {
let err = BinanceApiError::new(-1003, "Too many requests");
let core_err: CoreError = err.into();
assert!(matches!(core_err, CoreError::RateLimit { .. }));
}
#[test]
fn test_binance_api_error_to_core_error_invalid_symbol() {
let err = BinanceApiError::new(-1121, "Invalid symbol.");
let core_err: CoreError = err.into();
assert!(matches!(core_err, CoreError::InvalidRequest(_)));
}
#[test]
fn test_binance_api_error_to_core_error_insufficient_funds() {
let err = BinanceApiError::new(-2010, "Account has insufficient balance");
let core_err: CoreError = err.into();
assert!(matches!(core_err, CoreError::InsufficientBalance(_)));
}
#[test]
fn test_binance_api_error_to_core_error_order_not_found() {
let err = BinanceApiError::new(-2013, "Order does not exist");
let core_err: CoreError = err.into();
assert!(matches!(core_err, CoreError::OrderNotFound(_)));
}
#[test]
fn test_binance_api_error_to_core_error_invalid_order() {
let err = BinanceApiError::new(-1102, "Mandatory parameter was not sent");
let core_err: CoreError = err.into();
assert!(matches!(core_err, CoreError::InvalidOrder(_)));
}
#[test]
fn test_binance_api_error_to_core_error_network() {
let err = BinanceApiError::new(-1001, "Internal error; unable to process your request");
let core_err: CoreError = err.into();
assert!(matches!(core_err, CoreError::Network(_)));
}
#[test]
fn test_binance_api_error_to_core_error_exchange() {
let err = BinanceApiError::new(-9999, "Unknown error");
let core_err: CoreError = err.into();
assert!(matches!(core_err, CoreError::Exchange(_)));
}
}