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