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 std::num::NonZeroU32;
19
20use nautilus_model::identifiers::AccountId;
21use nautilus_network::{ratelimiter::quota::Quota, websocket::TransportBackend};
22use serde::{Deserialize, Serialize};
23
24use crate::{
25    common::{consts::DYDX_CHAIN_ID, enums::DydxNetwork, urls},
26    grpc::types::ChainId,
27};
28
29/// Configuration for the dYdX adapter.
30///
31/// URL fields (`base_url`, `ws_url`, `grpc_url`, `grpc_urls`) default to mainnet in the
32/// builder. Use [`DydxAdapterConfig::for_network`] to build a config whose URLs and chain
33/// ID match the target network, or override each URL explicitly.
34#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
35#[serde(deny_unknown_fields)]
36pub struct DydxAdapterConfig {
37    /// Network environment (mainnet or testnet).
38    #[serde(default)]
39    #[builder(default)]
40    pub network: DydxNetwork,
41    /// Base URL for the HTTP API.
42    #[builder(default = urls::http_base_url(DydxNetwork::Mainnet).to_string())]
43    pub base_url: String,
44    /// Base URL for the WebSocket API.
45    #[builder(default = urls::ws_url(DydxNetwork::Mainnet).to_string())]
46    pub ws_url: String,
47    /// Base URL for the gRPC API (Cosmos SDK transactions).
48    ///
49    /// For backwards compatibility, a single URL can be provided.
50    /// Consider using `grpc_urls` for fallback support.
51    #[builder(default = urls::grpc_urls(DydxNetwork::Mainnet)[0].to_string())]
52    pub grpc_url: String,
53    /// List of gRPC URLs with fallback support.
54    ///
55    /// If provided, the client will attempt to connect to each URL in order
56    /// until a successful connection is established. This is recommended for
57    /// production use in DEX environments where nodes can fail.
58    #[builder(default = urls::grpc_urls(DydxNetwork::Mainnet).iter().map(|&s| s.to_string()).collect())]
59    pub grpc_urls: Vec<String>,
60    /// Chain ID (e.g., "dydx-mainnet-1" for mainnet, "dydx-testnet-4" for testnet).
61    #[builder(default = DYDX_CHAIN_ID.to_string())]
62    pub chain_id: String,
63    /// Request timeout in seconds.
64    #[builder(default = 30)]
65    pub timeout_secs: u64,
66    /// Wallet address for the account.
67    ///
68    /// If not provided, falls back to environment variable:
69    /// - Mainnet: `DYDX_WALLET_ADDRESS`
70    /// - Testnet: `DYDX_TESTNET_WALLET_ADDRESS`
71    ///
72    /// Use `resolve_wallet_address()` to resolve from config or environment.
73    #[serde(default)]
74    pub wallet_address: Option<String>,
75    /// Subaccount number (default: 0).
76    #[serde(default)]
77    #[builder(default)]
78    pub subaccount: u32,
79    /// Private key (hex) for wallet signing.
80    ///
81    /// If not provided, falls back to environment variable:
82    /// - Mainnet: `DYDX_PRIVATE_KEY`
83    /// - Testnet: `DYDX_TESTNET_PRIVATE_KEY`
84    ///
85    /// Use `DydxCredential::resolve()` to resolve from config or environment.
86    #[serde(default)]
87    pub private_key: Option<String>,
88    /// Authenticator IDs for permissioned key trading.
89    ///
90    /// When provided, transactions will include a TxExtension to enable trading
91    /// via sub-accounts using delegated signing keys. This is an advanced feature
92    /// for institutional setups with separated hot/cold wallet architectures.
93    ///
94    /// See <https://docs.dydx.xyz/concepts/trading/authenticators> for details on
95    /// permissioned keys and authenticator configuration.
96    #[serde(default)]
97    #[builder(default)]
98    pub authenticator_ids: Vec<u64>,
99    /// Maximum number of retries for failed requests (default: 3).
100    #[serde(default = "default_max_retries")]
101    #[builder(default = 3)]
102    pub max_retries: u32,
103    /// Initial retry delay in milliseconds (default: 1000ms).
104    #[serde(default = "default_retry_delay_initial_ms")]
105    #[builder(default = 1000)]
106    pub retry_delay_initial_ms: u64,
107    /// Maximum retry delay in milliseconds (default: 10000ms).
108    #[serde(default = "default_retry_delay_max_ms")]
109    #[builder(default = 10000)]
110    pub retry_delay_max_ms: u64,
111    /// gRPC rate limit: maximum broadcast requests per second.
112    ///
113    /// Controls the rate of gRPC `broadcast_tx` calls to prevent 429 (ResourceExhausted)
114    /// errors from validator nodes. Known provider limits:
115    /// - Polkachu: 300 req/min (~5 req/s)
116    /// - KingNodes: 250 req/min (~4.2 req/s)
117    /// - AutoStake: 4 req/s
118    ///
119    /// Default: 4 requests per second (conservative, works across all public providers).
120    /// When `None`, rate limiting is disabled.
121    #[serde(default = "default_grpc_rate_limit_per_second")]
122    pub grpc_rate_limit_per_second: Option<u32>,
123    /// Optional proxy URL for HTTP and WebSocket transports.
124    #[serde(default)]
125    pub proxy_url: Option<String>,
126    /// WebSocket transport backend (defaults to `Tungstenite`).
127    #[serde(default)]
128    #[builder(default)]
129    pub transport_backend: TransportBackend,
130}
131
132fn default_max_retries() -> u32 {
133    3
134}
135
136fn default_retry_delay_initial_ms() -> u64 {
137    1000
138}
139
140fn default_retry_delay_max_ms() -> u64 {
141    10000
142}
143
144#[expect(
145    clippy::unnecessary_wraps,
146    reason = "serde default must match field type Option<u32>"
147)]
148fn default_grpc_rate_limit_per_second() -> Option<u32> {
149    Some(4)
150}
151
152fn default_data_http_timeout_secs() -> u64 {
153    60
154}
155
156fn default_data_max_retries() -> u64 {
157    3
158}
159
160fn default_data_retry_delay_initial_ms() -> u64 {
161    100
162}
163
164fn default_data_retry_delay_max_ms() -> u64 {
165    5000
166}
167
168fn default_max_ws_connections() -> usize {
169    8
170}
171
172fn default_per_channel_subscription_limit() -> usize {
173    32
174}
175
176impl DydxAdapterConfig {
177    /// Creates a config with URLs and chain ID resolved for the given network.
178    ///
179    /// Use this instead of `Default::default()` when constructing a testnet config
180    /// without explicit URL overrides. Retains the non-URL defaults from
181    /// [`Default::default`] (retries, timeouts, gRPC rate limit).
182    #[must_use]
183    pub fn for_network(network: DydxNetwork) -> Self {
184        let chain_id = match network {
185            DydxNetwork::Mainnet => crate::common::consts::DYDX_CHAIN_ID,
186            DydxNetwork::Testnet => crate::common::consts::DYDX_TESTNET_CHAIN_ID,
187        };
188        Self {
189            network,
190            base_url: urls::http_base_url(network).to_string(),
191            ws_url: urls::ws_url(network).to_string(),
192            grpc_url: urls::grpc_urls(network)[0].to_string(),
193            grpc_urls: urls::grpc_urls(network)
194                .iter()
195                .map(|&s| s.to_string())
196                .collect(),
197            chain_id: chain_id.to_string(),
198            ..Self::default()
199        }
200    }
201
202    /// Get the list of gRPC URLs to use for connection with fallback support.
203    ///
204    /// Returns `grpc_urls` if non-empty, otherwise falls back to a single-element
205    /// vector containing `grpc_url`.
206    #[must_use]
207    pub fn get_grpc_urls(&self) -> Vec<String> {
208        if self.grpc_urls.is_empty() {
209            vec![self.grpc_url.clone()]
210        } else {
211            self.grpc_urls.clone()
212        }
213    }
214
215    /// Map the configured network to the underlying chain ID.
216    ///
217    /// This is the recommended way to get the chain ID for transaction submission.
218    #[must_use]
219    pub const fn get_chain_id(&self) -> ChainId {
220        self.network.chain_id()
221    }
222
223    /// Returns whether this is a testnet configuration.
224    #[must_use]
225    pub const fn is_testnet(&self) -> bool {
226        matches!(self.network, DydxNetwork::Testnet)
227    }
228
229    /// Returns the gRPC rate limiting quota, if configured.
230    #[must_use]
231    pub fn grpc_quota(&self) -> Option<Quota> {
232        self.grpc_rate_limit_per_second
233            .and_then(NonZeroU32::new)
234            .and_then(Quota::per_second)
235    }
236}
237
238impl Default for DydxAdapterConfig {
239    fn default() -> Self {
240        Self {
241            grpc_rate_limit_per_second: default_grpc_rate_limit_per_second(),
242            ..Self::builder().build()
243        }
244    }
245}
246
247/// Configuration for the dYdX data client.
248#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
249#[serde(default, deny_unknown_fields)]
250#[cfg_attr(
251    feature = "python",
252    pyo3::pyclass(module = "nautilus_trader.adapters.dydx", from_py_object)
253)]
254#[cfg_attr(
255    feature = "python",
256    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.dydx")
257)]
258pub struct DydxDataClientConfig {
259    /// Base URL for the HTTP API.
260    pub base_url_http: Option<String>,
261    /// Base URL for the WebSocket API.
262    pub base_url_ws: Option<String>,
263    /// HTTP request timeout in seconds.
264    #[serde(default = "default_data_http_timeout_secs")]
265    #[builder(default = 60)]
266    pub http_timeout_secs: u64,
267    /// Maximum number of retry attempts for failed HTTP requests.
268    #[serde(default = "default_data_max_retries")]
269    #[builder(default = 3)]
270    pub max_retries: u64,
271    /// Initial retry delay in milliseconds.
272    #[serde(default = "default_data_retry_delay_initial_ms")]
273    #[builder(default = 100)]
274    pub retry_delay_initial_ms: u64,
275    /// Maximum retry delay in milliseconds.
276    #[serde(default = "default_data_retry_delay_max_ms")]
277    #[builder(default = 5000)]
278    pub retry_delay_max_ms: u64,
279    /// Network environment (mainnet or testnet).
280    #[serde(default)]
281    #[builder(default)]
282    pub network: DydxNetwork,
283    /// Optional proxy URL for HTTP and WebSocket transports.
284    pub proxy_url: Option<String>,
285    /// WebSocket transport backend (defaults to `Tungstenite`).
286    #[serde(default)]
287    #[builder(default)]
288    pub transport_backend: TransportBackend,
289    /// Maximum number of WebSocket connections in the Indexer pool.
290    ///
291    /// New connections are spun up lazily once the per-channel subscription
292    /// limit (32 by default on hosted Indexer) is reached on every existing
293    /// connection. Default `8` supports up to 256 markets per 32-limit channel.
294    #[serde(default = "default_max_ws_connections")]
295    #[builder(default = default_max_ws_connections())]
296    pub max_ws_connections: usize,
297    /// Per-connection subscription limit for each rate-limited Indexer channel
298    /// (`v4_trades`, `v4_candles`, `v4_orderbook`, `v4_markets`).
299    ///
300    /// Defaults to `32`, matching the hosted Indexer. Self-hosted Indexer
301    /// deployments may raise this.
302    #[serde(default = "default_per_channel_subscription_limit")]
303    #[builder(default = default_per_channel_subscription_limit())]
304    pub per_channel_subscription_limit: usize,
305}
306
307#[cfg(feature = "python")]
308nautilus_core::impl_pyo3_config_getters!(DydxDataClientConfig {
309    network: DydxNetwork,
310});
311
312impl DydxDataClientConfig {
313    /// Returns whether this is a testnet configuration.
314    #[must_use]
315    pub const fn is_testnet(&self) -> bool {
316        matches!(self.network, DydxNetwork::Testnet)
317    }
318}
319
320impl Default for DydxDataClientConfig {
321    fn default() -> Self {
322        Self::builder().build()
323    }
324}
325
326/// Configuration for the dYdX execution client.
327#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
328#[serde(default, deny_unknown_fields)]
329#[cfg_attr(
330    feature = "python",
331    pyo3::pyclass(module = "nautilus_trader.adapters.dydx", from_py_object)
332)]
333#[cfg_attr(
334    feature = "python",
335    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.dydx")
336)]
337pub struct DydxExecutionClientConfig {
338    /// The account ID for the client.
339    #[builder(default = AccountId::from("DYDX-001"))]
340    pub account_id: AccountId,
341    /// Network environment (mainnet or testnet).
342    #[serde(default)]
343    #[builder(default)]
344    pub network: DydxNetwork,
345    /// gRPC endpoint URL (optional, uses default for network if not provided).
346    pub grpc_endpoint: Option<String>,
347    /// Additional gRPC URLs for fallback support.
348    #[serde(default)]
349    #[builder(default)]
350    pub grpc_urls: Vec<String>,
351    /// WebSocket endpoint URL (optional, uses default for network if not provided).
352    pub ws_endpoint: Option<String>,
353    /// HTTP endpoint URL (optional, uses default for network if not provided).
354    pub http_endpoint: Option<String>,
355    /// Private key (hex) for wallet signing.
356    ///
357    /// If not provided, falls back to environment variable:
358    /// - Mainnet: `DYDX_PRIVATE_KEY`
359    /// - Testnet: `DYDX_TESTNET_PRIVATE_KEY`
360    pub private_key: Option<String>,
361    /// Wallet address.
362    ///
363    /// If not provided, falls back to environment variable:
364    /// - Mainnet: `DYDX_WALLET_ADDRESS`
365    /// - Testnet: `DYDX_TESTNET_WALLET_ADDRESS`
366    pub wallet_address: Option<String>,
367    /// Subaccount number (default: 0).
368    #[serde(default)]
369    #[builder(default)]
370    pub subaccount_number: u32,
371    /// Authenticator IDs for permissioned key trading.
372    #[serde(default)]
373    #[builder(default)]
374    pub authenticator_ids: Vec<u64>,
375    /// HTTP request timeout in seconds.
376    pub http_timeout_secs: Option<u64>,
377    /// Maximum number of retry attempts.
378    pub max_retries: Option<u32>,
379    /// Initial retry delay in milliseconds.
380    pub retry_delay_initial_ms: Option<u64>,
381    /// Maximum retry delay in milliseconds.
382    pub retry_delay_max_ms: Option<u64>,
383    /// gRPC rate limit: maximum broadcast requests per second.
384    /// When `None`, rate limiting is disabled.
385    #[serde(default = "default_grpc_rate_limit_per_second")]
386    pub grpc_rate_limit_per_second: Option<u32>,
387    /// Optional proxy URL for HTTP and WebSocket transports.
388    pub proxy_url: Option<String>,
389    /// WebSocket transport backend (defaults to `Tungstenite`).
390    #[serde(default)]
391    #[builder(default)]
392    pub transport_backend: TransportBackend,
393}
394
395#[cfg(feature = "python")]
396nautilus_core::impl_pyo3_config_getters!(DydxExecutionClientConfig {
397    account_id: AccountId,
398    network: DydxNetwork,
399    wallet_address: Option<String>,
400    subaccount_number: u32,
401});
402
403impl Default for DydxExecutionClientConfig {
404    fn default() -> Self {
405        Self {
406            grpc_rate_limit_per_second: default_grpc_rate_limit_per_second(),
407            ..Self::builder().build()
408        }
409    }
410}
411
412impl DydxExecutionClientConfig {
413    /// Returns the gRPC URLs to use, with fallback support.
414    ///
415    /// Returns `grpc_urls` if non-empty, otherwise uses `grpc_endpoint` if provided,
416    /// otherwise uses the default URLs for the configured network.
417    #[must_use]
418    pub fn get_grpc_urls(&self) -> Vec<String> {
419        if !self.grpc_urls.is_empty() {
420            return self.grpc_urls.clone();
421        }
422
423        if let Some(ref endpoint) = self.grpc_endpoint {
424            return vec![endpoint.clone()];
425        }
426        urls::grpc_urls(self.network)
427            .iter()
428            .map(|&s| s.to_string())
429            .collect()
430    }
431
432    /// Returns the WebSocket URL for the configured network.
433    #[must_use]
434    pub fn get_ws_url(&self) -> String {
435        self.ws_endpoint
436            .clone()
437            .unwrap_or_else(|| urls::ws_url(self.network).to_string())
438    }
439
440    /// Returns the HTTP URL for the configured network.
441    #[must_use]
442    pub fn get_http_url(&self) -> String {
443        self.http_endpoint
444            .clone()
445            .unwrap_or_else(|| urls::http_base_url(self.network).to_string())
446    }
447
448    /// Returns the chain ID for the configured network.
449    #[must_use]
450    pub const fn get_chain_id(&self) -> ChainId {
451        self.network.chain_id()
452    }
453
454    /// Returns whether this is a testnet configuration.
455    #[must_use]
456    pub const fn is_testnet(&self) -> bool {
457        matches!(self.network, DydxNetwork::Testnet)
458    }
459
460    /// Returns the gRPC rate limiting quota, if configured.
461    #[must_use]
462    pub fn grpc_quota(&self) -> Option<Quota> {
463        self.grpc_rate_limit_per_second
464            .and_then(NonZeroU32::new)
465            .and_then(Quota::per_second)
466    }
467}
468
469#[cfg(test)]
470mod tests {
471    use rstest::rstest;
472
473    use super::*;
474
475    #[rstest]
476    fn test_config_get_chain_id_mainnet() {
477        let config = DydxAdapterConfig {
478            network: DydxNetwork::Mainnet,
479            ..Default::default()
480        };
481        assert_eq!(config.get_chain_id(), ChainId::Mainnet1);
482    }
483
484    #[rstest]
485    fn test_config_get_chain_id_testnet() {
486        let config = DydxAdapterConfig {
487            network: DydxNetwork::Testnet,
488            ..Default::default()
489        };
490        assert_eq!(config.get_chain_id(), ChainId::Testnet4);
491    }
492
493    #[rstest]
494    fn test_config_is_testnet() {
495        let mainnet_config = DydxAdapterConfig {
496            network: DydxNetwork::Mainnet,
497            ..Default::default()
498        };
499        assert!(!mainnet_config.is_testnet());
500
501        let testnet_config = DydxAdapterConfig {
502            network: DydxNetwork::Testnet,
503            ..Default::default()
504        };
505        assert!(testnet_config.is_testnet());
506    }
507
508    #[rstest]
509    fn test_config_default_uses_mainnet() {
510        let config = DydxAdapterConfig::default();
511        assert_eq!(config.network, DydxNetwork::Mainnet);
512        assert!(!config.is_testnet());
513    }
514
515    #[rstest]
516    fn test_config_serde_backwards_compat() {
517        // Test that configs missing network field can deserialize with default
518        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,"max_retries":3,"retry_delay_initial_ms":1000,"retry_delay_max_ms":10000}"#;
519
520        let config: Result<DydxAdapterConfig, _> = serde_json::from_str(json);
521        assert!(config.is_ok());
522        let config = config.unwrap();
523        // Should default to Mainnet when network field is missing
524        assert_eq!(config.network, DydxNetwork::Mainnet);
525    }
526
527    #[rstest]
528    fn test_config_get_grpc_urls_fallback() {
529        let config = DydxAdapterConfig {
530            grpc_url: "https://primary.example.com".to_string(),
531            grpc_urls: vec![],
532            ..Default::default()
533        };
534
535        let urls = config.get_grpc_urls();
536        assert_eq!(urls.len(), 1);
537        assert_eq!(urls[0], "https://primary.example.com");
538    }
539
540    #[rstest]
541    fn test_config_get_grpc_urls_multiple() {
542        let config = DydxAdapterConfig {
543            grpc_url: "https://primary.example.com".to_string(),
544            grpc_urls: vec![
545                "https://fallback1.example.com".to_string(),
546                "https://fallback2.example.com".to_string(),
547            ],
548            ..Default::default()
549        };
550
551        let urls = config.get_grpc_urls();
552        assert_eq!(urls.len(), 2);
553        assert_eq!(urls[0], "https://fallback1.example.com");
554        assert_eq!(urls[1], "https://fallback2.example.com");
555    }
556
557    #[rstest]
558    fn test_for_network_mainnet_resolves_urls_and_chain_id() {
559        let config = DydxAdapterConfig::for_network(DydxNetwork::Mainnet);
560
561        assert_eq!(config.network, DydxNetwork::Mainnet);
562        assert_eq!(config.base_url, urls::http_base_url(DydxNetwork::Mainnet));
563        assert_eq!(config.ws_url, urls::ws_url(DydxNetwork::Mainnet));
564        assert_eq!(config.grpc_url, urls::grpc_urls(DydxNetwork::Mainnet)[0]);
565        let expected_grpc: Vec<String> = urls::grpc_urls(DydxNetwork::Mainnet)
566            .iter()
567            .map(|s| (*s).to_string())
568            .collect();
569        assert_eq!(config.grpc_urls, expected_grpc);
570        assert_eq!(config.chain_id, crate::common::consts::DYDX_CHAIN_ID);
571        assert_eq!(config.get_chain_id(), ChainId::Mainnet1);
572    }
573
574    #[rstest]
575    fn test_for_network_testnet_resolves_urls_and_chain_id() {
576        let config = DydxAdapterConfig::for_network(DydxNetwork::Testnet);
577
578        assert_eq!(config.network, DydxNetwork::Testnet);
579        assert_eq!(config.base_url, urls::http_base_url(DydxNetwork::Testnet));
580        assert_eq!(config.ws_url, urls::ws_url(DydxNetwork::Testnet));
581        assert_eq!(config.grpc_url, urls::grpc_urls(DydxNetwork::Testnet)[0]);
582        let expected_grpc: Vec<String> = urls::grpc_urls(DydxNetwork::Testnet)
583            .iter()
584            .map(|s| (*s).to_string())
585            .collect();
586        assert_eq!(config.grpc_urls, expected_grpc);
587        assert_eq!(
588            config.chain_id,
589            crate::common::consts::DYDX_TESTNET_CHAIN_ID,
590        );
591        assert_eq!(config.get_chain_id(), ChainId::Testnet4);
592    }
593
594    #[rstest]
595    #[case(DydxNetwork::Mainnet)]
596    #[case(DydxNetwork::Testnet)]
597    fn test_for_network_preserves_grpc_rate_limit_default(#[case] network: DydxNetwork) {
598        // Regression guard: earlier implementations spread `..Self::builder().build()`,
599        // which returned `None` and silently disabled gRPC throttling. The helper must
600        // retain the `Some(4)` default from `Default::default()`.
601        let config = DydxAdapterConfig::for_network(network);
602        assert_eq!(config.grpc_rate_limit_per_second, Some(4));
603        assert!(config.grpc_quota().is_some());
604    }
605
606    #[rstest]
607    fn test_adapter_config_toml_requires_url_fields() {
608        // URL fields and chain_id are intentionally required: deserializing a
609        // partial TOML must not silently mix the configured `network` with
610        // mainnet-defaulted URLs. Callers wanting per-network defaults should
611        // use `DydxAdapterConfig::for_network`.
612        let err = toml::from_str::<DydxAdapterConfig>(r#"network = "testnet""#).unwrap_err();
613        let message = err.to_string();
614        assert!(message.contains("missing field"), "{message}");
615    }
616
617    #[rstest]
618    fn test_exec_config_toml_empty_uses_defaults() {
619        let config: DydxExecutionClientConfig = toml::from_str("").unwrap();
620        let expected = DydxExecutionClientConfig::default();
621        assert_eq!(config.account_id, expected.account_id);
622        assert_eq!(config.network, expected.network);
623        assert_eq!(config.subaccount_number, expected.subaccount_number);
624        assert_eq!(
625            config.grpc_rate_limit_per_second,
626            expected.grpc_rate_limit_per_second,
627        );
628    }
629}