1use std::fmt::Debug;
19
20use nautilus_network::websocket::TransportBackend;
21use rust_decimal::Decimal;
22use serde::{Deserialize, Serialize};
23
24use crate::common::{enums::DeriveEnvironment, urls};
25
26#[derive(Clone, Debug, Serialize, Deserialize, bon::Builder)]
28#[serde(default, deny_unknown_fields)]
29#[cfg_attr(
30 feature = "python",
31 pyo3::pyclass(module = "nautilus_trader.adapters.derive", from_py_object)
32)]
33#[cfg_attr(
34 feature = "python",
35 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.derive")
36)]
37pub struct DeriveDataClientConfig {
38 pub base_url_rest: Option<String>,
40 pub base_url_ws: Option<String>,
42 pub proxy_url: Option<String>,
44 #[builder(default)]
46 pub environment: DeriveEnvironment,
47 #[builder(default = 10)]
49 pub http_timeout_secs: u64,
50 pub ws_timeout_secs: Option<u64>,
53 #[builder(default = 60)]
55 pub update_instruments_interval_mins: u64,
56 #[builder(default)]
59 pub currencies: Vec<String>,
60 #[builder(default)]
62 pub include_expired: bool,
63 #[builder(default = true)]
66 pub auto_load_missing_instruments: bool,
67 #[builder(default)]
69 pub transport_backend: TransportBackend,
70}
71
72#[cfg(feature = "python")]
73nautilus_core::impl_pyo3_config_getters!(DeriveDataClientConfig {
74 base_url_rest: Option<String>,
75 base_url_ws: Option<String>,
76 environment: DeriveEnvironment,
77 http_timeout_secs: u64,
78 ws_timeout_secs: Option<u64>,
79 update_instruments_interval_mins: u64,
80 currencies: Vec<String>,
81 include_expired: bool,
82 auto_load_missing_instruments: bool,
83 transport_backend: TransportBackend,
84});
85
86impl Default for DeriveDataClientConfig {
87 fn default() -> Self {
88 Self::builder().build()
89 }
90}
91
92impl DeriveDataClientConfig {
93 #[must_use]
94 pub fn new() -> Self {
95 Self::default()
96 }
97
98 #[must_use]
100 pub fn rest_url(&self) -> String {
101 self.base_url_rest
102 .clone()
103 .unwrap_or_else(|| urls::rest_url(self.environment).to_string())
104 }
105
106 #[must_use]
108 pub fn ws_url(&self) -> String {
109 self.base_url_ws
110 .clone()
111 .unwrap_or_else(|| urls::ws_url(self.environment).to_string())
112 }
113}
114
115#[derive(Clone, Serialize, Deserialize, bon::Builder)]
121#[serde(default, deny_unknown_fields)]
122#[cfg_attr(
123 feature = "python",
124 pyo3::pyclass(module = "nautilus_trader.adapters.derive", from_py_object)
125)]
126#[cfg_attr(
127 feature = "python",
128 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.derive")
129)]
130pub struct DeriveExecClientConfig {
131 pub wallet_address: Option<String>,
135 pub session_key: Option<String>,
139 pub subaccount_id: Option<u64>,
142 pub base_url_rest: Option<String>,
144 pub base_url_ws: Option<String>,
146 pub proxy_url: Option<String>,
148 #[builder(default)]
150 pub environment: DeriveEnvironment,
151 #[builder(default = 10)]
153 pub http_timeout_secs: u64,
154 #[builder(default = 3)]
156 pub max_retries: u32,
157 #[builder(default = 100)]
159 pub retry_delay_initial_ms: u64,
160 #[builder(default = 5000)]
162 pub retry_delay_max_ms: u64,
163 pub ws_timeout_secs: Option<u64>,
166 pub max_fee_per_contract: Option<Decimal>,
169 #[builder(default)]
171 pub transport_backend: TransportBackend,
172 pub domain_separator: Option<String>,
176 pub action_typehash: Option<String>,
179 pub trade_module_address: Option<String>,
182 #[builder(default = 600)]
186 pub signature_expiry_secs: u64,
187 #[builder(default = 50)]
191 pub market_order_slippage_bps: u32,
192 pub max_matching_requests_per_second: Option<u32>,
197 pub max_per_instrument_matching_requests_per_second: Option<u32>,
204}
205
206#[cfg(feature = "python")]
207nautilus_core::impl_pyo3_config_getters!(DeriveExecClientConfig {
208 wallet_address: Option<String>,
209 subaccount_id: Option<u64>,
210 base_url_rest: Option<String>,
211 base_url_ws: Option<String>,
212 environment: DeriveEnvironment,
213 http_timeout_secs: u64,
214 max_retries: u32,
215 retry_delay_initial_ms: u64,
216 retry_delay_max_ms: u64,
217 ws_timeout_secs: Option<u64>,
218 max_fee_per_contract: Option<Decimal>,
219 domain_separator: Option<String>,
220 action_typehash: Option<String>,
221 trade_module_address: Option<String>,
222 signature_expiry_secs: u64,
223 market_order_slippage_bps: u32,
224 max_matching_requests_per_second: Option<u32>,
225 max_per_instrument_matching_requests_per_second: Option<u32>,
226 transport_backend: TransportBackend,
227});
228
229impl Default for DeriveExecClientConfig {
230 fn default() -> Self {
231 Self::builder().build()
232 }
233}
234
235impl Debug for DeriveExecClientConfig {
236 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237 f.debug_struct(stringify!(DeriveExecClientConfig))
238 .field("wallet_address", &self.wallet_address)
239 .field(
240 "session_key",
241 &self.session_key.as_deref().map(|_| "***redacted***"),
242 )
243 .field("subaccount_id", &self.subaccount_id)
244 .field("base_url_rest", &self.base_url_rest)
245 .field("base_url_ws", &self.base_url_ws)
246 .field("proxy_url", &self.proxy_url)
247 .field("environment", &self.environment)
248 .field("http_timeout_secs", &self.http_timeout_secs)
249 .field("max_retries", &self.max_retries)
250 .field("retry_delay_initial_ms", &self.retry_delay_initial_ms)
251 .field("retry_delay_max_ms", &self.retry_delay_max_ms)
252 .field("max_fee_per_contract", &self.max_fee_per_contract)
253 .field("transport_backend", &self.transport_backend)
254 .field("domain_separator", &self.domain_separator)
255 .field("action_typehash", &self.action_typehash)
256 .field("trade_module_address", &self.trade_module_address)
257 .field("signature_expiry_secs", &self.signature_expiry_secs)
258 .field("market_order_slippage_bps", &self.market_order_slippage_bps)
259 .field(
260 "max_matching_requests_per_second",
261 &self.max_matching_requests_per_second,
262 )
263 .field(
264 "max_per_instrument_matching_requests_per_second",
265 &self.max_per_instrument_matching_requests_per_second,
266 )
267 .finish()
268 }
269}
270
271impl DeriveExecClientConfig {
272 #[must_use]
273 pub fn new() -> Self {
274 Self::default()
275 }
276
277 #[must_use]
283 pub fn has_credentials(&self) -> bool {
284 self.wallet_address
285 .as_deref()
286 .is_some_and(|s| !s.trim().is_empty())
287 && self
288 .session_key
289 .as_deref()
290 .is_some_and(|s| !s.trim().is_empty())
291 && self.subaccount_id.is_some()
292 }
293
294 pub fn validate(&self) -> anyhow::Result<()> {
301 let Some(max_fee_per_contract) = self.max_fee_per_contract else {
302 anyhow::bail!("max_fee_per_contract is required");
303 };
304
305 if max_fee_per_contract <= Decimal::ZERO {
306 anyhow::bail!("max_fee_per_contract must be greater than zero");
307 }
308 Ok(())
309 }
310
311 #[must_use]
313 pub fn rest_url(&self) -> String {
314 self.base_url_rest
315 .clone()
316 .unwrap_or_else(|| urls::rest_url(self.environment).to_string())
317 }
318
319 #[must_use]
321 pub fn ws_url(&self) -> String {
322 self.base_url_ws
323 .clone()
324 .unwrap_or_else(|| urls::ws_url(self.environment).to_string())
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use rstest::rstest;
331
332 use super::*;
333
334 #[rstest]
335 fn test_data_config_defaults() {
336 let config = DeriveDataClientConfig::default();
337 assert_eq!(config.environment, DeriveEnvironment::Mainnet);
338 assert_eq!(config.http_timeout_secs, 10);
339 assert_eq!(config.ws_timeout_secs, None);
340 assert_eq!(config.update_instruments_interval_mins, 60);
341 assert!(config.currencies.is_empty());
342 assert!(!config.include_expired);
343 assert!(config.auto_load_missing_instruments);
344 }
345
346 #[rstest]
347 fn test_data_config_urls_mainnet() {
348 let config = DeriveDataClientConfig::default();
349 assert!(config.rest_url().contains("api.lyra.finance"));
350 assert!(config.ws_url().contains("api.lyra.finance"));
351 }
352
353 #[rstest]
354 fn test_data_config_urls_testnet() {
355 let config = DeriveDataClientConfig {
356 environment: DeriveEnvironment::Testnet,
357 ..DeriveDataClientConfig::default()
358 };
359 assert!(config.rest_url().contains("demo"));
360 assert!(config.ws_url().contains("demo"));
361 }
362
363 #[rstest]
364 fn test_exec_config_defaults() {
365 let config = DeriveExecClientConfig::default();
366 assert_eq!(config.environment, DeriveEnvironment::Mainnet);
367 assert_eq!(config.http_timeout_secs, 10);
368 assert_eq!(config.max_retries, 3);
369 assert!(config.max_matching_requests_per_second.is_none());
370 assert!(
371 config
372 .max_per_instrument_matching_requests_per_second
373 .is_none()
374 );
375 assert!(!config.has_credentials());
376 }
377
378 #[rstest]
379 fn test_exec_config_has_credentials_requires_all_three_fields() {
380 let mut config = DeriveExecClientConfig {
381 wallet_address: Some("0x1234".to_string()),
382 ..DeriveExecClientConfig::default()
383 };
384 assert!(!config.has_credentials());
385
386 config.session_key = Some("0xabcd".to_string());
387 assert!(!config.has_credentials());
388
389 config.subaccount_id = Some(1);
390 assert!(config.has_credentials());
391 }
392
393 #[rstest]
394 fn test_exec_config_has_credentials_rejects_blank_strings() {
395 let config = DeriveExecClientConfig {
396 wallet_address: Some(" ".to_string()),
397 session_key: Some("0xabcd".to_string()),
398 subaccount_id: Some(1),
399 ..DeriveExecClientConfig::default()
400 };
401 assert!(!config.has_credentials());
402 }
403
404 #[rstest]
405 fn test_exec_config_debug_redacts_session_key() {
406 let session_key = "FAKE_SESSION_KEY_SENTINEL";
411 let config = DeriveExecClientConfig {
412 wallet_address: Some("0xWALLET".to_string()),
413 session_key: Some(session_key.to_string()),
414 subaccount_id: Some(42),
415 ..DeriveExecClientConfig::default()
416 };
417 let debug = format!("{config:?}");
418 assert!(debug.contains("redacted"));
419 assert!(!debug.contains(session_key));
420 assert!(debug.contains("0xWALLET"));
421 assert!(debug.contains("42"));
422 }
423
424 #[rstest]
425 fn test_exec_config_debug_omits_session_key_marker_when_unset() {
426 let config = DeriveExecClientConfig::default();
427 let debug = format!("{config:?}");
428 assert!(!debug.contains("redacted"));
429 assert!(debug.contains("session_key: None"));
430 }
431}