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