1use std::{any::Any, cell::RefCell, rc::Rc, sync::Arc};
19
20use log;
21use nautilus_common::{
22 cache::Cache,
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,
31};
32use nautilus_network::retry::RetryConfig;
33
34use crate::{
35 common::{
36 consts::DYDX_VENUE,
37 credential::{DydxCredential, resolve_wallet_address},
38 instrument_cache::InstrumentCache,
39 urls,
40 },
41 config::{DydxAdapterConfig, DydxDataClientConfig, DydxExecClientConfig},
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 DydxExecClientConfig {
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.core.nautilus_pyo3.dydx", from_py_object)
65)]
66#[cfg_attr(
67 feature = "python",
68 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.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: Rc<RefCell<Cache>>,
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
122 let http_client = DydxHttpClient::new(
123 Some(http_url),
124 dydx_config.http_timeout_secs,
125 dydx_config.proxy_url.clone(),
126 dydx_config.network,
127 retry_config,
128 )?;
129
130 let ws_client = DydxWebSocketClient::new_public_with_cache(
131 ws_url,
132 Arc::new(InstrumentCache::new()),
133 Some(20),
134 dydx_config.transport_backend,
135 dydx_config.proxy_url.clone(),
136 );
137
138 let client = DydxDataClient::new(client_id, dydx_config, http_client, ws_client)?;
139 Ok(Box::new(client))
140 }
141
142 fn name(&self) -> &'static str {
143 "DYDX"
144 }
145
146 fn config_type(&self) -> &'static str {
147 "DydxDataClientConfig"
148 }
149}
150
151#[derive(Debug, Clone)]
153#[cfg_attr(
154 feature = "python",
155 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.dydx", from_py_object)
156)]
157#[cfg_attr(
158 feature = "python",
159 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.dydx")
160)]
161pub struct DydxExecutionClientFactory;
162
163impl DydxExecutionClientFactory {
164 #[must_use]
166 pub const fn new() -> Self {
167 Self
168 }
169}
170
171impl Default for DydxExecutionClientFactory {
172 fn default() -> Self {
173 Self::new()
174 }
175}
176
177impl ExecutionClientFactory for DydxExecutionClientFactory {
178 fn create(
179 &self,
180 name: &str,
181 config: &dyn ClientConfig,
182 cache: Rc<RefCell<Cache>>,
183 ) -> anyhow::Result<Box<dyn ExecutionClient>> {
184 let dydx_config = config
185 .as_any()
186 .downcast_ref::<DydxExecClientConfig>()
187 .ok_or_else(|| {
188 anyhow::anyhow!(
189 "Invalid config type for DydxExecutionClientFactory. Expected DydxExecClientConfig, was {config:?}",
190 )
191 })?
192 .clone();
193
194 let oms_type = OmsType::Netting;
196
197 let account_type = AccountType::Margin;
199
200 let core = ExecutionClientCore::new(
201 dydx_config.trader_id,
202 ClientId::from(name),
203 *DYDX_VENUE,
204 oms_type,
205 dydx_config.account_id,
206 account_type,
207 None, cache,
209 );
210
211 let adapter_config = DydxAdapterConfig {
212 network: dydx_config.network,
213 base_url: dydx_config.get_http_url(),
214 ws_url: dydx_config.get_ws_url(),
215 grpc_url: dydx_config
216 .get_grpc_urls()
217 .first()
218 .cloned()
219 .unwrap_or_default(),
220 grpc_urls: dydx_config.get_grpc_urls(),
221 chain_id: dydx_config.get_chain_id().to_string(),
222 timeout_secs: dydx_config.http_timeout_secs.unwrap_or(30),
223 wallet_address: dydx_config.wallet_address.clone(),
224 subaccount: dydx_config.subaccount_number,
225 private_key: dydx_config.private_key.clone(),
226 authenticator_ids: dydx_config.authenticator_ids.clone(),
227 max_retries: dydx_config.max_retries.unwrap_or(3),
228 retry_delay_initial_ms: dydx_config.retry_delay_initial_ms.unwrap_or(1000),
229 retry_delay_max_ms: dydx_config.retry_delay_max_ms.unwrap_or(10000),
230 grpc_rate_limit_per_second: dydx_config.grpc_rate_limit_per_second,
231 proxy_url: dydx_config.proxy_url.clone(),
232 transport_backend: dydx_config.transport_backend,
233 };
234
235 log::info!(
236 "Resolving wallet address: config={:?}, network={}, env_var={}",
237 dydx_config.wallet_address,
238 dydx_config.network,
239 if dydx_config.is_testnet() {
240 "DYDX_TESTNET_WALLET_ADDRESS"
241 } else {
242 "DYDX_WALLET_ADDRESS"
243 }
244 );
245 let wallet_address = if let Some(addr) =
246 resolve_wallet_address(dydx_config.wallet_address.clone(), dydx_config.network)
247 {
248 log::info!("Using wallet address from config/env: {addr}");
249 addr
250 } else if let Some(credential) = DydxCredential::resolve(
251 dydx_config.private_key.as_deref(),
252 dydx_config.network,
253 dydx_config.authenticator_ids.clone(),
254 )? {
255 log::info!(
256 "Derived wallet address from private key: {}",
257 credential.address
258 );
259 credential.address
260 } else {
261 anyhow::bail!(
262 "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)"
263 )
264 };
265
266 let client = DydxExecutionClient::new(
267 core,
268 adapter_config,
269 wallet_address,
270 dydx_config.subaccount_number,
271 )?;
272
273 Ok(Box::new(client))
274 }
275
276 fn name(&self) -> &'static str {
277 "DYDX"
278 }
279
280 fn config_type(&self) -> &'static str {
281 "DydxExecClientConfig"
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use std::{cell::RefCell, rc::Rc};
288
289 use nautilus_common::{
290 cache::Cache,
291 clock::TestClock,
292 factories::{ClientConfig, DataClientFactory, ExecutionClientFactory},
293 };
294 use nautilus_model::identifiers::{AccountId, TraderId};
295 use rstest::rstest;
296
297 use super::*;
298 use crate::{
299 common::enums::DydxNetwork,
300 config::{DydxDataClientConfig, DydxExecClientConfig},
301 };
302
303 #[rstest]
304 fn test_dydx_data_client_factory_creation() {
305 let factory = DydxDataClientFactory::new();
306 assert_eq!(factory.name(), "DYDX");
307 assert_eq!(factory.config_type(), "DydxDataClientConfig");
308 }
309
310 #[rstest]
311 fn test_dydx_data_client_factory_default() {
312 let factory = DydxDataClientFactory;
313 assert_eq!(factory.name(), "DYDX");
314 }
315
316 #[rstest]
317 fn test_dydx_execution_client_factory_creation() {
318 let factory = DydxExecutionClientFactory::new();
319 assert_eq!(factory.name(), "DYDX");
320 assert_eq!(factory.config_type(), "DydxExecClientConfig");
321 }
322
323 #[rstest]
324 fn test_dydx_execution_client_factory_default() {
325 let factory = DydxExecutionClientFactory;
326 assert_eq!(factory.name(), "DYDX");
327 }
328
329 #[rstest]
330 fn test_dydx_data_client_config_implements_client_config() {
331 let config = DydxDataClientConfig::default();
332 let boxed_config: Box<dyn ClientConfig> = Box::new(config);
333 let downcasted = boxed_config.as_any().downcast_ref::<DydxDataClientConfig>();
334
335 assert!(downcasted.is_some());
336 }
337
338 #[rstest]
339 fn test_dydx_exec_client_config_implements_client_config() {
340 let config = DydxExecClientConfig {
341 trader_id: TraderId::from("TRADER-001"),
342 account_id: AccountId::from("DYDX-001"),
343 network: DydxNetwork::Mainnet,
344 grpc_endpoint: None,
345 grpc_urls: vec![],
346 ws_endpoint: None,
347 http_endpoint: None,
348 private_key: None,
349 wallet_address: Some("dydx1abc123".to_string()),
350 subaccount_number: 0,
351 authenticator_ids: vec![],
352 http_timeout_secs: None,
353 max_retries: None,
354 retry_delay_initial_ms: None,
355 retry_delay_max_ms: None,
356 grpc_rate_limit_per_second: Some(4),
357 proxy_url: None,
358 transport_backend: Default::default(),
359 };
360
361 let boxed_config: Box<dyn ClientConfig> = Box::new(config);
362 let downcasted = boxed_config.as_any().downcast_ref::<DydxExecClientConfig>();
363
364 assert!(downcasted.is_some());
365 }
366
367 #[rstest]
368 fn test_dydx_data_client_factory_rejects_wrong_config_type() {
369 let factory = DydxDataClientFactory::new();
370 let wrong_config = DydxExecClientConfig {
371 trader_id: TraderId::from("TRADER-001"),
372 account_id: AccountId::from("DYDX-001"),
373 network: DydxNetwork::Mainnet,
374 grpc_endpoint: None,
375 grpc_urls: vec![],
376 ws_endpoint: None,
377 http_endpoint: None,
378 private_key: None,
379 wallet_address: None,
380 subaccount_number: 0,
381 authenticator_ids: vec![],
382 http_timeout_secs: None,
383 max_retries: None,
384 retry_delay_initial_ms: None,
385 retry_delay_max_ms: None,
386 grpc_rate_limit_per_second: Some(4),
387 proxy_url: None,
388 transport_backend: Default::default(),
389 };
390
391 let cache = Rc::new(RefCell::new(Cache::default()));
392 let clock = Rc::new(RefCell::new(TestClock::new()));
393
394 let result = factory.create("DYDX-TEST", &wrong_config, cache, clock);
395 assert!(result.is_err());
396 assert!(
397 result
398 .err()
399 .unwrap()
400 .to_string()
401 .contains("Invalid config type")
402 );
403 }
404
405 #[rstest]
406 fn test_dydx_execution_client_factory_rejects_wrong_config_type() {
407 let factory = DydxExecutionClientFactory::new();
408 let wrong_config = DydxDataClientConfig::default();
409
410 let cache = Rc::new(RefCell::new(Cache::default()));
411
412 let result = factory.create("DYDX-TEST", &wrong_config, cache);
413 assert!(result.is_err());
414 assert!(
415 result
416 .err()
417 .unwrap()
418 .to_string()
419 .contains("Invalid config type")
420 );
421 }
422}