ccxt_exchanges/hyperliquid/
builder.rs1use 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#[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 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, timeout: None,
54 proxy: None,
55 retry_policy: None,
56 }
57 }
58
59 pub fn private_key(mut self, key: &str) -> Self {
77 self.private_key = Some(key.to_string());
78 self
79 }
80
81 pub fn sandbox(mut self, enabled: bool) -> Self {
93 self.testnet = enabled;
94 self
95 }
96
97 pub fn testnet(mut self, enabled: bool) -> Self {
109 self.testnet = enabled;
110 self
111 }
112
113 pub fn vault_address(mut self, address: &str) -> Self {
121 self.vault_address = Some(address.to_string());
122 self
123 }
124
125 pub fn default_leverage(mut self, leverage: u32) -> Self {
133 self.default_leverage = leverage.clamp(1, 50);
134 self
135 }
136
137 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 pub fn timeout(mut self, timeout: Duration) -> Self {
172 self.timeout = Some(timeout);
173 self
174 }
175
176 pub fn timeout_secs(mut self, seconds: u64) -> Self {
178 self.timeout = Some(Duration::from_secs(seconds));
179 self
180 }
181
182 pub fn connect_timeout(mut self, timeout: Duration) -> Self {
188 self.timeout = Some(timeout);
192 self
193 }
194
195 pub fn connect_timeout_secs(mut self, seconds: u64) -> Self {
201 self.timeout = Some(Duration::from_secs(seconds));
202 self
203 }
204
205 pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
207 self.retry_policy = Some(policy);
208 self
209 }
210
211 pub fn proxy(mut self, proxy: ProxyConfig) -> Self {
213 self.proxy = Some(proxy);
214 self
215 }
216
217 pub fn proxy_url(mut self, url: impl Into<String>) -> Self {
219 self.proxy = Some(ProxyConfig::new(url));
220 self
221 }
222
223 pub fn build(self) -> Result<HyperLiquid> {
236 let default_type = self.default_type.unwrap_or(DefaultType::Swap);
238 validate_default_type(default_type)?;
239
240 let auth = if let Some(ref key) = self.private_key {
242 Some(HyperLiquidAuth::from_private_key(key)?)
243 } else {
244 None
245 };
246
247 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 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
277pub 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()); }
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 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 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}