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