use std::fmt::Debug;
use nautilus_network::websocket::TransportBackend;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use crate::common::{enums::DeriveEnvironment, urls};
#[derive(Clone, Debug, Serialize, Deserialize, bon::Builder)]
#[serde(default, deny_unknown_fields)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.derive", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.derive")
)]
pub struct DeriveDataClientConfig {
pub base_url_rest: Option<String>,
pub base_url_ws: Option<String>,
pub proxy_url: Option<String>,
#[builder(default)]
pub environment: DeriveEnvironment,
#[builder(default = 10)]
pub http_timeout_secs: u64,
#[builder(default = 30)]
pub ws_timeout_secs: u64,
#[builder(default = 60)]
pub update_instruments_interval_mins: u64,
#[builder(default)]
pub currencies: Vec<String>,
#[builder(default)]
pub include_expired: bool,
#[builder(default = true)]
pub auto_load_missing_instruments: bool,
#[builder(default)]
pub transport_backend: TransportBackend,
}
impl Default for DeriveDataClientConfig {
fn default() -> Self {
Self::builder().build()
}
}
impl DeriveDataClientConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn rest_url(&self) -> String {
self.base_url_rest
.clone()
.unwrap_or_else(|| urls::rest_url(self.environment).to_string())
}
#[must_use]
pub fn ws_url(&self) -> String {
self.base_url_ws
.clone()
.unwrap_or_else(|| urls::ws_url(self.environment).to_string())
}
}
#[derive(Clone, Serialize, Deserialize, bon::Builder)]
#[serde(default, deny_unknown_fields)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.derive", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.derive")
)]
pub struct DeriveExecClientConfig {
pub wallet_address: Option<String>,
pub session_key: Option<String>,
pub subaccount_id: Option<u64>,
pub base_url_rest: Option<String>,
pub base_url_ws: Option<String>,
pub proxy_url: Option<String>,
#[builder(default)]
pub environment: DeriveEnvironment,
#[builder(default = 10)]
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,
pub max_fee_per_contract: Option<Decimal>,
#[builder(default)]
pub transport_backend: TransportBackend,
pub domain_separator: Option<String>,
pub action_typehash: Option<String>,
pub trade_module_address: Option<String>,
#[builder(default = 600)]
pub signature_expiry_secs: u64,
#[builder(default = 50)]
pub market_order_slippage_bps: u32,
pub max_matching_requests_per_second: Option<u32>,
}
impl Default for DeriveExecClientConfig {
fn default() -> Self {
Self::builder().build()
}
}
impl Debug for DeriveExecClientConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(stringify!(DeriveExecClientConfig))
.field("wallet_address", &self.wallet_address)
.field(
"session_key",
&self.session_key.as_deref().map(|_| "***redacted***"),
)
.field("subaccount_id", &self.subaccount_id)
.field("base_url_rest", &self.base_url_rest)
.field("base_url_ws", &self.base_url_ws)
.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("max_fee_per_contract", &self.max_fee_per_contract)
.field("transport_backend", &self.transport_backend)
.field("domain_separator", &self.domain_separator)
.field("action_typehash", &self.action_typehash)
.field("trade_module_address", &self.trade_module_address)
.field("signature_expiry_secs", &self.signature_expiry_secs)
.field("market_order_slippage_bps", &self.market_order_slippage_bps)
.field(
"max_matching_requests_per_second",
&self.max_matching_requests_per_second,
)
.finish()
}
}
impl DeriveExecClientConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn has_credentials(&self) -> bool {
self.wallet_address
.as_deref()
.is_some_and(|s| !s.trim().is_empty())
&& self
.session_key
.as_deref()
.is_some_and(|s| !s.trim().is_empty())
&& self.subaccount_id.is_some()
}
#[must_use]
pub fn rest_url(&self) -> String {
self.base_url_rest
.clone()
.unwrap_or_else(|| urls::rest_url(self.environment).to_string())
}
#[must_use]
pub fn ws_url(&self) -> String {
self.base_url_ws
.clone()
.unwrap_or_else(|| urls::ws_url(self.environment).to_string())
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
fn test_data_config_defaults() {
let config = DeriveDataClientConfig::default();
assert_eq!(config.environment, DeriveEnvironment::Mainnet);
assert_eq!(config.http_timeout_secs, 10);
assert_eq!(config.ws_timeout_secs, 30);
assert_eq!(config.update_instruments_interval_mins, 60);
assert!(config.currencies.is_empty());
assert!(!config.include_expired);
assert!(config.auto_load_missing_instruments);
}
#[rstest]
fn test_data_config_urls_mainnet() {
let config = DeriveDataClientConfig::default();
assert!(config.rest_url().contains("api.lyra.finance"));
assert!(config.ws_url().contains("api.lyra.finance"));
}
#[rstest]
fn test_data_config_urls_testnet() {
let config = DeriveDataClientConfig {
environment: DeriveEnvironment::Testnet,
..DeriveDataClientConfig::default()
};
assert!(config.rest_url().contains("demo"));
assert!(config.ws_url().contains("demo"));
}
#[rstest]
fn test_exec_config_defaults() {
let config = DeriveExecClientConfig::default();
assert_eq!(config.environment, DeriveEnvironment::Mainnet);
assert_eq!(config.http_timeout_secs, 10);
assert_eq!(config.max_retries, 3);
assert!(config.max_matching_requests_per_second.is_none());
assert!(!config.has_credentials());
}
#[rstest]
fn test_exec_config_has_credentials_requires_all_three_fields() {
let mut config = DeriveExecClientConfig {
wallet_address: Some("0x1234".to_string()),
..DeriveExecClientConfig::default()
};
assert!(!config.has_credentials());
config.session_key = Some("0xabcd".to_string());
assert!(!config.has_credentials());
config.subaccount_id = Some(1);
assert!(config.has_credentials());
}
#[rstest]
fn test_exec_config_has_credentials_rejects_blank_strings() {
let config = DeriveExecClientConfig {
wallet_address: Some(" ".to_string()),
session_key: Some("0xabcd".to_string()),
subaccount_id: Some(1),
..DeriveExecClientConfig::default()
};
assert!(!config.has_credentials());
}
#[rstest]
fn test_exec_config_debug_redacts_session_key() {
let session_key = "FAKE_SESSION_KEY_SENTINEL";
let config = DeriveExecClientConfig {
wallet_address: Some("0xWALLET".to_string()),
session_key: Some(session_key.to_string()),
subaccount_id: Some(42),
..DeriveExecClientConfig::default()
};
let debug = format!("{config:?}");
assert!(debug.contains("redacted"));
assert!(!debug.contains(session_key));
assert!(debug.contains("0xWALLET"));
assert!(debug.contains("42"));
}
#[rstest]
fn test_exec_config_debug_omits_session_key_marker_when_unset() {
let config = DeriveExecClientConfig::default();
let debug = format!("{config:?}");
assert!(!debug.contains("redacted"));
assert!(debug.contains("session_key: None"));
}
}