Skip to main content

kraken_api_client/
error.rs

1//! Error types for the Kraken client library.
2
3use thiserror::Error;
4
5/// The main error type for all Kraken client operations.
6#[derive(Error, Debug)]
7pub enum KrakenError {
8    /// HTTP request failed
9    #[error("HTTP request failed: {0}")]
10    Http(#[from] reqwest::Error),
11
12    /// HTTP request with middleware failed
13    #[error("HTTP request failed: {0}")]
14    HttpMiddleware(#[from] reqwest_middleware::Error),
15
16    /// WebSocket protocol error
17    #[error("WebSocket error: {0}")]
18    WebSocket(#[from] tokio_tungstenite::tungstenite::Error),
19
20    /// WebSocket communication error (with message)
21    #[error("WebSocket error: {0}")]
22    WebSocketMsg(String),
23
24    /// JSON serialization/deserialization error
25    #[error("JSON error: {0}")]
26    Json(#[from] serde_json::Error),
27
28    /// URL parsing error
29    #[error("URL parsing error: {0}")]
30    Url(#[from] url::ParseError),
31
32    /// Kraken API returned an error
33    #[error("Kraken API error: {0}")]
34    Api(ApiError),
35
36    /// Rate limit exceeded
37    #[error("Rate limit exceeded, retry after {retry_after_ms:?}ms")]
38    RateLimitExceeded {
39        /// Suggested wait time in milliseconds before retrying
40        retry_after_ms: Option<u64>,
41    },
42
43    /// Authentication error
44    #[error("Authentication error: {0}")]
45    Auth(String),
46
47    /// Invalid response from the API
48    #[error("Invalid response: {0}")]
49    InvalidResponse(String),
50
51    /// WebSocket connection closed unexpectedly
52    #[error("WebSocket connection closed: {reason}")]
53    ConnectionClosed {
54        /// Reason for the closure
55        reason: String,
56    },
57
58    /// Request timeout
59    #[error("Request timed out")]
60    Timeout,
61
62    /// Missing required credentials
63    #[error("Missing credentials: API key and secret required for private endpoints")]
64    MissingCredentials,
65}
66
67/// Kraken API error codes and messages.
68///
69/// These are errors returned by the Kraken API itself in the response body.
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct ApiError {
72    /// The error code/identifier from Kraken (e.g., "EGeneral:Invalid arguments")
73    pub code: String,
74    /// Human-readable error message
75    pub message: String,
76}
77
78impl std::fmt::Display for ApiError {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        write!(f, "{}: {}", self.code, self.message)
81    }
82}
83
84impl ApiError {
85    /// Create a new API error from code and message.
86    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
87        Self {
88            code: code.into(),
89            message: message.into(),
90        }
91    }
92
93    /// Parse API error from Kraken's error array format.
94    ///
95    /// Kraken returns errors as an array like `["EGeneral:Invalid arguments"]`
96    pub fn from_error_array(errors: &[String]) -> Option<Self> {
97        errors.first().map(|e| {
98            // Kraken errors are in format "ECategory:Message"
99            let parts: Vec<&str> = e.splitn(2, ':').collect();
100            if parts.len() == 2 {
101                Self::new(parts[0], parts[1])
102            } else {
103                Self::new("Unknown", e.clone())
104            }
105        })
106    }
107
108    /// Get the full error string in Kraken's format (code:message).
109    pub fn full_code(&self) -> String {
110        format!("{}:{}", self.code, self.message)
111    }
112
113    /// Check if this is a rate limit error.
114    pub fn is_rate_limit(&self) -> bool {
115        (self.code == "EAPI" && self.message.contains("Rate limit"))
116            || (self.code == "EOrder" && self.message.contains("Rate limit"))
117    }
118
119    /// Check if this is an invalid nonce error.
120    pub fn is_invalid_nonce(&self) -> bool {
121        self.code == "EAPI" && self.message.contains("Invalid nonce")
122    }
123
124    /// Check if this is an invalid key error.
125    pub fn is_invalid_key(&self) -> bool {
126        self.code == "EAPI" && self.message.contains("Invalid key")
127    }
128
129    /// Check if this is an invalid signature error.
130    pub fn is_invalid_signature(&self) -> bool {
131        self.code == "EAPI" && self.message.contains("Invalid signature")
132    }
133
134    /// Check if this is a permission denied error.
135    pub fn is_permission_denied(&self) -> bool {
136        self.code == "EGeneral" && self.message.contains("Permission denied")
137    }
138
139    /// Check if this is a service unavailable error.
140    pub fn is_service_unavailable(&self) -> bool {
141        self.code == "EService" && (self.message.contains("Unavailable") || self.message.contains("Busy"))
142    }
143}
144
145/// Known Kraken error codes for pattern matching.
146pub mod error_codes {
147    /// General errors
148    pub const INVALID_ARGUMENTS: &str = "EGeneral:Invalid arguments";
149    pub const PERMISSION_DENIED: &str = "EGeneral:Permission denied";
150    pub const UNKNOWN_METHOD: &str = "EGeneral:Unknown method";
151    pub const INTERNAL_ERROR: &str = "EGeneral:Internal error";
152
153    /// API errors
154    pub const INVALID_KEY: &str = "EAPI:Invalid key";
155    pub const INVALID_SIGNATURE: &str = "EAPI:Invalid signature";
156    pub const INVALID_NONCE: &str = "EAPI:Invalid nonce";
157    pub const RATE_LIMIT_EXCEEDED: &str = "EAPI:Rate limit exceeded";
158    pub const FEATURE_DISABLED: &str = "EAPI:Feature disabled";
159
160    /// Order errors
161    pub const ORDER_RATE_LIMIT: &str = "EOrder:Rate limit exceeded";
162    pub const INSUFFICIENT_FUNDS: &str = "EOrder:Insufficient funds";
163    pub const INVALID_ORDER: &str = "EOrder:Invalid order";
164    pub const ORDER_NOT_FOUND: &str = "EOrder:Unknown order";
165    pub const MARGIN_LIMIT: &str = "EOrder:Margin limit exceeded";
166
167    /// Service errors
168    pub const SERVICE_UNAVAILABLE: &str = "EService:Unavailable";
169    pub const SERVICE_BUSY: &str = "EService:Busy";
170    pub const SERVICE_MARKET_IN_CANCEL_ONLY: &str = "EService:Market in cancel_only mode";
171    pub const SERVICE_MARKET_IN_POST_ONLY: &str = "EService:Market in post_only mode";
172
173    /// Query errors
174    pub const UNKNOWN_ASSET_PAIR: &str = "EQuery:Unknown asset pair";
175    pub const UNKNOWN_ASSET: &str = "EQuery:Unknown asset";
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_api_error_from_array() {
184        let errors = vec!["EAPI:Invalid key".to_string()];
185        let error = ApiError::from_error_array(&errors).unwrap();
186        assert_eq!(error.code, "EAPI");
187        assert_eq!(error.message, "Invalid key");
188        assert!(error.is_invalid_key());
189    }
190
191    #[test]
192    fn test_api_error_display() {
193        let error = ApiError::new("EOrder", "Insufficient funds");
194        assert_eq!(error.to_string(), "EOrder: Insufficient funds");
195    }
196}