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