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