Skip to main content

aptos_sdk/
config.rs

1//! Network configuration for the Aptos SDK.
2//!
3//! This module provides configuration options for connecting to different
4//! Aptos networks (mainnet, testnet, devnet) or custom endpoints.
5
6use crate::error::{AptosError, AptosResult};
7use crate::retry::RetryConfig;
8use crate::types::ChainId;
9use std::time::Duration;
10use url::Url;
11
12/// Validates that a URL uses a safe scheme (http or https).
13///
14/// # Security
15///
16/// This prevents SSRF attacks via dangerous URL schemes like `file://`, `gopher://`, etc.
17/// For production use, HTTPS is strongly recommended. HTTP is permitted (e.g., for local
18/// development) but no host restrictions are enforced by this function.
19///
20/// # Errors
21///
22/// Returns [`AptosError::Config`] if the URL scheme is not `http` or `https`.
23pub fn validate_url_scheme(url: &Url) -> AptosResult<()> {
24    match url.scheme() {
25        "https" => Ok(()),
26        "http" => {
27            // HTTP is allowed for local development and testing
28            Ok(())
29        }
30        scheme => Err(AptosError::Config(format!(
31            "unsupported URL scheme '{scheme}': only 'http' and 'https' are allowed"
32        ))),
33    }
34}
35
36/// Reads a response body with an enforced size limit, aborting early if exceeded.
37///
38/// Unlike `response.bytes().await?` which buffers the entire response in memory
39/// before any size check, this function:
40/// 1. Pre-checks the `Content-Length` header (if present) to reject obviously
41///    oversized responses before reading any body data.
42/// 2. Reads the body incrementally via chunked streaming, aborting as soon as
43///    the accumulated size exceeds `max_size`.
44///
45/// This prevents memory exhaustion from malicious servers that send huge
46/// responses (including chunked transfer-encoding without `Content-Length`).
47///
48/// # Errors
49///
50/// Returns [`AptosError::Api`] with error code `RESPONSE_TOO_LARGE` if the
51/// response body exceeds `max_size` bytes.
52pub async fn read_response_bounded(
53    mut response: reqwest::Response,
54    max_size: usize,
55) -> AptosResult<Vec<u8>> {
56    // Pre-check Content-Length header for early rejection (avoids reading any body)
57    if let Some(content_length) = response.content_length()
58        && content_length > max_size as u64
59    {
60        return Err(AptosError::Api {
61            status_code: response.status().as_u16(),
62            message: format!(
63                "response too large: Content-Length {content_length} bytes exceeds limit of {max_size} bytes"
64            ),
65            error_code: Some("RESPONSE_TOO_LARGE".into()),
66            vm_error_code: None,
67        });
68    }
69
70    // Read body incrementally, aborting if accumulated size exceeds the limit.
71    // This protects against chunked transfer-encoding that bypasses Content-Length.
72    let mut body = Vec::with_capacity(std::cmp::min(max_size, 1024 * 1024));
73    while let Some(chunk) = response.chunk().await? {
74        if body.len().saturating_add(chunk.len()) > max_size {
75            return Err(AptosError::Api {
76                status_code: response.status().as_u16(),
77                message: format!(
78                    "response too large: exceeded limit of {max_size} bytes during streaming"
79                ),
80                error_code: Some("RESPONSE_TOO_LARGE".into()),
81                vm_error_code: None,
82            });
83        }
84        body.extend_from_slice(&chunk);
85    }
86
87    Ok(body)
88}
89
90/// Configuration for HTTP connection pooling.
91///
92/// Controls how connections are reused across requests for better performance.
93#[derive(Debug, Clone)]
94pub struct PoolConfig {
95    /// Maximum number of idle connections per host.
96    /// Default: unlimited (no limit)
97    pub max_idle_per_host: Option<usize>,
98    /// Maximum total idle connections in the pool.
99    /// Default: 100
100    pub max_idle_total: usize,
101    /// How long to keep idle connections alive.
102    /// Default: 90 seconds
103    pub idle_timeout: Duration,
104    /// Whether to enable TCP keepalive.
105    /// Default: true
106    pub tcp_keepalive: Option<Duration>,
107    /// Whether to enable TCP nodelay (disable Nagle's algorithm).
108    /// Default: true
109    pub tcp_nodelay: bool,
110    /// Maximum response body size in bytes.
111    /// Default: 10 MB (`10_485_760` bytes)
112    ///
113    /// # Security
114    ///
115    /// This limit helps prevent memory exhaustion from extremely large responses.
116    /// The Aptos API responses are typically much smaller than this limit.
117    pub max_response_size: usize,
118}
119
120/// Default maximum response size: 10 MB
121///
122/// # Security
123///
124/// This limit helps prevent memory exhaustion from malicious or compromised
125/// servers sending extremely large responses. The default of 10 MB is generous
126/// for normal Aptos API responses (typically under 1 MB). If you need to
127/// handle larger responses (e.g., bulk data exports), increase this via
128/// [`PoolConfigBuilder::max_response_size`].
129const DEFAULT_MAX_RESPONSE_SIZE: usize = 10 * 1024 * 1024;
130
131impl Default for PoolConfig {
132    fn default() -> Self {
133        Self {
134            max_idle_per_host: None, // unlimited
135            max_idle_total: 100,
136            idle_timeout: Duration::from_secs(90),
137            tcp_keepalive: Some(Duration::from_mins(1)),
138            tcp_nodelay: true,
139            max_response_size: DEFAULT_MAX_RESPONSE_SIZE,
140        }
141    }
142}
143
144impl PoolConfig {
145    /// Creates a new pool configuration builder.
146    pub fn builder() -> PoolConfigBuilder {
147        PoolConfigBuilder::default()
148    }
149
150    /// Creates a configuration optimized for high-throughput scenarios.
151    ///
152    /// - More idle connections
153    /// - Longer idle timeout
154    /// - TCP keepalive enabled
155    pub fn high_throughput() -> Self {
156        Self {
157            max_idle_per_host: Some(32),
158            max_idle_total: 256,
159            idle_timeout: Duration::from_mins(5),
160            tcp_keepalive: Some(Duration::from_secs(30)),
161            tcp_nodelay: true,
162            max_response_size: DEFAULT_MAX_RESPONSE_SIZE,
163        }
164    }
165
166    /// Creates a configuration optimized for low-latency scenarios.
167    ///
168    /// - Fewer idle connections (fresher connections)
169    /// - Shorter idle timeout
170    /// - TCP nodelay enabled
171    pub fn low_latency() -> Self {
172        Self {
173            max_idle_per_host: Some(8),
174            max_idle_total: 32,
175            idle_timeout: Duration::from_secs(30),
176            tcp_keepalive: Some(Duration::from_secs(15)),
177            tcp_nodelay: true,
178            max_response_size: DEFAULT_MAX_RESPONSE_SIZE,
179        }
180    }
181
182    /// Creates a minimal configuration for constrained environments.
183    ///
184    /// - Minimal idle connections
185    /// - Short idle timeout
186    pub fn minimal() -> Self {
187        Self {
188            max_idle_per_host: Some(2),
189            max_idle_total: 8,
190            idle_timeout: Duration::from_secs(10),
191            tcp_keepalive: None,
192            tcp_nodelay: true,
193            max_response_size: DEFAULT_MAX_RESPONSE_SIZE,
194        }
195    }
196}
197
198/// Builder for `PoolConfig`.
199#[derive(Debug, Clone, Default)]
200#[allow(clippy::option_option)] // Intentional: distinguishes "not set" from "explicitly set to None"
201pub struct PoolConfigBuilder {
202    max_idle_per_host: Option<usize>,
203    max_idle_total: Option<usize>,
204    idle_timeout: Option<Duration>,
205    /// None = not set (use default), Some(None) = explicitly disabled, Some(Some(d)) = explicitly set
206    tcp_keepalive: Option<Option<Duration>>,
207    tcp_nodelay: Option<bool>,
208    max_response_size: Option<usize>,
209}
210
211impl PoolConfigBuilder {
212    /// Sets the maximum idle connections per host.
213    #[must_use]
214    pub fn max_idle_per_host(mut self, max: usize) -> Self {
215        self.max_idle_per_host = Some(max);
216        self
217    }
218
219    /// Removes the limit on idle connections per host.
220    #[must_use]
221    pub fn unlimited_idle_per_host(mut self) -> Self {
222        self.max_idle_per_host = None;
223        self
224    }
225
226    /// Sets the maximum total idle connections.
227    #[must_use]
228    pub fn max_idle_total(mut self, max: usize) -> Self {
229        self.max_idle_total = Some(max);
230        self
231    }
232
233    /// Sets the idle connection timeout.
234    #[must_use]
235    pub fn idle_timeout(mut self, timeout: Duration) -> Self {
236        self.idle_timeout = Some(timeout);
237        self
238    }
239
240    /// Sets the TCP keepalive interval.
241    #[must_use]
242    pub fn tcp_keepalive(mut self, interval: Duration) -> Self {
243        self.tcp_keepalive = Some(Some(interval));
244        self
245    }
246
247    /// Disables TCP keepalive.
248    #[must_use]
249    pub fn no_tcp_keepalive(mut self) -> Self {
250        self.tcp_keepalive = Some(None);
251        self
252    }
253
254    /// Sets whether to enable TCP nodelay.
255    #[must_use]
256    pub fn tcp_nodelay(mut self, enabled: bool) -> Self {
257        self.tcp_nodelay = Some(enabled);
258        self
259    }
260
261    /// Sets the maximum response body size in bytes.
262    ///
263    /// # Security
264    ///
265    /// This helps prevent memory exhaustion from extremely large responses.
266    #[must_use]
267    pub fn max_response_size(mut self, size: usize) -> Self {
268        self.max_response_size = Some(size);
269        self
270    }
271
272    /// Builds the pool configuration.
273    pub fn build(self) -> PoolConfig {
274        let default = PoolConfig::default();
275        PoolConfig {
276            max_idle_per_host: self.max_idle_per_host.or(default.max_idle_per_host),
277            max_idle_total: self.max_idle_total.unwrap_or(default.max_idle_total),
278            idle_timeout: self.idle_timeout.unwrap_or(default.idle_timeout),
279            tcp_keepalive: self.tcp_keepalive.unwrap_or(default.tcp_keepalive),
280            tcp_nodelay: self.tcp_nodelay.unwrap_or(default.tcp_nodelay),
281            max_response_size: self.max_response_size.unwrap_or(default.max_response_size),
282        }
283    }
284}
285
286/// Configuration for the Aptos client.
287///
288/// Use the builder methods to customize the configuration, or use one of the
289/// preset configurations like [`AptosConfig::mainnet()`], [`AptosConfig::testnet()`],
290/// or [`AptosConfig::devnet()`].
291///
292/// # Example
293///
294/// ```rust
295/// use aptos_sdk::AptosConfig;
296/// use aptos_sdk::retry::RetryConfig;
297/// use aptos_sdk::config::PoolConfig;
298///
299/// // Use testnet with default settings
300/// let config = AptosConfig::testnet();
301///
302/// // Custom configuration with retry and connection pooling
303/// let config = AptosConfig::testnet()
304///     .with_timeout(std::time::Duration::from_secs(30))
305///     .with_retry(RetryConfig::aggressive())
306///     .with_pool(PoolConfig::high_throughput());
307/// ```
308#[derive(Debug, Clone)]
309pub struct AptosConfig {
310    /// The network to connect to
311    pub(crate) network: Network,
312    /// REST API URL (fullnode)
313    pub(crate) fullnode_url: Url,
314    /// Indexer GraphQL URL (optional)
315    pub(crate) indexer_url: Option<Url>,
316    /// Faucet URL (optional, for testnets)
317    pub(crate) faucet_url: Option<Url>,
318    /// Request timeout
319    pub(crate) timeout: Duration,
320    /// Retry configuration for transient failures
321    pub(crate) retry_config: RetryConfig,
322    /// Connection pool configuration
323    pub(crate) pool_config: PoolConfig,
324    /// Optional API key for authenticated access
325    pub(crate) api_key: Option<String>,
326}
327
328/// Known Aptos networks.
329#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
330pub enum Network {
331    /// Aptos mainnet
332    Mainnet,
333    /// Aptos testnet
334    Testnet,
335    /// Aptos devnet
336    Devnet,
337    /// Local development network
338    Local,
339    /// Custom network
340    Custom,
341}
342
343impl Network {
344    /// Returns the chain ID for this network.
345    ///
346    /// Devnet's chain ID is intentionally returned as `0` (unknown) because
347    /// it is reset on a regular cadence and any hardcoded value rapidly
348    /// goes stale. Returning `0` causes [`crate::Aptos::ensure_chain_id`] to
349    /// fetch the live chain ID from the configured fullnode and cache it.
350    pub fn chain_id(&self) -> ChainId {
351        match self {
352            Network::Mainnet => ChainId::mainnet(),
353            Network::Testnet => ChainId::testnet(),
354            Network::Devnet => ChainId::new(0),
355            Network::Local => ChainId::new(4),
356            Network::Custom => ChainId::new(0),
357        }
358    }
359
360    /// Returns the network name as a string.
361    pub fn as_str(&self) -> &'static str {
362        match self {
363            Network::Mainnet => "mainnet",
364            Network::Testnet => "testnet",
365            Network::Devnet => "devnet",
366            Network::Local => "local",
367            Network::Custom => "custom",
368        }
369    }
370
371    /// Returns the default Aptos-hosted pepper service URL for this network.
372    ///
373    /// These endpoints match the TypeScript SDK's `NetworkToPepperAPI` table.
374    /// [`Network::Custom`] has no default and returns `None`.
375    ///
376    /// Used with [`crate::account::HttpPepperService`] when deriving a
377    /// [`crate::account::KeylessAccount`] (requires the `keyless` feature).
378    #[must_use]
379    pub fn pepper_url(self) -> Option<&'static str> {
380        match self {
381            Network::Mainnet => Some("https://api.mainnet.aptoslabs.com/keyless/pepper/v0"),
382            Network::Testnet => Some("https://api.testnet.aptoslabs.com/keyless/pepper/v0"),
383            Network::Devnet => Some("https://api.devnet.aptoslabs.com/keyless/pepper/v0"),
384            // Local development uses the devnet-hosted services, like the TS SDK.
385            Network::Local => Some("https://api.devnet.aptoslabs.com/keyless/pepper/v0"),
386            Network::Custom => None,
387        }
388    }
389
390    /// Returns the default Aptos-hosted prover service URL for this network.
391    ///
392    /// These endpoints match the TypeScript SDK's `NetworkToProverAPI` table.
393    /// [`Network::Custom`] has no default and returns `None`.
394    ///
395    /// Used with [`crate::account::HttpProverService`] when deriving a
396    /// [`crate::account::KeylessAccount`] (requires the `keyless` feature).
397    #[must_use]
398    pub fn prover_url(self) -> Option<&'static str> {
399        match self {
400            Network::Mainnet => Some("https://api.mainnet.aptoslabs.com/keyless/prover/v0"),
401            Network::Testnet => Some("https://api.testnet.aptoslabs.com/keyless/prover/v0"),
402            Network::Devnet => Some("https://api.devnet.aptoslabs.com/keyless/prover/v0"),
403            Network::Local => Some("https://api.devnet.aptoslabs.com/keyless/prover/v0"),
404            Network::Custom => None,
405        }
406    }
407}
408
409impl Default for AptosConfig {
410    fn default() -> Self {
411        Self::devnet()
412    }
413}
414
415impl AptosConfig {
416    /// Creates a configuration for Aptos mainnet.
417    ///
418    /// # Example
419    ///
420    /// ```rust
421    /// use aptos_sdk::AptosConfig;
422    ///
423    /// let config = AptosConfig::mainnet();
424    /// ```
425    #[allow(clippy::missing_panics_doc)]
426    #[must_use]
427    pub fn mainnet() -> Self {
428        Self {
429            network: Network::Mainnet,
430            fullnode_url: Url::parse("https://fullnode.mainnet.aptoslabs.com/v1")
431                .expect("valid mainnet URL"),
432            indexer_url: Some(
433                Url::parse("https://indexer.mainnet.aptoslabs.com/v1/graphql")
434                    .expect("valid indexer URL"),
435            ),
436            faucet_url: None, // No faucet on mainnet
437            timeout: Duration::from_secs(30),
438            retry_config: RetryConfig::conservative(), // More conservative for mainnet
439            pool_config: PoolConfig::default(),
440            api_key: None,
441        }
442    }
443
444    /// Creates a configuration for Aptos testnet.
445    ///
446    /// # Example
447    ///
448    /// ```rust
449    /// use aptos_sdk::AptosConfig;
450    ///
451    /// let config = AptosConfig::testnet();
452    /// ```
453    #[allow(clippy::missing_panics_doc)]
454    #[must_use]
455    pub fn testnet() -> Self {
456        Self {
457            network: Network::Testnet,
458            fullnode_url: Url::parse("https://fullnode.testnet.aptoslabs.com/v1")
459                .expect("valid testnet URL"),
460            indexer_url: Some(
461                Url::parse("https://indexer.testnet.aptoslabs.com/v1/graphql")
462                    .expect("valid indexer URL"),
463            ),
464            faucet_url: Some(
465                Url::parse("https://faucet.testnet.aptoslabs.com").expect("valid faucet URL"),
466            ),
467            timeout: Duration::from_secs(30),
468            retry_config: RetryConfig::default(),
469            pool_config: PoolConfig::default(),
470            api_key: None,
471        }
472    }
473
474    /// Creates a configuration for Aptos devnet.
475    ///
476    /// # Example
477    ///
478    /// ```rust
479    /// use aptos_sdk::AptosConfig;
480    ///
481    /// let config = AptosConfig::devnet();
482    /// ```
483    #[allow(clippy::missing_panics_doc)]
484    #[must_use]
485    pub fn devnet() -> Self {
486        Self {
487            network: Network::Devnet,
488            fullnode_url: Url::parse("https://fullnode.devnet.aptoslabs.com/v1")
489                .expect("valid devnet URL"),
490            indexer_url: Some(
491                Url::parse("https://indexer.devnet.aptoslabs.com/v1/graphql")
492                    .expect("valid indexer URL"),
493            ),
494            faucet_url: Some(
495                Url::parse("https://faucet.devnet.aptoslabs.com").expect("valid faucet URL"),
496            ),
497            timeout: Duration::from_secs(30),
498            retry_config: RetryConfig::default(),
499            pool_config: PoolConfig::default(),
500            api_key: None,
501        }
502    }
503
504    /// Creates a configuration for a local development network.
505    ///
506    /// This assumes the local network is running on the default ports
507    /// (REST API on 8080, faucet on 8081).
508    ///
509    /// # Example
510    ///
511    /// ```rust
512    /// use aptos_sdk::AptosConfig;
513    ///
514    /// let config = AptosConfig::local();
515    /// ```
516    #[allow(clippy::missing_panics_doc)]
517    #[must_use]
518    pub fn local() -> Self {
519        Self {
520            network: Network::Local,
521            fullnode_url: Url::parse("http://127.0.0.1:8080/v1").expect("valid local URL"),
522            indexer_url: None,
523            faucet_url: Some(Url::parse("http://127.0.0.1:8081").expect("valid local faucet URL")),
524            timeout: Duration::from_secs(10),
525            retry_config: RetryConfig::aggressive(), // Fast retries for local dev
526            pool_config: PoolConfig::low_latency(),  // Low latency for local dev
527            api_key: None,
528        }
529    }
530
531    /// Creates a custom configuration with the specified fullnode URL.
532    ///
533    /// # Security
534    ///
535    /// Only `http://` and `https://` URL schemes are allowed. Using `https://` is
536    /// strongly recommended for production. HTTP is acceptable only for localhost
537    /// development environments.
538    ///
539    /// # Errors
540    ///
541    /// Returns an error if the `fullnode_url` cannot be parsed as a valid URL
542    /// or uses an unsupported scheme (e.g., `file://`, `ftp://`).
543    ///
544    /// # Example
545    ///
546    /// ```rust
547    /// use aptos_sdk::AptosConfig;
548    ///
549    /// let config = AptosConfig::custom("https://my-node.example.com/v1").unwrap();
550    /// ```
551    pub fn custom(fullnode_url: &str) -> AptosResult<Self> {
552        let url = Url::parse(fullnode_url)?;
553        validate_url_scheme(&url)?;
554        Ok(Self {
555            network: Network::Custom,
556            fullnode_url: url,
557            indexer_url: None,
558            faucet_url: None,
559            timeout: Duration::from_secs(30),
560            retry_config: RetryConfig::default(),
561            pool_config: PoolConfig::default(),
562            api_key: None,
563        })
564    }
565
566    /// Sets the request timeout.
567    #[must_use]
568    pub fn with_timeout(mut self, timeout: Duration) -> Self {
569        self.timeout = timeout;
570        self
571    }
572
573    /// Sets the retry configuration for transient failures.
574    ///
575    /// # Example
576    ///
577    /// ```rust
578    /// use aptos_sdk::AptosConfig;
579    /// use aptos_sdk::retry::RetryConfig;
580    ///
581    /// let config = AptosConfig::testnet()
582    ///     .with_retry(RetryConfig::aggressive());
583    /// ```
584    #[must_use]
585    pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
586        self.retry_config = retry_config;
587        self
588    }
589
590    /// Disables automatic retry for API calls.
591    ///
592    /// This is equivalent to `with_retry(RetryConfig::no_retry())`.
593    #[must_use]
594    pub fn without_retry(mut self) -> Self {
595        self.retry_config = RetryConfig::no_retry();
596        self
597    }
598
599    /// Sets the maximum number of retries for transient failures.
600    ///
601    /// This is a convenience method that modifies the retry config.
602    #[must_use]
603    pub fn with_max_retries(mut self, max_retries: u32) -> Self {
604        self.retry_config = RetryConfig::builder()
605            .max_retries(max_retries)
606            .initial_delay_ms(self.retry_config.initial_delay_ms)
607            .max_delay_ms(self.retry_config.max_delay_ms)
608            .exponential_base(self.retry_config.exponential_base)
609            .jitter(self.retry_config.jitter)
610            .build();
611        self
612    }
613
614    /// Sets the connection pool configuration.
615    ///
616    /// # Example
617    ///
618    /// ```rust
619    /// use aptos_sdk::AptosConfig;
620    /// use aptos_sdk::config::PoolConfig;
621    ///
622    /// let config = AptosConfig::testnet()
623    ///     .with_pool(PoolConfig::high_throughput());
624    /// ```
625    #[must_use]
626    pub fn with_pool(mut self, pool_config: PoolConfig) -> Self {
627        self.pool_config = pool_config;
628        self
629    }
630
631    /// Sets an API key for authenticated access.
632    ///
633    /// This is useful when using Aptos Build or other services that
634    /// provide higher rate limits with API keys.
635    #[must_use]
636    pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
637        self.api_key = Some(api_key.into());
638        self
639    }
640
641    /// Sets a custom indexer URL.
642    ///
643    /// # Security
644    ///
645    /// Only `http://` and `https://` URL schemes are allowed.
646    ///
647    /// # Errors
648    ///
649    /// Returns an error if the `url` cannot be parsed as a valid URL
650    /// or uses an unsupported scheme.
651    pub fn with_indexer_url(mut self, url: &str) -> AptosResult<Self> {
652        let parsed = Url::parse(url)?;
653        validate_url_scheme(&parsed)?;
654        self.indexer_url = Some(parsed);
655        Ok(self)
656    }
657
658    /// Sets a custom faucet URL.
659    ///
660    /// # Security
661    ///
662    /// Only `http://` and `https://` URL schemes are allowed.
663    ///
664    /// # Errors
665    ///
666    /// Returns an error if the `url` cannot be parsed as a valid URL
667    /// or uses an unsupported scheme.
668    pub fn with_faucet_url(mut self, url: &str) -> AptosResult<Self> {
669        let parsed = Url::parse(url)?;
670        validate_url_scheme(&parsed)?;
671        self.faucet_url = Some(parsed);
672        Ok(self)
673    }
674
675    /// Returns the network this config is for.
676    pub fn network(&self) -> Network {
677        self.network
678    }
679
680    /// Returns the fullnode URL.
681    pub fn fullnode_url(&self) -> &Url {
682        &self.fullnode_url
683    }
684
685    /// Returns the indexer URL, if configured.
686    pub fn indexer_url(&self) -> Option<&Url> {
687        self.indexer_url.as_ref()
688    }
689
690    /// Returns the faucet URL, if configured.
691    pub fn faucet_url(&self) -> Option<&Url> {
692        self.faucet_url.as_ref()
693    }
694
695    /// Returns the chain ID for this configuration.
696    pub fn chain_id(&self) -> ChainId {
697        self.network.chain_id()
698    }
699
700    /// Returns the retry configuration.
701    pub fn retry_config(&self) -> &RetryConfig {
702        &self.retry_config
703    }
704
705    /// Returns the request timeout.
706    pub fn timeout(&self) -> Duration {
707        self.timeout
708    }
709
710    /// Returns the connection pool configuration.
711    pub fn pool_config(&self) -> &PoolConfig {
712        &self.pool_config
713    }
714}
715
716#[cfg(test)]
717mod tests {
718    use super::*;
719
720    #[test]
721    fn test_mainnet_config() {
722        let config = AptosConfig::mainnet();
723        assert_eq!(config.network(), Network::Mainnet);
724        assert!(config.fullnode_url().as_str().contains("mainnet"));
725        assert!(config.faucet_url().is_none());
726    }
727
728    #[test]
729    fn test_testnet_config() {
730        let config = AptosConfig::testnet();
731        assert_eq!(config.network(), Network::Testnet);
732        assert!(config.fullnode_url().as_str().contains("testnet"));
733        assert!(config.faucet_url().is_some());
734    }
735
736    #[test]
737    fn test_devnet_config() {
738        let config = AptosConfig::devnet();
739        assert_eq!(config.network(), Network::Devnet);
740        assert!(config.fullnode_url().as_str().contains("devnet"));
741        assert!(config.faucet_url().is_some());
742        assert!(config.indexer_url().is_some());
743    }
744
745    #[test]
746    fn test_local_config() {
747        let config = AptosConfig::local();
748        assert_eq!(config.network(), Network::Local);
749        assert!(config.fullnode_url().as_str().contains("127.0.0.1"));
750        assert!(config.faucet_url().is_some());
751        assert!(config.indexer_url().is_none());
752    }
753
754    #[test]
755    fn test_custom_config() {
756        let config = AptosConfig::custom("https://custom.example.com/v1").unwrap();
757        assert_eq!(config.network(), Network::Custom);
758        assert_eq!(
759            config.fullnode_url().as_str(),
760            "https://custom.example.com/v1"
761        );
762    }
763
764    #[test]
765    fn test_custom_config_invalid_url() {
766        let result = AptosConfig::custom("not a valid url");
767        assert!(result.is_err());
768    }
769
770    #[test]
771    fn test_builder_methods() {
772        let config = AptosConfig::testnet()
773            .with_timeout(Duration::from_mins(1))
774            .with_max_retries(5)
775            .with_api_key("test-key");
776
777        assert_eq!(config.timeout, Duration::from_mins(1));
778        assert_eq!(config.retry_config.max_retries, 5);
779        assert_eq!(config.api_key, Some("test-key".to_string()));
780    }
781
782    #[test]
783    fn test_retry_config() {
784        let config = AptosConfig::testnet().with_retry(RetryConfig::aggressive());
785
786        assert_eq!(config.retry_config.max_retries, 5);
787        assert_eq!(config.retry_config.initial_delay_ms, 50);
788
789        let config = AptosConfig::testnet().without_retry();
790        assert_eq!(config.retry_config.max_retries, 0);
791    }
792
793    #[test]
794    fn test_network_retry_defaults() {
795        // Mainnet should be conservative
796        let mainnet = AptosConfig::mainnet();
797        assert_eq!(mainnet.retry_config.max_retries, 3);
798
799        // Local should be aggressive
800        let local = AptosConfig::local();
801        assert_eq!(local.retry_config.max_retries, 5);
802    }
803
804    #[test]
805    fn test_pool_config_default() {
806        let config = PoolConfig::default();
807        assert_eq!(config.max_idle_total, 100);
808        assert_eq!(config.idle_timeout, Duration::from_secs(90));
809        assert!(config.tcp_nodelay);
810    }
811
812    #[test]
813    fn test_pool_config_presets() {
814        let high = PoolConfig::high_throughput();
815        assert_eq!(high.max_idle_per_host, Some(32));
816        assert_eq!(high.max_idle_total, 256);
817
818        let low = PoolConfig::low_latency();
819        assert_eq!(low.max_idle_per_host, Some(8));
820        assert_eq!(low.idle_timeout, Duration::from_secs(30));
821
822        let minimal = PoolConfig::minimal();
823        assert_eq!(minimal.max_idle_per_host, Some(2));
824        assert_eq!(minimal.max_idle_total, 8);
825    }
826
827    #[test]
828    fn test_pool_config_builder() {
829        let config = PoolConfig::builder()
830            .max_idle_per_host(16)
831            .max_idle_total(64)
832            .idle_timeout(Duration::from_mins(1))
833            .tcp_nodelay(false)
834            .build();
835
836        assert_eq!(config.max_idle_per_host, Some(16));
837        assert_eq!(config.max_idle_total, 64);
838        assert_eq!(config.idle_timeout, Duration::from_mins(1));
839        assert!(!config.tcp_nodelay);
840    }
841
842    #[test]
843    fn test_pool_config_builder_tcp_keepalive() {
844        let config = PoolConfig::builder()
845            .tcp_keepalive(Duration::from_secs(30))
846            .build();
847        assert_eq!(config.tcp_keepalive, Some(Duration::from_secs(30)));
848
849        let config = PoolConfig::builder().no_tcp_keepalive().build();
850        assert_eq!(config.tcp_keepalive, None);
851    }
852
853    #[test]
854    fn test_pool_config_builder_unlimited_idle() {
855        let config = PoolConfig::builder().unlimited_idle_per_host().build();
856        assert_eq!(config.max_idle_per_host, None);
857    }
858
859    #[test]
860    fn test_aptos_config_with_pool() {
861        let config = AptosConfig::testnet().with_pool(PoolConfig::high_throughput());
862
863        assert_eq!(config.pool_config.max_idle_total, 256);
864    }
865
866    #[test]
867    fn test_aptos_config_with_indexer_url() {
868        let config = AptosConfig::testnet()
869            .with_indexer_url("https://custom-indexer.example.com/graphql")
870            .unwrap();
871        assert_eq!(
872            config.indexer_url().unwrap().as_str(),
873            "https://custom-indexer.example.com/graphql"
874        );
875    }
876
877    #[test]
878    fn test_aptos_config_with_faucet_url() {
879        let config = AptosConfig::mainnet()
880            .with_faucet_url("https://custom-faucet.example.com")
881            .unwrap();
882        assert_eq!(
883            config.faucet_url().unwrap().as_str(),
884            "https://custom-faucet.example.com/"
885        );
886    }
887
888    #[test]
889    fn test_aptos_config_default() {
890        let config = AptosConfig::default();
891        assert_eq!(config.network(), Network::Devnet);
892    }
893
894    #[test]
895    fn test_network_chain_id() {
896        assert_eq!(Network::Mainnet.chain_id().id(), 1);
897        assert_eq!(Network::Testnet.chain_id().id(), 2);
898        // Devnet chain ID is reported as 0 (unknown); see Network::chain_id
899        // doc comment. The SDK queries the fullnode to resolve the live ID.
900        assert_eq!(Network::Devnet.chain_id().id(), 0);
901        assert_eq!(Network::Local.chain_id().id(), 4);
902        assert_eq!(Network::Custom.chain_id().id(), 0);
903    }
904
905    #[test]
906    fn test_network_as_str() {
907        assert_eq!(Network::Mainnet.as_str(), "mainnet");
908        assert_eq!(Network::Testnet.as_str(), "testnet");
909        assert_eq!(Network::Devnet.as_str(), "devnet");
910        assert_eq!(Network::Local.as_str(), "local");
911        assert_eq!(Network::Custom.as_str(), "custom");
912    }
913
914    #[test]
915    fn test_network_keyless_service_urls() {
916        assert_eq!(
917            Network::Mainnet.pepper_url(),
918            Some("https://api.mainnet.aptoslabs.com/keyless/pepper/v0")
919        );
920        assert_eq!(
921            Network::Testnet.pepper_url(),
922            Some("https://api.testnet.aptoslabs.com/keyless/pepper/v0")
923        );
924        assert_eq!(
925            Network::Devnet.pepper_url(),
926            Some("https://api.devnet.aptoslabs.com/keyless/pepper/v0")
927        );
928        assert_eq!(
929            Network::Local.pepper_url(),
930            Some("https://api.devnet.aptoslabs.com/keyless/pepper/v0")
931        );
932        assert_eq!(Network::Custom.pepper_url(), None);
933
934        assert_eq!(
935            Network::Mainnet.prover_url(),
936            Some("https://api.mainnet.aptoslabs.com/keyless/prover/v0")
937        );
938        assert_eq!(
939            Network::Testnet.prover_url(),
940            Some("https://api.testnet.aptoslabs.com/keyless/prover/v0")
941        );
942        assert_eq!(
943            Network::Devnet.prover_url(),
944            Some("https://api.devnet.aptoslabs.com/keyless/prover/v0")
945        );
946        assert_eq!(
947            Network::Local.prover_url(),
948            Some("https://api.devnet.aptoslabs.com/keyless/prover/v0")
949        );
950        assert_eq!(Network::Custom.prover_url(), None);
951    }
952
953    #[test]
954    fn test_aptos_config_getters() {
955        let config = AptosConfig::testnet();
956
957        assert_eq!(config.timeout(), Duration::from_secs(30));
958        assert!(config.retry_config().max_retries > 0);
959        assert!(config.pool_config().max_idle_total > 0);
960        assert_eq!(config.chain_id().id(), 2);
961    }
962
963    #[tokio::test]
964    async fn test_read_response_bounded_normal() {
965        use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method};
966        let server = MockServer::start().await;
967        Mock::given(method("GET"))
968            .respond_with(ResponseTemplate::new(200).set_body_string("hello world"))
969            .mount(&server)
970            .await;
971
972        let response = reqwest::get(server.uri()).await.unwrap();
973        let body = read_response_bounded(response, 1024).await.unwrap();
974        assert_eq!(body, b"hello world");
975    }
976
977    #[tokio::test]
978    async fn test_read_response_bounded_rejects_oversized_content_length() {
979        use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method};
980        let server = MockServer::start().await;
981        // Send a body whose accurate Content-Length exceeds the limit.
982        // The function should reject based on Content-Length pre-check
983        // before streaming the full body.
984        let body = "x".repeat(200);
985        Mock::given(method("GET"))
986            .respond_with(ResponseTemplate::new(200).set_body_string(body))
987            .mount(&server)
988            .await;
989
990        let response = reqwest::get(server.uri()).await.unwrap();
991        // Limit is 100 but body is 200 -- should be rejected via Content-Length pre-check
992        let result = read_response_bounded(response, 100).await;
993        assert!(result.is_err());
994        let err = result.unwrap_err().to_string();
995        assert!(err.contains("response too large"));
996    }
997
998    #[tokio::test]
999    async fn test_read_response_bounded_rejects_oversized_body() {
1000        use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method};
1001        let server = MockServer::start().await;
1002        let large_body = "x".repeat(500);
1003        Mock::given(method("GET"))
1004            .respond_with(ResponseTemplate::new(200).set_body_string(large_body))
1005            .mount(&server)
1006            .await;
1007
1008        let response = reqwest::get(server.uri()).await.unwrap();
1009        let result = read_response_bounded(response, 100).await;
1010        assert!(result.is_err());
1011    }
1012
1013    #[tokio::test]
1014    async fn test_read_response_bounded_exact_limit() {
1015        use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method};
1016        let server = MockServer::start().await;
1017        let body = "x".repeat(100);
1018        Mock::given(method("GET"))
1019            .respond_with(ResponseTemplate::new(200).set_body_string(body.clone()))
1020            .mount(&server)
1021            .await;
1022
1023        let response = reqwest::get(server.uri()).await.unwrap();
1024        let result = read_response_bounded(response, 100).await.unwrap();
1025        assert_eq!(result.len(), 100);
1026    }
1027
1028    #[tokio::test]
1029    async fn test_read_response_bounded_empty() {
1030        use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method};
1031        let server = MockServer::start().await;
1032        Mock::given(method("GET"))
1033            .respond_with(ResponseTemplate::new(200))
1034            .mount(&server)
1035            .await;
1036
1037        let response = reqwest::get(server.uri()).await.unwrap();
1038        let result = read_response_bounded(response, 1024).await.unwrap();
1039        assert!(result.is_empty());
1040    }
1041}