ccxt_exchanges/hyperliquid/
error.rs

1//! HyperLiquid error handling module.
2//!
3//! Provides error types and parsing for HyperLiquid API responses.
4
5use ccxt_core::Error;
6use serde_json::Value;
7
8/// HyperLiquid-specific error codes.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum HyperLiquidErrorCode {
11    /// Invalid signature
12    InvalidSignature,
13    /// Insufficient margin/balance
14    InsufficientMargin,
15    /// Order not found
16    OrderNotFound,
17    /// Invalid parameter
18    InvalidParameter,
19    /// Rate limited
20    RateLimited,
21    /// Server error
22    ServerError,
23    /// User not found
24    UserNotFound,
25    /// Invalid asset
26    InvalidAsset,
27    /// Position not found
28    PositionNotFound,
29    /// Order would cross
30    OrderWouldCross,
31    /// Reduce only violation
32    ReduceOnlyViolation,
33    /// Unknown error
34    Unknown(String),
35}
36
37impl std::fmt::Display for HyperLiquidErrorCode {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match self {
40            Self::InvalidSignature => write!(f, "Invalid signature"),
41            Self::InsufficientMargin => write!(f, "Insufficient margin"),
42            Self::OrderNotFound => write!(f, "Order not found"),
43            Self::InvalidParameter => write!(f, "Invalid parameter"),
44            Self::RateLimited => write!(f, "Rate limited"),
45            Self::ServerError => write!(f, "Server error"),
46            Self::UserNotFound => write!(f, "User not found"),
47            Self::InvalidAsset => write!(f, "Invalid asset"),
48            Self::PositionNotFound => write!(f, "Position not found"),
49            Self::OrderWouldCross => write!(f, "Order would cross"),
50            Self::ReduceOnlyViolation => write!(f, "Reduce only violation"),
51            Self::Unknown(msg) => write!(f, "{}", msg),
52        }
53    }
54}
55
56impl From<HyperLiquidErrorCode> for Error {
57    fn from(code: HyperLiquidErrorCode) -> Self {
58        match code {
59            HyperLiquidErrorCode::InvalidSignature => Error::authentication("Invalid signature"),
60            HyperLiquidErrorCode::InsufficientMargin => {
61                Error::insufficient_balance("Insufficient margin")
62            }
63            HyperLiquidErrorCode::OrderNotFound => Error::invalid_request("Order not found"),
64            HyperLiquidErrorCode::InvalidParameter => Error::invalid_request("Invalid parameter"),
65            HyperLiquidErrorCode::RateLimited => Error::rate_limit("Rate limited", None),
66            HyperLiquidErrorCode::ServerError => Error::exchange("-1", "Server error"),
67            HyperLiquidErrorCode::UserNotFound => Error::authentication("User not found"),
68            HyperLiquidErrorCode::InvalidAsset => Error::bad_symbol("Invalid asset"),
69            HyperLiquidErrorCode::PositionNotFound => Error::invalid_request("Position not found"),
70            HyperLiquidErrorCode::OrderWouldCross => Error::invalid_request("Order would cross"),
71            HyperLiquidErrorCode::ReduceOnlyViolation => {
72                Error::invalid_request("Reduce only violation")
73            }
74            HyperLiquidErrorCode::Unknown(msg) => Error::exchange("-1", &msg),
75        }
76    }
77}
78
79/// Checks if a response is an error response.
80///
81/// HyperLiquid returns errors in various formats:
82/// - `{"error": "message"}`
83/// - `{"status": "err", "response": "message"}`
84///
85/// # Arguments
86///
87/// * `response` - The JSON response to check.
88///
89/// # Returns
90///
91/// `true` if the response indicates an error.
92pub fn is_error_response(response: &Value) -> bool {
93    // Check for explicit error field
94    if response.get("error").is_some() {
95        return true;
96    }
97
98    // Check for status: err
99    if let Some(status) = response.get("status") {
100        if status.as_str() == Some("err") {
101            return true;
102        }
103    }
104
105    // Check for response containing error message
106    if let Some(resp) = response.get("response") {
107        if let Some(s) = resp.as_str() {
108            if s.contains("error") || s.contains("Error") || s.contains("failed") {
109                return true;
110            }
111        }
112    }
113
114    false
115}
116
117/// Parses an error response into a ccxt_core::Error.
118///
119/// # Arguments
120///
121/// * `response` - The JSON error response.
122///
123/// # Returns
124///
125/// A ccxt_core::Error with appropriate type and message.
126pub fn parse_error(response: &Value) -> Error {
127    // Try to extract error message
128    let message = extract_error_message(response);
129
130    // Map to error code
131    let code = map_error_message(&message);
132
133    code.into()
134}
135
136/// Extracts the error message from a response.
137fn extract_error_message(response: &Value) -> String {
138    // Try "error" field
139    if let Some(error) = response.get("error") {
140        if let Some(s) = error.as_str() {
141            return s.to_string();
142        }
143    }
144
145    // Try "response" field
146    if let Some(resp) = response.get("response") {
147        if let Some(s) = resp.as_str() {
148            return s.to_string();
149        }
150    }
151
152    // Try "message" field
153    if let Some(msg) = response.get("message") {
154        if let Some(s) = msg.as_str() {
155            return s.to_string();
156        }
157    }
158
159    "Unknown error".to_string()
160}
161
162/// Maps an error message to an error code.
163fn map_error_message(message: &str) -> HyperLiquidErrorCode {
164    let lower = message.to_lowercase();
165
166    if lower.contains("signature") || lower.contains("auth") || lower.contains("unauthorized") {
167        HyperLiquidErrorCode::InvalidSignature
168    } else if lower.contains("insufficient")
169        || lower.contains("margin")
170        || lower.contains("balance")
171    {
172        HyperLiquidErrorCode::InsufficientMargin
173    } else if lower.contains("order") && lower.contains("not found") {
174        HyperLiquidErrorCode::OrderNotFound
175    } else if lower.contains("rate") || lower.contains("limit") || lower.contains("throttle") {
176        HyperLiquidErrorCode::RateLimited
177    } else if lower.contains("user") && lower.contains("not found") {
178        HyperLiquidErrorCode::UserNotFound
179    } else if lower.contains("asset") && (lower.contains("invalid") || lower.contains("not found"))
180    {
181        HyperLiquidErrorCode::InvalidAsset
182    } else if lower.contains("position") && lower.contains("not found") {
183        HyperLiquidErrorCode::PositionNotFound
184    } else if lower.contains("cross") || lower.contains("would cross") {
185        HyperLiquidErrorCode::OrderWouldCross
186    } else if lower.contains("reduce only") || lower.contains("reduce-only") {
187        HyperLiquidErrorCode::ReduceOnlyViolation
188    } else if lower.contains("invalid") || lower.contains("parameter") {
189        HyperLiquidErrorCode::InvalidParameter
190    } else if lower.contains("server") || lower.contains("internal") {
191        HyperLiquidErrorCode::ServerError
192    } else {
193        HyperLiquidErrorCode::Unknown(message.to_string())
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use serde_json::json;
201
202    #[test]
203    fn test_is_error_response_with_error_field() {
204        let response = json!({"error": "Invalid signature"});
205        assert!(is_error_response(&response));
206    }
207
208    #[test]
209    fn test_is_error_response_with_status_err() {
210        let response = json!({"status": "err", "response": "Something went wrong"});
211        assert!(is_error_response(&response));
212    }
213
214    #[test]
215    fn test_is_error_response_success() {
216        let response = json!({"status": "ok", "response": {"data": []}});
217        assert!(!is_error_response(&response));
218    }
219
220    #[test]
221    fn test_parse_error_insufficient_margin() {
222        let response = json!({"error": "Insufficient margin for order"});
223        let error = parse_error(&response);
224        assert!(error.to_string().contains("Insufficient"));
225    }
226
227    #[test]
228    fn test_parse_error_invalid_signature() {
229        let response = json!({"error": "Invalid signature"});
230        let error = parse_error(&response);
231        assert!(error.to_string().contains("signature") || error.to_string().contains("Signature"));
232    }
233
234    #[test]
235    fn test_parse_error_rate_limited() {
236        let response = json!({"error": "Rate limit exceeded"});
237        let error = parse_error(&response);
238        assert!(error.to_string().contains("Rate") || error.to_string().contains("rate"));
239    }
240
241    #[test]
242    fn test_parse_error_unknown() {
243        let response = json!({"error": "Some unknown error occurred"});
244        let error = parse_error(&response);
245        assert!(error.to_string().contains("unknown") || error.to_string().contains("Unknown"));
246    }
247
248    #[test]
249    fn test_error_code_display() {
250        assert_eq!(
251            HyperLiquidErrorCode::InvalidSignature.to_string(),
252            "Invalid signature"
253        );
254        assert_eq!(
255            HyperLiquidErrorCode::InsufficientMargin.to_string(),
256            "Insufficient margin"
257        );
258    }
259
260    #[test]
261    fn test_error_code_into_ccxt_error() {
262        let code = HyperLiquidErrorCode::InsufficientMargin;
263        let error: Error = code.into();
264        // Just verify it converts without panic
265        let _ = error.to_string();
266    }
267}