Skip to main content

iota_graphql_rpc/
config.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use 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/// The combination of all configurations for the GraphQL service.
22#[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/// Configuration for connections for the RPC, passed in as command-line
34/// arguments. This configures specific connections between this service and
35/// other services, and might differ from instance to instance of the GraphQL
36/// service.
37#[GraphQLConfig]
38#[derive(clap::Args, Clone, Eq, PartialEq)]
39pub struct ConnectionConfig {
40    /// Port to bind the server to
41    #[arg(short, long, default_value_t = ConnectionConfig::default().port)]
42    pub port: u16,
43    /// Host to bind the server to
44    #[arg(long, default_value_t = ConnectionConfig::default().host)]
45    pub host: String,
46    /// DB URL for data fetching
47    #[arg(short, long, default_value = DEFAULT_DB_URL)]
48    pub db_url: DbUrl,
49    /// Pool size for DB connections
50    #[arg(long, default_value_t = ConnectionConfig::default().db_pool_size)]
51    pub db_pool_size: u32,
52    /// Host to bind the prom server to
53    #[arg(long, default_value_t = ConnectionConfig::default().prom_host)]
54    pub prom_host: String,
55    /// Port to bind the prom server to
56    #[arg(long, default_value_t = ConnectionConfig::default().prom_port)]
57    pub prom_port: u16,
58    /// Skip checking whether the service is compatible with the DB it is about
59    /// to connect to, on start-up.
60    #[arg(long, default_value_t = ConnectionConfig::default().skip_migration_consistency_check)]
61    pub skip_migration_consistency_check: bool,
62    /// Maximum number of checkpoints to look back for consistent view queries.
63    /// Directly influences the `availableRange` size. Larger values let
64    /// pagination cursors stay valid for longer, downside is that older cursors
65    /// have higher DB cost.
66    #[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/// CLI options that control the archival fallback used when Postgres data has
75/// been pruned.
76#[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/// Configuration on features supported by the GraphQL service, passed in a
128/// TOML-based file. These configurations are shared across fleets of the
129/// service, i.e. all testnet services will have the same `ServiceConfig`.
130#[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    /// Maximum depth of nodes in the requests.
149    pub max_query_depth: u32,
150    /// Maximum number of nodes in the requests.
151    pub max_query_nodes: u32,
152    /// Maximum number of output nodes allowed in the response.
153    pub max_output_nodes: u32,
154    /// Maximum size in bytes allowed for the `txBytes` and `signatures` fields
155    /// of a GraphQL mutation request in the `executeTransactionBlock` node,
156    /// and for the `txBytes` of a `dryRunTransactionBlock` node.
157    pub max_tx_payload_size: u32,
158    /// Maximum size in bytes of the JSON payload of a GraphQL read request
159    /// (excluding `max_tx_payload_size`).
160    pub max_query_payload_size: u32,
161    /// Queries whose EXPLAIN cost are more than this will be logged. Given in
162    /// the units used by the database (where 1.0 is roughly the cost of a
163    /// sequential page access).
164    pub max_db_query_cost: u32,
165    /// Paginated queries will return this many elements if a page size is not
166    /// provided.
167    pub default_page_size: u32,
168    /// Paginated queries can return at most this many elements.
169    pub max_page_size: u32,
170    /// Time (in milliseconds) to wait for a transaction to be executed and the
171    /// results returned from GraphQL. If the transaction takes longer than
172    /// this time to execute, the request will return a timeout error, but
173    /// the transaction may continue executing.
174    pub mutation_timeout_ms: u32,
175    /// Time (in milliseconds) to wait for a read request from the GraphQL
176    /// service. Requests that take longer than this time to return a result
177    /// will return a timeout error.
178    pub request_timeout_ms: u32,
179    /// Maximum amount of nesting among type arguments (type arguments nest when
180    /// a type argument is itself generic and has arguments).
181    pub max_type_argument_depth: u32,
182    /// Maximum number of type parameters a type can have.
183    pub max_type_argument_width: u32,
184    /// Maximum size of a fully qualified type.
185    pub max_type_nodes: u32,
186    /// Maximum deph of a move value.
187    pub max_move_value_depth: u32,
188    /// Maximum number of transaction ids that can be passed to a
189    /// `TransactionBlockFilter` or to `transaction_blocks_by_digests`.
190    pub max_transaction_ids: u32,
191    /// Maximum number of candidates to scan when gathering a page of results.
192    pub max_scan_limit: u32,
193}
194
195#[GraphQLConfig]
196#[derive(Copy)]
197pub struct BackgroundTasksConfig {
198    /// How often the watermark task checks the indexer database to update the
199    /// checkpoint and epoch watermarks.
200    pub watermark_update_ms: u64,
201}
202
203/// The Version of the service. `year.month` represents the major release.
204/// New `patch` versions represent backwards compatible fixes for their major
205/// release. The `full` version is `year.month.patch-sha`.
206#[derive(Copy, Clone, Debug)]
207pub struct Version {
208    /// The year of this release.
209    pub year: &'static str,
210    /// The month of this release.
211    pub month: &'static str,
212    /// The patch is a positive number incremented for every compatible release
213    /// on top of the major.month release.
214    pub patch: &'static str,
215    /// The commit sha for this release.
216    pub sha: &'static str,
217    /// The full version string.
218    /// Note that this extra field is used only for the uptime_metric function
219    /// which requires a &'static str.
220    pub full: &'static str,
221}
222
223impl Version {
224    /// Use for testing when you need the Version obj and a year.month &str
225    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            // note that this full field is needed for metrics but not for testing
232            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    /// The title to display at the top of the web-based GraphiQL IDE.
248    #[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    // Add experimental flags here, to provide access to them through-out the GraphQL
256    // implementation.
257    #[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    /// RPC URL for the fullnode to send transactions to execute and dry-run.
278    #[arg(long)]
279    pub(crate) node_rpc_url: Option<String>,
280}
281
282/// The enabled features and service limits configured by the server.
283#[Object]
284impl ServiceConfig {
285    /// Check whether `feature` is enabled on this GraphQL service.
286    async fn is_enabled(&self, feature: FunctionalGroup) -> bool {
287        !self.disabled_features.contains(&feature)
288    }
289
290    /// List the available versions for this GraphQL service.
291    async fn available_versions(&self) -> Vec<String> {
292        self.versions.versions.clone()
293    }
294
295    /// List of all features that are enabled on this GraphQL service.
296    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    /// The maximum depth a GraphQL query can be to be accepted by this service.
305    pub async fn max_query_depth(&self) -> Result<i32> {
306        try_into_int(self.limits.max_query_depth).extend()
307    }
308
309    /// The maximum number of nodes (field names) the service will accept in a
310    /// single query.
311    pub async fn max_query_nodes(&self) -> Result<i32> {
312        try_into_int(self.limits.max_query_nodes).extend()
313    }
314
315    /// The maximum number of output nodes in a GraphQL response.
316    ///
317    /// Non-connection nodes have a count of 1, while connection nodes are
318    /// counted as the specified 'first' or 'last' number of items, or the
319    /// default_page_size as set by the server if those arguments are not
320    /// set.
321    ///
322    /// Counts accumulate multiplicatively down the query tree. For example, if
323    /// a query starts with a connection of first: 10 and has a field to a
324    /// connection with last: 20, the count at the second level would be 200
325    /// nodes. This is then summed to the count of 10 nodes at the first
326    /// level, for a total of 210 nodes.
327    pub async fn max_output_nodes(&self) -> Result<i32> {
328        try_into_int(self.limits.max_output_nodes).extend()
329    }
330
331    /// Maximum estimated cost of a database query used to serve a GraphQL
332    /// request.  This is measured in the same units that the database uses
333    /// in EXPLAIN queries.
334    async fn max_db_query_cost(&self) -> Result<i32> {
335        try_into_int(self.limits.max_db_query_cost).extend()
336    }
337
338    /// Default number of elements allowed on a single page of a connection.
339    async fn default_page_size(&self) -> Result<i32> {
340        try_into_int(self.limits.default_page_size).extend()
341    }
342
343    /// Maximum number of elements allowed on a single page of a connection.
344    async fn max_page_size(&self) -> Result<i32> {
345        try_into_int(self.limits.max_page_size).extend()
346    }
347
348    /// Maximum time in milliseconds spent waiting for a response from fullnode
349    /// after issuing a transaction to execute. Note that the transaction
350    /// may still succeed even in the case of a timeout. Transactions are
351    /// idempotent, so a transaction that times out should be resubmitted
352    /// until the network returns a definite response (success or failure, not
353    /// timeout).
354    async fn mutation_timeout_ms(&self) -> Result<i32> {
355        try_into_int(self.limits.mutation_timeout_ms).extend()
356    }
357
358    /// Maximum time in milliseconds that will be spent to serve one query
359    /// request.
360    async fn request_timeout_ms(&self) -> Result<i32> {
361        try_into_int(self.limits.request_timeout_ms).extend()
362    }
363
364    /// The maximum bytes allowed for transactions in queries.
365    ///
366    /// This corresponds to the `txBytes` and `signatures` fields of the GraphQL
367    /// mutation `executeTransactionBlock` node, or the `txBytes` of a
368    /// `dryRunTransactionBlock`.
369    ///
370    /// By default, this is set to the value of the maximum transaction bytes
371    /// (including the signatures) allowed by the protocol, plus the Base64
372    /// overhead (roughly 1/3 of the original string).
373    async fn max_transaction_payload_size(&self) -> Result<i32> {
374        try_into_int(self.limits.max_tx_payload_size).extend()
375    }
376
377    /// The maximum bytes allowed for the read part of GraphQL queries.
378    ///
379    /// In case of mutations or `dryRunTransactionBlocks` the `txBytes` and
380    /// `signatures` are not included in this limit.
381    async fn max_query_payload_size(&self) -> Result<i32> {
382        try_into_int(self.limits.max_query_payload_size).extend()
383    }
384
385    /// Maximum nesting allowed in type arguments in Move Types resolved by this
386    /// service.
387    async fn max_type_argument_depth(&self) -> Result<i32> {
388        try_into_int(self.limits.max_type_argument_depth).extend()
389    }
390
391    /// Maximum number of type arguments passed into a generic instantiation of
392    /// a Move Type resolved by this service.
393    async fn max_type_argument_width(&self) -> Result<i32> {
394        try_into_int(self.limits.max_type_argument_width).extend()
395    }
396
397    /// Maximum number of structs that need to be processed when calculating the
398    /// layout of a single Move Type.
399    async fn max_type_nodes(&self) -> Result<i32> {
400        try_into_int(self.limits.max_type_nodes).extend()
401    }
402
403    /// Maximum nesting allowed in struct fields when calculating the layout of
404    /// a single Move Type.
405    async fn max_move_value_depth(&self) -> Result<i32> {
406        try_into_int(self.limits.max_move_value_depth).extend()
407    }
408
409    /// Maximum number of transaction ids that can be passed to a
410    /// `TransactionBlockFilter`.
411    async fn max_transaction_ids(&self) -> Result<i32> {
412        try_into_int(self.limits.max_transaction_ids).extend()
413    }
414
415    /// Maximum number of candidates to scan when gathering a page of results.
416    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    /// Extract limits for the package resolver.
505    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, // Set to 100ms for testing
519        }
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        // Picked so that TS SDK shim layer queries all pass limit.
561        // TODO: calculate proper cost limits
562        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            // This default was picked as the sum of pre- and post- quorum timeouts from
571            // [`iota_core::authority_aggregator::TimeoutConfig`], with a 10% buffer.
572            //
573            // <https://github.com/iotaledger/iota/blob/eaf05fe5d293c06e3a2dfc22c87ba2aef419d8ea/crates/iota-core/src/authority_aggregator.rs#L84-L85>
574            mutation_timeout_ms: 74_000,
575            request_timeout_ms: 40_000,
576            // The following limits reflect the max values set in ProtocolConfig, at time of
577            // writing. <https://github.com/iotaledger/iota/blob/333f87061f0656607b1928aba423fa14ca16899e/crates/iota-protocol-config/src/lib.rs#L1580>
578            max_type_argument_depth: 16,
579            // <https://github.com/iotaledger/iota/blob/4b934f87acae862cecbcbefb3da34cabb79805aa/crates/iota-protocol-config/src/lib.rs#L1618>
580            max_type_argument_width: 32,
581            // <https://github.com/iotaledger/iota/blob/4b934f87acae862cecbcbefb3da34cabb79805aa/crates/iota-protocol-config/src/lib.rs#L1622>
582            max_type_nodes: 256,
583            // <https://github.com/iotaledger/iota/blob/4b934f87acae862cecbcbefb3da34cabb79805aa/crates/iota-protocol-config/src/lib.rs#L1988>
584            max_move_value_depth: 128,
585            // Filter-specific limits, such as the number of transaction ids that can be specified
586            // for the `TransactionBlockFilter`.
587            max_transaction_ids: 1000,
588            max_scan_limit: 1_000_000,
589            // Protocol limit for max transaction bytes allowed + base64
590            // overhead (roughly 1/3 of the original string). This is rounded up.
591            //
592            // <https://github.com/iotaledger/iota/blob/29c410ac809dd7c71dbf0237a96f08d72b406e52/crates/iota-protocol-config/src/lib.rs#L1566>
593            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        // When reading partially, the other parts will come from the default
793        // implementation.
794        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}