ccxt_exchanges/hyperliquid/mod.rs
1//! HyperLiquid exchange implementation.
2//!
3//! HyperLiquid is a decentralized perpetual futures exchange built on its own L1 blockchain.
4//! Unlike centralized exchanges (CEX) like Binance or Bitget, HyperLiquid uses:
5//! - Ethereum wallet private keys for authentication (EIP-712 typed data signatures)
6//! - Wallet addresses as account identifiers (no registration required)
7//! - USDC as the sole settlement currency
8//!
9//! # Features
10//!
11//! - Perpetual futures trading with up to 50x leverage
12//! - Cross-margin and isolated margin modes
13//! - Real-time WebSocket data streaming
14//! - EIP-712 compliant transaction signing
15//!
16//! # Note on Market Types
17//!
18//! HyperLiquid only supports perpetual futures (Swap). Attempting to configure
19//! other market types (Spot, Futures, Margin, Option) will result in an error.
20//!
21//! # Example
22//!
23//! ```no_run
24//! use ccxt_exchanges::hyperliquid::HyperLiquid;
25//!
26//! # async fn example() -> ccxt_core::Result<()> {
27//! // Create a public-only instance (no authentication)
28//! let exchange = HyperLiquid::builder()
29//! .testnet(true)
30//! .build()?;
31//!
32//! // Fetch markets
33//! let markets = exchange.fetch_markets().await?;
34//! println!("Found {} markets", markets.len());
35//!
36//! // Create an authenticated instance
37//! let exchange = HyperLiquid::builder()
38//! .private_key("0x...")
39//! .testnet(true)
40//! .build()?;
41//!
42//! // Fetch balance
43//! let balance = exchange.fetch_balance().await?;
44//! # Ok(())
45//! # }
46//! ```
47
48use ccxt_core::types::default_type::DefaultType;
49use ccxt_core::{BaseExchange, ExchangeConfig, Result};
50
51pub mod auth;
52pub mod builder;
53pub mod error;
54mod exchange_impl;
55pub mod parser;
56pub mod rest;
57pub mod ws;
58mod ws_exchange_impl;
59
60pub use auth::HyperLiquidAuth;
61pub use builder::{HyperLiquidBuilder, validate_default_type};
62pub use error::{HyperLiquidErrorCode, is_error_response, parse_error};
63
64/// HyperLiquid exchange structure.
65#[derive(Debug)]
66pub struct HyperLiquid {
67 /// Base exchange instance.
68 base: BaseExchange,
69 /// HyperLiquid-specific options.
70 options: HyperLiquidOptions,
71 /// Authentication instance (optional, for private API).
72 auth: Option<HyperLiquidAuth>,
73}
74
75/// HyperLiquid-specific options.
76///
77/// Note: HyperLiquid only supports perpetual futures (Swap). The `default_type`
78/// field defaults to `Swap` and attempting to set it to any other value will
79/// result in a validation error.
80#[derive(Debug, Clone)]
81pub struct HyperLiquidOptions {
82 /// Whether to use testnet.
83 pub testnet: bool,
84 /// Vault address for vault trading (optional).
85 pub vault_address: Option<String>,
86 /// Default leverage multiplier.
87 pub default_leverage: u32,
88 /// Default market type for trading.
89 ///
90 /// HyperLiquid only supports perpetual futures, so this must be `Swap`.
91 /// Attempting to set any other value will result in a validation error.
92 pub default_type: DefaultType,
93}
94
95impl Default for HyperLiquidOptions {
96 fn default() -> Self {
97 Self {
98 testnet: false,
99 vault_address: None,
100 default_leverage: 1,
101 // HyperLiquid only supports perpetual futures (Swap)
102 default_type: DefaultType::Swap,
103 }
104 }
105}
106
107impl HyperLiquid {
108 /// Creates a new HyperLiquid instance using the builder pattern.
109 ///
110 /// This is the recommended way to create a HyperLiquid instance.
111 ///
112 /// # Example
113 ///
114 /// ```no_run
115 /// use ccxt_exchanges::hyperliquid::HyperLiquid;
116 ///
117 /// let exchange = HyperLiquid::builder()
118 /// .private_key("0x...")
119 /// .testnet(true)
120 /// .build()
121 /// .unwrap();
122 /// ```
123 pub fn builder() -> HyperLiquidBuilder {
124 HyperLiquidBuilder::new()
125 }
126
127 /// Creates a new HyperLiquid instance with custom options.
128 ///
129 /// This is used internally by the builder pattern.
130 ///
131 /// # Arguments
132 ///
133 /// * `config` - Exchange configuration.
134 /// * `options` - HyperLiquid-specific options.
135 /// * `auth` - Optional authentication instance.
136 pub fn new_with_options(
137 config: ExchangeConfig,
138 options: HyperLiquidOptions,
139 auth: Option<HyperLiquidAuth>,
140 ) -> Result<Self> {
141 let base = BaseExchange::new(config)?;
142 Ok(Self {
143 base,
144 options,
145 auth,
146 })
147 }
148
149 /// Returns a reference to the base exchange.
150 pub fn base(&self) -> &BaseExchange {
151 &self.base
152 }
153
154 /// Returns a mutable reference to the base exchange.
155 pub fn base_mut(&mut self) -> &mut BaseExchange {
156 &mut self.base
157 }
158
159 /// Returns the HyperLiquid options.
160 pub fn options(&self) -> &HyperLiquidOptions {
161 &self.options
162 }
163
164 /// Returns a reference to the authentication instance.
165 pub fn auth(&self) -> Option<&HyperLiquidAuth> {
166 self.auth.as_ref()
167 }
168
169 /// Returns the exchange ID.
170 pub fn id(&self) -> &str {
171 "hyperliquid"
172 }
173
174 /// Returns the exchange name.
175 pub fn name(&self) -> &str {
176 "HyperLiquid"
177 }
178
179 /// Returns the API version.
180 pub fn version(&self) -> &str {
181 "1"
182 }
183
184 /// Returns `true` if the exchange is CCXT-certified.
185 pub fn certified(&self) -> bool {
186 false
187 }
188
189 /// Returns `true` if Pro version (WebSocket) is supported.
190 pub fn pro(&self) -> bool {
191 true
192 }
193
194 /// Returns the rate limit in requests per second.
195 pub fn rate_limit(&self) -> u32 {
196 // HyperLiquid has generous rate limits
197 100
198 }
199
200 /// Returns `true` if sandbox/testnet mode is enabled.
201 ///
202 /// Sandbox mode is enabled when either:
203 /// - `config.sandbox` is set to `true`
204 /// - `options.testnet` is set to `true`
205 ///
206 /// # Returns
207 ///
208 /// `true` if sandbox mode is enabled, `false` otherwise.
209 ///
210 /// # Example
211 ///
212 /// ```no_run
213 /// use ccxt_exchanges::hyperliquid::HyperLiquid;
214 ///
215 /// let exchange = HyperLiquid::builder()
216 /// .testnet(true)
217 /// .build()
218 /// .unwrap();
219 /// assert!(exchange.is_sandbox());
220 /// ```
221 pub fn is_sandbox(&self) -> bool {
222 self.base().config.sandbox || self.options.testnet
223 }
224
225 /// Returns the API URLs.
226 ///
227 /// Returns testnet URLs when sandbox mode is enabled (either via
228 /// `config.sandbox` or `options.testnet`), otherwise returns mainnet URLs.
229 pub fn urls(&self) -> HyperLiquidUrls {
230 if self.is_sandbox() {
231 HyperLiquidUrls::testnet()
232 } else {
233 HyperLiquidUrls::mainnet()
234 }
235 }
236
237 /// Returns the wallet address if authenticated.
238 pub fn wallet_address(&self) -> Option<&str> {
239 self.auth.as_ref().map(|a| a.wallet_address())
240 }
241
242 // TODO: Implement in task 11 (WebSocket Implementation)
243 // /// Creates a public WebSocket client.
244 // pub fn create_ws(&self) -> ws::HyperLiquidWs {
245 // let urls = self.urls();
246 // ws::HyperLiquidWs::new(urls.ws)
247 // }
248}
249
250/// HyperLiquid API URLs.
251#[derive(Debug, Clone)]
252pub struct HyperLiquidUrls {
253 /// REST API base URL.
254 pub rest: String,
255 /// WebSocket URL.
256 pub ws: String,
257}
258
259impl HyperLiquidUrls {
260 /// Returns mainnet environment URLs.
261 pub fn mainnet() -> Self {
262 Self {
263 rest: "https://api.hyperliquid.xyz".to_string(),
264 ws: "wss://api.hyperliquid.xyz/ws".to_string(),
265 }
266 }
267
268 /// Returns testnet environment URLs.
269 pub fn testnet() -> Self {
270 Self {
271 rest: "https://api.hyperliquid-testnet.xyz".to_string(),
272 ws: "wss://api.hyperliquid-testnet.xyz/ws".to_string(),
273 }
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn test_default_options() {
283 let options = HyperLiquidOptions::default();
284 assert!(!options.testnet);
285 assert!(options.vault_address.is_none());
286 assert_eq!(options.default_leverage, 1);
287 // HyperLiquid only supports perpetual futures, so default_type must be Swap
288 assert_eq!(options.default_type, DefaultType::Swap);
289 }
290
291 #[test]
292 fn test_mainnet_urls() {
293 let urls = HyperLiquidUrls::mainnet();
294 assert_eq!(urls.rest, "https://api.hyperliquid.xyz");
295 assert_eq!(urls.ws, "wss://api.hyperliquid.xyz/ws");
296 }
297
298 #[test]
299 fn test_testnet_urls() {
300 let urls = HyperLiquidUrls::testnet();
301 assert_eq!(urls.rest, "https://api.hyperliquid-testnet.xyz");
302 assert_eq!(urls.ws, "wss://api.hyperliquid-testnet.xyz/ws");
303 }
304
305 #[test]
306 fn test_is_sandbox_with_options_testnet() {
307 let config = ExchangeConfig::default();
308 let options = HyperLiquidOptions {
309 testnet: true,
310 ..Default::default()
311 };
312 let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
313 assert!(exchange.is_sandbox());
314 }
315
316 #[test]
317 fn test_is_sandbox_with_config_sandbox() {
318 let config = ExchangeConfig {
319 sandbox: true,
320 ..Default::default()
321 };
322 let options = HyperLiquidOptions::default();
323 let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
324 assert!(exchange.is_sandbox());
325 }
326
327 #[test]
328 fn test_is_sandbox_false_by_default() {
329 let config = ExchangeConfig::default();
330 let options = HyperLiquidOptions::default();
331 let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
332 assert!(!exchange.is_sandbox());
333 }
334
335 #[test]
336 fn test_urls_returns_mainnet_by_default() {
337 let config = ExchangeConfig::default();
338 let options = HyperLiquidOptions::default();
339 let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
340 let urls = exchange.urls();
341 assert_eq!(urls.rest, "https://api.hyperliquid.xyz");
342 assert_eq!(urls.ws, "wss://api.hyperliquid.xyz/ws");
343 }
344
345 #[test]
346 fn test_urls_returns_testnet_with_options_testnet() {
347 let config = ExchangeConfig::default();
348 let options = HyperLiquidOptions {
349 testnet: true,
350 ..Default::default()
351 };
352 let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
353 let urls = exchange.urls();
354 assert_eq!(urls.rest, "https://api.hyperliquid-testnet.xyz");
355 assert_eq!(urls.ws, "wss://api.hyperliquid-testnet.xyz/ws");
356 }
357
358 #[test]
359 fn test_urls_returns_testnet_with_config_sandbox() {
360 let config = ExchangeConfig {
361 sandbox: true,
362 ..Default::default()
363 };
364 let options = HyperLiquidOptions::default();
365 let exchange = HyperLiquid::new_with_options(config, options, None).unwrap();
366 let urls = exchange.urls();
367 assert_eq!(urls.rest, "https://api.hyperliquid-testnet.xyz");
368 assert_eq!(urls.ws, "wss://api.hyperliquid-testnet.xyz/ws");
369 }
370}