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, TraderId};
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.core.nautilus_pyo3.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
307impl DydxDataClientConfig {
308    /// Returns whether this is a testnet configuration.
309    #[must_use]
310    pub const fn is_testnet(&self) -> bool {
311        matches!(self.network, DydxNetwork::Testnet)
312    }
313}
314
315impl Default for DydxDataClientConfig {
316    fn default() -> Self {
317        Self::builder().build()
318    }
319}
320
321/// Configuration for the dYdX execution client.
322#[derive(Debug, Clone, Serialize, Deserialize, bon::Builder)]
323#[serde(default, deny_unknown_fields)]
324#[cfg_attr(
325    feature = "python",
326    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.dydx", from_py_object)
327)]
328#[cfg_attr(
329    feature = "python",
330    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.dydx")
331)]
332pub struct DydxExecClientConfig {
333    /// The trader ID for the client.
334    #[builder(default = TraderId::from("TRADER-001"))]
335    pub trader_id: TraderId,
336    /// The account ID for the client.
337    #[builder(default = AccountId::from("DYDX-001"))]
338    pub account_id: AccountId,
339    /// Network environment (mainnet or testnet).
340    #[serde(default)]
341    #[builder(default)]
342    pub network: DydxNetwork,
343    /// gRPC endpoint URL (optional, uses default for network if not provided).
344    pub grpc_endpoint: Option<String>,
345    /// Additional gRPC URLs for fallback support.
346    #[serde(default)]
347    #[builder(default)]
348    pub grpc_urls: Vec<String>,
349    /// WebSocket endpoint URL (optional, uses default for network if not provided).
350    pub ws_endpoint: Option<String>,
351    /// HTTP endpoint URL (optional, uses default for network if not provided).
352    pub http_endpoint: Option<String>,
353    /// Private key (hex) for wallet signing.
354    ///
355    /// If not provided, falls back to environment variable:
356    /// - Mainnet: `DYDX_PRIVATE_KEY`
357    /// - Testnet: `DYDX_TESTNET_PRIVATE_KEY`
358    pub private_key: Option<String>,
359    /// Wallet address.
360    ///
361    /// If not provided, falls back to environment variable:
362    /// - Mainnet: `DYDX_WALLET_ADDRESS`
363    /// - Testnet: `DYDX_TESTNET_WALLET_ADDRESS`
364    pub wallet_address: Option<String>,
365    /// Subaccount number (default: 0).
366    #[serde(default)]
367    #[builder(default)]
368    pub subaccount_number: u32,
369    /// Authenticator IDs for permissioned key trading.
370    #[serde(default)]
371    #[builder(default)]
372    pub authenticator_ids: Vec<u64>,
373    /// HTTP request timeout in seconds.
374    pub http_timeout_secs: Option<u64>,
375    /// Maximum number of retry attempts.
376    pub max_retries: Option<u32>,
377    /// Initial retry delay in milliseconds.
378    pub retry_delay_initial_ms: Option<u64>,
379    /// Maximum retry delay in milliseconds.
380    pub retry_delay_max_ms: Option<u64>,
381    /// gRPC rate limit: maximum broadcast requests per second.
382    /// When `None`, rate limiting is disabled.
383    #[serde(default = "default_grpc_rate_limit_per_second")]
384    pub grpc_rate_limit_per_second: Option<u32>,
385    /// Optional proxy URL for HTTP and WebSocket transports.
386    pub proxy_url: Option<String>,
387    /// WebSocket transport backend (defaults to `Tungstenite`).
388    #[serde(default)]
389    #[builder(default)]
390    pub transport_backend: TransportBackend,
391}
392
393impl Default for DydxExecClientConfig {
394    fn default() -> Self {
395        Self {
396            grpc_rate_limit_per_second: default_grpc_rate_limit_per_second(),
397            ..Self::builder().build()
398        }
399    }
400}
401
402impl DydxExecClientConfig {
403    /// Returns the gRPC URLs to use, with fallback support.
404    ///
405    /// Returns `grpc_urls` if non-empty, otherwise uses `grpc_endpoint` if provided,
406    /// otherwise uses the default URLs for the configured network.
407    #[must_use]
408    pub fn get_grpc_urls(&self) -> Vec<String> {
409        if !self.grpc_urls.is_empty() {
410            return self.grpc_urls.clone();
411        }
412
413        if let Some(ref endpoint) = self.grpc_endpoint {
414            return vec![endpoint.clone()];
415        }
416        urls::grpc_urls(self.network)
417            .iter()
418            .map(|&s| s.to_string())
419            .collect()
420    }
421
422    /// Returns the WebSocket URL for the configured network.
423    #[must_use]
424    pub fn get_ws_url(&self) -> String {
425        self.ws_endpoint
426            .clone()
427            .unwrap_or_else(|| urls::ws_url(self.network).to_string())
428    }
429
430    /// Returns the HTTP URL for the configured network.
431    #[must_use]
432    pub fn get_http_url(&self) -> String {
433        self.http_endpoint
434            .clone()
435            .unwrap_or_else(|| urls::http_base_url(self.network).to_string())
436    }
437
438    /// Returns the chain ID for the configured network.
439    #[must_use]
440    pub const fn get_chain_id(&self) -> ChainId {
441        self.network.chain_id()
442    }
443
444    /// Returns whether this is a testnet configuration.
445    #[must_use]
446    pub const fn is_testnet(&self) -> bool {
447        matches!(self.network, DydxNetwork::Testnet)
448    }
449
450    /// Returns the gRPC rate limiting quota, if configured.
451    #[must_use]
452    pub fn grpc_quota(&self) -> Option<Quota> {
453        self.grpc_rate_limit_per_second
454            .and_then(NonZeroU32::new)
455            .and_then(Quota::per_second)
456    }
457}
458
459#[cfg(test)]
460mod tests {
461    use rstest::rstest;
462
463    use super::*;
464
465    #[rstest]
466    fn test_config_get_chain_id_mainnet() {
467        let config = DydxAdapterConfig {
468            network: DydxNetwork::Mainnet,
469            ..Default::default()
470        };
471        assert_eq!(config.get_chain_id(), ChainId::Mainnet1);
472    }
473
474    #[rstest]
475    fn test_config_get_chain_id_testnet() {
476        let config = DydxAdapterConfig {
477            network: DydxNetwork::Testnet,
478            ..Default::default()
479        };
480        assert_eq!(config.get_chain_id(), ChainId::Testnet4);
481    }
482
483    #[rstest]
484    fn test_config_is_testnet() {
485        let mainnet_config = DydxAdapterConfig {
486            network: DydxNetwork::Mainnet,
487            ..Default::default()
488        };
489        assert!(!mainnet_config.is_testnet());
490
491        let testnet_config = DydxAdapterConfig {
492            network: DydxNetwork::Testnet,
493            ..Default::default()
494        };
495        assert!(testnet_config.is_testnet());
496    }
497
498    #[rstest]
499    fn test_config_default_uses_mainnet() {
500        let config = DydxAdapterConfig::default();
501        assert_eq!(config.network, DydxNetwork::Mainnet);
502        assert!(!config.is_testnet());
503    }
504
505    #[rstest]
506    fn test_config_serde_backwards_compat() {
507        // Test that configs missing network field can deserialize with default
508        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}"#;
509
510        let config: Result<DydxAdapterConfig, _> = serde_json::from_str(json);
511        assert!(config.is_ok());
512        let config = config.unwrap();
513        // Should default to Mainnet when network field is missing
514        assert_eq!(config.network, DydxNetwork::Mainnet);
515    }
516
517    #[rstest]
518    fn test_config_get_grpc_urls_fallback() {
519        let config = DydxAdapterConfig {
520            grpc_url: "https://primary.example.com".to_string(),
521            grpc_urls: vec![],
522            ..Default::default()
523        };
524
525        let urls = config.get_grpc_urls();
526        assert_eq!(urls.len(), 1);
527        assert_eq!(urls[0], "https://primary.example.com");
528    }
529
530    #[rstest]
531    fn test_config_get_grpc_urls_multiple() {
532        let config = DydxAdapterConfig {
533            grpc_url: "https://primary.example.com".to_string(),
534            grpc_urls: vec![
535                "https://fallback1.example.com".to_string(),
536                "https://fallback2.example.com".to_string(),
537            ],
538            ..Default::default()
539        };
540
541        let urls = config.get_grpc_urls();
542        assert_eq!(urls.len(), 2);
543        assert_eq!(urls[0], "https://fallback1.example.com");
544        assert_eq!(urls[1], "https://fallback2.example.com");
545    }
546
547    #[rstest]
548    fn test_for_network_mainnet_resolves_urls_and_chain_id() {
549        let config = DydxAdapterConfig::for_network(DydxNetwork::Mainnet);
550
551        assert_eq!(config.network, DydxNetwork::Mainnet);
552        assert_eq!(config.base_url, urls::http_base_url(DydxNetwork::Mainnet));
553        assert_eq!(config.ws_url, urls::ws_url(DydxNetwork::Mainnet));
554        assert_eq!(config.grpc_url, urls::grpc_urls(DydxNetwork::Mainnet)[0]);
555        let expected_grpc: Vec<String> = urls::grpc_urls(DydxNetwork::Mainnet)
556            .iter()
557            .map(|s| (*s).to_string())
558            .collect();
559        assert_eq!(config.grpc_urls, expected_grpc);
560        assert_eq!(config.chain_id, crate::common::consts::DYDX_CHAIN_ID);
561        assert_eq!(config.get_chain_id(), ChainId::Mainnet1);
562    }
563
564    #[rstest]
565    fn test_for_network_testnet_resolves_urls_and_chain_id() {
566        let config = DydxAdapterConfig::for_network(DydxNetwork::Testnet);
567
568        assert_eq!(config.network, DydxNetwork::Testnet);
569        assert_eq!(config.base_url, urls::http_base_url(DydxNetwork::Testnet));
570        assert_eq!(config.ws_url, urls::ws_url(DydxNetwork::Testnet));
571        assert_eq!(config.grpc_url, urls::grpc_urls(DydxNetwork::Testnet)[0]);
572        let expected_grpc: Vec<String> = urls::grpc_urls(DydxNetwork::Testnet)
573            .iter()
574            .map(|s| (*s).to_string())
575            .collect();
576        assert_eq!(config.grpc_urls, expected_grpc);
577        assert_eq!(
578            config.chain_id,
579            crate::common::consts::DYDX_TESTNET_CHAIN_ID,
580        );
581        assert_eq!(config.get_chain_id(), ChainId::Testnet4);
582    }
583
584    #[rstest]
585    #[case(DydxNetwork::Mainnet)]
586    #[case(DydxNetwork::Testnet)]
587    fn test_for_network_preserves_grpc_rate_limit_default(#[case] network: DydxNetwork) {
588        // Regression guard: earlier implementations spread `..Self::builder().build()`,
589        // which returned `None` and silently disabled gRPC throttling. The helper must
590        // retain the `Some(4)` default from `Default::default()`.
591        let config = DydxAdapterConfig::for_network(network);
592        assert_eq!(config.grpc_rate_limit_per_second, Some(4));
593        assert!(config.grpc_quota().is_some());
594    }
595
596    #[rstest]
597    fn test_adapter_config_toml_requires_url_fields() {
598        // URL fields and chain_id are intentionally required: deserializing a
599        // partial TOML must not silently mix the configured `network` with
600        // mainnet-defaulted URLs. Callers wanting per-network defaults should
601        // use `DydxAdapterConfig::for_network`.
602        let err = toml::from_str::<DydxAdapterConfig>(r#"network = "testnet""#).unwrap_err();
603        let message = err.to_string();
604        assert!(message.contains("missing field"), "{message}");
605    }
606
607    #[rstest]
608    fn test_exec_config_toml_empty_uses_defaults() {
609        let config: DydxExecClientConfig = toml::from_str("").unwrap();
610        let expected = DydxExecClientConfig::default();
611
612        assert_eq!(config.trader_id, expected.trader_id);
613        assert_eq!(config.account_id, expected.account_id);
614        assert_eq!(config.network, expected.network);
615        assert_eq!(config.subaccount_number, expected.subaccount_number);
616        assert_eq!(
617            config.grpc_rate_limit_per_second,
618            expected.grpc_rate_limit_per_second,
619        );
620    }
621}