ccxt_exchanges/hyperliquid/
builder.rs

1//! HyperLiquid builder module.
2//!
3//! Provides a builder pattern for creating HyperLiquid exchange instances.
4
5use ccxt_core::config::{ProxyConfig, RetryPolicy};
6use ccxt_core::types::default_type::DefaultType;
7use ccxt_core::{Error, ExchangeConfig, Result};
8use std::time::Duration;
9
10use super::{HyperLiquid, HyperLiquidAuth, HyperLiquidOptions};
11
12/// Builder for creating HyperLiquid exchange instances.
13///
14/// # Note on Market Types
15///
16/// HyperLiquid only supports perpetual futures (Swap). Attempting to set
17/// `default_type` to any other value (Spot, Futures, Margin, Option) will
18/// result in a validation error when calling `build()`.
19///
20/// # Example
21///
22/// ```no_run
23/// use ccxt_exchanges::hyperliquid::HyperLiquidBuilder;
24///
25/// let exchange = HyperLiquidBuilder::new()
26///     .private_key("0x...")
27///     .testnet(true)
28///     .default_leverage(10)
29///     .build()
30///     .unwrap();
31/// ```
32#[derive(Debug, Default)]
33pub struct HyperLiquidBuilder {
34    private_key: Option<String>,
35    testnet: bool,
36    vault_address: Option<String>,
37    default_leverage: u32,
38    default_type: Option<DefaultType>,
39    timeout: Option<Duration>,
40    proxy: Option<ProxyConfig>,
41    retry_policy: Option<RetryPolicy>,
42}
43
44impl HyperLiquidBuilder {
45    /// Creates a new HyperLiquidBuilder with default settings.
46    pub fn new() -> Self {
47        Self {
48            private_key: None,
49            testnet: false,
50            vault_address: None,
51            default_leverage: 1,
52            default_type: None, // Will default to Swap in build()
53            timeout: None,
54            proxy: None,
55            retry_policy: None,
56        }
57    }
58
59    /// Sets the Ethereum private key for authentication.
60    ///
61    /// The private key should be a 64-character hex string (32 bytes),
62    /// optionally prefixed with "0x".
63    ///
64    /// # Arguments
65    ///
66    /// * `key` - The private key in hex format.
67    ///
68    /// # Example
69    ///
70    /// ```no_run
71    /// use ccxt_exchanges::hyperliquid::HyperLiquidBuilder;
72    ///
73    /// let builder = HyperLiquidBuilder::new()
74    ///     .private_key("0x1234567890abcdef...");
75    /// ```
76    pub fn private_key(mut self, key: &str) -> Self {
77        self.private_key = Some(key.to_string());
78        self
79    }
80
81    /// Enables or disables sandbox/testnet mode.
82    ///
83    /// When enabled, the exchange will connect to HyperLiquid testnet
84    /// instead of mainnet.
85    ///
86    /// This method is equivalent to `testnet()` and is provided for
87    /// consistency with other exchanges.
88    ///
89    /// # Arguments
90    ///
91    /// * `enabled` - Whether to use sandbox mode.
92    pub fn sandbox(mut self, enabled: bool) -> Self {
93        self.testnet = enabled;
94        self
95    }
96
97    /// Enables or disables testnet mode.
98    ///
99    /// When enabled, the exchange will connect to HyperLiquid testnet
100    /// instead of mainnet.
101    ///
102    /// This method is equivalent to `sandbox()` and is provided for
103    /// backward compatibility.
104    ///
105    /// # Arguments
106    ///
107    /// * `enabled` - Whether to use testnet.
108    pub fn testnet(mut self, enabled: bool) -> Self {
109        self.testnet = enabled;
110        self
111    }
112
113    /// Sets the vault address for vault trading.
114    ///
115    /// When set, orders will be placed on behalf of the vault.
116    ///
117    /// # Arguments
118    ///
119    /// * `address` - The vault's Ethereum address.
120    pub fn vault_address(mut self, address: &str) -> Self {
121        self.vault_address = Some(address.to_string());
122        self
123    }
124
125    /// Sets the default leverage multiplier.
126    ///
127    /// This leverage will be used when placing orders if not specified.
128    ///
129    /// # Arguments
130    ///
131    /// * `leverage` - The leverage multiplier (1-50).
132    pub fn default_leverage(mut self, leverage: u32) -> Self {
133        self.default_leverage = leverage.clamp(1, 50);
134        self
135    }
136
137    /// Sets the default market type for trading.
138    ///
139    /// **Important**: HyperLiquid only supports perpetual futures (Swap).
140    /// Attempting to set any other value (Spot, Futures, Margin, Option)
141    /// will result in a validation error when calling `build()`.
142    ///
143    /// # Arguments
144    ///
145    /// * `default_type` - The default market type. Must be `Swap` for HyperLiquid.
146    ///
147    /// # Example
148    ///
149    /// ```no_run
150    /// use ccxt_exchanges::hyperliquid::HyperLiquidBuilder;
151    /// use ccxt_core::types::default_type::DefaultType;
152    ///
153    /// // This works - HyperLiquid supports Swap (perpetuals)
154    /// let exchange = HyperLiquidBuilder::new()
155    ///     .default_type(DefaultType::Swap)
156    ///     .build()
157    ///     .unwrap();
158    ///
159    /// // This will fail - HyperLiquid does not support Spot
160    /// let result = HyperLiquidBuilder::new()
161    ///     .default_type(DefaultType::Spot)
162    ///     .build();
163    /// assert!(result.is_err());
164    /// ```
165    pub fn default_type(mut self, default_type: impl Into<DefaultType>) -> Self {
166        self.default_type = Some(default_type.into());
167        self
168    }
169
170    /// Sets the request timeout.
171    pub fn timeout(mut self, timeout: Duration) -> Self {
172        self.timeout = Some(timeout);
173        self
174    }
175
176    /// Sets the request timeout in seconds (convenience method).
177    pub fn timeout_secs(mut self, seconds: u64) -> Self {
178        self.timeout = Some(Duration::from_secs(seconds));
179        self
180    }
181
182    /// Sets the TCP connection timeout.
183    ///
184    /// # Arguments
185    ///
186    /// * `timeout` - Connection timeout duration.
187    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
188        // Note: HyperLiquid builder doesn't have a separate connect_timeout field,
189        // but we store it in the ExchangeConfig during build()
190        // For now, we'll just accept it and apply it during build
191        self.timeout = Some(timeout);
192        self
193    }
194
195    /// Sets the TCP connection timeout in seconds (convenience method).
196    ///
197    /// # Arguments
198    ///
199    /// * `seconds` - Connection timeout duration in seconds.
200    pub fn connect_timeout_secs(mut self, seconds: u64) -> Self {
201        self.timeout = Some(Duration::from_secs(seconds));
202        self
203    }
204
205    /// Sets the retry policy.
206    pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
207        self.retry_policy = Some(policy);
208        self
209    }
210
211    /// Sets the HTTP proxy configuration.
212    pub fn proxy(mut self, proxy: ProxyConfig) -> Self {
213        self.proxy = Some(proxy);
214        self
215    }
216
217    /// Sets the HTTP proxy URL (convenience method).
218    pub fn proxy_url(mut self, url: impl Into<String>) -> Self {
219        self.proxy = Some(ProxyConfig::new(url));
220        self
221    }
222
223    /// Builds the HyperLiquid exchange instance.
224    ///
225    /// # Returns
226    ///
227    /// Returns a configured `HyperLiquid` instance.
228    ///
229    /// # Errors
230    ///
231    /// Returns an error if:
232    /// - The private key format is invalid
233    /// - The exchange configuration fails
234    /// - The `default_type` is set to a value other than `Swap` (HyperLiquid only supports perpetuals)
235    pub fn build(self) -> Result<HyperLiquid> {
236        // Validate default_type - HyperLiquid only supports perpetual futures (Swap)
237        let default_type = self.default_type.unwrap_or(DefaultType::Swap);
238        validate_default_type(default_type)?;
239
240        // Create authentication if private key is provided
241        let auth = if let Some(ref key) = self.private_key {
242            Some(HyperLiquidAuth::from_private_key(key)?)
243        } else {
244            None
245        };
246
247        // Create options
248        let options = HyperLiquidOptions {
249            testnet: self.testnet,
250            vault_address: self.vault_address,
251            default_leverage: self.default_leverage,
252            default_type,
253        };
254
255        // Create exchange config
256        let mut config = ExchangeConfig {
257            id: "hyperliquid".to_string(),
258            name: "HyperLiquid".to_string(),
259            sandbox: self.testnet,
260            ..Default::default()
261        };
262
263        if let Some(timeout) = self.timeout {
264            config.timeout = timeout;
265        }
266        if let Some(proxy) = self.proxy {
267            config.proxy = Some(proxy);
268        }
269        if let Some(retry_policy) = self.retry_policy {
270            config.retry_policy = Some(retry_policy);
271        }
272
273        HyperLiquid::new_with_options(config, options, auth)
274    }
275}
276
277/// Validates that the default_type is supported by HyperLiquid.
278///
279/// HyperLiquid only supports perpetual futures (Swap). This function returns
280/// an error if any other market type is specified.
281///
282/// # Arguments
283///
284/// * `default_type` - The default market type to validate.
285///
286/// # Returns
287///
288/// Returns `Ok(())` if the type is `Swap`, otherwise returns an error.
289pub fn validate_default_type(default_type: DefaultType) -> Result<()> {
290    match default_type {
291        DefaultType::Swap => Ok(()),
292        DefaultType::Spot => Err(Error::invalid_request(
293            "HyperLiquid does not support spot trading. Only perpetual futures (Swap) are available.",
294        )),
295        DefaultType::Futures => Err(Error::invalid_request(
296            "HyperLiquid does not support delivery futures. Only perpetual futures (Swap) are available.",
297        )),
298        DefaultType::Margin => Err(Error::invalid_request(
299            "HyperLiquid does not support margin trading. Only perpetual futures (Swap) are available.",
300        )),
301        DefaultType::Option => Err(Error::invalid_request(
302            "HyperLiquid does not support options trading. Only perpetual futures (Swap) are available.",
303        )),
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    #[test]
312    fn test_builder_default() {
313        let builder = HyperLiquidBuilder::new();
314        assert!(builder.private_key.is_none());
315        assert!(!builder.testnet);
316        assert!(builder.vault_address.is_none());
317        assert_eq!(builder.default_leverage, 1);
318        assert!(builder.default_type.is_none()); // Will default to Swap in build()
319    }
320
321    #[test]
322    fn test_builder_sandbox() {
323        let builder = HyperLiquidBuilder::new().sandbox(true);
324        assert!(builder.testnet);
325    }
326
327    #[test]
328    fn test_builder_testnet() {
329        let builder = HyperLiquidBuilder::new().testnet(true);
330        assert!(builder.testnet);
331    }
332
333    #[test]
334    fn test_builder_sandbox_testnet_equivalence() {
335        // Verify that sandbox() and testnet() produce equivalent results
336        let sandbox_builder = HyperLiquidBuilder::new().sandbox(true);
337        let testnet_builder = HyperLiquidBuilder::new().testnet(true);
338
339        assert_eq!(sandbox_builder.testnet, testnet_builder.testnet);
340    }
341
342    #[test]
343    fn test_builder_leverage_clamping() {
344        let builder = HyperLiquidBuilder::new().default_leverage(100);
345        assert_eq!(builder.default_leverage, 50);
346
347        let builder = HyperLiquidBuilder::new().default_leverage(0);
348        assert_eq!(builder.default_leverage, 1);
349    }
350
351    #[test]
352    fn test_builder_vault_address() {
353        let builder =
354            HyperLiquidBuilder::new().vault_address("0x1234567890abcdef1234567890abcdef12345678");
355        assert!(builder.vault_address.is_some());
356    }
357
358    #[test]
359    fn test_builder_default_type_swap() {
360        let builder = HyperLiquidBuilder::new().default_type(DefaultType::Swap);
361        assert_eq!(builder.default_type, Some(DefaultType::Swap));
362    }
363
364    #[test]
365    fn test_builder_default_type_from_string() {
366        let builder = HyperLiquidBuilder::new().default_type("swap");
367        assert_eq!(builder.default_type, Some(DefaultType::Swap));
368    }
369
370    #[test]
371    fn test_builder_timeout() {
372        let builder = HyperLiquidBuilder::new().timeout(Duration::from_secs(60));
373        assert_eq!(builder.timeout, Some(Duration::from_secs(60)));
374    }
375
376    #[test]
377    fn test_builder_timeout_secs() {
378        let builder = HyperLiquidBuilder::new().timeout_secs(45);
379        assert_eq!(builder.timeout, Some(Duration::from_secs(45)));
380    }
381
382    #[test]
383    fn test_builder_connect_timeout() {
384        let builder = HyperLiquidBuilder::new().connect_timeout(Duration::from_secs(15));
385        assert_eq!(builder.timeout, Some(Duration::from_secs(15)));
386    }
387
388    #[test]
389    fn test_builder_connect_timeout_secs() {
390        let builder = HyperLiquidBuilder::new().connect_timeout_secs(20);
391        assert_eq!(builder.timeout, Some(Duration::from_secs(20)));
392    }
393
394    #[test]
395    fn test_build_without_auth() {
396        let exchange = HyperLiquidBuilder::new().testnet(true).build();
397
398        assert!(exchange.is_ok());
399        let exchange = exchange.unwrap();
400        assert_eq!(exchange.id(), "hyperliquid");
401        assert!(exchange.options().testnet);
402        assert!(exchange.auth().is_none());
403        // Default type should be Swap
404        assert_eq!(exchange.options().default_type, DefaultType::Swap);
405    }
406
407    #[test]
408    fn test_build_with_swap_type() {
409        let exchange = HyperLiquidBuilder::new()
410            .testnet(true)
411            .default_type(DefaultType::Swap)
412            .build();
413
414        assert!(exchange.is_ok());
415        let exchange = exchange.unwrap();
416        assert_eq!(exchange.options().default_type, DefaultType::Swap);
417    }
418
419    #[test]
420    fn test_build_with_spot_type_fails() {
421        let result = HyperLiquidBuilder::new()
422            .testnet(true)
423            .default_type(DefaultType::Spot)
424            .build();
425
426        assert!(result.is_err());
427        let err = result.unwrap_err();
428        assert!(err.to_string().contains("spot"));
429    }
430
431    #[test]
432    fn test_build_with_futures_type_fails() {
433        let result = HyperLiquidBuilder::new()
434            .testnet(true)
435            .default_type(DefaultType::Futures)
436            .build();
437
438        assert!(result.is_err());
439        let err = result.unwrap_err();
440        assert!(err.to_string().contains("delivery futures"));
441    }
442
443    #[test]
444    fn test_build_with_margin_type_fails() {
445        let result = HyperLiquidBuilder::new()
446            .testnet(true)
447            .default_type(DefaultType::Margin)
448            .build();
449
450        assert!(result.is_err());
451        let err = result.unwrap_err();
452        assert!(err.to_string().contains("margin"));
453    }
454
455    #[test]
456    fn test_build_with_option_type_fails() {
457        let result = HyperLiquidBuilder::new()
458            .testnet(true)
459            .default_type(DefaultType::Option)
460            .build();
461
462        assert!(result.is_err());
463        let err = result.unwrap_err();
464        assert!(err.to_string().contains("options"));
465    }
466
467    #[test]
468    fn test_validate_default_type_swap() {
469        assert!(validate_default_type(DefaultType::Swap).is_ok());
470    }
471
472    #[test]
473    fn test_validate_default_type_spot() {
474        let result = validate_default_type(DefaultType::Spot);
475        assert!(result.is_err());
476    }
477
478    #[test]
479    fn test_validate_default_type_futures() {
480        let result = validate_default_type(DefaultType::Futures);
481        assert!(result.is_err());
482    }
483
484    #[test]
485    fn test_validate_default_type_margin() {
486        let result = validate_default_type(DefaultType::Margin);
487        assert!(result.is_err());
488    }
489
490    #[test]
491    fn test_validate_default_type_option() {
492        let result = validate_default_type(DefaultType::Option);
493        assert!(result.is_err());
494    }
495}