Skip to main content

rustrade_execution/client/hyperliquid/
error.rs

1//! Error mapping for Hyperliquid SDK errors to rustrade-execution error types.
2
3use crate::error::{
4    ApiError, ConnectivityError, UnindexedApiError, UnindexedClientError, UnindexedOrderError,
5};
6use rustrade_instrument::instrument::name::InstrumentNameExchange;
7
8/// Maps Hyperliquid SDK errors to `UnindexedClientError`.
9///
10/// The SDK uses its own error type internally, so we pattern-match on error messages
11/// to determine transient vs permanent errors.
12pub fn map_sdk_error(error: hyperliquid_rust_sdk::Error) -> UnindexedClientError {
13    let msg = error.to_string();
14    let msg_lower = msg.to_lowercase();
15
16    // Check for rate limiting first (transient) — avoid full msg allocation when possible
17    if msg_lower.contains("rate limit")
18        || msg_lower.contains("too many requests")
19        || msg_lower.contains("429")
20    {
21        return UnindexedClientError::Api(UnindexedApiError::RateLimit);
22    }
23
24    // Check for connectivity/network errors (transient)
25    // Includes HTTP 5xx server errors which are typically transient
26    if msg_lower.contains("connection")
27        || msg_lower.contains("timeout")
28        || msg_lower.contains("network")
29        || msg_lower.contains("dns")
30        || msg_lower.contains("tls")
31        || msg_lower.contains("ssl")
32        || msg_lower.contains(" 500")
33        || msg_lower.contains(" 502")
34        || msg_lower.contains(" 503")
35        || msg_lower.contains(" 504")
36        || msg_lower.contains("bad gateway")
37        || msg_lower.contains("service unavailable")
38        || msg_lower.contains("gateway timeout")
39    {
40        return UnindexedClientError::Connectivity(ConnectivityError::Socket(msg));
41    }
42
43    // Check for authentication errors (permanent)
44    // Includes "eip712" for SDK's Error::Eip712 variant (EIP-712 signing failures)
45    if msg_lower.contains("signature")
46        || msg_lower.contains("unauthorized")
47        || msg_lower.contains("invalid key")
48        || msg_lower.contains("authentication")
49        || msg_lower.contains("eip712")
50    {
51        return UnindexedClientError::Api(UnindexedApiError::Unauthenticated(msg));
52    }
53
54    // Default to internal error (assumed non-transient)
55    UnindexedClientError::Internal(msg)
56}
57
58/// Maps Hyperliquid order placement errors to `UnindexedOrderError`.
59///
60/// Hyperliquid returns specific error codes/messages for order rejections.
61pub fn map_order_error(
62    error: hyperliquid_rust_sdk::Error,
63    instrument: &InstrumentNameExchange,
64) -> UnindexedOrderError {
65    let msg = error.to_string();
66    let msg_lower = msg.to_lowercase();
67
68    // Instrument not found / invalid
69    if msg_lower.contains("unknown")
70        || msg_lower.contains("not found")
71        || msg_lower.contains("invalid symbol")
72    {
73        return UnindexedOrderError::Rejected(ApiError::InstrumentInvalid(instrument.clone(), msg));
74    }
75
76    // All other rejections (insufficient balance, post-only crossed, price precision, etc.)
77    // Use OrderRejected since we can't cleanly extract the specific error type without
78    // more structured error responses from the SDK.
79    UnindexedOrderError::Rejected(ApiError::OrderRejected(msg))
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use hyperliquid_rust_sdk::Error;
86
87    fn make_sdk_error(msg: &str) -> Error {
88        Error::GenericReader(msg.to_string())
89    }
90
91    #[test]
92    fn test_map_sdk_error_connectivity() {
93        let err = map_sdk_error(make_sdk_error("connection refused"));
94        assert!(matches!(err, UnindexedClientError::Connectivity(_)));
95
96        let err = map_sdk_error(make_sdk_error("request timeout occurred"));
97        assert!(matches!(err, UnindexedClientError::Connectivity(_)));
98
99        let err = map_sdk_error(make_sdk_error("DNS lookup failed"));
100        assert!(matches!(err, UnindexedClientError::Connectivity(_)));
101
102        let err = map_sdk_error(make_sdk_error("TLS handshake error"));
103        assert!(matches!(err, UnindexedClientError::Connectivity(_)));
104    }
105
106    #[test]
107    fn test_map_sdk_error_5xx_transient() {
108        // HTTP 5xx errors should be transient (server-side failures)
109        let err = map_sdk_error(make_sdk_error("HTTP 500 Internal Server Error"));
110        assert!(matches!(err, UnindexedClientError::Connectivity(_)));
111
112        let err = map_sdk_error(make_sdk_error("502 Bad Gateway"));
113        assert!(matches!(err, UnindexedClientError::Connectivity(_)));
114
115        let err = map_sdk_error(make_sdk_error("503 Service Unavailable"));
116        assert!(matches!(err, UnindexedClientError::Connectivity(_)));
117
118        let err = map_sdk_error(make_sdk_error("504 Gateway Timeout"));
119        assert!(matches!(err, UnindexedClientError::Connectivity(_)));
120    }
121
122    #[test]
123    fn test_map_sdk_error_rate_limit() {
124        let err = map_sdk_error(make_sdk_error("rate limit exceeded"));
125        assert!(matches!(
126            err,
127            UnindexedClientError::Api(UnindexedApiError::RateLimit)
128        ));
129
130        let err = map_sdk_error(make_sdk_error("too many requests"));
131        assert!(matches!(
132            err,
133            UnindexedClientError::Api(UnindexedApiError::RateLimit)
134        ));
135
136        let err = map_sdk_error(make_sdk_error("HTTP 429"));
137        assert!(matches!(
138            err,
139            UnindexedClientError::Api(UnindexedApiError::RateLimit)
140        ));
141    }
142
143    #[test]
144    fn test_map_sdk_error_auth() {
145        let err = map_sdk_error(make_sdk_error("invalid signature"));
146        assert!(matches!(
147            err,
148            UnindexedClientError::Api(UnindexedApiError::Unauthenticated(_))
149        ));
150
151        let err = map_sdk_error(make_sdk_error("unauthorized access"));
152        assert!(matches!(
153            err,
154            UnindexedClientError::Api(UnindexedApiError::Unauthenticated(_))
155        ));
156
157        // SDK's Error::Eip712 variant formats as "Error from Eip712 struct: ..."
158        let err = map_sdk_error(make_sdk_error("Error from Eip712 struct: invalid key"));
159        assert!(matches!(
160            err,
161            UnindexedClientError::Api(UnindexedApiError::Unauthenticated(_))
162        ));
163    }
164
165    #[test]
166    fn test_map_sdk_error_internal() {
167        let err = map_sdk_error(make_sdk_error("some unknown error"));
168        assert!(matches!(err, UnindexedClientError::Internal(_)));
169    }
170
171    #[test]
172    fn test_map_order_error_instrument_invalid() {
173        let instrument = InstrumentNameExchange::from("INVALID-USD-PERP");
174
175        let err = map_order_error(make_sdk_error("unknown asset"), &instrument);
176        assert!(matches!(
177            err,
178            UnindexedOrderError::Rejected(ApiError::InstrumentInvalid(_, _))
179        ));
180
181        let err = map_order_error(make_sdk_error("Asset not found"), &instrument);
182        assert!(matches!(
183            err,
184            UnindexedOrderError::Rejected(ApiError::InstrumentInvalid(_, _))
185        ));
186    }
187
188    #[test]
189    fn test_map_order_error_rejected() {
190        let instrument = InstrumentNameExchange::from("BTC-USD-PERP");
191
192        let err = map_order_error(make_sdk_error("insufficient margin"), &instrument);
193        assert!(matches!(
194            err,
195            UnindexedOrderError::Rejected(ApiError::OrderRejected(_))
196        ));
197
198        let err = map_order_error(make_sdk_error("price precision too high"), &instrument);
199        assert!(matches!(
200            err,
201            UnindexedOrderError::Rejected(ApiError::OrderRejected(_))
202        ));
203    }
204}