Skip to main content

nautilus_dydx/
config.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Configuration structures for the dYdX adapter.
17
18use 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/// Configuration for the dYdX adapter.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct DydxAdapterConfig {
33    /// Network environment (mainnet or testnet).
34    #[serde(default)]
35    pub network: DydxNetwork,
36    /// Base URL for the HTTP API.
37    pub base_url: String,
38    /// Base URL for the WebSocket API.
39    pub ws_url: String,
40    /// Base URL for the gRPC API (Cosmos SDK transactions).
41    ///
42    /// For backwards compatibility, a single URL can be provided.
43    /// Consider using `grpc_urls` for fallback support.
44    pub grpc_url: String,
45    /// List of gRPC URLs with fallback support.
46    ///
47    /// If provided, the client will attempt to connect to each URL in order
48    /// until a successful connection is established. This is recommended for
49    /// production use in DEX environments where nodes can fail.
50    #[serde(default)]
51    pub grpc_urls: Vec<String>,
52    /// Chain ID (e.g., "dydx-mainnet-1" for mainnet, "dydx-testnet-4" for testnet).
53    pub chain_id: String,
54    /// Request timeout in seconds.
55    pub timeout_secs: u64,
56    /// Wallet address for the account.
57    ///
58    /// If not provided, falls back to environment variable:
59    /// - Mainnet: `DYDX_WALLET_ADDRESS`
60    /// - Testnet: `DYDX_TESTNET_WALLET_ADDRESS`
61    ///
62    /// Use `resolve_wallet_address()` to resolve from config or environment.
63    #[serde(default)]
64    pub wallet_address: Option<String>,
65    /// Subaccount number (default: 0).
66    #[serde(default)]
67    pub subaccount: u32,
68    /// Whether this is a testnet configuration.
69    ///
70    /// Precedence: `network` is canonical. If both `network` and `is_testnet`
71    /// are provided and conflict, `network` takes precedence internally.
72    /// This flag exists for backwards compatibility and may be derived from
73    /// `network` in future versions.
74    #[serde(default)]
75    pub is_testnet: bool,
76    /// Private key (hex) for wallet signing.
77    ///
78    /// If not provided, falls back to environment variable:
79    /// - Mainnet: `DYDX_PRIVATE_KEY`
80    /// - Testnet: `DYDX_TESTNET_PRIVATE_KEY`
81    ///
82    /// Use `DydxCredential::resolve()` to resolve from config or environment.
83    #[serde(default)]
84    pub private_key: Option<String>,
85    /// Authenticator IDs for permissioned key trading.
86    ///
87    /// When provided, transactions will include a TxExtension to enable trading
88    /// via sub-accounts using delegated signing keys. This is an advanced feature
89    /// for institutional setups with separated hot/cold wallet architectures.
90    ///
91    /// See <https://docs.dydx.xyz/concepts/trading/authenticators> for details on
92    /// permissioned keys and authenticator configuration.
93    #[serde(default)]
94    pub authenticator_ids: Vec<u64>,
95    /// Maximum number of retries for failed requests (default: 3).
96    #[serde(default = "default_max_retries")]
97    pub max_retries: u32,
98    /// Initial retry delay in milliseconds (default: 1000ms).
99    #[serde(default = "default_retry_delay_initial_ms")]
100    pub retry_delay_initial_ms: u64,
101    /// Maximum retry delay in milliseconds (default: 10000ms).
102    #[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    /// Get the list of gRPC URLs to use for connection with fallback support.
120    ///
121    /// Returns `grpc_urls` if non-empty, otherwise falls back to a single-element
122    /// vector containing `grpc_url`.
123    #[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    /// Map the configured network to the underlying chain ID.
133    ///
134    /// This is the recommended way to get the chain ID for transaction submission.
135    #[must_use]
136    pub const fn get_chain_id(&self) -> ChainId {
137        self.network.chain_id()
138    }
139
140    /// Convenience: compute `is_testnet` from `network`.
141    ///
142    /// Prefer `network` as the source of truth; this method is provided to
143    /// avoid ambiguity when legacy configs include `is_testnet`.
144    #[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/// Configuration for the dYdX data client.
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct DydxDataClientConfig {
183    /// Base URL for the HTTP API.
184    pub base_url_http: Option<String>,
185    /// Base URL for the WebSocket API.
186    pub base_url_ws: Option<String>,
187    /// HTTP request timeout in seconds.
188    pub http_timeout_secs: Option<u64>,
189    /// Maximum number of retry attempts for failed HTTP requests.
190    pub max_retries: Option<u64>,
191    /// Initial retry delay in milliseconds.
192    pub retry_delay_initial_ms: Option<u64>,
193    /// Maximum retry delay in milliseconds.
194    pub retry_delay_max_ms: Option<u64>,
195    /// Whether this is a testnet configuration.
196    pub is_testnet: bool,
197    /// HTTP proxy URL.
198    pub http_proxy_url: Option<String>,
199    /// WebSocket proxy URL.
200    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/// Configuration for the dYdX execution client.
220#[derive(Debug, Clone, Serialize, Deserialize)]
221pub struct DYDXExecClientConfig {
222    /// The trader ID for the client.
223    pub trader_id: TraderId,
224    /// The account ID for the client.
225    pub account_id: AccountId,
226    /// Network environment (mainnet or testnet).
227    #[serde(default)]
228    pub network: DydxNetwork,
229    /// gRPC endpoint URL (optional, uses default for network if not provided).
230    pub grpc_endpoint: Option<String>,
231    /// Additional gRPC URLs for fallback support.
232    #[serde(default)]
233    pub grpc_urls: Vec<String>,
234    /// WebSocket endpoint URL (optional, uses default for network if not provided).
235    pub ws_endpoint: Option<String>,
236    /// HTTP endpoint URL (optional, uses default for network if not provided).
237    pub http_endpoint: Option<String>,
238    /// Private key (hex) for wallet signing.
239    ///
240    /// If not provided, falls back to environment variable:
241    /// - Mainnet: `DYDX_PRIVATE_KEY`
242    /// - Testnet: `DYDX_TESTNET_PRIVATE_KEY`
243    pub private_key: Option<String>,
244    /// Wallet address.
245    ///
246    /// If not provided, falls back to environment variable:
247    /// - Mainnet: `DYDX_WALLET_ADDRESS`
248    /// - Testnet: `DYDX_TESTNET_WALLET_ADDRESS`
249    pub wallet_address: Option<String>,
250    /// Subaccount number (default: 0).
251    #[serde(default)]
252    pub subaccount_number: u32,
253    /// Authenticator IDs for permissioned key trading.
254    #[serde(default)]
255    pub authenticator_ids: Vec<u64>,
256    /// HTTP request timeout in seconds.
257    pub http_timeout_secs: Option<u64>,
258    /// Maximum number of retry attempts.
259    pub max_retries: Option<u32>,
260    /// Initial retry delay in milliseconds.
261    pub retry_delay_initial_ms: Option<u64>,
262    /// Maximum retry delay in milliseconds.
263    pub retry_delay_max_ms: Option<u64>,
264}
265
266impl DYDXExecClientConfig {
267    /// Returns the gRPC URLs to use, with fallback support.
268    ///
269    /// Returns `grpc_urls` if non-empty, otherwise uses `grpc_endpoint` if provided,
270    /// otherwise uses the default URLs for the configured network.
271    #[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    /// Returns the WebSocket URL for the configured network.
287    #[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    /// Returns the HTTP URL for the configured network.
296    #[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    /// Returns the chain ID for the configured network.
305    #[must_use]
306    pub const fn get_chain_id(&self) -> ChainId {
307        self.network.chain_id()
308    }
309
310    /// Returns whether this is a testnet configuration.
311    #[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        // When network=mainnet but is_testnet=true, get_chain_id uses network
366        let config = DydxAdapterConfig {
367            network: DydxNetwork::Mainnet,
368            is_testnet: true, // Conflicting value
369            ..Default::default()
370        };
371        assert_eq!(config.get_chain_id(), ChainId::Mainnet1); // network wins
372        assert!(!config.compute_is_testnet()); // compute_is_testnet derives from network
373    }
374
375    #[rstest]
376    fn test_config_serde_backwards_compat() {
377        // Test that configs missing network field can deserialize with default
378        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        // Should default to Mainnet when network field is missing
384        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}