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 {config_path}: {err:?}"
52            ))
53        })?;
54
55        // Parse the loaded content into a configuration instance
56        let mut config = toml::from_str::<Self>(&config_content).map_err(|e| {
57            error!(target: "explorerd::config", "Failed parsing TOML config: {e}");
58            Error::ConfigError(format!("Failed to parse the configuration file {config_path}"))
59        })?;
60
61        // Set the configuration path
62        config.path = Some(config_path);
63
64        debug!(target: "explorerd::config", "Successfully loaded configuration: {config:?}");
65
66        Ok(config)
67    }
68
69    /// Returns the currently active network configuration.
70    #[allow(dead_code)] // Test case currently using
71    pub fn active_network_config(&self) -> Option<ExplorerNetworkConfig> {
72        self.get_network_config(self.network.as_str())
73    }
74
75    /// Returns the network configuration for specified network.
76    pub fn get_network_config(&self, network: &str) -> Option<ExplorerNetworkConfig> {
77        match network {
78            "localnet" => self.network_config.localnet.clone(),
79            "testnet" => self.network_config.testnet.clone(),
80            "mainnet" => self.network_config.mainnet.clone(),
81            _ => None,
82        }
83    }
84}
85
86/// Provides a default `ExplorerConfig` configuration using the `testnet` network.
87impl Default for ExplorerConfig {
88    fn default() -> Self {
89        Self {
90            network: String::from("testnet"),
91            network_config: NetworkConfigs::default(),
92            path: None,
93        }
94    }
95}
96
97/// Attempts to convert a [`PathBuff`] to an [`ExplorerConfig`] by loading and parsing from specified file path.
98impl TryFrom<&PathBuf> for ExplorerConfig {
99    type Error = Error;
100    fn try_from(path: &PathBuf) -> Result<Self> {
101        let path_str = path.to_str().ok_or_else(|| {
102            Error::ConfigError("Unable to convert PathBuf to a valid UTF-8 path string".to_string())
103        })?;
104
105        // Create configuration and return
106        ExplorerConfig::new(path_str.to_string())
107    }
108}
109
110/// Deserializes a `&str` containing explorer content in TOML format into an [`ExplorerConfig`] instance.
111impl FromStr for ExplorerConfig {
112    type Err = String;
113    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
114        let config: ExplorerConfig =
115            toml::from_str(s).map_err(|e| format!("Failed to parse ExplorerdConfig: {e}"))?;
116        Ok(config)
117    }
118}
119
120/// Represents network configurations for localnet, testnet, and mainnet.
121#[derive(Debug, Clone, Deserialize, StructOpt)]
122pub struct NetworkConfigs {
123    /// Local network configuration
124    pub localnet: Option<ExplorerNetworkConfig>,
125    /// Testnet network configuration
126    pub testnet: Option<ExplorerNetworkConfig>,
127    /// Mainnet network configuration
128    pub mainnet: Option<ExplorerNetworkConfig>,
129}
130
131/// Provides a default `NetworkConfigs` configuration using the `testnet` network.
132impl Default for NetworkConfigs {
133    fn default() -> Self {
134        NetworkConfigs {
135            localnet: None,
136            testnet: Some(ExplorerNetworkConfig::default()),
137            mainnet: None,
138        }
139    }
140}
141
142/// Deserializes a `&str` containing network configs content in TOML format into an [`NetworkConfigs`] instance.
143impl FromStr for NetworkConfigs {
144    type Err = String;
145    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
146        let config: NetworkConfigs =
147            toml::from_str(s).map_err(|e| format!("Failed to parse NetworkConfigs: {e}"))?;
148        Ok(config)
149    }
150}
151
152/// Struct representing the configuration for an explorer network.
153#[derive(Clone, Deserialize, StructOpt)]
154#[structopt()]
155#[serde(default)]
156pub struct ExplorerNetworkConfig {
157    #[structopt(flatten)]
158    /// JSON-RPC settings used to set up a server that the explorer listens on for incoming RPC requests.
159    pub rpc: RpcSettingsOpt,
160
161    #[structopt(long, default_value = "~/.local/share/darkfi/explorerd/testnet")]
162    /// Path to the explorer's database.
163    pub database: String,
164
165    #[structopt(short, long, default_value = "tcp://127.0.0.1:8340")]
166    /// Endpoint of the DarkFi node JSON-RPC server to sync with.
167    pub endpoint: Url,
168}
169
170/// Attempts to convert a tuple `(PathBuf, &str)` representing a configuration file path
171/// and network name into an `ExplorerNetworkConfig`.
172impl TryFrom<(&PathBuf, &String)> for ExplorerNetworkConfig {
173    type Error = Error;
174    fn try_from(path_and_network: (&PathBuf, &String)) -> Result<Self> {
175        // Load the ExplorerConfig from the given file path
176        let config: ExplorerConfig = path_and_network.0.try_into()?;
177        // Retrieve the network configuration for the specified network
178        match config.get_network_config(path_and_network.1) {
179            Some(config) => Ok(config),
180            None => Err(Error::ConfigError(format!(
181                "Failed to retrieve network configuration for network: {}",
182                path_and_network.1
183            ))),
184        }
185    }
186}
187
188/// Provides a default `ExplorerNetworkConfig` instance using `structopt` default values defined
189/// in the `ExplorerNetworkConfig` struct.
190impl Default for ExplorerNetworkConfig {
191    fn default() -> Self {
192        Self::from_iter(&[""])
193    }
194}
195
196/// Provides a user-friendly debug view of the `ExplorerdNetworkConfig` configuration.
197impl fmt::Debug for ExplorerNetworkConfig {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        let mut debug_struct = f.debug_struct("ExplorerdConfig");
200        debug_struct
201            .field("rpc_listen", &self.rpc.rpc_listen.to_string().trim_end_matches('/'))
202            .field("db_path", &self.database)
203            .field("endpoint", &self.endpoint.to_string().trim_end_matches('/'));
204        debug_struct.finish()
205    }
206}
207
208/// Deserializes a `&str` containing network config content in TOML format into an [`ExplorerNetworkConfig`] instance.
209impl FromStr for ExplorerNetworkConfig {
210    type Err = String;
211    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
212        let config: ExplorerNetworkConfig = toml::from_str(s)
213            .map_err(|e| format!("Failed to parse ExplorerdNetworkConfig: {e}"))?;
214        Ok(config)
215    }
216}
217
218#[cfg(test)]
219/// Contains test cases for validating the functionality and correctness of the `ExplorerConfig`
220/// and related components using a configuration loaded from a TOML file.
221mod tests {
222    use std::path::Path;
223
224    use super::*;
225
226    use crate::test_utils::init_logger;
227
228    /// Validates the functionality of initializing and interacting with `ExplorerConfig`
229    /// loaded from a TOML file, ensuring correctness of the network-specific configurations.
230    #[test]
231    fn test_explorerd_config_from_file() {
232        // Constants for expected configurations
233        const CONFIG_PATH: &str = "explorerd_config.toml";
234        const ACTIVE_NETWORK: &str = "testnet";
235
236        const NETWORK_CONFIGS: &[(&str, &str, &str, &str)] = &[
237            (
238                "localnet",
239                "~/.local/share/darkfi/explorerd/localnet",
240                "tcp://127.0.0.1:8240/",
241                "tcp://127.0.0.1:14567/",
242            ),
243            (
244                "testnet",
245                "~/.local/share/darkfi/explorerd/testnet",
246                "tcp://127.0.0.1:8340/",
247                "tcp://127.0.0.1:14667/",
248            ),
249            (
250                "mainnet",
251                "~/.local/share/darkfi/explorerd/mainnet",
252                "tcp://127.0.0.1:8440/",
253                "tcp://127.0.0.1:14767/",
254            ),
255        ];
256
257        init_logger(simplelog::LevelFilter::Info, vec!["sled", "runtime", "net"]);
258
259        // Ensure the configuration file exists
260        assert!(Path::new(CONFIG_PATH).exists());
261
262        // Load the configuration
263        let config = ExplorerConfig::new(CONFIG_PATH.to_string())
264            .expect("Failed to load configuration from file");
265
266        // Validate the expected network
267        assert_eq!(config.network, ACTIVE_NETWORK);
268
269        // Validate the path is correctly set
270        assert_eq!(config.path.as_deref(), Some(CONFIG_PATH));
271
272        // Validate that `active_network_config` correctly retrieves the testnet configuration
273        let active_config = config.active_network_config();
274        assert!(active_config.is_some(), "Active network configuration should not be None.");
275        let active_config = active_config.unwrap();
276        assert_eq!(active_config.database, NETWORK_CONFIGS[1].1); // Testnet database
277        assert_eq!(active_config.endpoint.to_string(), NETWORK_CONFIGS[1].2);
278        assert_eq!(&active_config.rpc.rpc_listen.to_string(), NETWORK_CONFIGS[1].3);
279
280        // Validate all network configurations values (localnet, testnet, mainnet)
281        for &(network, expected_db, expected_endpoint, expected_rpc) in NETWORK_CONFIGS {
282            let network_config = config.get_network_config(network);
283
284            if let Some(config) = network_config {
285                assert_eq!(config.database, expected_db);
286                assert_eq!(config.endpoint.to_string(), expected_endpoint);
287                assert_eq!(config.rpc.rpc_listen.to_string(), expected_rpc);
288            } else {
289                assert!(network_config.is_none(), "{network} configuration is missing");
290            }
291        }
292
293        // Validate (path, network).try_into()
294        let config_path_buf = &PathBuf::from(CONFIG_PATH);
295        let mainnet_string = &String::from("mainnet");
296        let mainnet_config: ExplorerNetworkConfig = (config_path_buf, mainnet_string)
297            .try_into()
298            .expect("Failed to load explorer network config");
299        assert_eq!(mainnet_config.database, NETWORK_CONFIGS[2].1); // Mainnet database
300        assert_eq!(mainnet_config.endpoint.to_string(), NETWORK_CONFIGS[2].2);
301        assert_eq!(&mainnet_config.rpc.rpc_listen.to_string(), NETWORK_CONFIGS[2].3);
302    }
303}