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