ccxt_exchanges/hyperliquid/
builder.rs

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