1use nautilus_model::identifiers::{AccountId, TraderId};
19use serde::{Deserialize, Serialize};
20
21use crate::{
22 common::{
23 consts::{DYDX_CHAIN_ID, DYDX_TESTNET_CHAIN_ID},
24 enums::DydxNetwork,
25 urls,
26 },
27 grpc::types::ChainId,
28};
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct DydxAdapterConfig {
33 #[serde(default)]
35 pub network: DydxNetwork,
36 pub base_url: String,
38 pub ws_url: String,
40 pub grpc_url: String,
45 #[serde(default)]
51 pub grpc_urls: Vec<String>,
52 pub chain_id: String,
54 pub timeout_secs: u64,
56 #[serde(default)]
64 pub wallet_address: Option<String>,
65 #[serde(default)]
67 pub subaccount: u32,
68 #[serde(default)]
75 pub is_testnet: bool,
76 #[serde(default)]
84 pub private_key: Option<String>,
85 #[serde(default)]
94 pub authenticator_ids: Vec<u64>,
95 #[serde(default = "default_max_retries")]
97 pub max_retries: u32,
98 #[serde(default = "default_retry_delay_initial_ms")]
100 pub retry_delay_initial_ms: u64,
101 #[serde(default = "default_retry_delay_max_ms")]
103 pub retry_delay_max_ms: u64,
104}
105
106fn default_max_retries() -> u32 {
107 3
108}
109
110fn default_retry_delay_initial_ms() -> u64 {
111 1000
112}
113
114fn default_retry_delay_max_ms() -> u64 {
115 10000
116}
117
118impl DydxAdapterConfig {
119 #[must_use]
124 pub fn get_grpc_urls(&self) -> Vec<String> {
125 if self.grpc_urls.is_empty() {
126 vec![self.grpc_url.clone()]
127 } else {
128 self.grpc_urls.clone()
129 }
130 }
131
132 #[must_use]
136 pub const fn get_chain_id(&self) -> ChainId {
137 self.network.chain_id()
138 }
139
140 #[must_use]
145 pub const fn compute_is_testnet(&self) -> bool {
146 matches!(self.network, DydxNetwork::Testnet)
147 }
148}
149
150impl Default for DydxAdapterConfig {
151 fn default() -> Self {
152 let network = DydxNetwork::default();
153 let is_testnet = matches!(network, DydxNetwork::Testnet);
154 let grpc_urls = urls::grpc_urls(is_testnet);
155 Self {
156 network,
157 base_url: urls::http_base_url(is_testnet).to_string(),
158 ws_url: urls::ws_url(is_testnet).to_string(),
159 grpc_url: grpc_urls[0].to_string(),
160 grpc_urls: grpc_urls.iter().map(|&s| s.to_string()).collect(),
161 chain_id: if is_testnet {
162 DYDX_TESTNET_CHAIN_ID
163 } else {
164 DYDX_CHAIN_ID
165 }
166 .to_string(),
167 timeout_secs: 30,
168 wallet_address: None,
169 subaccount: 0,
170 is_testnet,
171 private_key: None,
172 authenticator_ids: Vec::new(),
173 max_retries: default_max_retries(),
174 retry_delay_initial_ms: default_retry_delay_initial_ms(),
175 retry_delay_max_ms: default_retry_delay_max_ms(),
176 }
177 }
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct DydxDataClientConfig {
183 pub base_url_http: Option<String>,
185 pub base_url_ws: Option<String>,
187 pub http_timeout_secs: Option<u64>,
189 pub max_retries: Option<u64>,
191 pub retry_delay_initial_ms: Option<u64>,
193 pub retry_delay_max_ms: Option<u64>,
195 pub is_testnet: bool,
197 pub http_proxy_url: Option<String>,
199 pub ws_proxy_url: Option<String>,
201}
202
203impl Default for DydxDataClientConfig {
204 fn default() -> Self {
205 Self {
206 base_url_http: None,
207 base_url_ws: None,
208 http_timeout_secs: Some(60),
209 max_retries: Some(3),
210 retry_delay_initial_ms: Some(100),
211 retry_delay_max_ms: Some(5000),
212 is_testnet: false,
213 http_proxy_url: None,
214 ws_proxy_url: None,
215 }
216 }
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct DYDXExecClientConfig {
222 pub trader_id: TraderId,
224 pub account_id: AccountId,
226 #[serde(default)]
228 pub network: DydxNetwork,
229 pub grpc_endpoint: Option<String>,
231 #[serde(default)]
233 pub grpc_urls: Vec<String>,
234 pub ws_endpoint: Option<String>,
236 pub http_endpoint: Option<String>,
238 pub private_key: Option<String>,
244 pub wallet_address: Option<String>,
250 #[serde(default)]
252 pub subaccount_number: u32,
253 #[serde(default)]
255 pub authenticator_ids: Vec<u64>,
256 pub http_timeout_secs: Option<u64>,
258 pub max_retries: Option<u32>,
260 pub retry_delay_initial_ms: Option<u64>,
262 pub retry_delay_max_ms: Option<u64>,
264}
265
266impl DYDXExecClientConfig {
267 #[must_use]
272 pub fn get_grpc_urls(&self) -> Vec<String> {
273 if !self.grpc_urls.is_empty() {
274 return self.grpc_urls.clone();
275 }
276 if let Some(ref endpoint) = self.grpc_endpoint {
277 return vec![endpoint.clone()];
278 }
279 let is_testnet = matches!(self.network, DydxNetwork::Testnet);
280 urls::grpc_urls(is_testnet)
281 .iter()
282 .map(|&s| s.to_string())
283 .collect()
284 }
285
286 #[must_use]
288 pub fn get_ws_url(&self) -> String {
289 self.ws_endpoint.clone().unwrap_or_else(|| {
290 let is_testnet = matches!(self.network, DydxNetwork::Testnet);
291 urls::ws_url(is_testnet).to_string()
292 })
293 }
294
295 #[must_use]
297 pub fn get_http_url(&self) -> String {
298 self.http_endpoint.clone().unwrap_or_else(|| {
299 let is_testnet = matches!(self.network, DydxNetwork::Testnet);
300 urls::http_base_url(is_testnet).to_string()
301 })
302 }
303
304 #[must_use]
306 pub const fn get_chain_id(&self) -> ChainId {
307 self.network.chain_id()
308 }
309
310 #[must_use]
312 pub const fn is_testnet(&self) -> bool {
313 matches!(self.network, DydxNetwork::Testnet)
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use rstest::rstest;
320
321 use super::*;
322
323 #[rstest]
324 fn test_config_get_chain_id_mainnet() {
325 let config = DydxAdapterConfig {
326 network: DydxNetwork::Mainnet,
327 ..Default::default()
328 };
329 assert_eq!(config.get_chain_id(), ChainId::Mainnet1);
330 }
331
332 #[rstest]
333 fn test_config_get_chain_id_testnet() {
334 let config = DydxAdapterConfig {
335 network: DydxNetwork::Testnet,
336 ..Default::default()
337 };
338 assert_eq!(config.get_chain_id(), ChainId::Testnet4);
339 }
340
341 #[rstest]
342 fn test_config_compute_is_testnet() {
343 let mainnet_config = DydxAdapterConfig {
344 network: DydxNetwork::Mainnet,
345 ..Default::default()
346 };
347 assert!(!mainnet_config.compute_is_testnet());
348
349 let testnet_config = DydxAdapterConfig {
350 network: DydxNetwork::Testnet,
351 ..Default::default()
352 };
353 assert!(testnet_config.compute_is_testnet());
354 }
355
356 #[rstest]
357 fn test_config_default_uses_mainnet() {
358 let config = DydxAdapterConfig::default();
359 assert_eq!(config.network, DydxNetwork::Mainnet);
360 assert!(!config.is_testnet);
361 }
362
363 #[rstest]
364 fn test_config_network_canonical_over_is_testnet() {
365 let config = DydxAdapterConfig {
367 network: DydxNetwork::Mainnet,
368 is_testnet: true, ..Default::default()
370 };
371 assert_eq!(config.get_chain_id(), ChainId::Mainnet1); assert!(!config.compute_is_testnet()); }
374
375 #[rstest]
376 fn test_config_serde_backwards_compat() {
377 let json = r#"{"base_url":"https://indexer.dydx.trade","ws_url":"wss://indexer.dydx.trade/v4/ws","grpc_url":"https://dydx-ops-grpc.kingnodes.com:443","grpc_urls":[],"chain_id":"dydx-mainnet-1","timeout_secs":30,"subaccount":0,"is_testnet":false,"max_retries":3,"retry_delay_initial_ms":1000,"retry_delay_max_ms":10000}"#;
379
380 let config: Result<DydxAdapterConfig, _> = serde_json::from_str(json);
381 assert!(config.is_ok());
382 let config = config.unwrap();
383 assert_eq!(config.network, DydxNetwork::Mainnet);
385 }
386
387 #[rstest]
388 fn test_config_get_grpc_urls_fallback() {
389 let config = DydxAdapterConfig {
390 grpc_url: "https://primary.example.com".to_string(),
391 grpc_urls: vec![],
392 ..Default::default()
393 };
394
395 let urls = config.get_grpc_urls();
396 assert_eq!(urls.len(), 1);
397 assert_eq!(urls[0], "https://primary.example.com");
398 }
399
400 #[rstest]
401 fn test_config_get_grpc_urls_multiple() {
402 let config = DydxAdapterConfig {
403 grpc_url: "https://primary.example.com".to_string(),
404 grpc_urls: vec![
405 "https://fallback1.example.com".to_string(),
406 "https://fallback2.example.com".to_string(),
407 ],
408 ..Default::default()
409 };
410
411 let urls = config.get_grpc_urls();
412 assert_eq!(urls.len(), 2);
413 assert_eq!(urls[0], "https://fallback1.example.com");
414 assert_eq!(urls[1], "https://fallback2.example.com");
415 }
416}