1use std::fmt::Debug;
19
20use nautilus_network::websocket::TransportBackend;
21use serde::{Deserialize, Serialize};
22
23use crate::common::{
24 consts::{info_url, ws_url},
25 enums::HyperliquidEnvironment,
26};
27
28#[derive(Clone, Serialize, Deserialize, bon::Builder)]
37#[serde(default, deny_unknown_fields)]
38#[cfg_attr(
39 feature = "python",
40 pyo3::pyclass(module = "nautilus_trader.adapters.hyperliquid", from_py_object)
41)]
42#[cfg_attr(
43 feature = "python",
44 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.hyperliquid")
45)]
46pub struct HyperliquidDataClientConfig {
47 pub private_key: Option<String>,
49 pub base_url_ws: Option<String>,
51 pub base_url_http: Option<String>,
53 pub proxy_url: Option<String>,
55 #[builder(default)]
57 pub environment: HyperliquidEnvironment,
58 #[builder(default = 60)]
60 pub http_timeout_secs: u64,
61 #[builder(default = 30)]
63 pub ws_timeout_secs: u64,
64 #[builder(default = 120)]
68 pub stale_stream_receive_timeout_secs: u64,
69 #[builder(default = 15)]
72 pub stream_health_check_interval_secs: u64,
73 #[builder(default = 60)]
75 pub stale_stream_warning_cooldown_secs: u64,
76 #[builder(default = false)]
79 pub stale_stream_recovery_enabled: bool,
80 #[builder(default = 120)]
83 pub stale_stream_recovery_cooldown_secs: u64,
84 #[builder(default = 3)]
87 pub stale_stream_max_targeted_resubscribes: u32,
88 #[builder(default = 60)]
90 pub update_instruments_interval_mins: u64,
91 #[builder(default)]
94 pub transport_backend: TransportBackend,
95}
96
97#[cfg(feature = "python")]
98nautilus_core::impl_pyo3_config_getters!(HyperliquidDataClientConfig {
99 environment: HyperliquidEnvironment,
100 base_url_ws: Option<String>,
101 base_url_http: Option<String>,
102 http_timeout_secs: u64,
103 ws_timeout_secs: u64,
104 update_instruments_interval_mins: u64,
105 transport_backend: TransportBackend,
106 stale_stream_receive_timeout_secs: u64,
107 stream_health_check_interval_secs: u64,
108 stale_stream_warning_cooldown_secs: u64,
109 stale_stream_recovery_enabled: bool,
110 stale_stream_recovery_cooldown_secs: u64,
111 stale_stream_max_targeted_resubscribes: u32,
112});
113
114impl Default for HyperliquidDataClientConfig {
115 fn default() -> Self {
116 Self::builder().build()
117 }
118}
119
120impl HyperliquidDataClientConfig {
121 #[must_use]
123 pub fn new() -> Self {
124 Self::default()
125 }
126
127 #[must_use]
129 pub fn has_credentials(&self) -> bool {
130 self.private_key
131 .as_deref()
132 .is_some_and(|s| !s.trim().is_empty())
133 }
134
135 #[must_use]
137 pub fn ws_url(&self) -> String {
138 self.base_url_ws
139 .clone()
140 .unwrap_or_else(|| ws_url(self.environment).to_string())
141 }
142
143 #[must_use]
145 pub fn http_url(&self) -> String {
146 self.base_url_http
147 .clone()
148 .unwrap_or_else(|| info_url(self.environment).to_string())
149 }
150}
151
152impl Debug for HyperliquidDataClientConfig {
153 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 formatter
155 .debug_struct(stringify!(HyperliquidDataClientConfig))
156 .field(
157 "private_key",
158 &self.private_key.as_ref().map(|_| "[REDACTED]"),
159 )
160 .field("base_url_ws", &self.base_url_ws)
161 .field("base_url_http", &self.base_url_http)
162 .field("proxy_url", &self.proxy_url)
163 .field("environment", &self.environment)
164 .field("http_timeout_secs", &self.http_timeout_secs)
165 .field("ws_timeout_secs", &self.ws_timeout_secs)
166 .field(
167 "stale_stream_receive_timeout_secs",
168 &self.stale_stream_receive_timeout_secs,
169 )
170 .field(
171 "stream_health_check_interval_secs",
172 &self.stream_health_check_interval_secs,
173 )
174 .field(
175 "stale_stream_warning_cooldown_secs",
176 &self.stale_stream_warning_cooldown_secs,
177 )
178 .field(
179 "stale_stream_recovery_enabled",
180 &self.stale_stream_recovery_enabled,
181 )
182 .field(
183 "stale_stream_recovery_cooldown_secs",
184 &self.stale_stream_recovery_cooldown_secs,
185 )
186 .field(
187 "stale_stream_max_targeted_resubscribes",
188 &self.stale_stream_max_targeted_resubscribes,
189 )
190 .field(
191 "update_instruments_interval_mins",
192 &self.update_instruments_interval_mins,
193 )
194 .field("transport_backend", &self.transport_backend)
195 .finish()
196 }
197}
198
199#[derive(Clone, Serialize, Deserialize, bon::Builder)]
201#[serde(default, deny_unknown_fields)]
202#[cfg_attr(
203 feature = "python",
204 pyo3::pyclass(module = "nautilus_trader.adapters.hyperliquid", from_py_object)
205)]
206#[cfg_attr(
207 feature = "python",
208 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.hyperliquid")
209)]
210pub struct HyperliquidExecClientConfig {
211 pub private_key: Option<String>,
217 pub vault_address: Option<String>,
223 pub account_address: Option<String>,
230 pub base_url_ws: Option<String>,
232 pub base_url_http: Option<String>,
234 pub base_url_exchange: Option<String>,
236 pub proxy_url: Option<String>,
238 #[builder(default)]
240 pub environment: HyperliquidEnvironment,
241 #[builder(default = 60)]
243 pub http_timeout_secs: u64,
244 #[builder(default = 3)]
246 pub max_retries: u32,
247 #[builder(default = 100)]
249 pub retry_delay_initial_ms: u64,
250 #[builder(default = 5000)]
252 pub retry_delay_max_ms: u64,
253 #[builder(default = true)]
256 pub normalize_prices: bool,
257 #[builder(default = 50)]
261 pub market_order_slippage_bps: u32,
262 #[builder(default = true)]
264 pub include_builder_attribution: bool,
265 #[builder(default)]
268 pub transport_backend: TransportBackend,
269 #[builder(default = 10)]
271 pub ws_post_timeout_secs: u64,
272 #[builder(default = 0)]
277 pub outcome_settlement_poll_secs: u64,
278}
279
280#[cfg(feature = "python")]
281nautilus_core::impl_pyo3_config_getters!(HyperliquidExecClientConfig {
282 vault_address: Option<String>,
283 account_address: Option<String>,
284 environment: HyperliquidEnvironment,
285 base_url_ws: Option<String>,
286 base_url_http: Option<String>,
287 base_url_exchange: Option<String>,
288 http_timeout_secs: u64,
289 max_retries: u32,
290 retry_delay_initial_ms: u64,
291 retry_delay_max_ms: u64,
292 normalize_prices: bool,
293 market_order_slippage_bps: u32,
294 include_builder_attribution: bool,
295 ws_post_timeout_secs: u64,
296 transport_backend: TransportBackend,
297});
298
299impl Default for HyperliquidExecClientConfig {
300 fn default() -> Self {
301 Self::builder().build()
302 }
303}
304
305impl HyperliquidExecClientConfig {
306 #[must_use]
308 pub fn has_credentials(&self) -> bool {
309 self.private_key
310 .as_deref()
311 .is_some_and(|s| !s.trim().is_empty())
312 }
313
314 #[must_use]
316 pub fn ws_url(&self) -> String {
317 self.base_url_ws
318 .clone()
319 .unwrap_or_else(|| ws_url(self.environment).to_string())
320 }
321
322 #[must_use]
324 pub fn http_url(&self) -> String {
325 self.base_url_http
326 .clone()
327 .unwrap_or_else(|| info_url(self.environment).to_string())
328 }
329}
330
331impl Debug for HyperliquidExecClientConfig {
332 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333 formatter
334 .debug_struct(stringify!(HyperliquidExecClientConfig))
335 .field(
336 "private_key",
337 &self.private_key.as_ref().map(|_| "[REDACTED]"),
338 )
339 .field("vault_address", &self.vault_address)
340 .field("account_address", &self.account_address)
341 .field("base_url_ws", &self.base_url_ws)
342 .field("base_url_http", &self.base_url_http)
343 .field("base_url_exchange", &self.base_url_exchange)
344 .field("proxy_url", &self.proxy_url)
345 .field("environment", &self.environment)
346 .field("http_timeout_secs", &self.http_timeout_secs)
347 .field("max_retries", &self.max_retries)
348 .field("retry_delay_initial_ms", &self.retry_delay_initial_ms)
349 .field("retry_delay_max_ms", &self.retry_delay_max_ms)
350 .field("normalize_prices", &self.normalize_prices)
351 .field("market_order_slippage_bps", &self.market_order_slippage_bps)
352 .field(
353 "include_builder_attribution",
354 &self.include_builder_attribution,
355 )
356 .field("transport_backend", &self.transport_backend)
357 .field("ws_post_timeout_secs", &self.ws_post_timeout_secs)
358 .field(
359 "outcome_settlement_poll_secs",
360 &self.outcome_settlement_poll_secs,
361 )
362 .finish()
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use rstest::rstest;
369
370 use super::*;
371
372 #[rstest]
373 fn test_exec_config_default_account_address_is_none() {
374 let config = HyperliquidExecClientConfig::default();
375 assert!(config.account_address.is_none());
376 }
377
378 #[rstest]
379 fn test_exec_config_with_account_address() {
380 let config = HyperliquidExecClientConfig {
381 account_address: Some("0x1234".to_string()),
382 ..HyperliquidExecClientConfig::default()
383 };
384 assert_eq!(config.account_address.as_deref(), Some("0x1234"));
385 }
386
387 #[rstest]
388 fn test_data_config_toml_minimal() {
389 let config: HyperliquidDataClientConfig = toml::from_str(
390 r#"
391environment = "testnet"
392http_timeout_secs = 30
393update_instruments_interval_mins = 10
394transport_backend = "tungstenite"
395"#,
396 )
397 .unwrap();
398
399 assert_eq!(config.environment, HyperliquidEnvironment::Testnet);
400 assert_eq!(config.http_timeout_secs, 30);
401 assert_eq!(config.update_instruments_interval_mins, 10);
402 assert_eq!(config.transport_backend, TransportBackend::Tungstenite);
403 assert_eq!(config.stale_stream_receive_timeout_secs, 120);
404 assert_eq!(config.stream_health_check_interval_secs, 15);
405 assert_eq!(config.stale_stream_warning_cooldown_secs, 60);
406 assert!(!config.stale_stream_recovery_enabled);
407 assert_eq!(config.stale_stream_recovery_cooldown_secs, 120);
408 assert_eq!(config.stale_stream_max_targeted_resubscribes, 3);
409 }
410
411 #[rstest]
412 fn test_data_config_toml_stale_stream_settings() {
413 let config: HyperliquidDataClientConfig = toml::from_str(
414 "
415stale_stream_receive_timeout_secs = 30
416stream_health_check_interval_secs = 5
417stale_stream_warning_cooldown_secs = 20
418stale_stream_recovery_enabled = true
419stale_stream_recovery_cooldown_secs = 45
420stale_stream_max_targeted_resubscribes = 5
421",
422 )
423 .unwrap();
424
425 assert_eq!(config.stale_stream_receive_timeout_secs, 30);
426 assert_eq!(config.stream_health_check_interval_secs, 5);
427 assert_eq!(config.stale_stream_warning_cooldown_secs, 20);
428 assert!(config.stale_stream_recovery_enabled);
429 assert_eq!(config.stale_stream_recovery_cooldown_secs, 45);
430 assert_eq!(config.stale_stream_max_targeted_resubscribes, 5);
431 }
432
433 #[rstest]
434 fn test_exec_config_toml_empty_uses_defaults() {
435 let config: HyperliquidExecClientConfig = toml::from_str("").unwrap();
436 let expected = HyperliquidExecClientConfig::default();
437
438 assert_eq!(config.environment, expected.environment);
439 assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
440 assert_eq!(config.max_retries, expected.max_retries);
441 assert_eq!(config.normalize_prices, expected.normalize_prices);
442 assert_eq!(
443 config.market_order_slippage_bps,
444 expected.market_order_slippage_bps,
445 );
446 assert_eq!(
447 config.include_builder_attribution,
448 expected.include_builder_attribution,
449 );
450 assert_eq!(config.transport_backend, expected.transport_backend);
451 assert_eq!(config.ws_post_timeout_secs, expected.ws_post_timeout_secs);
452 assert_eq!(
453 config.outcome_settlement_poll_secs,
454 expected.outcome_settlement_poll_secs,
455 );
456 }
457
458 #[rstest]
459 fn test_exec_config_toml_include_builder_attribution_false() {
460 let config: HyperliquidExecClientConfig =
461 toml::from_str("include_builder_attribution = false").unwrap();
462
463 assert!(!config.include_builder_attribution);
464 }
465
466 #[rstest]
467 fn test_data_config_debug_redacts_private_key() {
468 let config = HyperliquidDataClientConfig {
469 private_key: Some(
470 "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
471 ),
472 ..HyperliquidDataClientConfig::default()
473 };
474 let debug = format!("{config:?}");
475
476 assert!(debug.contains("[REDACTED]"));
477 assert!(!debug.contains("0123456789abcdef"));
478 }
479
480 #[rstest]
481 fn test_exec_config_debug_redacts_private_key() {
482 let config = HyperliquidExecClientConfig {
483 private_key: Some(
484 "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
485 ),
486 ..HyperliquidExecClientConfig::default()
487 };
488 let debug = format!("{config:?}");
489
490 assert!(debug.contains("[REDACTED]"));
491 assert!(!debug.contains("0123456789abcdef"));
492 }
493}