1use std::fmt::Display;
19
20#[derive(Debug)]
22pub enum BinanceWsError {
23 ClientError(String),
25 AuthenticationError(String),
27 ParseError(String),
29 NetworkError(String),
31 Timeout(String),
33}
34
35impl Display for BinanceWsError {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 Self::ClientError(msg) => write!(f, "Client error: {msg}"),
39 Self::AuthenticationError(msg) => write!(f, "Authentication error: {msg}"),
40 Self::ParseError(msg) => write!(f, "Parse error: {msg}"),
41 Self::NetworkError(msg) => write!(f, "Network error: {msg}"),
42 Self::Timeout(msg) => write!(f, "Timeout: {msg}"),
43 }
44 }
45}
46
47impl std::error::Error for BinanceWsError {}
48
49pub type BinanceWsResult<T> = Result<T, BinanceWsError>;
51
52#[derive(Debug, thiserror::Error)]
54pub enum BinanceError {
55 #[error("Spot HTTP error: {0}")]
57 SpotHttp(#[from] crate::spot::http::error::BinanceSpotHttpError),
58
59 #[error("Futures HTTP error: {0}")]
61 FuturesHttp(#[from] crate::futures::http::error::BinanceFuturesHttpError),
62
63 #[error("WebSocket error: {0}")]
65 WebSocket(#[from] BinanceWsError),
66
67 #[error("Spot WS API error: {0}")]
69 SpotWsApi(#[from] crate::spot::websocket::trading::error::BinanceWsApiError),
70
71 #[error("Futures WS API error: {0}")]
73 FuturesWsApi(#[from] crate::futures::websocket::trading::error::BinanceFuturesWsApiError),
74
75 #[error("Config error: {0}")]
77 Config(String),
78}
79
80const BINANCE_AUTH_ERROR_CODES: [i64; 3] = [
82 -2015, -2014, -1022, ];
86
87const BINANCE_RATE_LIMIT_ERROR_CODES: [i64; 2] = [
89 -1003, -1015, ];
92
93impl BinanceError {
94 #[must_use]
96 pub fn is_retryable(&self) -> bool {
97 match self {
98 Self::SpotHttp(e) => match e {
99 crate::spot::http::error::BinanceSpotHttpError::NetworkError(_)
100 | crate::spot::http::error::BinanceSpotHttpError::Timeout(_) => true,
101 crate::spot::http::error::BinanceSpotHttpError::BinanceError { code, .. } => {
102 BINANCE_RATE_LIMIT_ERROR_CODES.contains(code)
103 }
104 crate::spot::http::error::BinanceSpotHttpError::UnexpectedStatus {
105 status, ..
106 } => *status == 429 || *status >= 500,
107 _ => false,
108 },
109 Self::FuturesHttp(e) => match e {
110 crate::futures::http::error::BinanceFuturesHttpError::NetworkError(_)
111 | crate::futures::http::error::BinanceFuturesHttpError::Timeout(_) => true,
112 crate::futures::http::error::BinanceFuturesHttpError::BinanceError {
113 code, ..
114 } => BINANCE_RATE_LIMIT_ERROR_CODES.contains(code),
115 crate::futures::http::error::BinanceFuturesHttpError::UnexpectedStatus {
116 status,
117 ..
118 } => *status == 429 || *status >= 500,
119 _ => false,
120 },
121 Self::WebSocket(e) => matches!(
122 e,
123 BinanceWsError::NetworkError(_) | BinanceWsError::Timeout(_)
124 ),
125 Self::SpotWsApi(e) => matches!(
126 e,
127 crate::spot::websocket::trading::error::BinanceWsApiError::ConnectionError(_)
128 | crate::spot::websocket::trading::error::BinanceWsApiError::Timeout(_)
129 ),
130 Self::FuturesWsApi(e) => matches!(
131 e,
132 crate::futures::websocket::trading::error::BinanceFuturesWsApiError::ConnectionError(_)
133 ),
134 Self::Config(_) => false,
135 }
136 }
137
138 #[must_use]
140 pub fn is_fatal(&self) -> bool {
141 match self {
142 Self::SpotHttp(e) => match e {
143 crate::spot::http::error::BinanceSpotHttpError::MissingCredentials => true,
144 crate::spot::http::error::BinanceSpotHttpError::BinanceError { code, .. } => {
145 BINANCE_AUTH_ERROR_CODES.contains(code)
146 }
147 crate::spot::http::error::BinanceSpotHttpError::UnexpectedStatus {
148 status, ..
149 } => *status == 401 || *status == 403,
150 _ => false,
151 },
152 Self::FuturesHttp(e) => match e {
153 crate::futures::http::error::BinanceFuturesHttpError::MissingCredentials => true,
154 crate::futures::http::error::BinanceFuturesHttpError::BinanceError {
155 code, ..
156 } => BINANCE_AUTH_ERROR_CODES.contains(code),
157 crate::futures::http::error::BinanceFuturesHttpError::UnexpectedStatus {
158 status,
159 ..
160 } => *status == 401 || *status == 403,
161 _ => false,
162 },
163 Self::WebSocket(e) => {
164 matches!(e, BinanceWsError::AuthenticationError(_))
165 }
166 Self::SpotWsApi(e) => matches!(
167 e,
168 crate::spot::websocket::trading::error::BinanceWsApiError::AuthenticationError(_)
169 ),
170 Self::FuturesWsApi(_) => false,
171 Self::Config(_) => true,
172 }
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use rstest::rstest;
179
180 use super::*;
181 use crate::{
182 futures::http::error::BinanceFuturesHttpError, spot::http::error::BinanceSpotHttpError,
183 };
184
185 #[rstest]
186 fn test_spot_http_network_error_is_retryable() {
187 let err = BinanceError::SpotHttp(BinanceSpotHttpError::NetworkError(
188 "connection reset".to_string(),
189 ));
190 assert!(err.is_retryable());
191 assert!(!err.is_fatal());
192 }
193
194 #[rstest]
195 fn test_spot_http_timeout_is_retryable() {
196 let err = BinanceError::SpotHttp(BinanceSpotHttpError::Timeout("timed out".to_string()));
197 assert!(err.is_retryable());
198 }
199
200 #[rstest]
201 fn test_spot_http_missing_credentials_is_fatal() {
202 let err = BinanceError::SpotHttp(BinanceSpotHttpError::MissingCredentials);
203 assert!(err.is_fatal());
204 assert!(!err.is_retryable());
205 }
206
207 #[rstest]
208 fn test_spot_http_binance_error_is_not_retryable() {
209 let err = BinanceError::SpotHttp(BinanceSpotHttpError::BinanceError {
210 code: -1021,
211 message: "Timestamp for this request was 1000ms ahead".to_string(),
212 });
213 assert!(!err.is_retryable());
214 assert!(!err.is_fatal());
215 }
216
217 #[rstest]
218 fn test_futures_http_network_error_is_retryable() {
219 let err = BinanceError::FuturesHttp(BinanceFuturesHttpError::NetworkError(
220 "connection refused".to_string(),
221 ));
222 assert!(err.is_retryable());
223 assert!(!err.is_fatal());
224 }
225
226 #[rstest]
227 fn test_futures_http_missing_credentials_is_fatal() {
228 let err = BinanceError::FuturesHttp(BinanceFuturesHttpError::MissingCredentials);
229 assert!(err.is_fatal());
230 assert!(!err.is_retryable());
231 }
232
233 #[rstest]
234 fn test_ws_auth_error_is_fatal() {
235 let err = BinanceError::WebSocket(BinanceWsError::AuthenticationError(
236 "invalid key".to_string(),
237 ));
238 assert!(err.is_fatal());
239 assert!(!err.is_retryable());
240 }
241
242 #[rstest]
243 fn test_ws_network_error_is_retryable() {
244 let err =
245 BinanceError::WebSocket(BinanceWsError::NetworkError("connection lost".to_string()));
246 assert!(err.is_retryable());
247 assert!(!err.is_fatal());
248 }
249
250 #[rstest]
251 fn test_config_error_is_fatal() {
252 let err = BinanceError::Config("invalid product type".to_string());
253 assert!(err.is_fatal());
254 assert!(!err.is_retryable());
255 }
256
257 #[rstest]
258 fn test_spot_http_auth_error_code_is_fatal() {
259 let err = BinanceError::SpotHttp(BinanceSpotHttpError::BinanceError {
260 code: -2015,
261 message: "Invalid API-key, IP, or permissions for action".to_string(),
262 });
263 assert!(err.is_fatal());
264 assert!(!err.is_retryable());
265 }
266
267 #[rstest]
268 fn test_futures_http_auth_error_code_is_fatal() {
269 let err = BinanceError::FuturesHttp(BinanceFuturesHttpError::BinanceError {
270 code: -2015,
271 message: "Invalid API-key".to_string(),
272 });
273 assert!(err.is_fatal());
274 assert!(!err.is_retryable());
275 }
276
277 #[rstest]
278 fn test_spot_http_invalid_signature_is_fatal() {
279 let err = BinanceError::SpotHttp(BinanceSpotHttpError::BinanceError {
280 code: -1022,
281 message: "Signature for this request is not valid".to_string(),
282 });
283 assert!(err.is_fatal());
284 }
285
286 #[rstest]
287 fn test_spot_http_rate_limit_is_retryable() {
288 let err = BinanceError::SpotHttp(BinanceSpotHttpError::BinanceError {
289 code: -1015,
290 message: "Too many new orders".to_string(),
291 });
292 assert!(err.is_retryable());
293 assert!(!err.is_fatal());
294 }
295
296 #[rstest]
297 fn test_futures_http_rate_limit_is_retryable() {
298 let err = BinanceError::FuturesHttp(BinanceFuturesHttpError::BinanceError {
299 code: -1003,
300 message: "Too many requests".to_string(),
301 });
302 assert!(err.is_retryable());
303 assert!(!err.is_fatal());
304 }
305
306 #[rstest]
307 fn test_spot_http_unexpected_status_429_is_retryable() {
308 let err = BinanceError::SpotHttp(BinanceSpotHttpError::UnexpectedStatus {
309 status: 429,
310 body: "rate limited".to_string(),
311 });
312 assert!(err.is_retryable());
313 }
314
315 #[rstest]
316 fn test_spot_http_unexpected_status_500_is_retryable() {
317 let err = BinanceError::SpotHttp(BinanceSpotHttpError::UnexpectedStatus {
318 status: 500,
319 body: "internal server error".to_string(),
320 });
321 assert!(err.is_retryable());
322 }
323
324 #[rstest]
325 fn test_spot_http_unexpected_status_401_is_fatal() {
326 let err = BinanceError::SpotHttp(BinanceSpotHttpError::UnexpectedStatus {
327 status: 401,
328 body: "unauthorized".to_string(),
329 });
330 assert!(err.is_fatal());
331 assert!(!err.is_retryable());
332 }
333
334 #[rstest]
335 fn test_display_formatting() {
336 let err = BinanceError::SpotHttp(BinanceSpotHttpError::BinanceError {
337 code: -1100,
338 message: "Illegal characters found".to_string(),
339 });
340 let msg = err.to_string();
341 assert!(msg.contains("Spot HTTP error"));
342 assert!(msg.contains("-1100"));
343 }
344}