1use crate::error::{AptosError, AptosResult};
7use crate::retry::RetryConfig;
8use crate::types::ChainId;
9use std::time::Duration;
10use url::Url;
11
12pub fn validate_url_scheme(url: &Url) -> AptosResult<()> {
24 match url.scheme() {
25 "https" => Ok(()),
26 "http" => {
27 Ok(())
29 }
30 scheme => Err(AptosError::Config(format!(
31 "unsupported URL scheme '{scheme}': only 'http' and 'https' are allowed"
32 ))),
33 }
34}
35
36pub async fn read_response_bounded(
53 response: reqwest::Response,
54 max_size: usize,
55) -> AptosResult<Vec<u8>> {
56 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 #[cfg(not(target_arch = "wasm32"))]
73 {
74 let mut body = Vec::with_capacity(std::cmp::min(max_size, 1024 * 1024));
75 let mut response = response;
76 while let Some(chunk) = response.chunk().await? {
77 if body.len().saturating_add(chunk.len()) > max_size {
78 return Err(AptosError::Api {
79 status_code: response.status().as_u16(),
80 message: format!(
81 "response too large: exceeded limit of {max_size} bytes during streaming"
82 ),
83 error_code: Some("RESPONSE_TOO_LARGE".into()),
84 vm_error_code: None,
85 });
86 }
87 body.extend_from_slice(&chunk);
88 }
89 Ok(body)
90 }
91
92 #[cfg(target_arch = "wasm32")]
96 {
97 let status_code = response.status().as_u16();
98 let body = response.bytes().await?.to_vec();
99 if body.len() > max_size {
100 return Err(AptosError::Api {
101 status_code,
102 message: format!(
103 "response too large: exceeded limit of {max_size} bytes during streaming"
104 ),
105 error_code: Some("RESPONSE_TOO_LARGE".into()),
106 vm_error_code: None,
107 });
108 }
109 Ok(body)
110 }
111}
112
113#[derive(Debug, Clone)]
117pub struct PoolConfig {
118 pub max_idle_per_host: Option<usize>,
121 pub max_idle_total: usize,
124 pub idle_timeout: Duration,
127 pub tcp_keepalive: Option<Duration>,
130 pub tcp_nodelay: bool,
133 pub max_response_size: usize,
141}
142
143const DEFAULT_MAX_RESPONSE_SIZE: usize = 10 * 1024 * 1024;
153
154impl Default for PoolConfig {
155 fn default() -> Self {
156 Self {
157 max_idle_per_host: None, max_idle_total: 100,
159 idle_timeout: Duration::from_secs(90),
160 tcp_keepalive: Some(Duration::from_mins(1)),
161 tcp_nodelay: true,
162 max_response_size: DEFAULT_MAX_RESPONSE_SIZE,
163 }
164 }
165}
166
167impl PoolConfig {
168 pub fn builder() -> PoolConfigBuilder {
170 PoolConfigBuilder::default()
171 }
172
173 pub fn high_throughput() -> Self {
179 Self {
180 max_idle_per_host: Some(32),
181 max_idle_total: 256,
182 idle_timeout: Duration::from_mins(5),
183 tcp_keepalive: Some(Duration::from_secs(30)),
184 tcp_nodelay: true,
185 max_response_size: DEFAULT_MAX_RESPONSE_SIZE,
186 }
187 }
188
189 pub fn low_latency() -> Self {
195 Self {
196 max_idle_per_host: Some(8),
197 max_idle_total: 32,
198 idle_timeout: Duration::from_secs(30),
199 tcp_keepalive: Some(Duration::from_secs(15)),
200 tcp_nodelay: true,
201 max_response_size: DEFAULT_MAX_RESPONSE_SIZE,
202 }
203 }
204
205 pub fn minimal() -> Self {
210 Self {
211 max_idle_per_host: Some(2),
212 max_idle_total: 8,
213 idle_timeout: Duration::from_secs(10),
214 tcp_keepalive: None,
215 tcp_nodelay: true,
216 max_response_size: DEFAULT_MAX_RESPONSE_SIZE,
217 }
218 }
219}
220
221#[derive(Debug, Clone, Default)]
223#[allow(clippy::option_option)] pub struct PoolConfigBuilder {
225 max_idle_per_host: Option<usize>,
226 max_idle_total: Option<usize>,
227 idle_timeout: Option<Duration>,
228 tcp_keepalive: Option<Option<Duration>>,
230 tcp_nodelay: Option<bool>,
231 max_response_size: Option<usize>,
232}
233
234impl PoolConfigBuilder {
235 #[must_use]
237 pub fn max_idle_per_host(mut self, max: usize) -> Self {
238 self.max_idle_per_host = Some(max);
239 self
240 }
241
242 #[must_use]
244 pub fn unlimited_idle_per_host(mut self) -> Self {
245 self.max_idle_per_host = None;
246 self
247 }
248
249 #[must_use]
251 pub fn max_idle_total(mut self, max: usize) -> Self {
252 self.max_idle_total = Some(max);
253 self
254 }
255
256 #[must_use]
258 pub fn idle_timeout(mut self, timeout: Duration) -> Self {
259 self.idle_timeout = Some(timeout);
260 self
261 }
262
263 #[must_use]
265 pub fn tcp_keepalive(mut self, interval: Duration) -> Self {
266 self.tcp_keepalive = Some(Some(interval));
267 self
268 }
269
270 #[must_use]
272 pub fn no_tcp_keepalive(mut self) -> Self {
273 self.tcp_keepalive = Some(None);
274 self
275 }
276
277 #[must_use]
279 pub fn tcp_nodelay(mut self, enabled: bool) -> Self {
280 self.tcp_nodelay = Some(enabled);
281 self
282 }
283
284 #[must_use]
290 pub fn max_response_size(mut self, size: usize) -> Self {
291 self.max_response_size = Some(size);
292 self
293 }
294
295 pub fn build(self) -> PoolConfig {
297 let default = PoolConfig::default();
298 PoolConfig {
299 max_idle_per_host: self.max_idle_per_host.or(default.max_idle_per_host),
300 max_idle_total: self.max_idle_total.unwrap_or(default.max_idle_total),
301 idle_timeout: self.idle_timeout.unwrap_or(default.idle_timeout),
302 tcp_keepalive: self.tcp_keepalive.unwrap_or(default.tcp_keepalive),
303 tcp_nodelay: self.tcp_nodelay.unwrap_or(default.tcp_nodelay),
304 max_response_size: self.max_response_size.unwrap_or(default.max_response_size),
305 }
306 }
307}
308
309#[derive(Debug, Clone)]
332pub struct AptosConfig {
333 pub(crate) network: Network,
335 pub(crate) fullnode_url: Url,
337 pub(crate) indexer_url: Option<Url>,
339 pub(crate) faucet_url: Option<Url>,
341 pub(crate) timeout: Duration,
343 pub(crate) retry_config: RetryConfig,
345 pub(crate) pool_config: PoolConfig,
347 pub(crate) api_key: Option<String>,
349}
350
351#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
353pub enum Network {
354 Mainnet,
356 Testnet,
358 Devnet,
360 Local,
362 Custom,
364}
365
366impl Network {
367 pub fn chain_id(&self) -> ChainId {
374 match self {
375 Network::Mainnet => ChainId::mainnet(),
376 Network::Testnet => ChainId::testnet(),
377 Network::Devnet => ChainId::new(0),
378 Network::Local => ChainId::new(4),
379 Network::Custom => ChainId::new(0),
380 }
381 }
382
383 pub fn as_str(&self) -> &'static str {
385 match self {
386 Network::Mainnet => "mainnet",
387 Network::Testnet => "testnet",
388 Network::Devnet => "devnet",
389 Network::Local => "local",
390 Network::Custom => "custom",
391 }
392 }
393}
394
395impl Default for AptosConfig {
396 fn default() -> Self {
397 Self::devnet()
398 }
399}
400
401impl AptosConfig {
402 #[allow(clippy::missing_panics_doc)]
412 #[must_use]
413 pub fn mainnet() -> Self {
414 Self {
415 network: Network::Mainnet,
416 fullnode_url: Url::parse("https://fullnode.mainnet.aptoslabs.com/v1")
417 .expect("valid mainnet URL"),
418 indexer_url: Some(
419 Url::parse("https://indexer.mainnet.aptoslabs.com/v1/graphql")
420 .expect("valid indexer URL"),
421 ),
422 faucet_url: None, timeout: Duration::from_secs(30),
424 retry_config: RetryConfig::conservative(), pool_config: PoolConfig::default(),
426 api_key: None,
427 }
428 }
429
430 #[allow(clippy::missing_panics_doc)]
440 #[must_use]
441 pub fn testnet() -> Self {
442 Self {
443 network: Network::Testnet,
444 fullnode_url: Url::parse("https://fullnode.testnet.aptoslabs.com/v1")
445 .expect("valid testnet URL"),
446 indexer_url: Some(
447 Url::parse("https://indexer.testnet.aptoslabs.com/v1/graphql")
448 .expect("valid indexer URL"),
449 ),
450 faucet_url: Some(
451 Url::parse("https://faucet.testnet.aptoslabs.com").expect("valid faucet URL"),
452 ),
453 timeout: Duration::from_secs(30),
454 retry_config: RetryConfig::default(),
455 pool_config: PoolConfig::default(),
456 api_key: None,
457 }
458 }
459
460 #[allow(clippy::missing_panics_doc)]
470 #[must_use]
471 pub fn devnet() -> Self {
472 Self {
473 network: Network::Devnet,
474 fullnode_url: Url::parse("https://fullnode.devnet.aptoslabs.com/v1")
475 .expect("valid devnet URL"),
476 indexer_url: Some(
477 Url::parse("https://indexer.devnet.aptoslabs.com/v1/graphql")
478 .expect("valid indexer URL"),
479 ),
480 faucet_url: Some(
481 Url::parse("https://faucet.devnet.aptoslabs.com").expect("valid faucet URL"),
482 ),
483 timeout: Duration::from_secs(30),
484 retry_config: RetryConfig::default(),
485 pool_config: PoolConfig::default(),
486 api_key: None,
487 }
488 }
489
490 #[allow(clippy::missing_panics_doc)]
503 #[must_use]
504 pub fn local() -> Self {
505 Self {
506 network: Network::Local,
507 fullnode_url: Url::parse("http://127.0.0.1:8080/v1").expect("valid local URL"),
508 indexer_url: None,
509 faucet_url: Some(Url::parse("http://127.0.0.1:8081").expect("valid local faucet URL")),
510 timeout: Duration::from_secs(10),
511 retry_config: RetryConfig::aggressive(), pool_config: PoolConfig::low_latency(), api_key: None,
514 }
515 }
516
517 pub fn custom(fullnode_url: &str) -> AptosResult<Self> {
538 let url = Url::parse(fullnode_url)?;
539 validate_url_scheme(&url)?;
540 Ok(Self {
541 network: Network::Custom,
542 fullnode_url: url,
543 indexer_url: None,
544 faucet_url: None,
545 timeout: Duration::from_secs(30),
546 retry_config: RetryConfig::default(),
547 pool_config: PoolConfig::default(),
548 api_key: None,
549 })
550 }
551
552 #[must_use]
554 pub fn with_timeout(mut self, timeout: Duration) -> Self {
555 self.timeout = timeout;
556 self
557 }
558
559 #[must_use]
571 pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
572 self.retry_config = retry_config;
573 self
574 }
575
576 #[must_use]
580 pub fn without_retry(mut self) -> Self {
581 self.retry_config = RetryConfig::no_retry();
582 self
583 }
584
585 #[must_use]
592 pub fn with_max_retries(mut self, max_retries: u32) -> Self {
593 self.retry_config.max_retries = max_retries;
594 self
595 }
596
597 #[must_use]
609 pub fn with_pool(mut self, pool_config: PoolConfig) -> Self {
610 self.pool_config = pool_config;
611 self
612 }
613
614 #[must_use]
619 pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
620 self.api_key = Some(api_key.into());
621 self
622 }
623
624 pub fn with_indexer_url(mut self, url: &str) -> AptosResult<Self> {
635 let parsed = Url::parse(url)?;
636 validate_url_scheme(&parsed)?;
637 self.indexer_url = Some(parsed);
638 Ok(self)
639 }
640
641 pub fn with_faucet_url(mut self, url: &str) -> AptosResult<Self> {
652 let parsed = Url::parse(url)?;
653 validate_url_scheme(&parsed)?;
654 self.faucet_url = Some(parsed);
655 Ok(self)
656 }
657
658 pub fn network(&self) -> Network {
660 self.network
661 }
662
663 pub fn fullnode_url(&self) -> &Url {
665 &self.fullnode_url
666 }
667
668 pub fn indexer_url(&self) -> Option<&Url> {
670 self.indexer_url.as_ref()
671 }
672
673 pub fn faucet_url(&self) -> Option<&Url> {
675 self.faucet_url.as_ref()
676 }
677
678 pub fn chain_id(&self) -> ChainId {
680 self.network.chain_id()
681 }
682
683 pub fn retry_config(&self) -> &RetryConfig {
685 &self.retry_config
686 }
687
688 pub fn timeout(&self) -> Duration {
690 self.timeout
691 }
692
693 pub fn pool_config(&self) -> &PoolConfig {
695 &self.pool_config
696 }
697}
698
699#[cfg(test)]
700mod tests {
701 use super::*;
702
703 #[test]
704 fn test_mainnet_config() {
705 let config = AptosConfig::mainnet();
706 assert_eq!(config.network(), Network::Mainnet);
707 assert!(config.fullnode_url().as_str().contains("mainnet"));
708 assert!(config.faucet_url().is_none());
709 }
710
711 #[test]
712 fn test_testnet_config() {
713 let config = AptosConfig::testnet();
714 assert_eq!(config.network(), Network::Testnet);
715 assert!(config.fullnode_url().as_str().contains("testnet"));
716 assert!(config.faucet_url().is_some());
717 }
718
719 #[test]
720 fn test_devnet_config() {
721 let config = AptosConfig::devnet();
722 assert_eq!(config.network(), Network::Devnet);
723 assert!(config.fullnode_url().as_str().contains("devnet"));
724 assert!(config.faucet_url().is_some());
725 assert!(config.indexer_url().is_some());
726 }
727
728 #[test]
729 fn test_local_config() {
730 let config = AptosConfig::local();
731 assert_eq!(config.network(), Network::Local);
732 assert!(config.fullnode_url().as_str().contains("127.0.0.1"));
733 assert!(config.faucet_url().is_some());
734 assert!(config.indexer_url().is_none());
735 }
736
737 #[test]
738 fn test_custom_config() {
739 let config = AptosConfig::custom("https://custom.example.com/v1").unwrap();
740 assert_eq!(config.network(), Network::Custom);
741 assert_eq!(
742 config.fullnode_url().as_str(),
743 "https://custom.example.com/v1"
744 );
745 }
746
747 #[test]
748 fn test_custom_config_invalid_url() {
749 let result = AptosConfig::custom("not a valid url");
750 assert!(result.is_err());
751 }
752
753 #[test]
754 fn test_builder_methods() {
755 let config = AptosConfig::testnet()
756 .with_timeout(Duration::from_mins(1))
757 .with_max_retries(5)
758 .with_api_key("test-key");
759
760 assert_eq!(config.timeout, Duration::from_mins(1));
761 assert_eq!(config.retry_config.max_retries, 5);
762 assert_eq!(config.api_key, Some("test-key".to_string()));
763 }
764
765 #[test]
766 fn test_with_max_retries_preserves_other_fields() {
767 let custom_retry = RetryConfig::builder()
769 .max_retries(2)
770 .initial_delay_ms(321)
771 .max_delay_ms(54_321)
772 .exponential_base(1.75)
773 .jitter(false)
774 .jitter_factor(0.125)
775 .retryable_status_codes([418, 599])
776 .build();
777
778 let config = AptosConfig::testnet()
779 .with_retry(custom_retry)
780 .with_max_retries(9);
781
782 let retry = config.retry_config();
783 assert_eq!(retry.max_retries, 9);
785 assert_eq!(retry.initial_delay_ms, 321);
787 assert_eq!(retry.max_delay_ms, 54_321);
788 assert!((retry.exponential_base - 1.75).abs() < f64::EPSILON);
789 assert!(!retry.jitter);
790 assert!((retry.jitter_factor - 0.125).abs() < f64::EPSILON);
791 assert!(retry.is_retryable_status(418));
792 assert!(retry.is_retryable_status(599));
793 assert!(!retry.is_retryable_status(500));
795 }
796
797 #[test]
798 fn test_retry_config() {
799 let config = AptosConfig::testnet().with_retry(RetryConfig::aggressive());
800
801 assert_eq!(config.retry_config.max_retries, 5);
802 assert_eq!(config.retry_config.initial_delay_ms, 50);
803
804 let config = AptosConfig::testnet().without_retry();
805 assert_eq!(config.retry_config.max_retries, 0);
806 }
807
808 #[test]
809 fn test_network_retry_defaults() {
810 let mainnet = AptosConfig::mainnet();
812 assert_eq!(mainnet.retry_config.max_retries, 3);
813
814 let local = AptosConfig::local();
816 assert_eq!(local.retry_config.max_retries, 5);
817 }
818
819 #[test]
820 fn test_pool_config_default() {
821 let config = PoolConfig::default();
822 assert_eq!(config.max_idle_total, 100);
823 assert_eq!(config.idle_timeout, Duration::from_secs(90));
824 assert!(config.tcp_nodelay);
825 }
826
827 #[test]
828 fn test_pool_config_presets() {
829 let high = PoolConfig::high_throughput();
830 assert_eq!(high.max_idle_per_host, Some(32));
831 assert_eq!(high.max_idle_total, 256);
832
833 let low = PoolConfig::low_latency();
834 assert_eq!(low.max_idle_per_host, Some(8));
835 assert_eq!(low.idle_timeout, Duration::from_secs(30));
836
837 let minimal = PoolConfig::minimal();
838 assert_eq!(minimal.max_idle_per_host, Some(2));
839 assert_eq!(minimal.max_idle_total, 8);
840 }
841
842 #[test]
843 fn test_pool_config_builder() {
844 let config = PoolConfig::builder()
845 .max_idle_per_host(16)
846 .max_idle_total(64)
847 .idle_timeout(Duration::from_mins(1))
848 .tcp_nodelay(false)
849 .build();
850
851 assert_eq!(config.max_idle_per_host, Some(16));
852 assert_eq!(config.max_idle_total, 64);
853 assert_eq!(config.idle_timeout, Duration::from_mins(1));
854 assert!(!config.tcp_nodelay);
855 }
856
857 #[test]
858 fn test_pool_config_builder_tcp_keepalive() {
859 let config = PoolConfig::builder()
860 .tcp_keepalive(Duration::from_secs(30))
861 .build();
862 assert_eq!(config.tcp_keepalive, Some(Duration::from_secs(30)));
863
864 let config = PoolConfig::builder().no_tcp_keepalive().build();
865 assert_eq!(config.tcp_keepalive, None);
866 }
867
868 #[test]
869 fn test_pool_config_builder_unlimited_idle() {
870 let config = PoolConfig::builder().unlimited_idle_per_host().build();
871 assert_eq!(config.max_idle_per_host, None);
872 }
873
874 #[test]
875 fn test_aptos_config_with_pool() {
876 let config = AptosConfig::testnet().with_pool(PoolConfig::high_throughput());
877
878 assert_eq!(config.pool_config.max_idle_total, 256);
879 }
880
881 #[test]
882 fn test_aptos_config_with_indexer_url() {
883 let config = AptosConfig::testnet()
884 .with_indexer_url("https://custom-indexer.example.com/graphql")
885 .unwrap();
886 assert_eq!(
887 config.indexer_url().unwrap().as_str(),
888 "https://custom-indexer.example.com/graphql"
889 );
890 }
891
892 #[test]
893 fn test_aptos_config_with_faucet_url() {
894 let config = AptosConfig::mainnet()
895 .with_faucet_url("https://custom-faucet.example.com")
896 .unwrap();
897 assert_eq!(
898 config.faucet_url().unwrap().as_str(),
899 "https://custom-faucet.example.com/"
900 );
901 }
902
903 #[test]
904 fn test_aptos_config_default() {
905 let config = AptosConfig::default();
906 assert_eq!(config.network(), Network::Devnet);
907 }
908
909 #[test]
910 fn test_network_chain_id() {
911 assert_eq!(Network::Mainnet.chain_id().id(), 1);
912 assert_eq!(Network::Testnet.chain_id().id(), 2);
913 assert_eq!(Network::Devnet.chain_id().id(), 0);
916 assert_eq!(Network::Local.chain_id().id(), 4);
917 assert_eq!(Network::Custom.chain_id().id(), 0);
918 }
919
920 #[test]
921 fn test_network_as_str() {
922 assert_eq!(Network::Mainnet.as_str(), "mainnet");
923 assert_eq!(Network::Testnet.as_str(), "testnet");
924 assert_eq!(Network::Devnet.as_str(), "devnet");
925 assert_eq!(Network::Local.as_str(), "local");
926 assert_eq!(Network::Custom.as_str(), "custom");
927 }
928
929 #[test]
930 fn test_aptos_config_getters() {
931 let config = AptosConfig::testnet();
932
933 assert_eq!(config.timeout(), Duration::from_secs(30));
934 assert!(config.retry_config().max_retries > 0);
935 assert!(config.pool_config().max_idle_total > 0);
936 assert_eq!(config.chain_id().id(), 2);
937 }
938
939 #[tokio::test]
940 async fn test_read_response_bounded_normal() {
941 use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method};
942 let server = MockServer::start().await;
943 Mock::given(method("GET"))
944 .respond_with(ResponseTemplate::new(200).set_body_string("hello world"))
945 .mount(&server)
946 .await;
947
948 let response = reqwest::get(server.uri()).await.unwrap();
949 let body = read_response_bounded(response, 1024).await.unwrap();
950 assert_eq!(body, b"hello world");
951 }
952
953 #[tokio::test]
954 async fn test_read_response_bounded_rejects_oversized_content_length() {
955 use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method};
956 let server = MockServer::start().await;
957 let body = "x".repeat(200);
961 Mock::given(method("GET"))
962 .respond_with(ResponseTemplate::new(200).set_body_string(body))
963 .mount(&server)
964 .await;
965
966 let response = reqwest::get(server.uri()).await.unwrap();
967 let result = read_response_bounded(response, 100).await;
969 assert!(result.is_err());
970 let err = result.unwrap_err().to_string();
971 assert!(err.contains("response too large"));
972 }
973
974 #[tokio::test]
975 async fn test_read_response_bounded_rejects_oversized_body() {
976 use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method};
977 let server = MockServer::start().await;
978 let large_body = "x".repeat(500);
979 Mock::given(method("GET"))
980 .respond_with(ResponseTemplate::new(200).set_body_string(large_body))
981 .mount(&server)
982 .await;
983
984 let response = reqwest::get(server.uri()).await.unwrap();
985 let result = read_response_bounded(response, 100).await;
986 assert!(result.is_err());
987 }
988
989 #[tokio::test]
990 async fn test_read_response_bounded_exact_limit() {
991 use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method};
992 let server = MockServer::start().await;
993 let body = "x".repeat(100);
994 Mock::given(method("GET"))
995 .respond_with(ResponseTemplate::new(200).set_body_string(body.clone()))
996 .mount(&server)
997 .await;
998
999 let response = reqwest::get(server.uri()).await.unwrap();
1000 let result = read_response_bounded(response, 100).await.unwrap();
1001 assert_eq!(result.len(), 100);
1002 }
1003
1004 #[tokio::test]
1005 async fn test_read_response_bounded_empty() {
1006 use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method};
1007 let server = MockServer::start().await;
1008 Mock::given(method("GET"))
1009 .respond_with(ResponseTemplate::new(200))
1010 .mount(&server)
1011 .await;
1012
1013 let response = reqwest::get(server.uri()).await.unwrap();
1014 let result = read_response_bounded(response, 1024).await.unwrap();
1015 assert!(result.is_empty());
1016 }
1017}