1use std::{any::Any, cell::RefCell, rc::Rc, sync::Arc};
19
20use log;
21use nautilus_common::{
22 cache::CacheView,
23 clients::{DataClient, ExecutionClient},
24 clock::Clock,
25 factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
26};
27use nautilus_live::ExecutionClientCore;
28use nautilus_model::{
29 enums::{AccountType, OmsType},
30 identifiers::{ClientId, TraderId},
31};
32use nautilus_network::retry::RetryConfig;
33
34use crate::{
35 common::{
36 consts::{DYDX, DYDX_VENUE},
37 credential::{DydxCredential, resolve_wallet_address},
38 instrument_cache::InstrumentCache,
39 urls,
40 },
41 config::{DydxAdapterConfig, DydxDataClientConfig, DydxExecutionClientConfig},
42 data::DydxDataClient,
43 execution::DydxExecutionClient,
44 http::client::DydxHttpClient,
45 websocket::client::DydxWebSocketClient,
46};
47
48impl ClientConfig for DydxDataClientConfig {
49 fn as_any(&self) -> &dyn Any {
50 self
51 }
52}
53
54impl ClientConfig for DydxExecutionClientConfig {
55 fn as_any(&self) -> &dyn Any {
56 self
57 }
58}
59
60#[derive(Debug, Clone)]
62#[cfg_attr(
63 feature = "python",
64 pyo3::pyclass(module = "nautilus_trader.adapters.dydx", from_py_object)
65)]
66#[cfg_attr(
67 feature = "python",
68 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.dydx")
69)]
70pub struct DydxDataClientFactory;
71
72impl DydxDataClientFactory {
73 #[must_use]
75 pub const fn new() -> Self {
76 Self
77 }
78}
79
80impl Default for DydxDataClientFactory {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86impl DataClientFactory for DydxDataClientFactory {
87 fn create(
88 &self,
89 name: &str,
90 config: &dyn ClientConfig,
91 _cache: CacheView,
92 _clock: Rc<RefCell<dyn Clock>>,
93 ) -> anyhow::Result<Box<dyn DataClient>> {
94 let dydx_config = config
95 .as_any()
96 .downcast_ref::<DydxDataClientConfig>()
97 .ok_or_else(|| {
98 anyhow::anyhow!(
99 "Invalid config type for DydxDataClientFactory. Expected DydxDataClientConfig, was {config:?}",
100 )
101 })?
102 .clone();
103
104 let client_id = ClientId::from(name);
105
106 let http_url = dydx_config
107 .base_url_http
108 .clone()
109 .unwrap_or_else(|| urls::http_base_url(dydx_config.network).to_string());
110 let ws_url = dydx_config
111 .base_url_ws
112 .clone()
113 .unwrap_or_else(|| urls::ws_url(dydx_config.network).to_string());
114
115 let retry_config = Some(RetryConfig {
116 max_retries: dydx_config.max_retries as u32,
117 initial_delay_ms: dydx_config.retry_delay_initial_ms,
118 max_delay_ms: dydx_config.retry_delay_max_ms,
119 ..Default::default()
120 });
121 let proxy_url = dydx_config
122 .proxy_url
123 .as_ref()
124 .map(|value| value.expose_secret().to_owned());
125
126 let http_client = DydxHttpClient::new(
127 Some(http_url),
128 dydx_config.http_timeout_secs,
129 proxy_url.clone(),
130 dydx_config.network,
131 retry_config,
132 )?;
133
134 let ws_client = DydxWebSocketClient::new_public_with_cache_and_pool(
135 ws_url,
136 Arc::new(InstrumentCache::new()),
137 Some(20),
138 dydx_config.transport_backend,
139 proxy_url,
140 dydx_config.max_ws_connections,
141 dydx_config.per_channel_subscription_limit,
142 );
143
144 let client = DydxDataClient::new(client_id, dydx_config, http_client, ws_client)?;
145 Ok(Box::new(client))
146 }
147
148 fn name(&self) -> &'static str {
149 DYDX
150 }
151
152 fn config_type(&self) -> &'static str {
153 "DydxDataClientConfig"
154 }
155}
156
157#[derive(Debug, Clone)]
159#[cfg_attr(
160 feature = "python",
161 pyo3::pyclass(module = "nautilus_trader.adapters.dydx", from_py_object)
162)]
163#[cfg_attr(
164 feature = "python",
165 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.dydx")
166)]
167pub struct DydxExecutionClientFactory;
168
169impl DydxExecutionClientFactory {
170 #[must_use]
172 pub const fn new() -> Self {
173 Self
174 }
175}
176
177impl Default for DydxExecutionClientFactory {
178 fn default() -> Self {
179 Self::new()
180 }
181}
182
183impl ExecutionClientFactory for DydxExecutionClientFactory {
184 fn create(
185 &self,
186 trader_id: TraderId,
187 name: &str,
188 config: &dyn ClientConfig,
189 cache: CacheView,
190 _clock: Rc<RefCell<dyn Clock>>,
191 ) -> anyhow::Result<Box<dyn ExecutionClient>> {
192 let dydx_config = config
193 .as_any()
194 .downcast_ref::<DydxExecutionClientConfig>()
195 .ok_or_else(|| {
196 anyhow::anyhow!(
197 "Invalid config type for DydxExecutionClientFactory. Expected DydxExecutionClientConfig, was {config:?}",
198 )
199 })?
200 .clone();
201
202 dydx_config.validate()?;
203
204 let oms_type = OmsType::Netting;
206
207 let account_type = AccountType::Margin;
209
210 let core = ExecutionClientCore::new(
211 trader_id,
212 ClientId::from(name),
213 *DYDX_VENUE,
214 oms_type,
215 dydx_config.account_id,
216 account_type,
217 None, cache,
219 );
220
221 let adapter_config = DydxAdapterConfig {
222 network: dydx_config.network,
223 base_url: dydx_config.get_http_url(),
224 ws_url: dydx_config.get_ws_url(),
225 grpc_url: dydx_config
226 .get_grpc_urls()
227 .first()
228 .cloned()
229 .unwrap_or_default(),
230 grpc_urls: dydx_config.get_grpc_urls(),
231 chain_id: dydx_config.get_chain_id().to_string(),
232 timeout_secs: dydx_config.http_timeout_secs.unwrap_or(30),
233 wallet_address: dydx_config.wallet_address.clone(),
234 subaccount: dydx_config.subaccount_number,
235 private_key: dydx_config.private_key.clone(),
236 authenticator_ids: dydx_config.authenticator_ids.clone(),
237 max_retries: dydx_config.max_retries.unwrap_or(3),
238 retry_delay_initial_ms: dydx_config.retry_delay_initial_ms.unwrap_or(1000),
239 retry_delay_max_ms: dydx_config.retry_delay_max_ms.unwrap_or(10000),
240 grpc_rate_limit_per_second: dydx_config.grpc_rate_limit_per_second,
241 proxy_url: dydx_config.proxy_url.clone(),
242 transport_backend: dydx_config.transport_backend,
243 };
244
245 log::debug!(
246 "Resolving wallet address: config={:?}, network={}, env_var={}",
247 dydx_config.wallet_address,
248 dydx_config.network,
249 if dydx_config.is_testnet() {
250 "DYDX_TESTNET_WALLET_ADDRESS"
251 } else {
252 "DYDX_WALLET_ADDRESS"
253 }
254 );
255 let wallet_address = if let Some(addr) =
256 resolve_wallet_address(dydx_config.wallet_address.clone(), dydx_config.network)
257 {
258 log::debug!("Using wallet address from config/env: {addr}");
259 addr
260 } else if let Some(credential) = DydxCredential::resolve(
261 dydx_config
262 .private_key
263 .as_ref()
264 .map(|value| value.expose_secret()),
265 dydx_config.network,
266 dydx_config.authenticator_ids.clone(),
267 )? {
268 log::debug!(
269 "Derived wallet address from private key: {}",
270 credential.address
271 );
272 credential.address
273 } else {
274 anyhow::bail!(
275 "No wallet credentials found: set wallet_address or private_key in config, or use environment variables (DYDX_WALLET_ADDRESS/DYDX_PRIVATE_KEY for mainnet, DYDX_TESTNET_* for testnet)"
276 )
277 };
278
279 let client = DydxExecutionClient::new(
280 core,
281 adapter_config,
282 wallet_address,
283 dydx_config.subaccount_number,
284 )?;
285
286 Ok(Box::new(client))
287 }
288
289 fn name(&self) -> &'static str {
290 DYDX
291 }
292
293 fn config_type(&self) -> &'static str {
294 "DydxExecutionClientConfig"
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use std::{cell::RefCell, rc::Rc};
301
302 use nautilus_common::{
303 cache::Cache,
304 clock::VirtualClock,
305 factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
306 };
307 use nautilus_model::identifiers::{AccountId, TraderId};
308 use rstest::rstest;
309
310 use super::*;
311 use crate::{
312 common::enums::DydxNetwork,
313 config::{DydxDataClientConfig, DydxExecutionClientConfig},
314 };
315
316 #[rstest]
317 fn test_dydx_data_client_factory_creation() {
318 let factory = DydxDataClientFactory::new();
319 assert_eq!(factory.name(), DYDX);
320 assert_eq!(factory.config_type(), "DydxDataClientConfig");
321 }
322
323 #[rstest]
324 fn test_dydx_data_client_factory_default() {
325 let factory = DydxDataClientFactory;
326 assert_eq!(factory.name(), DYDX);
327 }
328
329 #[rstest]
330 fn test_dydx_execution_client_factory_creation() {
331 let factory = DydxExecutionClientFactory::new();
332 assert_eq!(factory.name(), DYDX);
333 assert_eq!(factory.config_type(), "DydxExecutionClientConfig");
334 }
335
336 #[rstest]
337 fn test_dydx_execution_client_factory_default() {
338 let factory = DydxExecutionClientFactory;
339 assert_eq!(factory.name(), DYDX);
340 }
341
342 #[rstest]
343 fn test_dydx_data_client_config_implements_client_config() {
344 let config = DydxDataClientConfig::default();
345 let boxed_config: Box<dyn ClientConfig> = Box::new(config);
346 let downcasted = boxed_config.as_any().downcast_ref::<DydxDataClientConfig>();
347
348 assert!(downcasted.is_some());
349 }
350
351 #[rstest]
352 fn test_dydx_exec_client_config_implements_client_config() {
353 let config = DydxExecutionClientConfig {
354 account_id: AccountId::from("DYDX-001"),
355 network: DydxNetwork::Mainnet,
356 grpc_endpoint: None,
357 grpc_urls: vec![],
358 ws_endpoint: None,
359 http_endpoint: None,
360 private_key: None,
361 wallet_address: Some("dydx1abc123".to_string()),
362 subaccount_number: 0,
363 authenticator_ids: vec![],
364 http_timeout_secs: None,
365 max_retries: None,
366 retry_delay_initial_ms: None,
367 retry_delay_max_ms: None,
368 grpc_rate_limit_per_second: Some(4),
369 proxy_url: None,
370 transport_backend: Default::default(),
371 };
372
373 let boxed_config: Box<dyn ClientConfig> = Box::new(config);
374 let downcasted = boxed_config
375 .as_any()
376 .downcast_ref::<DydxExecutionClientConfig>();
377
378 assert!(downcasted.is_some());
379 }
380
381 #[rstest]
382 fn test_dydx_data_client_factory_rejects_wrong_config_type() {
383 let factory = DydxDataClientFactory::new();
384 let wrong_config = DydxExecutionClientConfig {
385 account_id: AccountId::from("DYDX-001"),
386 network: DydxNetwork::Mainnet,
387 grpc_endpoint: None,
388 grpc_urls: vec![],
389 ws_endpoint: None,
390 http_endpoint: None,
391 private_key: None,
392 wallet_address: None,
393 subaccount_number: 0,
394 authenticator_ids: vec![],
395 http_timeout_secs: None,
396 max_retries: None,
397 retry_delay_initial_ms: None,
398 retry_delay_max_ms: None,
399 grpc_rate_limit_per_second: Some(4),
400 proxy_url: None,
401 transport_backend: Default::default(),
402 };
403
404 let cache = Rc::new(RefCell::new(Cache::default()));
405 let clock = Rc::new(RefCell::new(VirtualClock::new()));
406
407 let result = factory.create("DYDX-TEST", &wrong_config, cache.into(), clock);
408 assert!(result.is_err());
409 assert!(
410 result
411 .err()
412 .unwrap()
413 .to_string()
414 .contains("Invalid config type")
415 );
416 }
417
418 #[rstest]
419 fn test_dydx_execution_client_factory_rejects_wrong_config_type() {
420 let factory = DydxExecutionClientFactory::new();
421 let wrong_config = DydxDataClientConfig::default();
422
423 let cache = Rc::new(RefCell::new(Cache::default()));
424
425 let result = factory.create(
426 TraderId::from("TRADER-001"),
427 "DYDX-TEST",
428 &wrong_config,
429 cache.into(),
430 Rc::new(RefCell::new(VirtualClock::new())),
431 );
432 assert!(result.is_err());
433 assert!(
434 result
435 .err()
436 .unwrap()
437 .to_string()
438 .contains("Invalid config type")
439 );
440 }
441}