1use std::{collections::BTreeSet, fmt::Display};
6
7use async_graphql::*;
8use iota_graphql_config::GraphQLConfig;
9use iota_indexer::db::DbUrl;
10use iota_names::config::IotaNamesConfig;
11use serde::{Deserialize, Serialize};
12use url::Url;
13
14use crate::{functional_group::FunctionalGroup, types::int::try_into_int};
15
16const DEFAULT_DB_URL: &str = "postgres://postgres:postgrespw@localhost:5432/iota_indexer";
17
18pub(crate) const DEFAULT_PAGE_SIZE: u32 = 20;
19pub(crate) const MAX_PAGE_SIZE: u32 = 50;
20
21#[GraphQLConfig]
23#[derive(Default)]
24pub struct ServerConfig {
25 pub service: ServiceConfig,
26 pub connection: ConnectionConfig,
27 pub internal_features: InternalFeatureConfig,
28 pub tx_exec_full_node: TxExecFullNodeConfig,
29 pub ide: Ide,
30 pub historic_fallback: HistoricFallbackOptions,
31}
32
33#[GraphQLConfig]
38#[derive(clap::Args, Clone, Eq, PartialEq)]
39pub struct ConnectionConfig {
40 #[arg(short, long, default_value_t = ConnectionConfig::default().port)]
42 pub port: u16,
43 #[arg(long, default_value_t = ConnectionConfig::default().host)]
45 pub host: String,
46 #[arg(short, long, default_value = DEFAULT_DB_URL)]
48 pub db_url: DbUrl,
49 #[arg(long, default_value_t = ConnectionConfig::default().db_pool_size)]
51 pub db_pool_size: u32,
52 #[arg(long, default_value_t = ConnectionConfig::default().prom_host)]
54 pub prom_host: String,
55 #[arg(long, default_value_t = ConnectionConfig::default().prom_port)]
57 pub prom_port: u16,
58 #[arg(long, default_value_t = ConnectionConfig::default().skip_migration_consistency_check)]
61 pub skip_migration_consistency_check: bool,
62 #[arg(
67 long,
68 default_value_t = ConnectionConfig::default().max_available_range,
69 env = "MAX_AVAILABLE_RANGE",
70 )]
71 pub max_available_range: u64,
72}
73
74#[GraphQLConfig]
77#[derive(clap::Args, Debug, Clone)]
78pub struct HistoricFallbackOptions {
79 #[arg(
80 long,
81 help = "Experimental: REST KV store URL for historic fallback. Depends on the iota-rest-kv API which is still being finalized."
82 )]
83 pub fallback_kv_url: Option<Url>,
84
85 #[arg(
86 long,
87 default_value_t = HistoricFallbackOptions::DEFAULT_MULTI_FETCH_BATCH_SIZE,
88 env = "FALLBACK_KV_MULTI_FETCH_BATCH_SIZE",
89 help = "Experimental: Maximum number of keys per batch request to fallback KV store."
90 )]
91 pub fallback_kv_multi_fetch_batch_size: usize,
92
93 #[arg(
94 long,
95 default_value_t = HistoricFallbackOptions::DEFAULT_CONCURRENT_FETCHES,
96 env = "FALLBACK_KV_CONCURRENT_FETCHES",
97 help = "Experimental: Maximum number of concurrent batch requests to fallback KV store."
98 )]
99 pub fallback_kv_concurrent_fetches: usize,
100
101 #[arg(
102 long,
103 default_value_t = HistoricFallbackOptions::DEFAULT_CACHE_SIZE,
104 env = "FALLBACK_KV_CACHE_SIZE",
105 help = "Experimental: Cache size for historic fallback."
106 )]
107 pub fallback_kv_cache_size: u64,
108}
109
110impl HistoricFallbackOptions {
111 pub const DEFAULT_MULTI_FETCH_BATCH_SIZE: usize = 100;
112 pub const DEFAULT_CONCURRENT_FETCHES: usize = 10;
113 pub const DEFAULT_CACHE_SIZE: u64 = 100_000;
114}
115
116impl Default for HistoricFallbackOptions {
117 fn default() -> Self {
118 Self {
119 fallback_kv_url: None,
120 fallback_kv_multi_fetch_batch_size: Self::DEFAULT_MULTI_FETCH_BATCH_SIZE,
121 fallback_kv_concurrent_fetches: Self::DEFAULT_CONCURRENT_FETCHES,
122 fallback_kv_cache_size: Self::DEFAULT_CACHE_SIZE,
123 }
124 }
125}
126
127#[GraphQLConfig]
131#[derive(Default)]
132pub struct ServiceConfig {
133 pub versions: Versions,
134 pub limits: Limits,
135 pub disabled_features: BTreeSet<FunctionalGroup>,
136 pub experiments: Experiments,
137 pub iota_names: IotaNamesConfig,
138 pub background_tasks: BackgroundTasksConfig,
139}
140
141#[GraphQLConfig]
142pub struct Versions {
143 versions: Vec<String>,
144}
145
146#[GraphQLConfig]
147pub struct Limits {
148 pub max_query_depth: u32,
150 pub max_query_nodes: u32,
152 pub max_output_nodes: u32,
154 pub max_tx_payload_size: u32,
158 pub max_query_payload_size: u32,
161 pub max_db_query_cost: u32,
165 pub default_page_size: u32,
168 pub max_page_size: u32,
170 pub mutation_timeout_ms: u32,
175 pub request_timeout_ms: u32,
179 pub max_type_argument_depth: u32,
182 pub max_type_argument_width: u32,
184 pub max_type_nodes: u32,
186 pub max_move_value_depth: u32,
188 pub max_transaction_ids: u32,
191 pub max_scan_limit: u32,
193}
194
195#[GraphQLConfig]
196#[derive(Copy)]
197pub struct BackgroundTasksConfig {
198 pub watermark_update_ms: u64,
201}
202
203#[derive(Copy, Clone, Debug)]
207pub struct Version {
208 pub year: &'static str,
210 pub month: &'static str,
212 pub patch: &'static str,
215 pub sha: &'static str,
217 pub full: &'static str,
221}
222
223impl Version {
224 pub fn for_testing() -> Self {
226 Self {
227 year: env!("CARGO_PKG_VERSION_MAJOR"),
228 month: env!("CARGO_PKG_VERSION_MINOR"),
229 patch: env!("CARGO_PKG_VERSION_PATCH"),
230 sha: "testing-no-sha",
231 full: const_str::concat!(
233 env!("CARGO_PKG_VERSION_MAJOR"),
234 ".",
235 env!("CARGO_PKG_VERSION_MINOR"),
236 ".",
237 env!("CARGO_PKG_VERSION_PATCH"),
238 "-testing-no-sha"
239 ),
240 }
241 }
242}
243
244#[GraphQLConfig]
245#[derive(clap::Args)]
246pub struct Ide {
247 #[arg(short, long, default_value_t = Ide::default().ide_title)]
249 pub(crate) ide_title: String,
250}
251
252#[GraphQLConfig]
253#[derive(Default)]
254pub struct Experiments {
255 #[cfg(test)]
258 test_flag: bool,
259}
260
261#[GraphQLConfig]
262pub struct InternalFeatureConfig {
263 pub(crate) query_limits_checker: bool,
264 pub(crate) directive_checker: bool,
265 pub(crate) feature_gate: bool,
266 pub(crate) logger: bool,
267 pub(crate) query_timeout: bool,
268 pub(crate) metrics: bool,
269 pub(crate) tracing: bool,
270 pub(crate) apollo_tracing: bool,
271 pub(crate) open_telemetry: bool,
272}
273
274#[GraphQLConfig]
275#[derive(clap::Args, Default)]
276pub struct TxExecFullNodeConfig {
277 #[arg(long)]
279 pub(crate) node_rpc_url: Option<String>,
280}
281
282#[Object]
284impl ServiceConfig {
285 async fn is_enabled(&self, feature: FunctionalGroup) -> bool {
287 !self.disabled_features.contains(&feature)
288 }
289
290 async fn available_versions(&self) -> Vec<String> {
292 self.versions.versions.clone()
293 }
294
295 async fn enabled_features(&self) -> Vec<FunctionalGroup> {
297 FunctionalGroup::all()
298 .iter()
299 .filter(|g| !self.disabled_features.contains(g))
300 .copied()
301 .collect()
302 }
303
304 pub async fn max_query_depth(&self) -> Result<i32> {
306 try_into_int(self.limits.max_query_depth).extend()
307 }
308
309 pub async fn max_query_nodes(&self) -> Result<i32> {
312 try_into_int(self.limits.max_query_nodes).extend()
313 }
314
315 pub async fn max_output_nodes(&self) -> Result<i32> {
328 try_into_int(self.limits.max_output_nodes).extend()
329 }
330
331 async fn max_db_query_cost(&self) -> Result<i32> {
335 try_into_int(self.limits.max_db_query_cost).extend()
336 }
337
338 async fn default_page_size(&self) -> Result<i32> {
340 try_into_int(self.limits.default_page_size).extend()
341 }
342
343 async fn max_page_size(&self) -> Result<i32> {
345 try_into_int(self.limits.max_page_size).extend()
346 }
347
348 async fn mutation_timeout_ms(&self) -> Result<i32> {
355 try_into_int(self.limits.mutation_timeout_ms).extend()
356 }
357
358 async fn request_timeout_ms(&self) -> Result<i32> {
361 try_into_int(self.limits.request_timeout_ms).extend()
362 }
363
364 async fn max_transaction_payload_size(&self) -> Result<i32> {
374 try_into_int(self.limits.max_tx_payload_size).extend()
375 }
376
377 async fn max_query_payload_size(&self) -> Result<i32> {
382 try_into_int(self.limits.max_query_payload_size).extend()
383 }
384
385 async fn max_type_argument_depth(&self) -> Result<i32> {
388 try_into_int(self.limits.max_type_argument_depth).extend()
389 }
390
391 async fn max_type_argument_width(&self) -> Result<i32> {
394 try_into_int(self.limits.max_type_argument_width).extend()
395 }
396
397 async fn max_type_nodes(&self) -> Result<i32> {
400 try_into_int(self.limits.max_type_nodes).extend()
401 }
402
403 async fn max_move_value_depth(&self) -> Result<i32> {
406 try_into_int(self.limits.max_move_value_depth).extend()
407 }
408
409 async fn max_transaction_ids(&self) -> Result<i32> {
412 try_into_int(self.limits.max_transaction_ids).extend()
413 }
414
415 async fn max_scan_limit(&self) -> Result<i32> {
417 try_into_int(self.limits.max_scan_limit).extend()
418 }
419}
420
421impl ConnectionConfig {
422 pub fn new(
423 port: Option<u16>,
424 host: Option<String>,
425 db_url: Option<String>,
426 db_pool_size: Option<u32>,
427 prom_host: Option<String>,
428 prom_port: Option<u16>,
429 skip_migration_consistency_check: Option<bool>,
430 max_available_range: Option<u64>,
431 ) -> Self {
432 let default = Self::default();
433 Self {
434 port: port.unwrap_or(default.port),
435 host: host.unwrap_or(default.host),
436 db_url: db_url.map_or(default.db_url, DbUrl::from),
437 db_pool_size: db_pool_size.unwrap_or(default.db_pool_size),
438 prom_host: prom_host.unwrap_or(default.prom_host),
439 prom_port: prom_port.unwrap_or(default.prom_port),
440 skip_migration_consistency_check: skip_migration_consistency_check
441 .unwrap_or(default.skip_migration_consistency_check),
442 max_available_range: max_available_range.unwrap_or(default.max_available_range),
443 }
444 }
445
446 pub fn ci_integration_test_cfg() -> Self {
447 Self {
448 db_url: "postgres://postgres:postgrespw@localhost:5432/iota_graphql_rpc_e2e_tests"
449 .to_string()
450 .into(),
451 ..Default::default()
452 }
453 }
454
455 pub fn ci_integration_test_cfg_with_db_name(
456 db_name: String,
457 port: u16,
458 prom_port: u16,
459 ) -> Self {
460 Self {
461 db_url: format!("postgres://postgres:postgrespw@localhost:5432/{db_name}").into(),
462 port,
463 prom_port,
464 ..Default::default()
465 }
466 }
467
468 pub fn db_name(&self) -> String {
469 self.db_url
470 .as_str()
471 .split('/')
472 .next_back()
473 .unwrap()
474 .to_string()
475 }
476
477 pub fn db_url(&self) -> String {
478 self.db_url.as_str().to_string()
479 }
480
481 pub fn db_pool_size(&self) -> u32 {
482 self.db_pool_size
483 }
484
485 pub fn server_address(&self) -> String {
486 format!("{}:{}", self.host, self.port)
487 }
488}
489
490impl ServiceConfig {
491 pub fn read(contents: &str) -> Result<Self, toml::de::Error> {
492 toml::de::from_str::<Self>(contents)
493 }
494
495 pub fn test_defaults() -> Self {
496 Self {
497 background_tasks: BackgroundTasksConfig::test_defaults(),
498 ..Default::default()
499 }
500 }
501}
502
503impl Limits {
504 pub fn package_resolver_limits(&self) -> iota_package_resolver::Limits {
506 iota_package_resolver::Limits {
507 max_type_argument_depth: self.max_type_argument_depth as usize,
508 max_type_argument_width: self.max_type_argument_width as usize,
509 max_type_nodes: self.max_type_nodes as usize,
510 max_move_value_depth: self.max_move_value_depth as usize,
511 }
512 }
513}
514
515impl BackgroundTasksConfig {
516 pub fn test_defaults() -> Self {
517 Self {
518 watermark_update_ms: 100, }
520 }
521}
522
523impl Default for Versions {
524 fn default() -> Self {
525 Self {
526 versions: vec![format!(
527 "{}.{}",
528 env!("CARGO_PKG_VERSION_MAJOR"),
529 env!("CARGO_PKG_VERSION_MINOR")
530 )],
531 }
532 }
533}
534
535impl Default for Ide {
536 fn default() -> Self {
537 Self {
538 ide_title: "IOTA GraphQL IDE".to_string(),
539 }
540 }
541}
542
543impl Default for ConnectionConfig {
544 fn default() -> Self {
545 Self {
546 port: 8000,
547 host: "127.0.0.1".to_string(),
548 db_url: DEFAULT_DB_URL.to_string().into(),
549 db_pool_size: 10,
550 prom_host: "0.0.0.0".to_string(),
551 prom_port: 9184,
552 skip_migration_consistency_check: false,
553 max_available_range: 9_000,
554 }
555 }
556}
557
558impl Default for Limits {
559 fn default() -> Self {
560 Self {
563 max_query_depth: 20,
564 max_query_nodes: 300,
565 max_output_nodes: 100_000,
566 max_query_payload_size: 5_000,
567 max_db_query_cost: 20_000,
568 default_page_size: DEFAULT_PAGE_SIZE,
569 max_page_size: MAX_PAGE_SIZE,
570 mutation_timeout_ms: 74_000,
575 request_timeout_ms: 40_000,
576 max_type_argument_depth: 16,
579 max_type_argument_width: 32,
581 max_type_nodes: 256,
583 max_move_value_depth: 128,
585 max_transaction_ids: 1000,
588 max_scan_limit: 1_000_000,
589 max_tx_payload_size: (128u32 * 1024u32 * 4u32).div_ceil(3),
594 }
595 }
596}
597
598impl Default for InternalFeatureConfig {
599 fn default() -> Self {
600 Self {
601 query_limits_checker: true,
602 directive_checker: true,
603 feature_gate: true,
604 logger: true,
605 query_timeout: true,
606 metrics: true,
607 tracing: false,
608 apollo_tracing: false,
609 open_telemetry: false,
610 }
611 }
612}
613
614impl Default for BackgroundTasksConfig {
615 fn default() -> Self {
616 Self {
617 watermark_update_ms: 500,
618 }
619 }
620}
621
622impl Display for Version {
623 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
624 write!(f, "{}", self.full)
625 }
626}
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631
632 #[test]
633 fn test_read_empty_service_config() {
634 let actual = ServiceConfig::read("").unwrap();
635 let expect = ServiceConfig::default();
636 assert_eq!(actual, expect);
637 }
638
639 #[test]
640 fn test_read_limits_in_service_config() {
641 let actual = ServiceConfig::read(
642 r#" [limits]
643 max-query-depth = 100
644 max-query-nodes = 300
645 max-output-nodes = 200000
646 max-tx-payload-size = 174763
647 max-query-payload-size = 2000
648 max-db-query-cost = 50
649 default-page-size = 20
650 max-page-size = 50
651 mutation-timeout-ms = 74000
652 request-timeout-ms = 27000
653 max-type-argument-depth = 32
654 max-type-argument-width = 64
655 max-type-nodes = 128
656 max-move-value-depth = 256
657 max-transaction-ids = 11
658 max-scan-limit = 50
659 "#,
660 )
661 .unwrap();
662
663 let expect = ServiceConfig {
664 limits: Limits {
665 max_query_depth: 100,
666 max_query_nodes: 300,
667 max_output_nodes: 200000,
668 max_tx_payload_size: 174763,
669 max_query_payload_size: 2000,
670 max_db_query_cost: 50,
671 default_page_size: 20,
672 max_page_size: 50,
673 mutation_timeout_ms: 74_000,
674 request_timeout_ms: 27_000,
675 max_type_argument_depth: 32,
676 max_type_argument_width: 64,
677 max_type_nodes: 128,
678 max_move_value_depth: 256,
679 max_transaction_ids: 11,
680 max_scan_limit: 50,
681 },
682 ..Default::default()
683 };
684
685 assert_eq!(actual, expect)
686 }
687
688 #[test]
689 fn test_read_enabled_features_in_service_config() {
690 let actual = ServiceConfig::read(
691 r#" disabled-features = [
692 "coins",
693 ]
694 "#,
695 )
696 .unwrap();
697
698 use FunctionalGroup as G;
699 let expect = ServiceConfig {
700 disabled_features: BTreeSet::from([G::Coins]),
701 ..Default::default()
702 };
703
704 assert_eq!(actual, expect)
705 }
706
707 #[test]
708 fn test_read_experiments_in_service_config() {
709 let actual = ServiceConfig::read(
710 r#" [experiments]
711 test-flag = true
712 "#,
713 )
714 .unwrap();
715
716 let expect = ServiceConfig {
717 experiments: Experiments { test_flag: true },
718 ..Default::default()
719 };
720
721 assert_eq!(actual, expect)
722 }
723
724 #[test]
725 fn test_read_everything_in_service_config() {
726 let actual = ServiceConfig::read(
727 r#" disabled-features = ["analytics"]
728
729 [limits]
730 max-query-depth = 42
731 max-query-nodes = 320
732 max-output-nodes = 200000
733 max-tx-payload-size = 181017
734 max-query-payload-size = 200
735 max-db-query-cost = 20
736 default-page-size = 10
737 max-page-size = 20
738 mutation-timeout-ms = 74000
739 request-timeout-ms = 30000
740 max-type-argument-depth = 32
741 max-type-argument-width = 64
742 max-type-nodes = 128
743 max-move-value-depth = 256
744 max-transaction-ids = 42
745 max-scan-limit = 420
746
747 [experiments]
748 test-flag = true
749 "#,
750 )
751 .unwrap();
752
753 let expect = ServiceConfig {
754 limits: Limits {
755 max_query_depth: 42,
756 max_query_nodes: 320,
757 max_output_nodes: 200000,
758 max_tx_payload_size: 181017,
759 max_query_payload_size: 200,
760 max_db_query_cost: 20,
761 default_page_size: 10,
762 max_page_size: 20,
763 mutation_timeout_ms: 74_000,
764 request_timeout_ms: 30_000,
765 max_type_argument_depth: 32,
766 max_type_argument_width: 64,
767 max_type_nodes: 128,
768 max_move_value_depth: 256,
769 max_transaction_ids: 42,
770 max_scan_limit: 420,
771 },
772 disabled_features: BTreeSet::from([FunctionalGroup::Analytics]),
773 experiments: Experiments { test_flag: true },
774 ..Default::default()
775 };
776
777 assert_eq!(actual, expect);
778 }
779
780 #[test]
781 fn test_read_partial_in_service_config() {
782 let actual = ServiceConfig::read(
783 r#" disabled-features = ["analytics"]
784
785 [limits]
786 max-query-depth = 42
787 max-query-nodes = 320
788 "#,
789 )
790 .unwrap();
791
792 let expect = ServiceConfig {
795 limits: Limits {
796 max_query_depth: 42,
797 max_query_nodes: 320,
798 ..Default::default()
799 },
800 disabled_features: BTreeSet::from([FunctionalGroup::Analytics]),
801 ..Default::default()
802 };
803
804 assert_eq!(actual, expect);
805 }
806
807 #[test]
808 fn test_server_config_debug_hides_db_password() {
809 let config = ServerConfig {
810 connection: ConnectionConfig {
811 db_url: "postgres://user:hunter2@localhost:5432/iota_indexer"
812 .to_string()
813 .into(),
814 ..Default::default()
815 },
816 ..Default::default()
817 };
818
819 let printed = format!("{config:#?}");
820 assert!(!printed.contains("hunter2"), "{printed}");
821 assert!(printed.contains("****"), "{printed}");
822 }
823}