1#[cfg(test)]
19use nautilus_core::string::secret::REDACTED;
20use nautilus_core::string::secret::SecretString;
21use nautilus_live::book::DEFAULT_BOOK_SNAPSHOT_TIMEOUT_SECS;
22use nautilus_model::identifiers::AccountId;
23use nautilus_network::websocket::TransportBackend;
24use serde::{Deserialize, Serialize};
25
26use crate::common::{
27 consts::{info_url, ws_url},
28 enums::HyperliquidEnvironment,
29};
30
31#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
42#[serde(default, deny_unknown_fields)]
43#[cfg_attr(
44 feature = "python",
45 pyo3::pyclass(module = "nautilus_trader.adapters.hyperliquid", from_py_object)
46)]
47#[cfg_attr(
48 feature = "python",
49 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.hyperliquid")
50)]
51pub struct HyperliquidDataClientConfig {
52 pub private_key: Option<SecretString>,
54 #[builder(default)]
56 pub environment: HyperliquidEnvironment,
57 pub base_url_http: Option<String>,
59 pub base_url_ws: Option<String>,
61 pub proxy_url: Option<SecretString>,
63 #[builder(default = 60)]
65 pub http_timeout_secs: u64,
66 #[builder(default = 30)]
68 pub ws_timeout_secs: u64,
69 #[builder(default = 120)]
73 pub stale_stream_receive_timeout_secs: u64,
74 #[builder(default = 15)]
77 pub stream_health_check_interval_secs: u64,
78 #[builder(default = 60)]
80 pub stale_stream_warning_cooldown_secs: u64,
81 #[builder(default = false)]
84 pub stale_stream_recovery_enabled: bool,
85 #[builder(default = 120)]
88 pub stale_stream_recovery_cooldown_secs: u64,
89 #[builder(default = 3)]
92 pub stale_stream_max_targeted_resubscribes: u32,
93 #[builder(default = DEFAULT_BOOK_SNAPSHOT_TIMEOUT_SECS)]
98 pub book_snapshot_timeout_secs: u64,
99 #[builder(default = 60)]
103 pub update_instruments_interval_mins: u64,
104 #[builder(default)]
107 pub transport_backend: TransportBackend,
108}
109
110#[cfg(feature = "python")]
111nautilus_core::impl_pyo3_config_getters!(HyperliquidDataClientConfig {
112 environment: HyperliquidEnvironment,
113 base_url_http: Option<String>,
114 base_url_ws: Option<String>,
115 http_timeout_secs: u64,
116 ws_timeout_secs: u64,
117 update_instruments_interval_mins: u64,
118 transport_backend: TransportBackend,
119 stale_stream_receive_timeout_secs: u64,
120 stream_health_check_interval_secs: u64,
121 stale_stream_warning_cooldown_secs: u64,
122 stale_stream_recovery_enabled: bool,
123 stale_stream_recovery_cooldown_secs: u64,
124 stale_stream_max_targeted_resubscribes: u32,
125 book_snapshot_timeout_secs: u64,
126});
127
128impl Default for HyperliquidDataClientConfig {
129 fn default() -> Self {
130 Self::builder().build()
131 }
132}
133
134impl HyperliquidDataClientConfig {
135 #[must_use]
137 pub fn new() -> Self {
138 Self::default()
139 }
140
141 #[must_use]
143 pub fn has_credentials(&self) -> bool {
144 self.private_key
145 .as_ref()
146 .map(SecretString::expose_secret)
147 .is_some_and(|s| !s.trim().is_empty())
148 }
149
150 #[must_use]
152 pub fn ws_url(&self) -> String {
153 self.base_url_ws
154 .clone()
155 .unwrap_or_else(|| ws_url(self.environment).to_string())
156 }
157
158 #[must_use]
160 pub fn http_url(&self) -> String {
161 self.base_url_http
162 .clone()
163 .unwrap_or_else(|| info_url(self.environment).to_string())
164 }
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
169#[serde(default, deny_unknown_fields)]
170#[cfg_attr(
171 feature = "python",
172 pyo3::pyclass(module = "nautilus_trader.adapters.hyperliquid", from_py_object)
173)]
174#[cfg_attr(
175 feature = "python",
176 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.hyperliquid")
177)]
178pub struct HyperliquidExecutionClientConfig {
179 #[builder(default = AccountId::from("HYPERLIQUID-001"))]
181 pub account_id: AccountId,
182 pub private_key: Option<SecretString>,
188 pub vault_address: Option<String>,
194 pub account_address: Option<String>,
201 #[builder(default)]
203 pub environment: HyperliquidEnvironment,
204 pub base_url_http: Option<String>,
206 pub base_url_ws: Option<String>,
208 pub base_url_exchange: Option<String>,
210 pub proxy_url: Option<SecretString>,
212 #[builder(default = 60)]
214 pub http_timeout_secs: u64,
215 #[builder(default = 3)]
217 pub max_retries: u32,
218 #[builder(default = 100)]
220 pub retry_delay_initial_ms: u64,
221 #[builder(default = 5000)]
223 pub retry_delay_max_ms: u64,
224 #[builder(default = true)]
227 pub normalize_prices: bool,
228 #[builder(default = 50)]
232 pub market_order_slippage_bps: u32,
233 #[builder(default = true)]
235 pub include_builder_attribution: bool,
236 #[builder(default)]
239 pub transport_backend: TransportBackend,
240 #[builder(default = 10)]
242 pub ws_post_timeout_secs: u64,
243 #[builder(default = 0)]
248 pub outcome_settlement_poll_secs: u64,
249}
250
251#[cfg(feature = "python")]
252nautilus_core::impl_pyo3_config_getters!(HyperliquidExecutionClientConfig {
253 account_id: AccountId,
254 vault_address: Option<String>,
255 account_address: Option<String>,
256 environment: HyperliquidEnvironment,
257 base_url_http: Option<String>,
258 base_url_ws: Option<String>,
259 base_url_exchange: Option<String>,
260 http_timeout_secs: u64,
261 max_retries: u32,
262 retry_delay_initial_ms: u64,
263 retry_delay_max_ms: u64,
264 normalize_prices: bool,
265 market_order_slippage_bps: u32,
266 include_builder_attribution: bool,
267 ws_post_timeout_secs: u64,
268 transport_backend: TransportBackend,
269});
270
271impl Default for HyperliquidExecutionClientConfig {
272 fn default() -> Self {
273 Self::builder().build()
274 }
275}
276
277impl HyperliquidExecutionClientConfig {
278 #[must_use]
280 pub fn has_credentials(&self) -> bool {
281 self.private_key
282 .as_ref()
283 .map(SecretString::expose_secret)
284 .is_some_and(|s| !s.trim().is_empty())
285 }
286
287 #[must_use]
289 pub fn ws_url(&self) -> String {
290 self.base_url_ws
291 .clone()
292 .unwrap_or_else(|| ws_url(self.environment).to_string())
293 }
294
295 #[must_use]
297 pub fn http_url(&self) -> String {
298 self.base_url_http
299 .clone()
300 .unwrap_or_else(|| info_url(self.environment).to_string())
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use rstest::rstest;
307
308 use super::*;
309
310 #[rstest]
311 fn test_exec_config_default_account_address_is_none() {
312 let config = HyperliquidExecutionClientConfig::default();
313 assert!(config.account_address.is_none());
314 }
315
316 #[rstest]
317 fn test_exec_config_with_account_address() {
318 let config = HyperliquidExecutionClientConfig {
319 account_address: Some("0x1234".to_string()),
320 ..HyperliquidExecutionClientConfig::default()
321 };
322 assert_eq!(config.account_address.as_deref(), Some("0x1234"));
323 }
324
325 #[rstest]
326 fn test_data_config_toml_minimal() {
327 let config: HyperliquidDataClientConfig = toml::from_str(
328 r#"
329environment = "testnet"
330http_timeout_secs = 30
331update_instruments_interval_mins = 10
332transport_backend = "tungstenite"
333"#,
334 )
335 .unwrap();
336
337 assert_eq!(config.environment, HyperliquidEnvironment::Testnet);
338 assert_eq!(config.http_timeout_secs, 30);
339 assert_eq!(config.update_instruments_interval_mins, 10);
340 assert_eq!(config.transport_backend, TransportBackend::Tungstenite);
341 assert_eq!(config.stale_stream_receive_timeout_secs, 120);
342 assert_eq!(config.stream_health_check_interval_secs, 15);
343 assert_eq!(config.stale_stream_warning_cooldown_secs, 60);
344 assert!(!config.stale_stream_recovery_enabled);
345 assert_eq!(config.stale_stream_recovery_cooldown_secs, 120);
346 assert_eq!(config.stale_stream_max_targeted_resubscribes, 3);
347 assert_eq!(config.book_snapshot_timeout_secs, 10);
348 }
349
350 #[rstest]
351 fn test_data_config_toml_stale_stream_settings() {
352 let config: HyperliquidDataClientConfig = toml::from_str(
353 "
354stale_stream_receive_timeout_secs = 30
355stream_health_check_interval_secs = 5
356stale_stream_warning_cooldown_secs = 20
357stale_stream_recovery_enabled = true
358stale_stream_recovery_cooldown_secs = 45
359stale_stream_max_targeted_resubscribes = 5
360book_snapshot_timeout_secs = 4
361",
362 )
363 .unwrap();
364
365 assert_eq!(config.stale_stream_receive_timeout_secs, 30);
366 assert_eq!(config.stream_health_check_interval_secs, 5);
367 assert_eq!(config.stale_stream_warning_cooldown_secs, 20);
368 assert!(config.stale_stream_recovery_enabled);
369 assert_eq!(config.stale_stream_recovery_cooldown_secs, 45);
370 assert_eq!(config.stale_stream_max_targeted_resubscribes, 5);
371 assert_eq!(config.book_snapshot_timeout_secs, 4);
372 }
373
374 #[rstest]
375 fn test_exec_config_toml_empty_uses_defaults() {
376 let config: HyperliquidExecutionClientConfig = toml::from_str("").unwrap();
377 let expected = HyperliquidExecutionClientConfig::default();
378
379 assert_eq!(config.environment, expected.environment);
380 assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
381 assert_eq!(config.max_retries, expected.max_retries);
382 assert_eq!(config.normalize_prices, expected.normalize_prices);
383 assert_eq!(
384 config.market_order_slippage_bps,
385 expected.market_order_slippage_bps,
386 );
387 assert_eq!(
388 config.include_builder_attribution,
389 expected.include_builder_attribution,
390 );
391 assert_eq!(config.transport_backend, expected.transport_backend);
392 assert_eq!(config.ws_post_timeout_secs, expected.ws_post_timeout_secs);
393 assert_eq!(
394 config.outcome_settlement_poll_secs,
395 expected.outcome_settlement_poll_secs,
396 );
397 }
398
399 #[rstest]
400 fn test_exec_config_toml_include_builder_attribution_false() {
401 let config: HyperliquidExecutionClientConfig =
402 toml::from_str("include_builder_attribution = false").unwrap();
403
404 assert!(!config.include_builder_attribution);
405 }
406
407 #[rstest]
408 fn test_data_config_debug_redacts_private_key() {
409 let config = HyperliquidDataClientConfig {
410 private_key: Some(
411 "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(),
412 ),
413 ..HyperliquidDataClientConfig::default()
414 };
415 let debug = format!("{config:?}");
416
417 assert!(debug.contains(REDACTED));
418 assert!(!debug.contains("0123456789abcdef"));
419 }
420
421 #[rstest]
422 fn test_exec_config_debug_redacts_private_key() {
423 let config = HyperliquidExecutionClientConfig {
424 private_key: Some(
425 "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(),
426 ),
427 ..HyperliquidExecutionClientConfig::default()
428 };
429 let debug = format!("{config:?}");
430
431 assert!(debug.contains(REDACTED));
432 assert!(!debug.contains("0123456789abcdef"));
433 }
434}