1use std::num::NonZeroU32;
19
20use nautilus_model::identifiers::{AccountId, TraderId};
21use nautilus_network::{ratelimiter::quota::Quota, websocket::TransportBackend};
22use serde::{Deserialize, Serialize};
23
24use crate::{
25 common::{consts::DYDX_CHAIN_ID, enums::DydxNetwork, urls},
26 grpc::types::ChainId,
27};
28
29#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
35#[serde(deny_unknown_fields)]
36pub struct DydxAdapterConfig {
37 #[serde(default)]
39 #[builder(default)]
40 pub network: DydxNetwork,
41 #[builder(default = urls::http_base_url(DydxNetwork::Mainnet).to_string())]
43 pub base_url: String,
44 #[builder(default = urls::ws_url(DydxNetwork::Mainnet).to_string())]
46 pub ws_url: String,
47 #[builder(default = urls::grpc_urls(DydxNetwork::Mainnet)[0].to_string())]
52 pub grpc_url: String,
53 #[serde(default)]
59 #[builder(default = urls::grpc_urls(DydxNetwork::Mainnet).iter().map(|&s| s.to_string()).collect())]
60 pub grpc_urls: Vec<String>,
61 #[builder(default = DYDX_CHAIN_ID.to_string())]
63 pub chain_id: String,
64 #[builder(default = 30)]
66 pub timeout_secs: u64,
67 #[serde(default)]
75 pub wallet_address: Option<String>,
76 #[serde(default)]
78 #[builder(default)]
79 pub subaccount: u32,
80 #[serde(default)]
88 pub private_key: Option<String>,
89 #[serde(default)]
98 #[builder(default)]
99 pub authenticator_ids: Vec<u64>,
100 #[serde(default = "default_max_retries")]
102 #[builder(default = 3)]
103 pub max_retries: u32,
104 #[serde(default = "default_retry_delay_initial_ms")]
106 #[builder(default = 1000)]
107 pub retry_delay_initial_ms: u64,
108 #[serde(default = "default_retry_delay_max_ms")]
110 #[builder(default = 10000)]
111 pub retry_delay_max_ms: u64,
112 #[serde(default = "default_grpc_rate_limit_per_second")]
123 pub grpc_rate_limit_per_second: Option<u32>,
124 #[serde(default)]
126 pub proxy_url: Option<String>,
127 #[serde(default)]
129 #[builder(default)]
130 pub transport_backend: TransportBackend,
131}
132
133fn default_max_retries() -> u32 {
134 3
135}
136
137fn default_retry_delay_initial_ms() -> u64 {
138 1000
139}
140
141fn default_retry_delay_max_ms() -> u64 {
142 10000
143}
144
145#[expect(
146 clippy::unnecessary_wraps,
147 reason = "serde default must match field type Option<u32>"
148)]
149fn default_grpc_rate_limit_per_second() -> Option<u32> {
150 Some(4)
151}
152
153fn default_data_http_timeout_secs() -> u64 {
154 60
155}
156
157fn default_data_max_retries() -> u64 {
158 3
159}
160
161fn default_data_retry_delay_initial_ms() -> u64 {
162 100
163}
164
165fn default_data_retry_delay_max_ms() -> u64 {
166 5000
167}
168
169impl DydxAdapterConfig {
170 #[must_use]
176 pub fn for_network(network: DydxNetwork) -> Self {
177 let chain_id = match network {
178 DydxNetwork::Mainnet => crate::common::consts::DYDX_CHAIN_ID,
179 DydxNetwork::Testnet => crate::common::consts::DYDX_TESTNET_CHAIN_ID,
180 };
181 Self {
182 network,
183 base_url: urls::http_base_url(network).to_string(),
184 ws_url: urls::ws_url(network).to_string(),
185 grpc_url: urls::grpc_urls(network)[0].to_string(),
186 grpc_urls: urls::grpc_urls(network)
187 .iter()
188 .map(|&s| s.to_string())
189 .collect(),
190 chain_id: chain_id.to_string(),
191 ..Self::default()
192 }
193 }
194
195 #[must_use]
200 pub fn get_grpc_urls(&self) -> Vec<String> {
201 if self.grpc_urls.is_empty() {
202 vec![self.grpc_url.clone()]
203 } else {
204 self.grpc_urls.clone()
205 }
206 }
207
208 #[must_use]
212 pub const fn get_chain_id(&self) -> ChainId {
213 self.network.chain_id()
214 }
215
216 #[must_use]
218 pub const fn is_testnet(&self) -> bool {
219 matches!(self.network, DydxNetwork::Testnet)
220 }
221
222 #[must_use]
224 pub fn grpc_quota(&self) -> Option<Quota> {
225 self.grpc_rate_limit_per_second
226 .and_then(NonZeroU32::new)
227 .and_then(Quota::per_second)
228 }
229}
230
231impl Default for DydxAdapterConfig {
232 fn default() -> Self {
233 Self {
234 grpc_rate_limit_per_second: default_grpc_rate_limit_per_second(),
235 ..Self::builder().build()
236 }
237 }
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
242#[serde(deny_unknown_fields)]
243#[cfg_attr(
244 feature = "python",
245 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.dydx", from_py_object)
246)]
247#[cfg_attr(
248 feature = "python",
249 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.dydx")
250)]
251pub struct DydxDataClientConfig {
252 pub base_url_http: Option<String>,
254 pub base_url_ws: Option<String>,
256 #[serde(default = "default_data_http_timeout_secs")]
258 #[builder(default = 60)]
259 pub http_timeout_secs: u64,
260 #[serde(default = "default_data_max_retries")]
262 #[builder(default = 3)]
263 pub max_retries: u64,
264 #[serde(default = "default_data_retry_delay_initial_ms")]
266 #[builder(default = 100)]
267 pub retry_delay_initial_ms: u64,
268 #[serde(default = "default_data_retry_delay_max_ms")]
270 #[builder(default = 5000)]
271 pub retry_delay_max_ms: u64,
272 #[serde(default)]
274 #[builder(default)]
275 pub network: DydxNetwork,
276 pub proxy_url: Option<String>,
278 #[serde(default)]
280 #[builder(default)]
281 pub transport_backend: TransportBackend,
282}
283
284impl DydxDataClientConfig {
285 #[must_use]
287 pub const fn is_testnet(&self) -> bool {
288 matches!(self.network, DydxNetwork::Testnet)
289 }
290}
291
292impl Default for DydxDataClientConfig {
293 fn default() -> Self {
294 Self::builder().build()
295 }
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
300#[serde(deny_unknown_fields)]
301#[cfg_attr(
302 feature = "python",
303 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.dydx", from_py_object)
304)]
305#[cfg_attr(
306 feature = "python",
307 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.dydx")
308)]
309pub struct DydxExecClientConfig {
310 #[builder(default = TraderId::from("TRADER-001"))]
312 pub trader_id: TraderId,
313 #[builder(default = AccountId::from("DYDX-001"))]
315 pub account_id: AccountId,
316 #[serde(default)]
318 #[builder(default)]
319 pub network: DydxNetwork,
320 pub grpc_endpoint: Option<String>,
322 #[serde(default)]
324 #[builder(default)]
325 pub grpc_urls: Vec<String>,
326 pub ws_endpoint: Option<String>,
328 pub http_endpoint: Option<String>,
330 pub private_key: Option<String>,
336 pub wallet_address: Option<String>,
342 #[serde(default)]
344 #[builder(default)]
345 pub subaccount_number: u32,
346 #[serde(default)]
348 #[builder(default)]
349 pub authenticator_ids: Vec<u64>,
350 pub http_timeout_secs: Option<u64>,
352 pub max_retries: Option<u32>,
354 pub retry_delay_initial_ms: Option<u64>,
356 pub retry_delay_max_ms: Option<u64>,
358 #[serde(default = "default_grpc_rate_limit_per_second")]
361 pub grpc_rate_limit_per_second: Option<u32>,
362 pub proxy_url: Option<String>,
364 #[serde(default)]
366 #[builder(default)]
367 pub transport_backend: TransportBackend,
368}
369
370impl Default for DydxExecClientConfig {
371 fn default() -> Self {
372 Self {
373 grpc_rate_limit_per_second: default_grpc_rate_limit_per_second(),
374 ..Self::builder().build()
375 }
376 }
377}
378
379impl DydxExecClientConfig {
380 #[must_use]
385 pub fn get_grpc_urls(&self) -> Vec<String> {
386 if !self.grpc_urls.is_empty() {
387 return self.grpc_urls.clone();
388 }
389
390 if let Some(ref endpoint) = self.grpc_endpoint {
391 return vec![endpoint.clone()];
392 }
393 urls::grpc_urls(self.network)
394 .iter()
395 .map(|&s| s.to_string())
396 .collect()
397 }
398
399 #[must_use]
401 pub fn get_ws_url(&self) -> String {
402 self.ws_endpoint
403 .clone()
404 .unwrap_or_else(|| urls::ws_url(self.network).to_string())
405 }
406
407 #[must_use]
409 pub fn get_http_url(&self) -> String {
410 self.http_endpoint
411 .clone()
412 .unwrap_or_else(|| urls::http_base_url(self.network).to_string())
413 }
414
415 #[must_use]
417 pub const fn get_chain_id(&self) -> ChainId {
418 self.network.chain_id()
419 }
420
421 #[must_use]
423 pub const fn is_testnet(&self) -> bool {
424 matches!(self.network, DydxNetwork::Testnet)
425 }
426
427 #[must_use]
429 pub fn grpc_quota(&self) -> Option<Quota> {
430 self.grpc_rate_limit_per_second
431 .and_then(NonZeroU32::new)
432 .and_then(Quota::per_second)
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use rstest::rstest;
439
440 use super::*;
441
442 #[rstest]
443 fn test_config_get_chain_id_mainnet() {
444 let config = DydxAdapterConfig {
445 network: DydxNetwork::Mainnet,
446 ..Default::default()
447 };
448 assert_eq!(config.get_chain_id(), ChainId::Mainnet1);
449 }
450
451 #[rstest]
452 fn test_config_get_chain_id_testnet() {
453 let config = DydxAdapterConfig {
454 network: DydxNetwork::Testnet,
455 ..Default::default()
456 };
457 assert_eq!(config.get_chain_id(), ChainId::Testnet4);
458 }
459
460 #[rstest]
461 fn test_config_is_testnet() {
462 let mainnet_config = DydxAdapterConfig {
463 network: DydxNetwork::Mainnet,
464 ..Default::default()
465 };
466 assert!(!mainnet_config.is_testnet());
467
468 let testnet_config = DydxAdapterConfig {
469 network: DydxNetwork::Testnet,
470 ..Default::default()
471 };
472 assert!(testnet_config.is_testnet());
473 }
474
475 #[rstest]
476 fn test_config_default_uses_mainnet() {
477 let config = DydxAdapterConfig::default();
478 assert_eq!(config.network, DydxNetwork::Mainnet);
479 assert!(!config.is_testnet());
480 }
481
482 #[rstest]
483 fn test_config_serde_backwards_compat() {
484 let json = r#"{"base_url":"https://indexer.dydx.trade","ws_url":"wss://indexer.dydx.trade/v4/ws","grpc_url":"https://dydx-ops-grpc.kingnodes.com:443","grpc_urls":[],"chain_id":"dydx-mainnet-1","timeout_secs":30,"subaccount":0,"max_retries":3,"retry_delay_initial_ms":1000,"retry_delay_max_ms":10000}"#;
486
487 let config: Result<DydxAdapterConfig, _> = serde_json::from_str(json);
488 assert!(config.is_ok());
489 let config = config.unwrap();
490 assert_eq!(config.network, DydxNetwork::Mainnet);
492 }
493
494 #[rstest]
495 fn test_config_get_grpc_urls_fallback() {
496 let config = DydxAdapterConfig {
497 grpc_url: "https://primary.example.com".to_string(),
498 grpc_urls: vec![],
499 ..Default::default()
500 };
501
502 let urls = config.get_grpc_urls();
503 assert_eq!(urls.len(), 1);
504 assert_eq!(urls[0], "https://primary.example.com");
505 }
506
507 #[rstest]
508 fn test_config_get_grpc_urls_multiple() {
509 let config = DydxAdapterConfig {
510 grpc_url: "https://primary.example.com".to_string(),
511 grpc_urls: vec![
512 "https://fallback1.example.com".to_string(),
513 "https://fallback2.example.com".to_string(),
514 ],
515 ..Default::default()
516 };
517
518 let urls = config.get_grpc_urls();
519 assert_eq!(urls.len(), 2);
520 assert_eq!(urls[0], "https://fallback1.example.com");
521 assert_eq!(urls[1], "https://fallback2.example.com");
522 }
523
524 #[rstest]
525 fn test_for_network_mainnet_resolves_urls_and_chain_id() {
526 let config = DydxAdapterConfig::for_network(DydxNetwork::Mainnet);
527
528 assert_eq!(config.network, DydxNetwork::Mainnet);
529 assert_eq!(config.base_url, urls::http_base_url(DydxNetwork::Mainnet));
530 assert_eq!(config.ws_url, urls::ws_url(DydxNetwork::Mainnet));
531 assert_eq!(config.grpc_url, urls::grpc_urls(DydxNetwork::Mainnet)[0]);
532 let expected_grpc: Vec<String> = urls::grpc_urls(DydxNetwork::Mainnet)
533 .iter()
534 .map(|s| (*s).to_string())
535 .collect();
536 assert_eq!(config.grpc_urls, expected_grpc);
537 assert_eq!(config.chain_id, crate::common::consts::DYDX_CHAIN_ID);
538 assert_eq!(config.get_chain_id(), ChainId::Mainnet1);
539 }
540
541 #[rstest]
542 fn test_for_network_testnet_resolves_urls_and_chain_id() {
543 let config = DydxAdapterConfig::for_network(DydxNetwork::Testnet);
544
545 assert_eq!(config.network, DydxNetwork::Testnet);
546 assert_eq!(config.base_url, urls::http_base_url(DydxNetwork::Testnet));
547 assert_eq!(config.ws_url, urls::ws_url(DydxNetwork::Testnet));
548 assert_eq!(config.grpc_url, urls::grpc_urls(DydxNetwork::Testnet)[0]);
549 let expected_grpc: Vec<String> = urls::grpc_urls(DydxNetwork::Testnet)
550 .iter()
551 .map(|s| (*s).to_string())
552 .collect();
553 assert_eq!(config.grpc_urls, expected_grpc);
554 assert_eq!(
555 config.chain_id,
556 crate::common::consts::DYDX_TESTNET_CHAIN_ID,
557 );
558 assert_eq!(config.get_chain_id(), ChainId::Testnet4);
559 }
560
561 #[rstest]
562 #[case(DydxNetwork::Mainnet)]
563 #[case(DydxNetwork::Testnet)]
564 fn test_for_network_preserves_grpc_rate_limit_default(#[case] network: DydxNetwork) {
565 let config = DydxAdapterConfig::for_network(network);
569 assert_eq!(config.grpc_rate_limit_per_second, Some(4));
570 assert!(config.grpc_quota().is_some());
571 }
572}