nautilus_derive/
config.rs1use 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.core.nautilus_pyo3.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 #[builder(default = 30)]
52 pub ws_timeout_secs: 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
72impl Default for DeriveDataClientConfig {
73 fn default() -> Self {
74 Self::builder().build()
75 }
76}
77
78impl DeriveDataClientConfig {
79 #[must_use]
80 pub fn new() -> Self {
81 Self::default()
82 }
83
84 #[must_use]
86 pub fn rest_url(&self) -> String {
87 self.base_url_rest
88 .clone()
89 .unwrap_or_else(|| urls::rest_url(self.environment).to_string())
90 }
91
92 #[must_use]
94 pub fn ws_url(&self) -> String {
95 self.base_url_ws
96 .clone()
97 .unwrap_or_else(|| urls::ws_url(self.environment).to_string())
98 }
99}
100
101#[derive(Clone, Serialize, Deserialize, bon::Builder)]
107#[serde(default, deny_unknown_fields)]
108#[cfg_attr(
109 feature = "python",
110 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.derive", from_py_object)
111)]
112#[cfg_attr(
113 feature = "python",
114 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.derive")
115)]
116pub struct DeriveExecClientConfig {
117 pub wallet_address: Option<String>,
121 pub session_key: Option<String>,
125 pub subaccount_id: Option<u64>,
128 pub base_url_rest: Option<String>,
130 pub base_url_ws: Option<String>,
132 pub proxy_url: Option<String>,
134 #[builder(default)]
136 pub environment: DeriveEnvironment,
137 #[builder(default = 10)]
139 pub http_timeout_secs: u64,
140 #[builder(default = 3)]
142 pub max_retries: u32,
143 #[builder(default = 100)]
145 pub retry_delay_initial_ms: u64,
146 #[builder(default = 5000)]
148 pub retry_delay_max_ms: u64,
149 pub max_fee_per_contract: Option<Decimal>,
151 #[builder(default)]
153 pub transport_backend: TransportBackend,
154 pub domain_separator: Option<String>,
158 pub action_typehash: Option<String>,
161 pub trade_module_address: Option<String>,
164 #[builder(default = 600)]
168 pub signature_expiry_secs: u64,
169 #[builder(default = 50)]
173 pub market_order_slippage_bps: u32,
174 pub max_matching_requests_per_second: Option<u32>,
179}
180
181impl Default for DeriveExecClientConfig {
182 fn default() -> Self {
183 Self::builder().build()
184 }
185}
186
187impl Debug for DeriveExecClientConfig {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 f.debug_struct(stringify!(DeriveExecClientConfig))
190 .field("wallet_address", &self.wallet_address)
191 .field(
192 "session_key",
193 &self.session_key.as_deref().map(|_| "***redacted***"),
194 )
195 .field("subaccount_id", &self.subaccount_id)
196 .field("base_url_rest", &self.base_url_rest)
197 .field("base_url_ws", &self.base_url_ws)
198 .field("proxy_url", &self.proxy_url)
199 .field("environment", &self.environment)
200 .field("http_timeout_secs", &self.http_timeout_secs)
201 .field("max_retries", &self.max_retries)
202 .field("retry_delay_initial_ms", &self.retry_delay_initial_ms)
203 .field("retry_delay_max_ms", &self.retry_delay_max_ms)
204 .field("max_fee_per_contract", &self.max_fee_per_contract)
205 .field("transport_backend", &self.transport_backend)
206 .field("domain_separator", &self.domain_separator)
207 .field("action_typehash", &self.action_typehash)
208 .field("trade_module_address", &self.trade_module_address)
209 .field("signature_expiry_secs", &self.signature_expiry_secs)
210 .field("market_order_slippage_bps", &self.market_order_slippage_bps)
211 .field(
212 "max_matching_requests_per_second",
213 &self.max_matching_requests_per_second,
214 )
215 .finish()
216 }
217}
218
219impl DeriveExecClientConfig {
220 #[must_use]
221 pub fn new() -> Self {
222 Self::default()
223 }
224
225 #[must_use]
231 pub fn has_credentials(&self) -> bool {
232 self.wallet_address
233 .as_deref()
234 .is_some_and(|s| !s.trim().is_empty())
235 && self
236 .session_key
237 .as_deref()
238 .is_some_and(|s| !s.trim().is_empty())
239 && self.subaccount_id.is_some()
240 }
241
242 #[must_use]
244 pub fn rest_url(&self) -> String {
245 self.base_url_rest
246 .clone()
247 .unwrap_or_else(|| urls::rest_url(self.environment).to_string())
248 }
249
250 #[must_use]
252 pub fn ws_url(&self) -> String {
253 self.base_url_ws
254 .clone()
255 .unwrap_or_else(|| urls::ws_url(self.environment).to_string())
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use rstest::rstest;
262
263 use super::*;
264
265 #[rstest]
266 fn test_data_config_defaults() {
267 let config = DeriveDataClientConfig::default();
268 assert_eq!(config.environment, DeriveEnvironment::Mainnet);
269 assert_eq!(config.http_timeout_secs, 10);
270 assert_eq!(config.ws_timeout_secs, 30);
271 assert_eq!(config.update_instruments_interval_mins, 60);
272 assert!(config.currencies.is_empty());
273 assert!(!config.include_expired);
274 assert!(config.auto_load_missing_instruments);
275 }
276
277 #[rstest]
278 fn test_data_config_urls_mainnet() {
279 let config = DeriveDataClientConfig::default();
280 assert!(config.rest_url().contains("api.lyra.finance"));
281 assert!(config.ws_url().contains("api.lyra.finance"));
282 }
283
284 #[rstest]
285 fn test_data_config_urls_testnet() {
286 let config = DeriveDataClientConfig {
287 environment: DeriveEnvironment::Testnet,
288 ..DeriveDataClientConfig::default()
289 };
290 assert!(config.rest_url().contains("demo"));
291 assert!(config.ws_url().contains("demo"));
292 }
293
294 #[rstest]
295 fn test_exec_config_defaults() {
296 let config = DeriveExecClientConfig::default();
297 assert_eq!(config.environment, DeriveEnvironment::Mainnet);
298 assert_eq!(config.http_timeout_secs, 10);
299 assert_eq!(config.max_retries, 3);
300 assert!(config.max_matching_requests_per_second.is_none());
301 assert!(!config.has_credentials());
302 }
303
304 #[rstest]
305 fn test_exec_config_has_credentials_requires_all_three_fields() {
306 let mut config = DeriveExecClientConfig {
307 wallet_address: Some("0x1234".to_string()),
308 ..DeriveExecClientConfig::default()
309 };
310 assert!(!config.has_credentials());
311
312 config.session_key = Some("0xabcd".to_string());
313 assert!(!config.has_credentials());
314
315 config.subaccount_id = Some(1);
316 assert!(config.has_credentials());
317 }
318
319 #[rstest]
320 fn test_exec_config_has_credentials_rejects_blank_strings() {
321 let config = DeriveExecClientConfig {
322 wallet_address: Some(" ".to_string()),
323 session_key: Some("0xabcd".to_string()),
324 subaccount_id: Some(1),
325 ..DeriveExecClientConfig::default()
326 };
327 assert!(!config.has_credentials());
328 }
329
330 #[rstest]
331 fn test_exec_config_debug_redacts_session_key() {
332 let session_key = "FAKE_SESSION_KEY_SENTINEL";
337 let config = DeriveExecClientConfig {
338 wallet_address: Some("0xWALLET".to_string()),
339 session_key: Some(session_key.to_string()),
340 subaccount_id: Some(42),
341 ..DeriveExecClientConfig::default()
342 };
343 let debug = format!("{config:?}");
344 assert!(debug.contains("redacted"));
345 assert!(!debug.contains(session_key));
346 assert!(debug.contains("0xWALLET"));
347 assert!(debug.contains("42"));
348 }
349
350 #[rstest]
351 fn test_exec_config_debug_omits_session_key_marker_when_unset() {
352 let config = DeriveExecClientConfig::default();
353 let debug = format!("{config:?}");
354 assert!(!debug.contains("redacted"));
355 assert!(debug.contains("session_key: None"));
356 }
357}