explorerd/
config.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2024 Dyne.org foundation
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Affero General Public License as
7 * published by the Free Software Foundation, either version 3 of the
8 * License, or (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19use std::{
20    fmt,
21    path::{Path, PathBuf},
22    str::FromStr,
23};
24
25use log::{debug, error};
26use serde::Deserialize;
27use structopt::StructOpt;
28use url::Url;
29
30use darkfi::{rpc::settings::RpcSettingsOpt, util::file::load_file, Error, Result};
31
32/// Represents an explorer configuration
33#[derive(Clone, Debug, Deserialize, StructOpt)]
34pub struct ExplorerConfig {
35    /// Current active network
36    #[allow(dead_code)] // Part of the config file
37    pub network: String,
38    /// Supported network configurations
39    pub network_config: NetworkConfigs,
40    /// Path to the configuration if read from a file
41    pub path: Option<String>,
42}
43
44impl ExplorerConfig {
45    /// Creates a new configuration from a given file path.
46    /// If the file cannot be loaded or parsed, an error is returned.
47    pub fn new(config_path: String) -> Result<Self> {
48        // Load the configuration file from the specified path
49        let config_content = load_file(Path::new(&config_path)).map_err(|err| {
50            Error::ConfigError(format!(
51                "Failed to read the configuration file {}: {:?}",
52                config_path, err
53            ))
54        })?;
55
56        // Parse the loaded content into a configuration instance
57        let mut config = toml::from_str::<Self>(&config_content).map_err(|e| {
58            error!(target: "explorerd::config", "Failed parsing TOML config: {}", e);
59            Error::ConfigError(format!("Failed to parse the configuration file {}", config_path))
60        })?;
61
62        // Set the configuration path
63        config.path = Some(config_path);
64
65        debug!(target: "explorerd::config", "Successfully loaded configuration: {:?}", config);
66
67        Ok(config)
68    }
69
70    /// Returns the currently active network configuration.
71    #[allow(dead_code)] // Test case currently using
72    pub fn active_network_config(&self) -> Option<ExplorerNetworkConfig> {
73        self.get_network_config(self.network.as_str())
74    }
75
76    /// Returns the network configuration for specified network.
77    pub fn get_network_config(&self, network: &str) -> Option<ExplorerNetworkConfig> {
78        match network {
79            "localnet" => self.network_config.localnet.clone(),
80            "testnet" => self.network_config.testnet.clone(),
81            "mainnet" => self.network_config.mainnet.clone(),
82            _ => None,
83        }
84    }
85}
86
87/// Provides a default `ExplorerConfig` configuration using the `testnet` network.
88impl Default for ExplorerConfig {
89    fn default() -> Self {
90        Self {
91            network: String::from("testnet"),
92            network_config: NetworkConfigs::default(),
93            path: None,
94        }
95    }
96}
97
98/// Attempts to convert a [`PathBuff`] to an [`ExplorerConfig`] by loading and parsing from specified file path.
99impl TryFrom<&PathBuf> for ExplorerConfig {
100    type Error = Error;
101    fn try_from(path: &PathBuf) -> Result<Self> {
102        let path_str = path.to_str().ok_or_else(|| {
103            Error::ConfigError("Unable to convert PathBuf to a valid UTF-8 path string".to_string())
104        })?;
105
106        // Create configuration and return
107        ExplorerConfig::new(path_str.to_string())
108    }
109}
110
111/// Deserializes a `&str` containing explorer content in TOML format into an [`ExplorerConfig`] instance.
112impl FromStr for ExplorerConfig {
113    type Err = String;
114    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
115        let config: ExplorerConfig =
116            toml::from_str(s).map_err(|e| format!("Failed to parse ExplorerdConfig: {}", e))?;
117        Ok(config)
118    }
119}
120
121/// Represents network configurations for localnet, testnet, and mainnet.
122#[derive(Debug, Clone, Deserialize, StructOpt)]
123pub struct NetworkConfigs {
124    /// Local network configuration
125    pub localnet: Option<ExplorerNetworkConfig>,
126    /// Testnet network configuration
127    pub testnet: Option<ExplorerNetworkConfig>,
128    /// Mainnet network configuration
129    pub mainnet: Option<ExplorerNetworkConfig>,
130}
131
132/// Provides a default `NetworkConfigs` configuration using the `testnet` network.
133impl Default for NetworkConfigs {
134    fn default() -> Self {
135        NetworkConfigs {
136            localnet: None,
137            testnet: Some(ExplorerNetworkConfig::default()),
138            mainnet: None,
139        }
140    }
141}
142
143/// Deserializes a `&str` containing network configs content in TOML format into an [`NetworkConfigs`] instance.
144impl FromStr for NetworkConfigs {
145    type Err = String;
146    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
147        let config: NetworkConfigs =
148            toml::from_str(s).map_err(|e| format!("Failed to parse NetworkConfigs: {}", e))?;
149        Ok(config)
150    }
151}
152
153/// Struct representing the configuration for an explorer network.
154#[derive(Clone, Deserialize, StructOpt)]
155#[structopt()]
156#[serde(default)]
157pub struct ExplorerNetworkConfig {
158    #[structopt(flatten)]
159    /// JSON-RPC settings used to set up a server that the explorer listens on for incoming RPC requests.
160    pub rpc: RpcSettingsOpt,
161
162    #[structopt(long, default_value = "~/.local/share/darkfi/explorerd/testnet")]
163    /// Path to the explorer's database.
164    pub database: String,
165
166    #[structopt(short, long, default_value = "tcp://127.0.0.1:8340")]
167    /// Endpoint of the DarkFi node JSON-RPC server to sync with.
168    pub endpoint: Url,
169}
170
171/// Attempts to convert a tuple `(PathBuf, &str)` representing a configuration file path
172/// and network name into an `ExplorerNetworkConfig`.
173impl TryFrom<(&PathBuf, &String)> for ExplorerNetworkConfig {
174    type Error = Error;
175    fn try_from(path_and_network: (&PathBuf, &String)) -> Result<Self> {
176        // Load the ExplorerConfig from the given file path
177        let config: ExplorerConfig = path_and_network.0.try_into()?;
178        // Retrieve the network configuration for the specified network
179        match config.get_network_config(path_and_network.1) {
180            Some(config) => Ok(config),
181            None => Err(Error::ConfigError(format!(
182                "Failed to retrieve network configuration for network: {}",
183                path_and_network.1
184            ))),
185        }
186    }
187}
188
189/// Provides a default `ExplorerNetworkConfig` instance using `structopt` default values defined
190/// in the `ExplorerNetworkConfig` struct.
191impl Default for ExplorerNetworkConfig {
192    fn default() -> Self {
193        Self::from_iter(&[""])
194    }
195}
196
197/// Provides a user-friendly debug view of the `ExplorerdNetworkConfig` configuration.
198impl fmt::Debug for ExplorerNetworkConfig {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        let mut debug_struct = f.debug_struct("ExplorerdConfig");
201        debug_struct
202            .field("rpc_listen", &self.rpc.rpc_listen.to_string().trim_end_matches('/'))
203            .field("db_path", &self.database)
204            .field("endpoint", &self.endpoint.to_string().trim_end_matches('/'));
205        debug_struct.finish()
206    }
207}
208
209/// Deserializes a `&str` containing network config content in TOML format into an [`ExplorerNetworkConfig`] instance.
210impl FromStr for ExplorerNetworkConfig {
211    type Err = String;
212    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
213        let config: ExplorerNetworkConfig = toml::from_str(s)
214            .map_err(|e| format!("Failed to parse ExplorerdNetworkConfig: {}", e))?;
215        Ok(config)
216    }
217}
218
219#[cfg(test)]
220/// Contains test cases for validating the functionality and correctness of the `ExplorerConfig`
221/// and related components using a configuration loaded from a TOML file.
222mod tests {
223    use std::path::Path;
224
225    use super::*;
226
227    use crate::test_utils::init_logger;
228
229    /// Validates the functionality of initializing and interacting with `ExplorerConfig`
230    /// loaded from a TOML file, ensuring correctness of the network-specific configurations.
231    #[test]
232    fn test_explorerd_config_from_file() {
233        // Constants for expected configurations
234        const CONFIG_PATH: &str = "explorerd_config.toml";
235        const ACTIVE_NETWORK: &str = "testnet";
236
237        const NETWORK_CONFIGS: &[(&str, &str, &str, &str)] = &[
238            (
239                "localnet",
240                "~/.local/share/darkfi/explorerd/localnet",
241                "tcp://127.0.0.1:8240/",
242                "tcp://127.0.0.1:14567/",
243            ),
244            (
245                "testnet",
246                "~/.local/share/darkfi/explorerd/testnet",
247                "tcp://127.0.0.1:8340/",
248                "tcp://127.0.0.1:14667/",
249            ),
250            (
251                "mainnet",
252                "~/.local/share/darkfi/explorerd/mainnet",
253                "tcp://127.0.0.1:8440/",
254                "tcp://127.0.0.1:14767/",
255            ),
256        ];
257
258        init_logger(simplelog::LevelFilter::Info, vec!["sled", "runtime", "net"]);
259
260        // Ensure the configuration file exists
261        assert!(Path::new(CONFIG_PATH).exists());
262
263        // Load the configuration
264        let config = ExplorerConfig::new(CONFIG_PATH.to_string())
265            .expect("Failed to load configuration from file");
266
267        // Validate the expected network
268        assert_eq!(config.network, ACTIVE_NETWORK);
269
270        // Validate the path is correctly set
271        assert_eq!(config.path.as_deref(), Some(CONFIG_PATH));
272
273        // Validate that `active_network_config` correctly retrieves the testnet configuration
274        let active_config = config.active_network_config();
275        assert!(active_config.is_some(), "Active network configuration should not be None.");
276        let active_config = active_config.unwrap();
277        assert_eq!(active_config.database, NETWORK_CONFIGS[1].1); // Testnet database
278        assert_eq!(active_config.endpoint.to_string(), NETWORK_CONFIGS[1].2);
279        assert_eq!(&active_config.rpc.rpc_listen.to_string(), NETWORK_CONFIGS[1].3);
280
281        // Validate all network configurations values (localnet, testnet, mainnet)
282        for &(network, expected_db, expected_endpoint, expected_rpc) in NETWORK_CONFIGS {
283            let network_config = config.get_network_config(network);
284
285            if let Some(config) = network_config {
286                assert_eq!(config.database, expected_db);
287                assert_eq!(config.endpoint.to_string(), expected_endpoint);
288                assert_eq!(config.rpc.rpc_listen.to_string(), expected_rpc);
289            } else {
290                assert!(network_config.is_none(), "{} configuration is missing", network);
291            }
292        }
293
294        // Validate (path, network).try_into()
295        let config_path_buf = &PathBuf::from(CONFIG_PATH);
296        let mainnet_string = &String::from("mainnet");
297        let mainnet_config: ExplorerNetworkConfig = (config_path_buf, mainnet_string)
298            .try_into()
299            .expect("Failed to load explorer network config");
300        assert_eq!(mainnet_config.database, NETWORK_CONFIGS[2].1); // Mainnet database
301        assert_eq!(mainnet_config.endpoint.to_string(), NETWORK_CONFIGS[2].2);
302        assert_eq!(&mainnet_config.rpc.rpc_listen.to_string(), NETWORK_CONFIGS[2].3);
303    }
304}