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(|v| v.as_str())
117        .unwrap_or("unknown");
118
119    let msg = response
120        .get("msg")
121        .and_then(|v| v.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 => Error::exchange(code, msg),
143        OkxErrorCode::Unknown(_) => Error::exchange(code, msg),
144    }
145}
146
147/// Checks if an OKX API response indicates an error.
148///
149/// OKX uses "0" as the success code. Any other code indicates an error.
150///
151/// # Arguments
152///
153/// * `response` - The JSON response from OKX API
154///
155/// # Returns
156///
157/// `true` if the response indicates an error, `false` otherwise.
158pub fn is_error_response(response: &Value) -> bool {
159    response
160        .get("code")
161        .and_then(|v| v.as_str())
162        .map(|code| code != "0")
163        .unwrap_or(true)
164}
165
166/// Extracts the error code from an OKX API response.
167///
168/// # Arguments
169///
170/// * `response` - The JSON response from OKX API
171///
172/// # Returns
173///
174/// The error code as a string, or "unknown" if not found.
175pub fn extract_error_code(response: &Value) -> &str {
176    response
177        .get("code")
178        .and_then(|v| v.as_str())
179        .unwrap_or("unknown")
180}
181
182/// Extracts the error message from an OKX API response.
183///
184/// # Arguments
185///
186/// * `response` - The JSON response from OKX API
187///
188/// # Returns
189///
190/// The error message as a string, or "Unknown error" if not found.
191pub fn extract_error_message(response: &Value) -> &str {
192    response
193        .get("msg")
194        .and_then(|v| v.as_str())
195        .unwrap_or("Unknown error")
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use serde_json::json;
202
203    #[test]
204    fn test_parse_authentication_error_invalid_api_key() {
205        let response = json!({
206            "code": "50001",
207            "msg": "Invalid API key"
208        });
209
210        let error = parse_error(&response);
211        assert!(error.as_authentication().is_some());
212        assert!(error.to_string().contains("Invalid API key"));
213    }
214
215    #[test]
216    fn test_parse_authentication_error_invalid_signature() {
217        let response = json!({
218            "code": "50002",
219            "msg": "Invalid signature"
220        });
221
222        let error = parse_error(&response);
223        assert!(error.as_authentication().is_some());
224        assert!(error.to_string().contains("Invalid signature"));
225    }
226
227    #[test]
228    fn test_parse_authentication_error_invalid_passphrase() {
229        let response = json!({
230            "code": "50005",
231            "msg": "Invalid passphrase"
232        });
233
234        let error = parse_error(&response);
235        assert!(error.as_authentication().is_some());
236        assert!(error.to_string().contains("Invalid passphrase"));
237    }
238
239    #[test]
240    fn test_parse_rate_limit_error() {
241        let response = json!({
242            "code": "50004",
243            "msg": "Rate limit exceeded"
244        });
245
246        let error = parse_error(&response);
247        assert!(error.as_rate_limit().is_some());
248        let (msg, retry_after) = error.as_rate_limit().unwrap();
249        assert!(msg.contains("Rate limit"));
250        assert!(retry_after.is_some());
251    }
252
253    #[test]
254    fn test_parse_invalid_request_error() {
255        let response = json!({
256            "code": "50000",
257            "msg": "Invalid request parameters"
258        });
259
260        let error = parse_error(&response);
261        let display = error.to_string();
262        assert!(display.contains("Invalid request"));
263    }
264
265    #[test]
266    fn test_parse_parameter_error() {
267        let response = json!({
268            "code": "51000",
269            "msg": "Parameter error"
270        });
271
272        let error = parse_error(&response);
273        let display = error.to_string();
274        assert!(display.contains("Parameter error"));
275    }
276
277    #[test]
278    fn test_parse_insufficient_funds_error() {
279        let response = json!({
280            "code": "51001",
281            "msg": "Insufficient balance"
282        });
283
284        let error = parse_error(&response);
285        let display = error.to_string();
286        assert!(display.contains("Insufficient balance"));
287    }
288
289    #[test]
290    fn test_parse_insufficient_balance_error() {
291        let response = json!({
292            "code": "51008",
293            "msg": "Insufficient balance for order"
294        });
295
296        let error = parse_error(&response);
297        let display = error.to_string();
298        assert!(display.contains("Insufficient balance"));
299    }
300
301    #[test]
302    fn test_parse_bad_symbol_error() {
303        let response = json!({
304            "code": "50011",
305            "msg": "Invalid instrument"
306        });
307
308        let error = parse_error(&response);
309        let display = error.to_string();
310        assert!(display.contains("Bad symbol"));
311    }
312
313    #[test]
314    fn test_parse_order_not_found_error() {
315        let response = json!({
316            "code": "51400",
317            "msg": "Order does not exist"
318        });
319
320        let error = parse_error(&response);
321        let display = error.to_string();
322        assert!(display.contains("Order does not exist"));
323    }
324
325    #[test]
326    fn test_parse_unknown_error() {
327        let response = json!({
328            "code": "99999",
329            "msg": "Some unknown error"
330        });
331
332        let error = parse_error(&response);
333        let display = error.to_string();
334        assert!(display.contains("Some unknown error"));
335    }
336
337    #[test]
338    fn test_parse_error_missing_code() {
339        let response = json!({
340            "msg": "Error without code"
341        });
342
343        let error = parse_error(&response);
344        let display = error.to_string();
345        assert!(display.contains("Error without code"));
346    }
347
348    #[test]
349    fn test_parse_error_missing_message() {
350        let response = json!({
351            "code": "50001"
352        });
353
354        let error = parse_error(&response);
355        let display = error.to_string();
356        assert!(display.contains("Unknown error"));
357    }
358
359    #[test]
360    fn test_is_error_response_success() {
361        let response = json!({
362            "code": "0",
363            "msg": "success",
364            "data": []
365        });
366
367        assert!(!is_error_response(&response));
368    }
369
370    #[test]
371    fn test_is_error_response_error() {
372        let response = json!({
373            "code": "50001",
374            "msg": "Invalid API key"
375        });
376
377        assert!(is_error_response(&response));
378    }
379
380    #[test]
381    fn test_is_error_response_missing_code() {
382        let response = json!({
383            "msg": "No code field"
384        });
385
386        // Missing code should be treated as error
387        assert!(is_error_response(&response));
388    }
389
390    #[test]
391    fn test_extract_error_code() {
392        let response = json!({
393            "code": "50001",
394            "msg": "Invalid API key"
395        });
396
397        assert_eq!(extract_error_code(&response), "50001");
398    }
399
400    #[test]
401    fn test_extract_error_code_missing() {
402        let response = json!({
403            "msg": "No code"
404        });
405
406        assert_eq!(extract_error_code(&response), "unknown");
407    }
408
409    #[test]
410    fn test_extract_error_message() {
411        let response = json!({
412            "code": "50001",
413            "msg": "Invalid API key"
414        });
415
416        assert_eq!(extract_error_message(&response), "Invalid API key");
417    }
418
419    #[test]
420    fn test_extract_error_message_missing() {
421        let response = json!({
422            "code": "50001"
423        });
424
425        assert_eq!(extract_error_message(&response), "Unknown error");
426    }
427
428    #[test]
429    fn test_okx_error_code_from_code() {
430        assert_eq!(
431            OkxErrorCode::from_code("50000"),
432            OkxErrorCode::InvalidRequest
433        );
434        assert_eq!(
435            OkxErrorCode::from_code("50001"),
436            OkxErrorCode::InvalidApiKey
437        );
438        assert_eq!(
439            OkxErrorCode::from_code("50002"),
440            OkxErrorCode::InvalidSignature
441        );
442        assert_eq!(
443            OkxErrorCode::from_code("50003"),
444            OkxErrorCode::InvalidTimestamp
445        );
446        assert_eq!(
447            OkxErrorCode::from_code("50004"),
448            OkxErrorCode::RateLimitExceeded
449        );
450        assert_eq!(
451            OkxErrorCode::from_code("50005"),
452            OkxErrorCode::InvalidPassphrase
453        );
454        assert_eq!(OkxErrorCode::from_code("50011"), OkxErrorCode::BadSymbol);
455        assert_eq!(
456            OkxErrorCode::from_code("51000"),
457            OkxErrorCode::ParameterError
458        );
459        assert_eq!(
460            OkxErrorCode::from_code("51001"),
461            OkxErrorCode::InsufficientFunds
462        );
463        assert_eq!(
464            OkxErrorCode::from_code("51008"),
465            OkxErrorCode::InsufficientBalance
466        );
467        assert_eq!(
468            OkxErrorCode::from_code("51400"),
469            OkxErrorCode::OrderNotFound
470        );
471        assert_eq!(
472            OkxErrorCode::from_code("99999"),
473            OkxErrorCode::Unknown(99999)
474        );
475        assert_eq!(OkxErrorCode::from_code("invalid"), OkxErrorCode::Unknown(0));
476    }
477
478    #[test]
479    fn test_okx_error_code_code() {
480        assert_eq!(OkxErrorCode::InvalidRequest.code(), 50000);
481        assert_eq!(OkxErrorCode::InvalidApiKey.code(), 50001);
482        assert_eq!(OkxErrorCode::InvalidSignature.code(), 50002);
483        assert_eq!(OkxErrorCode::InvalidTimestamp.code(), 50003);
484        assert_eq!(OkxErrorCode::RateLimitExceeded.code(), 50004);
485        assert_eq!(OkxErrorCode::InvalidPassphrase.code(), 50005);
486        assert_eq!(OkxErrorCode::BadSymbol.code(), 50011);
487        assert_eq!(OkxErrorCode::ParameterError.code(), 51000);
488        assert_eq!(OkxErrorCode::InsufficientFunds.code(), 51001);
489        assert_eq!(OkxErrorCode::InsufficientBalance.code(), 51008);
490        assert_eq!(OkxErrorCode::OrderNotFound.code(), 51400);
491        assert_eq!(OkxErrorCode::Unknown(12345).code(), 12345);
492    }
493}