1use serde::{Deserialize, Serialize};
6use std::fmt;
7use thiserror::Error;
8
9pub type Result<T> = std::result::Result<T, Error>;
11
12#[derive(Error, Debug)]
14pub enum Error {
15 #[error("Configuration error: {0}")]
17 ConfigError(String),
18
19 #[error("Network error: {0}")]
21 NetworkError(String),
22
23 #[error("JSON error: {0}")]
25 JsonError(String),
26
27 #[error("Signing error: {0}")]
29 SigningError(String),
30
31 #[error("Validation error: {0}")]
33 ValidationError(String),
34
35 #[error("Order error: {0}")]
37 OrderError(String),
38
39 #[error("{message}")]
41 ApiError {
42 code: ErrorCode,
43 message: String,
44 guidance: String,
45 raw: Option<String>,
46 },
47
48 #[error("Builder fee approval required")]
50 ApprovalRequired {
51 user: String,
52 builder: String,
53 max_fee_rate: String,
54 approval_hash: Option<String>,
55 },
56
57 #[error("No position found for {asset}")]
59 NoPosition { asset: String },
60
61 #[error("Order {oid} not found")]
63 OrderNotFound { oid: u64 },
64
65 #[error("Rate limited: {message}")]
67 RateLimited { message: String },
68
69 #[error("Access denied from your region")]
71 GeoBlocked,
72
73 #[error("WebSocket error: {0}")]
75 WebSocketError(String),
76
77 #[error("gRPC error: {0}")]
79 GrpcError(String),
80}
81
82impl Error {
83 pub fn from_api_error(raw: &str) -> Self {
85 let (code, message, guidance) = parse_hl_error(raw);
86 Self::ApiError {
87 code,
88 message,
89 guidance,
90 raw: Some(raw.to_string()),
91 }
92 }
93
94 pub fn code(&self) -> ErrorCode {
96 match self {
97 Error::ConfigError(_) => ErrorCode::ConfigError,
98 Error::NetworkError(_) => ErrorCode::NetworkError,
99 Error::JsonError(_) => ErrorCode::JsonError,
100 Error::SigningError(_) => ErrorCode::SignatureInvalid,
101 Error::ValidationError(_) => ErrorCode::InvalidParams,
102 Error::OrderError(_) => ErrorCode::OrderError,
103 Error::ApiError { code, .. } => *code,
104 Error::ApprovalRequired { .. } => ErrorCode::NotApproved,
105 Error::NoPosition { .. } => ErrorCode::NoPosition,
106 Error::OrderNotFound { .. } => ErrorCode::OrderNotFound,
107 Error::RateLimited { .. } => ErrorCode::RateLimited,
108 Error::GeoBlocked => ErrorCode::GeoBlocked,
109 Error::WebSocketError(_) => ErrorCode::WebSocketError,
110 Error::GrpcError(_) => ErrorCode::GrpcError,
111 }
112 }
113
114 pub fn guidance(&self) -> &str {
116 match self {
117 Error::ConfigError(_) => {
118 "Check your SDK configuration: endpoint URL, private key format, and chain selection."
119 }
120 Error::NetworkError(_) => {
121 "Network request failed. Check your internet connection and try again."
122 }
123 Error::JsonError(_) => {
124 "JSON parsing failed. This may indicate an API change or invalid response."
125 }
126 Error::SigningError(_) => {
127 "Signature verification failed. Ensure you're using the correct private key."
128 }
129 Error::ValidationError(_) => {
130 "Order validation failed. Check size, price, and asset parameters."
131 }
132 Error::OrderError(_) => {
133 "Order operation failed. Check the order state and try again."
134 }
135 Error::ApiError { guidance, .. } => guidance,
136 Error::ApprovalRequired { .. } => {
137 "You need to approve the builder fee before trading. \
138 Call sdk.approve_builder_fee() or visit /approve in a browser."
139 }
140 Error::NoPosition { .. } => {
141 "No open position found. Check your positions with sdk.info().clearinghouse_state()."
142 }
143 Error::OrderNotFound { .. } => {
144 "Order not found. It may have been filled or cancelled."
145 }
146 Error::RateLimited { .. } => {
147 "You've exceeded the rate limit. Wait a moment and try again."
148 }
149 Error::GeoBlocked => {
150 "Access is restricted from your region."
151 }
152 Error::WebSocketError(_) => {
153 "WebSocket connection failed. Check your endpoint and network connection."
154 }
155 Error::GrpcError(_) => {
156 "gRPC connection failed. Ensure gRPC port 10000 is accessible."
157 }
158 }
159 }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
164#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
165pub enum ErrorCode {
166 ConfigError,
168 NetworkError,
169 JsonError,
170 SignatureInvalid,
171 InvalidParams,
172 OrderError,
173 WebSocketError,
174 GrpcError,
175
176 NotApproved,
178 FeeExceedsApproved,
179 FeeExceedsMax,
180 InsufficientMargin,
181 LeverageConflict,
182 InvalidPriceTick,
183 InvalidSizeDecimals,
184 MaxOrdersExceeded,
185 ReduceOnlyViolation,
186 DuplicateOrder,
187 UserNotFound,
188 MustDeposit,
189 InvalidNonce,
190 NoPosition,
191 OrderNotFound,
192 RateLimited,
193 GeoBlocked,
194 Unknown,
195}
196
197impl fmt::Display for ErrorCode {
198 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199 match self {
200 ErrorCode::ConfigError => write!(f, "CONFIG_ERROR"),
201 ErrorCode::NetworkError => write!(f, "NETWORK_ERROR"),
202 ErrorCode::JsonError => write!(f, "JSON_ERROR"),
203 ErrorCode::SignatureInvalid => write!(f, "SIGNATURE_INVALID"),
204 ErrorCode::InvalidParams => write!(f, "INVALID_PARAMS"),
205 ErrorCode::OrderError => write!(f, "ORDER_ERROR"),
206 ErrorCode::WebSocketError => write!(f, "WEBSOCKET_ERROR"),
207 ErrorCode::GrpcError => write!(f, "GRPC_ERROR"),
208 ErrorCode::NotApproved => write!(f, "NOT_APPROVED"),
209 ErrorCode::FeeExceedsApproved => write!(f, "FEE_EXCEEDS_APPROVED"),
210 ErrorCode::FeeExceedsMax => write!(f, "FEE_EXCEEDS_MAX"),
211 ErrorCode::InsufficientMargin => write!(f, "INSUFFICIENT_MARGIN"),
212 ErrorCode::LeverageConflict => write!(f, "LEVERAGE_CONFLICT"),
213 ErrorCode::InvalidPriceTick => write!(f, "INVALID_PRICE_TICK"),
214 ErrorCode::InvalidSizeDecimals => write!(f, "INVALID_SIZE_DECIMALS"),
215 ErrorCode::MaxOrdersExceeded => write!(f, "MAX_ORDERS_EXCEEDED"),
216 ErrorCode::ReduceOnlyViolation => write!(f, "REDUCE_ONLY_VIOLATION"),
217 ErrorCode::DuplicateOrder => write!(f, "DUPLICATE_ORDER"),
218 ErrorCode::UserNotFound => write!(f, "USER_NOT_FOUND"),
219 ErrorCode::MustDeposit => write!(f, "MUST_DEPOSIT"),
220 ErrorCode::InvalidNonce => write!(f, "INVALID_NONCE"),
221 ErrorCode::NoPosition => write!(f, "NO_POSITION"),
222 ErrorCode::OrderNotFound => write!(f, "ORDER_NOT_FOUND"),
223 ErrorCode::RateLimited => write!(f, "RATE_LIMITED"),
224 ErrorCode::GeoBlocked => write!(f, "GEO_BLOCKED"),
225 ErrorCode::Unknown => write!(f, "UNKNOWN"),
226 }
227 }
228}
229
230fn parse_hl_error(raw: &str) -> (ErrorCode, String, String) {
232 let lower = raw.to_lowercase();
233
234 if lower.contains("insufficient margin") || lower.contains("not enough margin") {
236 (
237 ErrorCode::InsufficientMargin,
238 "Insufficient margin for this order".to_string(),
239 "Reduce position size or add more margin to your account.".to_string(),
240 )
241 } else if lower.contains("leverage") && lower.contains("conflict") {
242 (
243 ErrorCode::LeverageConflict,
244 "Leverage conflict with existing position".to_string(),
245 "Update leverage before placing this order.".to_string(),
246 )
247 } else if lower.contains("price") && (lower.contains("tick") || lower.contains("decimal")) {
248 (
249 ErrorCode::InvalidPriceTick,
250 "Invalid price tick size".to_string(),
251 "Round your price to the valid tick size for this asset.".to_string(),
252 )
253 } else if lower.contains("size") && lower.contains("decimal") {
254 (
255 ErrorCode::InvalidSizeDecimals,
256 "Invalid size decimals".to_string(),
257 "Round your size to the valid decimal places for this asset.".to_string(),
258 )
259 } else if lower.contains("max") && lower.contains("order") {
260 (
261 ErrorCode::MaxOrdersExceeded,
262 "Maximum orders exceeded".to_string(),
263 "Cancel some existing orders before placing new ones.".to_string(),
264 )
265 } else if lower.contains("reduce only") {
266 (
267 ErrorCode::ReduceOnlyViolation,
268 "Reduce-only order would increase position".to_string(),
269 "Check your position direction and order side.".to_string(),
270 )
271 } else if lower.contains("duplicate") {
272 (
273 ErrorCode::DuplicateOrder,
274 "Duplicate order".to_string(),
275 "This exact order already exists. Use a different cloid if intentional.".to_string(),
276 )
277 } else if lower.contains("user not found") || lower.contains("unknown user") {
278 (
279 ErrorCode::UserNotFound,
280 "User not found".to_string(),
281 "Ensure the address is correct and has been used on Hyperliquid.".to_string(),
282 )
283 } else if lower.contains("must deposit") || lower.contains("no deposit") {
284 (
285 ErrorCode::MustDeposit,
286 "Account must deposit first".to_string(),
287 "Deposit USDC to your Hyperliquid account before trading.".to_string(),
288 )
289 } else if lower.contains("nonce") {
290 (
291 ErrorCode::InvalidNonce,
292 "Invalid nonce".to_string(),
293 "Retry the request - the SDK will generate a fresh nonce.".to_string(),
294 )
295 } else if lower.contains("rate limit") {
296 (
297 ErrorCode::RateLimited,
298 "Rate limited".to_string(),
299 "Wait a moment and try again. Consider using reserve_request_weight().".to_string(),
300 )
301 } else if lower.contains("geo") || lower.contains("blocked") || lower.contains("restricted") {
302 (
303 ErrorCode::GeoBlocked,
304 "Access denied from your region".to_string(),
305 "Trading is not available in your jurisdiction.".to_string(),
306 )
307 } else {
308 (
309 ErrorCode::Unknown,
310 raw.to_string(),
311 "An unexpected error occurred. Check the raw error message for details.".to_string(),
312 )
313 }
314}
315
316impl From<reqwest::Error> for Error {
319 fn from(err: reqwest::Error) -> Self {
320 Error::NetworkError(err.to_string())
321 }
322}
323
324impl From<serde_json::Error> for Error {
325 fn from(err: serde_json::Error) -> Self {
326 Error::JsonError(err.to_string())
327 }
328}
329
330impl From<url::ParseError> for Error {
331 fn from(err: url::ParseError) -> Self {
332 Error::ConfigError(format!("Invalid URL: {}", err))
333 }
334}
335
336impl From<std::env::VarError> for Error {
337 fn from(err: std::env::VarError) -> Self {
338 Error::ConfigError(format!("Environment variable error: {}", err))
339 }
340}
341
342impl From<alloy::signers::local::LocalSignerError> for Error {
343 fn from(err: alloy::signers::local::LocalSignerError) -> Self {
344 Error::SigningError(err.to_string())
345 }
346}