1use crate::error::{AptosError, AptosResult};
7use crate::retry::RetryConfig;
8use crate::types::ChainId;
9use std::time::Duration;
10use url::Url;
11
12fn validate_url_scheme(url: &Url) -> AptosResult<()> {
20 match url.scheme() {
21 "https" => Ok(()),
22 "http" => {
23 Ok(())
26 }
27 scheme => Err(AptosError::Config(format!(
28 "unsupported URL scheme '{scheme}': only 'http' and 'https' are allowed"
29 ))),
30 }
31}
32
33#[derive(Debug, Clone)]
37pub struct PoolConfig {
38 pub max_idle_per_host: Option<usize>,
41 pub max_idle_total: usize,
44 pub idle_timeout: Duration,
47 pub tcp_keepalive: Option<Duration>,
50 pub tcp_nodelay: bool,
53 pub max_response_size: usize,
61}
62
63const DEFAULT_MAX_RESPONSE_SIZE: usize = 100 * 1024 * 1024;
65
66impl Default for PoolConfig {
67 fn default() -> Self {
68 Self {
69 max_idle_per_host: None, max_idle_total: 100,
71 idle_timeout: Duration::from_secs(90),
72 tcp_keepalive: Some(Duration::from_secs(60)),
73 tcp_nodelay: true,
74 max_response_size: DEFAULT_MAX_RESPONSE_SIZE,
75 }
76 }
77}
78
79impl PoolConfig {
80 pub fn builder() -> PoolConfigBuilder {
82 PoolConfigBuilder::default()
83 }
84
85 pub fn high_throughput() -> Self {
91 Self {
92 max_idle_per_host: Some(32),
93 max_idle_total: 256,
94 idle_timeout: Duration::from_secs(300),
95 tcp_keepalive: Some(Duration::from_secs(30)),
96 tcp_nodelay: true,
97 max_response_size: DEFAULT_MAX_RESPONSE_SIZE,
98 }
99 }
100
101 pub fn low_latency() -> Self {
107 Self {
108 max_idle_per_host: Some(8),
109 max_idle_total: 32,
110 idle_timeout: Duration::from_secs(30),
111 tcp_keepalive: Some(Duration::from_secs(15)),
112 tcp_nodelay: true,
113 max_response_size: DEFAULT_MAX_RESPONSE_SIZE,
114 }
115 }
116
117 pub fn minimal() -> Self {
122 Self {
123 max_idle_per_host: Some(2),
124 max_idle_total: 8,
125 idle_timeout: Duration::from_secs(10),
126 tcp_keepalive: None,
127 tcp_nodelay: true,
128 max_response_size: DEFAULT_MAX_RESPONSE_SIZE,
129 }
130 }
131}
132
133#[derive(Debug, Clone, Default)]
135#[allow(clippy::option_option)] pub struct PoolConfigBuilder {
137 max_idle_per_host: Option<usize>,
138 max_idle_total: Option<usize>,
139 idle_timeout: Option<Duration>,
140 tcp_keepalive: Option<Option<Duration>>,
142 tcp_nodelay: Option<bool>,
143 max_response_size: Option<usize>,
144}
145
146impl PoolConfigBuilder {
147 #[must_use]
149 pub fn max_idle_per_host(mut self, max: usize) -> Self {
150 self.max_idle_per_host = Some(max);
151 self
152 }
153
154 #[must_use]
156 pub fn unlimited_idle_per_host(mut self) -> Self {
157 self.max_idle_per_host = None;
158 self
159 }
160
161 #[must_use]
163 pub fn max_idle_total(mut self, max: usize) -> Self {
164 self.max_idle_total = Some(max);
165 self
166 }
167
168 #[must_use]
170 pub fn idle_timeout(mut self, timeout: Duration) -> Self {
171 self.idle_timeout = Some(timeout);
172 self
173 }
174
175 #[must_use]
177 pub fn tcp_keepalive(mut self, interval: Duration) -> Self {
178 self.tcp_keepalive = Some(Some(interval));
179 self
180 }
181
182 #[must_use]
184 pub fn no_tcp_keepalive(mut self) -> Self {
185 self.tcp_keepalive = Some(None);
186 self
187 }
188
189 #[must_use]
191 pub fn tcp_nodelay(mut self, enabled: bool) -> Self {
192 self.tcp_nodelay = Some(enabled);
193 self
194 }
195
196 #[must_use]
202 pub fn max_response_size(mut self, size: usize) -> Self {
203 self.max_response_size = Some(size);
204 self
205 }
206
207 pub fn build(self) -> PoolConfig {
209 let default = PoolConfig::default();
210 PoolConfig {
211 max_idle_per_host: self.max_idle_per_host.or(default.max_idle_per_host),
212 max_idle_total: self.max_idle_total.unwrap_or(default.max_idle_total),
213 idle_timeout: self.idle_timeout.unwrap_or(default.idle_timeout),
214 tcp_keepalive: self.tcp_keepalive.unwrap_or(default.tcp_keepalive),
215 tcp_nodelay: self.tcp_nodelay.unwrap_or(default.tcp_nodelay),
216 max_response_size: self.max_response_size.unwrap_or(default.max_response_size),
217 }
218 }
219}
220
221#[derive(Debug, Clone)]
244pub struct AptosConfig {
245 pub(crate) network: Network,
247 pub(crate) fullnode_url: Url,
249 pub(crate) indexer_url: Option<Url>,
251 pub(crate) faucet_url: Option<Url>,
253 pub(crate) timeout: Duration,
255 pub(crate) retry_config: RetryConfig,
257 pub(crate) pool_config: PoolConfig,
259 pub(crate) api_key: Option<String>,
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
265pub enum Network {
266 Mainnet,
268 Testnet,
270 Devnet,
272 Local,
274 Custom,
276}
277
278impl Network {
279 pub fn chain_id(&self) -> ChainId {
281 match self {
282 Network::Mainnet => ChainId::mainnet(),
283 Network::Testnet => ChainId::testnet(),
284 Network::Devnet => ChainId::new(165), Network::Local => ChainId::new(4), Network::Custom => ChainId::new(0), }
288 }
289
290 pub fn as_str(&self) -> &'static str {
292 match self {
293 Network::Mainnet => "mainnet",
294 Network::Testnet => "testnet",
295 Network::Devnet => "devnet",
296 Network::Local => "local",
297 Network::Custom => "custom",
298 }
299 }
300}
301
302impl Default for AptosConfig {
303 fn default() -> Self {
304 Self::devnet()
305 }
306}
307
308impl AptosConfig {
309 #[allow(clippy::missing_panics_doc)]
319 #[must_use]
320 pub fn mainnet() -> Self {
321 Self {
322 network: Network::Mainnet,
323 fullnode_url: Url::parse("https://fullnode.mainnet.aptoslabs.com/v1")
324 .expect("valid mainnet URL"),
325 indexer_url: Some(
326 Url::parse("https://indexer.mainnet.aptoslabs.com/v1/graphql")
327 .expect("valid indexer URL"),
328 ),
329 faucet_url: None, timeout: Duration::from_secs(30),
331 retry_config: RetryConfig::conservative(), pool_config: PoolConfig::default(),
333 api_key: None,
334 }
335 }
336
337 #[allow(clippy::missing_panics_doc)]
347 #[must_use]
348 pub fn testnet() -> Self {
349 Self {
350 network: Network::Testnet,
351 fullnode_url: Url::parse("https://fullnode.testnet.aptoslabs.com/v1")
352 .expect("valid testnet URL"),
353 indexer_url: Some(
354 Url::parse("https://indexer.testnet.aptoslabs.com/v1/graphql")
355 .expect("valid indexer URL"),
356 ),
357 faucet_url: Some(
358 Url::parse("https://faucet.testnet.aptoslabs.com").expect("valid faucet URL"),
359 ),
360 timeout: Duration::from_secs(30),
361 retry_config: RetryConfig::default(),
362 pool_config: PoolConfig::default(),
363 api_key: None,
364 }
365 }
366
367 #[allow(clippy::missing_panics_doc)]
377 #[must_use]
378 pub fn devnet() -> Self {
379 Self {
380 network: Network::Devnet,
381 fullnode_url: Url::parse("https://fullnode.devnet.aptoslabs.com/v1")
382 .expect("valid devnet URL"),
383 indexer_url: Some(
384 Url::parse("https://indexer.devnet.aptoslabs.com/v1/graphql")
385 .expect("valid indexer URL"),
386 ),
387 faucet_url: Some(
388 Url::parse("https://faucet.devnet.aptoslabs.com").expect("valid faucet URL"),
389 ),
390 timeout: Duration::from_secs(30),
391 retry_config: RetryConfig::default(),
392 pool_config: PoolConfig::default(),
393 api_key: None,
394 }
395 }
396
397 #[allow(clippy::missing_panics_doc)]
410 #[must_use]
411 pub fn local() -> Self {
412 Self {
413 network: Network::Local,
414 fullnode_url: Url::parse("http://127.0.0.1:8080/v1").expect("valid local URL"),
415 indexer_url: None,
416 faucet_url: Some(Url::parse("http://127.0.0.1:8081").expect("valid local faucet URL")),
417 timeout: Duration::from_secs(10),
418 retry_config: RetryConfig::aggressive(), pool_config: PoolConfig::low_latency(), api_key: None,
421 }
422 }
423
424 pub fn custom(fullnode_url: &str) -> AptosResult<Self> {
445 let url = Url::parse(fullnode_url)?;
446 validate_url_scheme(&url)?;
447 Ok(Self {
448 network: Network::Custom,
449 fullnode_url: url,
450 indexer_url: None,
451 faucet_url: None,
452 timeout: Duration::from_secs(30),
453 retry_config: RetryConfig::default(),
454 pool_config: PoolConfig::default(),
455 api_key: None,
456 })
457 }
458
459 #[must_use]
461 pub fn with_timeout(mut self, timeout: Duration) -> Self {
462 self.timeout = timeout;
463 self
464 }
465
466 #[must_use]
478 pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
479 self.retry_config = retry_config;
480 self
481 }
482
483 #[must_use]
487 pub fn without_retry(mut self) -> Self {
488 self.retry_config = RetryConfig::no_retry();
489 self
490 }
491
492 #[must_use]
496 pub fn with_max_retries(mut self, max_retries: u32) -> Self {
497 self.retry_config = RetryConfig::builder()
498 .max_retries(max_retries)
499 .initial_delay_ms(self.retry_config.initial_delay_ms)
500 .max_delay_ms(self.retry_config.max_delay_ms)
501 .exponential_base(self.retry_config.exponential_base)
502 .jitter(self.retry_config.jitter)
503 .build();
504 self
505 }
506
507 #[must_use]
519 pub fn with_pool(mut self, pool_config: PoolConfig) -> Self {
520 self.pool_config = pool_config;
521 self
522 }
523
524 #[must_use]
529 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
530 self.api_key = Some(api_key.into());
531 self
532 }
533
534 pub fn with_indexer_url(mut self, url: &str) -> AptosResult<Self> {
545 let parsed = Url::parse(url)?;
546 validate_url_scheme(&parsed)?;
547 self.indexer_url = Some(parsed);
548 Ok(self)
549 }
550
551 pub fn with_faucet_url(mut self, url: &str) -> AptosResult<Self> {
562 let parsed = Url::parse(url)?;
563 validate_url_scheme(&parsed)?;
564 self.faucet_url = Some(parsed);
565 Ok(self)
566 }
567
568 pub fn network(&self) -> Network {
570 self.network
571 }
572
573 pub fn fullnode_url(&self) -> &Url {
575 &self.fullnode_url
576 }
577
578 pub fn indexer_url(&self) -> Option<&Url> {
580 self.indexer_url.as_ref()
581 }
582
583 pub fn faucet_url(&self) -> Option<&Url> {
585 self.faucet_url.as_ref()
586 }
587
588 pub fn chain_id(&self) -> ChainId {
590 self.network.chain_id()
591 }
592
593 pub fn retry_config(&self) -> &RetryConfig {
595 &self.retry_config
596 }
597
598 pub fn timeout(&self) -> Duration {
600 self.timeout
601 }
602
603 pub fn pool_config(&self) -> &PoolConfig {
605 &self.pool_config
606 }
607}
608
609#[cfg(test)]
610mod tests {
611 use super::*;
612
613 #[test]
614 fn test_mainnet_config() {
615 let config = AptosConfig::mainnet();
616 assert_eq!(config.network(), Network::Mainnet);
617 assert!(config.fullnode_url().as_str().contains("mainnet"));
618 assert!(config.faucet_url().is_none());
619 }
620
621 #[test]
622 fn test_testnet_config() {
623 let config = AptosConfig::testnet();
624 assert_eq!(config.network(), Network::Testnet);
625 assert!(config.fullnode_url().as_str().contains("testnet"));
626 assert!(config.faucet_url().is_some());
627 }
628
629 #[test]
630 fn test_devnet_config() {
631 let config = AptosConfig::devnet();
632 assert_eq!(config.network(), Network::Devnet);
633 assert!(config.fullnode_url().as_str().contains("devnet"));
634 assert!(config.faucet_url().is_some());
635 assert!(config.indexer_url().is_some());
636 }
637
638 #[test]
639 fn test_local_config() {
640 let config = AptosConfig::local();
641 assert_eq!(config.network(), Network::Local);
642 assert!(config.fullnode_url().as_str().contains("127.0.0.1"));
643 assert!(config.faucet_url().is_some());
644 assert!(config.indexer_url().is_none());
645 }
646
647 #[test]
648 fn test_custom_config() {
649 let config = AptosConfig::custom("https://custom.example.com/v1").unwrap();
650 assert_eq!(config.network(), Network::Custom);
651 assert_eq!(
652 config.fullnode_url().as_str(),
653 "https://custom.example.com/v1"
654 );
655 }
656
657 #[test]
658 fn test_custom_config_invalid_url() {
659 let result = AptosConfig::custom("not a valid url");
660 assert!(result.is_err());
661 }
662
663 #[test]
664 fn test_builder_methods() {
665 let config = AptosConfig::testnet()
666 .with_timeout(Duration::from_secs(60))
667 .with_max_retries(5)
668 .with_api_key("test-key");
669
670 assert_eq!(config.timeout, Duration::from_secs(60));
671 assert_eq!(config.retry_config.max_retries, 5);
672 assert_eq!(config.api_key, Some("test-key".to_string()));
673 }
674
675 #[test]
676 fn test_retry_config() {
677 let config = AptosConfig::testnet().with_retry(RetryConfig::aggressive());
678
679 assert_eq!(config.retry_config.max_retries, 5);
680 assert_eq!(config.retry_config.initial_delay_ms, 50);
681
682 let config = AptosConfig::testnet().without_retry();
683 assert_eq!(config.retry_config.max_retries, 0);
684 }
685
686 #[test]
687 fn test_network_retry_defaults() {
688 let mainnet = AptosConfig::mainnet();
690 assert_eq!(mainnet.retry_config.max_retries, 3);
691
692 let local = AptosConfig::local();
694 assert_eq!(local.retry_config.max_retries, 5);
695 }
696
697 #[test]
698 fn test_pool_config_default() {
699 let config = PoolConfig::default();
700 assert_eq!(config.max_idle_total, 100);
701 assert_eq!(config.idle_timeout, Duration::from_secs(90));
702 assert!(config.tcp_nodelay);
703 }
704
705 #[test]
706 fn test_pool_config_presets() {
707 let high = PoolConfig::high_throughput();
708 assert_eq!(high.max_idle_per_host, Some(32));
709 assert_eq!(high.max_idle_total, 256);
710
711 let low = PoolConfig::low_latency();
712 assert_eq!(low.max_idle_per_host, Some(8));
713 assert_eq!(low.idle_timeout, Duration::from_secs(30));
714
715 let minimal = PoolConfig::minimal();
716 assert_eq!(minimal.max_idle_per_host, Some(2));
717 assert_eq!(minimal.max_idle_total, 8);
718 }
719
720 #[test]
721 fn test_pool_config_builder() {
722 let config = PoolConfig::builder()
723 .max_idle_per_host(16)
724 .max_idle_total(64)
725 .idle_timeout(Duration::from_secs(60))
726 .tcp_nodelay(false)
727 .build();
728
729 assert_eq!(config.max_idle_per_host, Some(16));
730 assert_eq!(config.max_idle_total, 64);
731 assert_eq!(config.idle_timeout, Duration::from_secs(60));
732 assert!(!config.tcp_nodelay);
733 }
734
735 #[test]
736 fn test_pool_config_builder_tcp_keepalive() {
737 let config = PoolConfig::builder()
738 .tcp_keepalive(Duration::from_secs(30))
739 .build();
740 assert_eq!(config.tcp_keepalive, Some(Duration::from_secs(30)));
741
742 let config = PoolConfig::builder().no_tcp_keepalive().build();
743 assert_eq!(config.tcp_keepalive, None);
744 }
745
746 #[test]
747 fn test_pool_config_builder_unlimited_idle() {
748 let config = PoolConfig::builder().unlimited_idle_per_host().build();
749 assert_eq!(config.max_idle_per_host, None);
750 }
751
752 #[test]
753 fn test_aptos_config_with_pool() {
754 let config = AptosConfig::testnet().with_pool(PoolConfig::high_throughput());
755
756 assert_eq!(config.pool_config.max_idle_total, 256);
757 }
758
759 #[test]
760 fn test_aptos_config_with_indexer_url() {
761 let config = AptosConfig::testnet()
762 .with_indexer_url("https://custom-indexer.example.com/graphql")
763 .unwrap();
764 assert_eq!(
765 config.indexer_url().unwrap().as_str(),
766 "https://custom-indexer.example.com/graphql"
767 );
768 }
769
770 #[test]
771 fn test_aptos_config_with_faucet_url() {
772 let config = AptosConfig::mainnet()
773 .with_faucet_url("https://custom-faucet.example.com")
774 .unwrap();
775 assert_eq!(
776 config.faucet_url().unwrap().as_str(),
777 "https://custom-faucet.example.com/"
778 );
779 }
780
781 #[test]
782 fn test_aptos_config_default() {
783 let config = AptosConfig::default();
784 assert_eq!(config.network(), Network::Devnet);
785 }
786
787 #[test]
788 fn test_network_chain_id() {
789 assert_eq!(Network::Mainnet.chain_id().id(), 1);
790 assert_eq!(Network::Testnet.chain_id().id(), 2);
791 assert_eq!(Network::Devnet.chain_id().id(), 165);
792 assert_eq!(Network::Local.chain_id().id(), 4);
793 assert_eq!(Network::Custom.chain_id().id(), 0);
794 }
795
796 #[test]
797 fn test_network_as_str() {
798 assert_eq!(Network::Mainnet.as_str(), "mainnet");
799 assert_eq!(Network::Testnet.as_str(), "testnet");
800 assert_eq!(Network::Devnet.as_str(), "devnet");
801 assert_eq!(Network::Local.as_str(), "local");
802 assert_eq!(Network::Custom.as_str(), "custom");
803 }
804
805 #[test]
806 fn test_aptos_config_getters() {
807 let config = AptosConfig::testnet();
808
809 assert_eq!(config.timeout(), Duration::from_secs(30));
810 assert!(config.retry_config().max_retries > 0);
811 assert!(config.pool_config().max_idle_total > 0);
812 assert_eq!(config.chain_id().id(), 2);
813 }
814}