use std::fmt::Debug;
use nautilus_network::websocket::TransportBackend;
use serde::{Deserialize, Serialize};
use crate::common::{
consts::{info_url, ws_url},
enums::HyperliquidEnvironment,
};
#[derive(Clone, Serialize, Deserialize, bon::Builder)]
#[serde(default, deny_unknown_fields)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.adapters.hyperliquid", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.hyperliquid")
)]
pub struct HyperliquidDataClientConfig {
pub private_key: Option<String>,
pub base_url_ws: Option<String>,
pub base_url_http: Option<String>,
pub proxy_url: Option<String>,
#[builder(default)]
pub environment: HyperliquidEnvironment,
#[builder(default = 60)]
pub http_timeout_secs: u64,
#[builder(default = 30)]
pub ws_timeout_secs: u64,
#[builder(default = 120)]
pub stale_stream_receive_timeout_secs: u64,
#[builder(default = 15)]
pub stream_health_check_interval_secs: u64,
#[builder(default = 60)]
pub stale_stream_warning_cooldown_secs: u64,
#[builder(default = false)]
pub stale_stream_recovery_enabled: bool,
#[builder(default = 120)]
pub stale_stream_recovery_cooldown_secs: u64,
#[builder(default = 3)]
pub stale_stream_max_targeted_resubscribes: u32,
#[builder(default = 60)]
pub update_instruments_interval_mins: u64,
#[builder(default)]
pub transport_backend: TransportBackend,
}
#[cfg(feature = "python")]
nautilus_core::impl_pyo3_config_getters!(HyperliquidDataClientConfig {
environment: HyperliquidEnvironment,
base_url_ws: Option<String>,
base_url_http: Option<String>,
http_timeout_secs: u64,
ws_timeout_secs: u64,
update_instruments_interval_mins: u64,
transport_backend: TransportBackend,
stale_stream_receive_timeout_secs: u64,
stream_health_check_interval_secs: u64,
stale_stream_warning_cooldown_secs: u64,
stale_stream_recovery_enabled: bool,
stale_stream_recovery_cooldown_secs: u64,
stale_stream_max_targeted_resubscribes: u32,
});
impl Default for HyperliquidDataClientConfig {
fn default() -> Self {
Self::builder().build()
}
}
impl HyperliquidDataClientConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn has_credentials(&self) -> bool {
self.private_key
.as_deref()
.is_some_and(|s| !s.trim().is_empty())
}
#[must_use]
pub fn ws_url(&self) -> String {
self.base_url_ws
.clone()
.unwrap_or_else(|| ws_url(self.environment).to_string())
}
#[must_use]
pub fn http_url(&self) -> String {
self.base_url_http
.clone()
.unwrap_or_else(|| info_url(self.environment).to_string())
}
}
impl Debug for HyperliquidDataClientConfig {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct(stringify!(HyperliquidDataClientConfig))
.field(
"private_key",
&self.private_key.as_ref().map(|_| "[REDACTED]"),
)
.field("base_url_ws", &self.base_url_ws)
.field("base_url_http", &self.base_url_http)
.field("proxy_url", &self.proxy_url)
.field("environment", &self.environment)
.field("http_timeout_secs", &self.http_timeout_secs)
.field("ws_timeout_secs", &self.ws_timeout_secs)
.field(
"stale_stream_receive_timeout_secs",
&self.stale_stream_receive_timeout_secs,
)
.field(
"stream_health_check_interval_secs",
&self.stream_health_check_interval_secs,
)
.field(
"stale_stream_warning_cooldown_secs",
&self.stale_stream_warning_cooldown_secs,
)
.field(
"stale_stream_recovery_enabled",
&self.stale_stream_recovery_enabled,
)
.field(
"stale_stream_recovery_cooldown_secs",
&self.stale_stream_recovery_cooldown_secs,
)
.field(
"stale_stream_max_targeted_resubscribes",
&self.stale_stream_max_targeted_resubscribes,
)
.field(
"update_instruments_interval_mins",
&self.update_instruments_interval_mins,
)
.field("transport_backend", &self.transport_backend)
.finish()
}
}
#[derive(Clone, Serialize, Deserialize, bon::Builder)]
#[serde(default, deny_unknown_fields)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.adapters.hyperliquid", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.hyperliquid")
)]
pub struct HyperliquidExecClientConfig {
pub private_key: Option<String>,
pub vault_address: Option<String>,
pub account_address: Option<String>,
pub base_url_ws: Option<String>,
pub base_url_http: Option<String>,
pub base_url_exchange: Option<String>,
pub proxy_url: Option<String>,
#[builder(default)]
pub environment: HyperliquidEnvironment,
#[builder(default = 60)]
pub http_timeout_secs: u64,
#[builder(default = 3)]
pub max_retries: u32,
#[builder(default = 100)]
pub retry_delay_initial_ms: u64,
#[builder(default = 5000)]
pub retry_delay_max_ms: u64,
#[builder(default = true)]
pub normalize_prices: bool,
#[builder(default = 50)]
pub market_order_slippage_bps: u32,
#[builder(default = true)]
pub include_builder_attribution: bool,
#[builder(default)]
pub transport_backend: TransportBackend,
#[builder(default = 10)]
pub ws_post_timeout_secs: u64,
#[builder(default = 0)]
pub outcome_settlement_poll_secs: u64,
}
#[cfg(feature = "python")]
nautilus_core::impl_pyo3_config_getters!(HyperliquidExecClientConfig {
vault_address: Option<String>,
account_address: Option<String>,
environment: HyperliquidEnvironment,
base_url_ws: Option<String>,
base_url_http: Option<String>,
base_url_exchange: Option<String>,
http_timeout_secs: u64,
max_retries: u32,
retry_delay_initial_ms: u64,
retry_delay_max_ms: u64,
normalize_prices: bool,
market_order_slippage_bps: u32,
include_builder_attribution: bool,
ws_post_timeout_secs: u64,
transport_backend: TransportBackend,
});
impl Default for HyperliquidExecClientConfig {
fn default() -> Self {
Self::builder().build()
}
}
impl HyperliquidExecClientConfig {
#[must_use]
pub fn has_credentials(&self) -> bool {
self.private_key
.as_deref()
.is_some_and(|s| !s.trim().is_empty())
}
#[must_use]
pub fn ws_url(&self) -> String {
self.base_url_ws
.clone()
.unwrap_or_else(|| ws_url(self.environment).to_string())
}
#[must_use]
pub fn http_url(&self) -> String {
self.base_url_http
.clone()
.unwrap_or_else(|| info_url(self.environment).to_string())
}
}
impl Debug for HyperliquidExecClientConfig {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct(stringify!(HyperliquidExecClientConfig))
.field(
"private_key",
&self.private_key.as_ref().map(|_| "[REDACTED]"),
)
.field("vault_address", &self.vault_address)
.field("account_address", &self.account_address)
.field("base_url_ws", &self.base_url_ws)
.field("base_url_http", &self.base_url_http)
.field("base_url_exchange", &self.base_url_exchange)
.field("proxy_url", &self.proxy_url)
.field("environment", &self.environment)
.field("http_timeout_secs", &self.http_timeout_secs)
.field("max_retries", &self.max_retries)
.field("retry_delay_initial_ms", &self.retry_delay_initial_ms)
.field("retry_delay_max_ms", &self.retry_delay_max_ms)
.field("normalize_prices", &self.normalize_prices)
.field("market_order_slippage_bps", &self.market_order_slippage_bps)
.field(
"include_builder_attribution",
&self.include_builder_attribution,
)
.field("transport_backend", &self.transport_backend)
.field("ws_post_timeout_secs", &self.ws_post_timeout_secs)
.field(
"outcome_settlement_poll_secs",
&self.outcome_settlement_poll_secs,
)
.finish()
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
fn test_exec_config_default_account_address_is_none() {
let config = HyperliquidExecClientConfig::default();
assert!(config.account_address.is_none());
}
#[rstest]
fn test_exec_config_with_account_address() {
let config = HyperliquidExecClientConfig {
account_address: Some("0x1234".to_string()),
..HyperliquidExecClientConfig::default()
};
assert_eq!(config.account_address.as_deref(), Some("0x1234"));
}
#[rstest]
fn test_data_config_toml_minimal() {
let config: HyperliquidDataClientConfig = toml::from_str(
r#"
environment = "testnet"
http_timeout_secs = 30
update_instruments_interval_mins = 10
transport_backend = "tungstenite"
"#,
)
.unwrap();
assert_eq!(config.environment, HyperliquidEnvironment::Testnet);
assert_eq!(config.http_timeout_secs, 30);
assert_eq!(config.update_instruments_interval_mins, 10);
assert_eq!(config.transport_backend, TransportBackend::Tungstenite);
assert_eq!(config.stale_stream_receive_timeout_secs, 120);
assert_eq!(config.stream_health_check_interval_secs, 15);
assert_eq!(config.stale_stream_warning_cooldown_secs, 60);
assert!(!config.stale_stream_recovery_enabled);
assert_eq!(config.stale_stream_recovery_cooldown_secs, 120);
assert_eq!(config.stale_stream_max_targeted_resubscribes, 3);
}
#[rstest]
fn test_data_config_toml_stale_stream_settings() {
let config: HyperliquidDataClientConfig = toml::from_str(
"
stale_stream_receive_timeout_secs = 30
stream_health_check_interval_secs = 5
stale_stream_warning_cooldown_secs = 20
stale_stream_recovery_enabled = true
stale_stream_recovery_cooldown_secs = 45
stale_stream_max_targeted_resubscribes = 5
",
)
.unwrap();
assert_eq!(config.stale_stream_receive_timeout_secs, 30);
assert_eq!(config.stream_health_check_interval_secs, 5);
assert_eq!(config.stale_stream_warning_cooldown_secs, 20);
assert!(config.stale_stream_recovery_enabled);
assert_eq!(config.stale_stream_recovery_cooldown_secs, 45);
assert_eq!(config.stale_stream_max_targeted_resubscribes, 5);
}
#[rstest]
fn test_exec_config_toml_empty_uses_defaults() {
let config: HyperliquidExecClientConfig = toml::from_str("").unwrap();
let expected = HyperliquidExecClientConfig::default();
assert_eq!(config.environment, expected.environment);
assert_eq!(config.http_timeout_secs, expected.http_timeout_secs);
assert_eq!(config.max_retries, expected.max_retries);
assert_eq!(config.normalize_prices, expected.normalize_prices);
assert_eq!(
config.market_order_slippage_bps,
expected.market_order_slippage_bps,
);
assert_eq!(
config.include_builder_attribution,
expected.include_builder_attribution,
);
assert_eq!(config.transport_backend, expected.transport_backend);
assert_eq!(config.ws_post_timeout_secs, expected.ws_post_timeout_secs);
assert_eq!(
config.outcome_settlement_poll_secs,
expected.outcome_settlement_poll_secs,
);
}
#[rstest]
fn test_exec_config_toml_include_builder_attribution_false() {
let config: HyperliquidExecClientConfig =
toml::from_str("include_builder_attribution = false").unwrap();
assert!(!config.include_builder_attribution);
}
#[rstest]
fn test_data_config_debug_redacts_private_key() {
let config = HyperliquidDataClientConfig {
private_key: Some(
"0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
),
..HyperliquidDataClientConfig::default()
};
let debug = format!("{config:?}");
assert!(debug.contains("[REDACTED]"));
assert!(!debug.contains("0123456789abcdef"));
}
#[rstest]
fn test_exec_config_debug_redacts_private_key() {
let config = HyperliquidExecClientConfig {
private_key: Some(
"0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
),
..HyperliquidExecClientConfig::default()
};
let debug = format!("{config:?}");
assert!(debug.contains("[REDACTED]"));
assert!(!debug.contains("0123456789abcdef"));
}
}