ccxt_exchanges/okx/
error.rs

1//! OKX-specific error handling.
2//!
3//! This module provides error parsing for OKX API responses,
4//! mapping OKX error codes to ccxt-core Error types.
5
6use ccxt_core::error::Error;
7use serde_json::Value;
8use std::time::Duration;
9
10/// OKX error codes and their meanings.
11///
12/// Reference: https://www.okx.com/docs-v5/en/#error-code
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum OkxErrorCode {
15    /// Invalid request (50000)
16    InvalidRequest,
17    /// Invalid API key (50001)
18    InvalidApiKey,
19    /// Invalid signature (50002)
20    InvalidSignature,
21    /// Invalid timestamp (50003)
22    InvalidTimestamp,
23    /// Rate limit exceeded (50004)
24    RateLimitExceeded,
25    /// Invalid passphrase (50005)
26    InvalidPassphrase,
27    /// Bad symbol / Invalid instrument (50011)
28    BadSymbol,
29    /// General parameter error (51000)
30    ParameterError,
31    /// Insufficient funds (51001)
32    InsufficientFunds,
33    /// Order does not exist (51400)
34    OrderNotFound,
35    /// Insufficient balance (51008)
36    InsufficientBalance,
37    /// Unknown error code
38    Unknown(i64),
39}
40
41impl OkxErrorCode {
42    /// Parses an OKX error code string into an OkxErrorCode.
43    pub fn from_code(code: &str) -> Self {
44        match code.parse::<i64>() {
45            Ok(50000) => OkxErrorCode::InvalidRequest,
46            Ok(50001) => OkxErrorCode::InvalidApiKey,
47            Ok(50002) => OkxErrorCode::InvalidSignature,
48            Ok(50003) => OkxErrorCode::InvalidTimestamp,
49            Ok(50004) => OkxErrorCode::RateLimitExceeded,
50            Ok(50005) => OkxErrorCode::InvalidPassphrase,
51            Ok(50011) => OkxErrorCode::BadSymbol,
52            Ok(51000) => OkxErrorCode::ParameterError,
53            Ok(51001) => OkxErrorCode::InsufficientFunds,
54            Ok(51008) => OkxErrorCode::InsufficientBalance,
55            Ok(51400) => OkxErrorCode::OrderNotFound,
56            Ok(n) => OkxErrorCode::Unknown(n),
57            Err(_) => OkxErrorCode::Unknown(0),
58        }
59    }
60
61    /// Returns the numeric code for this error.
62    pub fn code(&self) -> i64 {
63        match self {
64            OkxErrorCode::InvalidRequest => 50000,
65            OkxErrorCode::InvalidApiKey => 50001,
66            OkxErrorCode::InvalidSignature => 50002,
67            OkxErrorCode::InvalidTimestamp => 50003,
68            OkxErrorCode::RateLimitExceeded => 50004,
69            OkxErrorCode::InvalidPassphrase => 50005,
70            OkxErrorCode::BadSymbol => 50011,
71            OkxErrorCode::ParameterError => 51000,
72            OkxErrorCode::InsufficientFunds => 51001,
73            OkxErrorCode::InsufficientBalance => 51008,
74            OkxErrorCode::OrderNotFound => 51400,
75            OkxErrorCode::Unknown(n) => *n,
76        }
77    }
78}
79
80/// Parses an OKX API error response and converts it to a ccxt-core Error.
81///
82/// OKX API responses follow this format:
83/// ```json
84/// {
85///     "code": "50001",
86///     "msg": "Invalid API key",
87///     "data": []
88/// }
89/// ```
90///
91/// # Arguments
92///
93/// * `response` - The JSON response from OKX API
94///
95/// # Returns
96///
97/// A ccxt-core Error mapped from the OKX error code.
98///
99/// # Example
100///
101/// ```rust
102/// use ccxt_exchanges::okx::error::parse_error;
103/// use serde_json::json;
104///
105/// let response = json!({
106///     "code": "50001",
107///     "msg": "Invalid API key"
108/// });
109///
110/// let error = parse_error(&response);
111/// assert!(error.as_authentication().is_some());
112/// ```
113pub fn parse_error(response: &Value) -> Error {
114    let code = response
115        .get("code")
116        .and_then(serde_json::Value::as_str)
117        .unwrap_or("unknown");
118
119    let msg = response
120        .get("msg")
121        .and_then(serde_json::Value::as_str)
122        .unwrap_or("Unknown error");
123
124    let error_code = OkxErrorCode::from_code(code);
125
126    match error_code {
127        OkxErrorCode::InvalidApiKey
128        | OkxErrorCode::InvalidSignature
129        | OkxErrorCode::InvalidTimestamp
130        | OkxErrorCode::InvalidPassphrase => Error::authentication(msg.to_string()),
131        OkxErrorCode::RateLimitExceeded => {
132            // OKX typically suggests waiting 1 second on rate limit
133            Error::rate_limit(msg.to_string(), Some(Duration::from_secs(1)))
134        }
135        OkxErrorCode::InvalidRequest | OkxErrorCode::ParameterError => {
136            Error::invalid_request(msg.to_string())
137        }
138        OkxErrorCode::InsufficientFunds | OkxErrorCode::InsufficientBalance => {
139            Error::insufficient_balance(msg.to_string())
140        }
141        OkxErrorCode::BadSymbol => Error::bad_symbol(msg.to_string()),
142        OkxErrorCode::OrderNotFound | OkxErrorCode::Unknown(_) => Error::exchange(code, msg),
143    }
144}
145
146/// Checks if an OKX API response indicates an error.
147///
148/// OKX uses "0" as the success code. Any other code indicates an error.
149///
150/// # Arguments
151///
152/// * `response` - The JSON response from OKX API
153///
154/// # Returns
155///
156/// `true` if the response indicates an error, `false` otherwise.
157pub fn is_error_response(response: &Value) -> bool {
158    response.get("code").and_then(serde_json::Value::as_str) != Some("0")
159}
160
161/// Extracts the error code from an OKX API response.
162///
163/// # Arguments
164///
165/// * `response` - The JSON response from OKX API
166///
167/// # Returns
168///
169/// The error code as a string, or "unknown" if not found.
170pub fn extract_error_code(response: &Value) -> &str {
171    response
172        .get("code")
173        .and_then(serde_json::Value::as_str)
174        .unwrap_or("unknown")
175}
176
177/// Extracts the error message from an OKX API response.
178///
179/// # Arguments
180///
181/// * `response` - The JSON response from OKX API
182///
183/// # Returns
184///
185/// The error message as a string, or "Unknown error" if not found.
186pub fn extract_error_message(response: &Value) -> &str {
187    response
188        .get("msg")
189        .and_then(serde_json::Value::as_str)
190        .unwrap_or("Unknown error")
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use serde_json::json;
197
198    #[test]
199    fn test_parse_authentication_error_invalid_api_key() {
200        let response = json!({
201            "code": "50001",
202            "msg": "Invalid API key"
203        });
204
205        let error = parse_error(&response);
206        assert!(error.as_authentication().is_some());
207        assert!(error.to_string().contains("Invalid API key"));
208    }
209
210    #[test]
211    fn test_parse_authentication_error_invalid_signature() {
212        let response = json!({
213            "code": "50002",
214            "msg": "Invalid signature"
215        });
216
217        let error = parse_error(&response);
218        assert!(error.as_authentication().is_some());
219        assert!(error.to_string().contains("Invalid signature"));
220    }
221
222    #[test]
223    fn test_parse_authentication_error_invalid_passphrase() {
224        let response = json!({
225            "code": "50005",
226            "msg": "Invalid passphrase"
227        });
228
229        let error = parse_error(&response);
230        assert!(error.as_authentication().is_some());
231        assert!(error.to_string().contains("Invalid passphrase"));
232    }
233
234    #[test]
235    fn test_parse_rate_limit_error() {
236        let response = json!({
237            "code": "50004",
238            "msg": "Rate limit exceeded"
239        });
240
241        let error = parse_error(&response);
242        assert!(error.as_rate_limit().is_some());
243        let (msg, retry_after) = error.as_rate_limit().unwrap();
244        assert!(msg.contains("Rate limit"));
245        assert!(retry_after.is_some());
246    }
247
248    #[test]
249    fn test_parse_invalid_request_error() {
250        let response = json!({
251            "code": "50000",
252            "msg": "Invalid request parameters"
253        });
254
255        let error = parse_error(&response);
256        let display = error.to_string();
257        assert!(display.contains("Invalid request"));
258    }
259
260    #[test]
261    fn test_parse_parameter_error() {
262        let response = json!({
263            "code": "51000",
264            "msg": "Parameter error"
265        });
266
267        let error = parse_error(&response);
268        let display = error.to_string();
269        assert!(display.contains("Parameter error"));
270    }
271
272    #[test]
273    fn test_parse_insufficient_funds_error() {
274        let response = json!({
275            "code": "51001",
276            "msg": "Insufficient balance"
277        });
278
279        let error = parse_error(&response);
280        let display = error.to_string();
281        assert!(display.contains("Insufficient balance"));
282    }
283
284    #[test]
285    fn test_parse_insufficient_balance_error() {
286        let response = json!({
287            "code": "51008",
288            "msg": "Insufficient balance for order"
289        });
290
291        let error = parse_error(&response);
292        let display = error.to_string();
293        assert!(display.contains("Insufficient balance"));
294    }
295
296    #[test]
297    fn test_parse_bad_symbol_error() {
298        let response = json!({
299            "code": "50011",
300            "msg": "Invalid instrument"
301        });
302
303        let error = parse_error(&response);
304        let display = error.to_string();
305        assert!(display.contains("Bad symbol"));
306    }
307
308    #[test]
309    fn test_parse_order_not_found_error() {
310        let response = json!({
311            "code": "51400",
312            "msg": "Order does not exist"
313        });
314
315        let error = parse_error(&response);
316        let display = error.to_string();
317        assert!(display.contains("Order does not exist"));
318    }
319
320    #[test]
321    fn test_parse_unknown_error() {
322        let response = json!({
323            "code": "99999",
324            "msg": "Some unknown error"
325        });
326
327        let error = parse_error(&response);
328        let display = error.to_string();
329        assert!(display.contains("Some unknown error"));
330    }
331
332    #[test]
333    fn test_parse_error_missing_code() {
334        let response = json!({
335            "msg": "Error without code"
336        });
337
338        let error = parse_error(&response);
339        let display = error.to_string();
340        assert!(display.contains("Error without code"));
341    }
342
343    #[test]
344    fn test_parse_error_missing_message() {
345        let response = json!({
346            "code": "50001"
347        });
348
349        let error = parse_error(&response);
350        let display = error.to_string();
351        assert!(display.contains("Unknown error"));
352    }
353
354    #[test]
355    fn test_is_error_response_success() {
356        let response = json!({
357            "code": "0",
358            "msg": "success",
359            "data": []
360        });
361
362        assert!(!is_error_response(&response));
363    }
364
365    #[test]
366    fn test_is_error_response_error() {
367        let response = json!({
368            "code": "50001",
369            "msg": "Invalid API key"
370        });
371
372        assert!(is_error_response(&response));
373    }
374
375    #[test]
376    fn test_is_error_response_missing_code() {
377        let response = json!({
378            "msg": "No code field"
379        });
380
381        // Missing code should be treated as error
382        assert!(is_error_response(&response));
383    }
384
385    #[test]
386    fn test_extract_error_code() {
387        let response = json!({
388            "code": "50001",
389            "msg": "Invalid API key"
390        });
391
392        assert_eq!(extract_error_code(&response), "50001");
393    }
394
395    #[test]
396    fn test_extract_error_code_missing() {
397        let response = json!({
398            "msg": "No code"
399        });
400
401        assert_eq!(extract_error_code(&response), "unknown");
402    }
403
404    #[test]
405    fn test_extract_error_message() {
406        let response = json!({
407            "code": "50001",
408            "msg": "Invalid API key"
409        });
410
411        assert_eq!(extract_error_message(&response), "Invalid API key");
412    }
413
414    #[test]
415    fn test_extract_error_message_missing() {
416        let response = json!({
417            "code": "50001"
418        });
419
420        assert_eq!(extract_error_message(&response), "Unknown error");
421    }
422
423    #[test]
424    fn test_okx_error_code_from_code() {
425        assert_eq!(
426            OkxErrorCode::from_code("50000"),
427            OkxErrorCode::InvalidRequest
428        );
429        assert_eq!(
430            OkxErrorCode::from_code("50001"),
431            OkxErrorCode::InvalidApiKey
432        );
433        assert_eq!(
434            OkxErrorCode::from_code("50002"),
435            OkxErrorCode::InvalidSignature
436        );
437        assert_eq!(
438            OkxErrorCode::from_code("50003"),
439            OkxErrorCode::InvalidTimestamp
440        );
441        assert_eq!(
442            OkxErrorCode::from_code("50004"),
443            OkxErrorCode::RateLimitExceeded
444        );
445        assert_eq!(
446            OkxErrorCode::from_code("50005"),
447            OkxErrorCode::InvalidPassphrase
448        );
449        assert_eq!(OkxErrorCode::from_code("50011"), OkxErrorCode::BadSymbol);
450        assert_eq!(
451            OkxErrorCode::from_code("51000"),
452            OkxErrorCode::ParameterError
453        );
454        assert_eq!(
455            OkxErrorCode::from_code("51001"),
456            OkxErrorCode::InsufficientFunds
457        );
458        assert_eq!(
459            OkxErrorCode::from_code("51008"),
460            OkxErrorCode::InsufficientBalance
461        );
462        assert_eq!(
463            OkxErrorCode::from_code("51400"),
464            OkxErrorCode::OrderNotFound
465        );
466        assert_eq!(
467            OkxErrorCode::from_code("99999"),
468            OkxErrorCode::Unknown(99999)
469        );
470        assert_eq!(OkxErrorCode::from_code("invalid"), OkxErrorCode::Unknown(0));
471    }
472
473    #[test]
474    fn test_okx_error_code_code() {
475        assert_eq!(OkxErrorCode::InvalidRequest.code(), 50000);
476        assert_eq!(OkxErrorCode::InvalidApiKey.code(), 50001);
477        assert_eq!(OkxErrorCode::InvalidSignature.code(), 50002);
478        assert_eq!(OkxErrorCode::InvalidTimestamp.code(), 50003);
479        assert_eq!(OkxErrorCode::RateLimitExceeded.code(), 50004);
480        assert_eq!(OkxErrorCode::InvalidPassphrase.code(), 50005);
481        assert_eq!(OkxErrorCode::BadSymbol.code(), 50011);
482        assert_eq!(OkxErrorCode::ParameterError.code(), 51000);
483        assert_eq!(OkxErrorCode::InsufficientFunds.code(), 51001);
484        assert_eq!(OkxErrorCode::InsufficientBalance.code(), 51008);
485        assert_eq!(OkxErrorCode::OrderNotFound.code(), 51400);
486        assert_eq!(OkxErrorCode::Unknown(12345).code(), 12345);
487    }
488}