iota_light_client/
config.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2025 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use core::str::FromStr;
6use std::path::{Path, PathBuf};
7
8use anyhow::{Result, anyhow, bail};
9use iota_config::object_storage_config::{ObjectStoreConfig, ObjectStoreType};
10use serde::{Deserialize, Serialize};
11use tokio::fs::{create_dir_all, read_to_string};
12use url::Url;
13
14const GENESIS_FILE_NAME: &str = "genesis.blob";
15const CHECKPOINTS_FILE_NAME: &str = "checkpoints.yaml";
16
17/// The config file for the light client.
18#[derive(Clone, Debug, Deserialize, Serialize)]
19pub struct Config {
20    /// An RPC endpoint to a full node.
21    pub rpc_url: Url,
22    /// A GraphQL endpoint to a full node.
23    pub graphql_url: Option<Url>,
24    /// The directory containing synced checkpoints.
25    pub checkpoints_dir: PathBuf,
26    /// The URL to download the genesis.blob file from.
27    pub genesis_blob_download_url: Option<Url>,
28    /// Flag to enable automatic syncing before running one of the check
29    /// commands.
30    pub sync_before_check: bool,
31    /// A config to sync the light client from a checkpoint store. If provided,
32    /// will also be used to check objects/transactions for inclusion.
33    pub checkpoint_store_config: Option<ObjectStoreConfig>,
34    /// A config to sync the light client from an archive store. Since the
35    /// archive does not store full checkpoints, it cannot be used to
36    /// check objects/transactions.
37    pub archive_store_config: Option<ObjectStoreConfig>,
38}
39
40impl Config {
41    pub fn get_mainnet_config() -> Self {
42        Self::get_config_by_network("mainnet")
43    }
44
45    pub fn get_testnet_config() -> Self {
46        Self::get_config_by_network("testnet")
47    }
48
49    pub fn get_devnet_config() -> Self {
50        Self::get_config_by_network("devnet")
51    }
52
53    fn get_config_by_network(network: &str) -> Self {
54        Self {
55            rpc_url: Url::parse(&format!("https://api.{network}.iota.cafe")).unwrap(),
56            graphql_url: Some(Url::parse(&format!("https://graphql.{network}.iota.cafe")).unwrap()),
57            checkpoints_dir: PathBuf::from_str(&format!("checkpoints_{network}")).unwrap(),
58            genesis_blob_download_url: Some(
59                Url::parse(&format!("https://dbfiles.{network}.iota.cafe/genesis.blob")).unwrap(),
60            ),
61            sync_before_check: false,
62            checkpoint_store_config: Some(ObjectStoreConfig {
63                object_store: Some(ObjectStoreType::S3),
64                object_store_connection_limit: 20,
65                aws_endpoint: Some(format!("https://checkpoints.{network}.iota.cafe")),
66                aws_virtual_hosted_style_request: true,
67                aws_region: Some("weur".to_string()),
68                no_sign_request: true,
69                ..Default::default()
70            }),
71            archive_store_config: Some(ObjectStoreConfig {
72                object_store: Some(ObjectStoreType::S3),
73                object_store_connection_limit: 20,
74                aws_endpoint: Some(format!("https://archive.{network}.iota.cafe")),
75                aws_virtual_hosted_style_request: true,
76                aws_region: Some("weur".to_string()),
77                no_sign_request: true,
78                ..Default::default()
79            }),
80        }
81    }
82}
83
84impl Config {
85    /// Loads the config from file.
86    pub async fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
87        let content = read_to_string(path).await?;
88        let config: Config = serde_yaml::from_str(&content)?;
89        config.validate()?;
90        Ok(config)
91    }
92
93    /// Creates the necessary checkpoint directory and files if not already
94    /// present.
95    pub async fn setup(&self) -> Result<()> {
96        // Create the checkpoints directory if it doesn't exist yet
97        if !self.checkpoints_dir.is_dir() {
98            create_dir_all(&self.checkpoints_dir).await?;
99        }
100        // Download or copy the genesis blob if it doesn't exist yet
101        if !self.genesis_blob_file_path().is_file() {
102            if let Some(url) = &self.genesis_blob_download_url {
103                match url.scheme() {
104                    "file" => {
105                        let path = url
106                            .to_file_path()
107                            .map_err(|_| anyhow!("invalid file path '{url}'"))?;
108                        tokio::fs::copy(path, self.genesis_blob_file_path()).await?;
109                    }
110                    _ => {
111                        let contents = reqwest::get(url.as_str()).await?.bytes().await?;
112                        tokio::fs::write(self.genesis_blob_file_path(), contents).await?;
113                    }
114                }
115            }
116        }
117        Ok(())
118    }
119
120    pub fn validate(&self) -> Result<()> {
121        if self.graphql_url.is_none() && self.archive_store_config.is_none() {
122            bail!("Invalid config: either GraphQL URL or archive store config must be provided");
123        }
124        Ok(())
125    }
126
127    pub fn checkpoints_list_file_path(&self) -> PathBuf {
128        self.checkpoints_dir.join(CHECKPOINTS_FILE_NAME)
129    }
130
131    pub fn genesis_blob_file_path(&self) -> PathBuf {
132        self.checkpoints_dir.join(GENESIS_FILE_NAME)
133    }
134
135    pub fn full_checkpoint_file_path<'a>(
136        &self,
137        seq: u64,
138        custom_path: impl Into<Option<&'a str>>,
139    ) -> PathBuf {
140        let mut path = self.checkpoints_dir.clone();
141        if let Some(custom) = custom_path.into() {
142            path.push(custom);
143        }
144        path.push(format!("{seq}.chk"));
145        path
146    }
147
148    pub fn checkpoint_summary_file_path(&self, seq: u64) -> PathBuf {
149        Path::new(&self.checkpoints_dir).join(format!("{seq}.sum"))
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use iota_config::object_storage_config::ObjectStoreType;
156    use tempfile::TempDir;
157
158    use super::*;
159
160    fn create_test_config() -> (Config, TempDir) {
161        let temp_dir = TempDir::new().unwrap();
162        std::fs::File::create(temp_dir.path().join("genesis.blob")).unwrap();
163        let config = Config {
164            rpc_url: "http://localhost:9000".parse().unwrap(),
165            graphql_url: Some("http://localhost:9003".parse().unwrap()),
166            checkpoints_dir: temp_dir.path().to_path_buf(),
167            genesis_blob_download_url: None,
168            sync_before_check: false,
169            checkpoint_store_config: Some(ObjectStoreConfig {
170                object_store: Some(ObjectStoreType::S3),
171                aws_endpoint: Some("http://localhost:9001".to_string()),
172                ..Default::default()
173            }),
174            archive_store_config: Some(ObjectStoreConfig {
175                object_store: Some(ObjectStoreType::File),
176                directory: Some(temp_dir.path().to_path_buf()),
177                ..Default::default()
178            }),
179        };
180        config.validate().expect("invalid");
181        (config, temp_dir)
182    }
183
184    #[test]
185    fn test_config_validation() {
186        let (config, _temp_dir) = create_test_config();
187        assert!(config.validate().is_ok());
188    }
189
190    #[test]
191    fn test_checkpoint_paths() {
192        let (config, _temp_dir) = create_test_config();
193
194        let list_path = config.checkpoints_list_file_path();
195        assert_eq!(list_path.file_name().unwrap(), "checkpoints.yaml");
196
197        let checkpoint_path = config.full_checkpoint_file_path(123, None);
198        assert_eq!(checkpoint_path.file_name().unwrap(), "123.chk");
199
200        let custom_checkpoint_path = config.full_checkpoint_file_path(456, Some("custom"));
201        assert!(custom_checkpoint_path.to_str().unwrap().contains("custom"));
202        assert_eq!(custom_checkpoint_path.file_name().unwrap(), "456.chk");
203    }
204
205    #[test]
206    fn test_genesis_path() {
207        let (config, _temp_dir) = create_test_config();
208        let genesis_path = config.genesis_blob_file_path();
209        assert_eq!(genesis_path.file_name().unwrap(), "genesis.blob");
210    }
211}