use serde::Deserialize;
use std::collections::HashMap;
use thiserror::Error;
use crate::models::account::{CancelReplaceErrorData, CancelReplaceErrorResponse};
#[derive(Debug, Deserialize)]
pub struct BinanceApiError {
pub code: i32,
pub msg: String,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
impl std::fmt::Display for BinanceApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Binance API error {}: {}", self.code, self.msg)
}
}
impl std::error::Error for BinanceApiError {}
#[derive(Debug, Error)]
pub enum Error {
#[error("Binance API error {code}: {message}")]
Api { code: i32, message: String },
#[error("Binance API cancel-replace error {code}: {message}")]
CancelReplace {
code: i32,
message: String,
data: Box<CancelReplaceErrorData>,
},
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("HTTP middleware error: {0}")]
Middleware(#[from] reqwest_middleware::Error),
#[error("WebSocket error: {0}")]
WebSocket(#[from] tokio_tungstenite::tungstenite::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("URL parse error: {0}")]
UrlParse(#[from] url::ParseError),
#[error("Invalid configuration: {0}")]
InvalidConfig(String),
#[error("Authentication required for this endpoint")]
AuthenticationRequired,
#[error("System time error: {0}")]
SystemTime(#[from] std::time::SystemTimeError),
#[error("Invalid header value: {0}")]
InvalidHeader(#[from] reqwest::header::InvalidHeaderValue),
#[error("Environment variable error: {0}")]
EnvVar(#[from] std::env::VarError),
#[error("Invalid credentials: {0}")]
InvalidCredentials(String),
}
impl Error {
pub fn from_binance_error(error: BinanceApiError) -> Self {
Error::Api {
code: error.code,
message: error.msg,
}
}
pub fn from_cancel_replace_error(error: CancelReplaceErrorResponse) -> Self {
Error::CancelReplace {
code: error.code,
message: error.msg.clone(),
data: Box::new(error.data),
}
}
pub fn is_rate_limit(&self) -> bool {
matches!(self, Error::Api { code: -1003, .. })
}
pub fn is_invalid_signature(&self) -> bool {
matches!(self, Error::Api { code: -1022, .. })
}
pub fn is_timestamp_error(&self) -> bool {
matches!(self, Error::Api { code: -1021, .. })
}
pub fn is_unauthorized(&self) -> bool {
matches!(
self,
Error::Api {
code: -1002 | -2015,
..
}
)
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_api_error_display() {
let err = Error::Api {
code: -1003,
message: "Too many requests".to_string(),
};
assert_eq!(
format!("{}", err),
"Binance API error -1003: Too many requests"
);
}
#[test]
fn test_is_rate_limit() {
let rate_limit_err = Error::Api {
code: -1003,
message: "Too many requests".to_string(),
};
assert!(rate_limit_err.is_rate_limit());
let other_err = Error::Api {
code: -1000,
message: "Unknown error".to_string(),
};
assert!(!other_err.is_rate_limit());
}
#[test]
fn test_is_invalid_signature() {
let sig_err = Error::Api {
code: -1022,
message: "Invalid signature".to_string(),
};
assert!(sig_err.is_invalid_signature());
}
#[test]
fn test_is_timestamp_error() {
let ts_err = Error::Api {
code: -1021,
message: "Timestamp outside recv window".to_string(),
};
assert!(ts_err.is_timestamp_error());
}
#[test]
fn test_is_unauthorized() {
let unauth_err = Error::Api {
code: -2015,
message: "Invalid API key".to_string(),
};
assert!(unauth_err.is_unauthorized());
let unauth_err2 = Error::Api {
code: -1002,
message: "Unauthorized".to_string(),
};
assert!(unauth_err2.is_unauthorized());
}
#[test]
fn test_binance_api_error_deserialize() {
let json = r#"{"code": -1000, "msg": "Unknown error"}"#;
let err: BinanceApiError = serde_json::from_str(json).unwrap();
assert_eq!(err.code, -1000);
assert_eq!(err.msg, "Unknown error");
}
}