rustrade_execution/client/hyperliquid/
error.rs1use crate::error::{
4 ApiError, ConnectivityError, UnindexedApiError, UnindexedClientError, UnindexedOrderError,
5};
6use rustrade_instrument::instrument::name::InstrumentNameExchange;
7use smol_str::SmolStr;
8
9#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
11#[non_exhaustive]
12pub enum HyperliquidConnectError {
13 #[error("{0}")]
16 Connectivity(#[from] ConnectivityError),
17
18 #[error("Hyperliquid lists no perpetual DEX named {0:?} on this network")]
21 UnknownDex(SmolStr),
22
23 #[error("Hyperliquid perpetual DEX metadata: {0}")]
27 Metadata(String),
28}
29
30impl From<UnindexedClientError> for HyperliquidConnectError {
31 fn from(error: UnindexedClientError) -> Self {
34 match error {
35 UnindexedClientError::Connectivity(error) => Self::Connectivity(error),
36 UnindexedClientError::Api(UnindexedApiError::RateLimit) => {
37 Self::Connectivity(ConnectivityError::Socket(error.to_string()))
38 }
39 other => Self::Metadata(other.to_string()),
40 }
41 }
42}
43
44pub fn map_sdk_error(error: hyperliquid_rust_sdk::Error) -> UnindexedClientError {
49 let msg = error.to_string();
50 let msg_lower = msg.to_lowercase();
51
52 if msg_lower.contains("rate limit")
54 || msg_lower.contains("too many requests")
55 || msg_lower.contains("429")
56 {
57 return UnindexedClientError::Api(UnindexedApiError::RateLimit);
58 }
59
60 if msg_lower.contains("connection")
63 || msg_lower.contains("timeout")
64 || msg_lower.contains("network")
65 || msg_lower.contains("dns")
66 || msg_lower.contains("tls")
67 || msg_lower.contains("ssl")
68 || msg_lower.contains(" 500")
69 || msg_lower.contains(" 502")
70 || msg_lower.contains(" 503")
71 || msg_lower.contains(" 504")
72 || msg_lower.contains("bad gateway")
73 || msg_lower.contains("service unavailable")
74 || msg_lower.contains("gateway timeout")
75 {
76 return UnindexedClientError::Connectivity(ConnectivityError::Socket(msg));
77 }
78
79 if msg_lower.contains("signature")
82 || msg_lower.contains("unauthorized")
83 || msg_lower.contains("invalid key")
84 || msg_lower.contains("authentication")
85 || msg_lower.contains("eip712")
86 {
87 return UnindexedClientError::Api(UnindexedApiError::Unauthenticated(msg));
88 }
89
90 UnindexedClientError::Internal(msg)
92}
93
94pub fn map_order_error(
98 error: hyperliquid_rust_sdk::Error,
99 instrument: &InstrumentNameExchange,
100) -> UnindexedOrderError {
101 let msg = error.to_string();
102 let msg_lower = msg.to_lowercase();
103
104 if msg_lower.contains("unknown")
106 || msg_lower.contains("not found")
107 || msg_lower.contains("invalid symbol")
108 {
109 return UnindexedOrderError::Rejected(ApiError::InstrumentInvalid(instrument.clone(), msg));
110 }
111
112 UnindexedOrderError::Rejected(ApiError::OrderRejected(msg))
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use hyperliquid_rust_sdk::Error;
122
123 fn make_sdk_error(msg: &str) -> Error {
124 Error::GenericReader(msg.to_string())
125 }
126
127 #[test]
128 fn test_map_sdk_error_connectivity() {
129 let err = map_sdk_error(make_sdk_error("connection refused"));
130 assert!(matches!(err, UnindexedClientError::Connectivity(_)));
131
132 let err = map_sdk_error(make_sdk_error("request timeout occurred"));
133 assert!(matches!(err, UnindexedClientError::Connectivity(_)));
134
135 let err = map_sdk_error(make_sdk_error("DNS lookup failed"));
136 assert!(matches!(err, UnindexedClientError::Connectivity(_)));
137
138 let err = map_sdk_error(make_sdk_error("TLS handshake error"));
139 assert!(matches!(err, UnindexedClientError::Connectivity(_)));
140 }
141
142 #[test]
143 fn test_map_sdk_error_5xx_transient() {
144 let err = map_sdk_error(make_sdk_error("HTTP 500 Internal Server Error"));
146 assert!(matches!(err, UnindexedClientError::Connectivity(_)));
147
148 let err = map_sdk_error(make_sdk_error("502 Bad Gateway"));
149 assert!(matches!(err, UnindexedClientError::Connectivity(_)));
150
151 let err = map_sdk_error(make_sdk_error("503 Service Unavailable"));
152 assert!(matches!(err, UnindexedClientError::Connectivity(_)));
153
154 let err = map_sdk_error(make_sdk_error("504 Gateway Timeout"));
155 assert!(matches!(err, UnindexedClientError::Connectivity(_)));
156 }
157
158 #[test]
159 fn test_map_sdk_error_rate_limit() {
160 let err = map_sdk_error(make_sdk_error("rate limit exceeded"));
161 assert!(matches!(
162 err,
163 UnindexedClientError::Api(UnindexedApiError::RateLimit)
164 ));
165
166 let err = map_sdk_error(make_sdk_error("too many requests"));
167 assert!(matches!(
168 err,
169 UnindexedClientError::Api(UnindexedApiError::RateLimit)
170 ));
171
172 let err = map_sdk_error(make_sdk_error("HTTP 429"));
173 assert!(matches!(
174 err,
175 UnindexedClientError::Api(UnindexedApiError::RateLimit)
176 ));
177 }
178
179 #[test]
180 fn test_map_sdk_error_auth() {
181 let err = map_sdk_error(make_sdk_error("invalid signature"));
182 assert!(matches!(
183 err,
184 UnindexedClientError::Api(UnindexedApiError::Unauthenticated(_))
185 ));
186
187 let err = map_sdk_error(make_sdk_error("unauthorized access"));
188 assert!(matches!(
189 err,
190 UnindexedClientError::Api(UnindexedApiError::Unauthenticated(_))
191 ));
192
193 let err = map_sdk_error(make_sdk_error("Error from Eip712 struct: invalid key"));
195 assert!(matches!(
196 err,
197 UnindexedClientError::Api(UnindexedApiError::Unauthenticated(_))
198 ));
199 }
200
201 #[test]
202 fn test_map_sdk_error_internal() {
203 let err = map_sdk_error(make_sdk_error("some unknown error"));
204 assert!(matches!(err, UnindexedClientError::Internal(_)));
205 }
206
207 #[test]
208 fn test_map_order_error_instrument_invalid() {
209 let instrument = InstrumentNameExchange::from("INVALID-USDC-PERP");
210
211 let err = map_order_error(make_sdk_error("unknown asset"), &instrument);
212 assert!(matches!(
213 err,
214 UnindexedOrderError::Rejected(ApiError::InstrumentInvalid(_, _))
215 ));
216
217 let err = map_order_error(make_sdk_error("Asset not found"), &instrument);
218 assert!(matches!(
219 err,
220 UnindexedOrderError::Rejected(ApiError::InstrumentInvalid(_, _))
221 ));
222 }
223
224 #[test]
225 fn test_map_order_error_rejected() {
226 let instrument = InstrumentNameExchange::from("BTC-USDC-PERP");
227
228 let err = map_order_error(make_sdk_error("insufficient margin"), &instrument);
229 assert!(matches!(
230 err,
231 UnindexedOrderError::Rejected(ApiError::OrderRejected(_))
232 ));
233
234 let err = map_order_error(make_sdk_error("price precision too high"), &instrument);
235 assert!(matches!(
236 err,
237 UnindexedOrderError::Rejected(ApiError::OrderRejected(_))
238 ));
239 }
240
241 #[test]
242 fn a_connect_error_is_transient_only_when_the_request_was() {
243 let socket = ConnectivityError::Socket("reset".to_owned());
244 assert_eq!(
245 HyperliquidConnectError::from(UnindexedClientError::Connectivity(socket.clone())),
246 HyperliquidConnectError::Connectivity(socket)
247 );
248 assert!(matches!(
249 HyperliquidConnectError::from(UnindexedClientError::Api(UnindexedApiError::RateLimit)),
250 HyperliquidConnectError::Connectivity(ConnectivityError::Socket(_))
251 ));
252 assert!(matches!(
253 HyperliquidConnectError::from(UnindexedClientError::Internal("bad json".to_owned())),
254 HyperliquidConnectError::Metadata(reason) if reason.contains("bad json")
255 ));
256 }
257}