Skip to main content

hyperliquid_sdk/
error.rs

1//! Error types for the Hyperliquid SDK.
2//!
3//! Provides comprehensive error handling with actionable guidance.
4
5use serde::{Deserialize, Serialize};
6use std::fmt;
7use thiserror::Error;
8
9/// Result type for the SDK
10pub type Result<T> = std::result::Result<T, Error>;
11
12/// SDK error types
13#[derive(Error, Debug)]
14pub enum Error {
15    /// Configuration error
16    #[error("Configuration error: {0}")]
17    ConfigError(String),
18
19    /// Network/HTTP error
20    #[error("Network error: {0}")]
21    NetworkError(String),
22
23    /// JSON serialization/deserialization error
24    #[error("JSON error: {0}")]
25    JsonError(String),
26
27    /// Signing error
28    #[error("Signing error: {0}")]
29    SigningError(String),
30
31    /// Validation error
32    #[error("Validation error: {0}")]
33    ValidationError(String),
34
35    /// Order-related error
36    #[error("Order error: {0}")]
37    OrderError(String),
38
39    /// API error from Hyperliquid
40    #[error("{message}")]
41    ApiError {
42        code: ErrorCode,
43        message: String,
44        guidance: String,
45        raw: Option<String>,
46    },
47
48    /// Builder fee approval required
49    #[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    /// No position found
58    #[error("No position found for {asset}")]
59    NoPosition { asset: String },
60
61    /// Order not found
62    #[error("Order {oid} not found")]
63    OrderNotFound { oid: u64 },
64
65    /// Rate limited
66    #[error("Rate limited: {message}")]
67    RateLimited { message: String },
68
69    /// Geo-blocked
70    #[error("Access denied from your region")]
71    GeoBlocked,
72
73    /// WebSocket error
74    #[error("WebSocket error: {0}")]
75    WebSocketError(String),
76
77    /// gRPC error
78    #[error("gRPC error: {0}")]
79    GrpcError(String),
80}
81
82impl Error {
83    /// Create an API error from a raw error string
84    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    /// Get the semantic error code
95    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    /// Get actionable guidance for this error
115    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/// Semantic error codes
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
164#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
165pub enum ErrorCode {
166    // SDK errors
167    ConfigError,
168    NetworkError,
169    JsonError,
170    SignatureInvalid,
171    InvalidParams,
172    OrderError,
173    WebSocketError,
174    GrpcError,
175
176    // API errors
177    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
230/// Parse a raw Hyperliquid error into (code, message, guidance)
231fn parse_hl_error(raw: &str) -> (ErrorCode, String, String) {
232    let lower = raw.to_lowercase();
233
234    // Pattern match on error messages
235    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
316// Implement From traits for common error types
317
318impl 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}