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 19 use std::{ 20 fmt, 21 path::{Path, PathBuf}, 22 str::FromStr, 23 }; 24 25 use serde::Deserialize; 26 use structopt::StructOpt; 27 use tracing::{debug, error}; 28 use url::Url; 29 30 use darkfi::{rpc::settings::RpcSettingsOpt, util::file::load_file, Error, Result}; 31 32 /// Represents an explorer configuration 33 #[derive(Clone, Debug, Deserialize, StructOpt)] 34 pub 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 44 impl 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. 87 impl 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. 98 impl 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. 111 impl 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)] 122 pub 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. 132 impl 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. 143 impl 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)] 156 pub 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`. 172 impl 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. 190 impl 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. 197 impl 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. 209 impl 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. 221 mod tests { 222 use std::path::Path; 223 224 use darkfi::util::logger::{setup_test_logger, Level}; 225 use tracing::warn; 226 227 use super::*; 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 if setup_test_logger( 259 &["sled", "runtime", "net"], 260 false, 261 Level::Info, 262 //Level::Verbose, 263 //Level::Debug, 264 //Level::Trace, 265 ) 266 .is_err() 267 { 268 warn!("Logger already initialized"); 269 } 270 271 // Ensure the configuration file exists 272 assert!(Path::new(CONFIG_PATH).exists()); 273 274 // Load the configuration 275 let config = ExplorerConfig::new(CONFIG_PATH.to_string()) 276 .expect("Failed to load configuration from file"); 277 278 // Validate the expected network 279 assert_eq!(config.network, ACTIVE_NETWORK); 280 281 // Validate the path is correctly set 282 assert_eq!(config.path.as_deref(), Some(CONFIG_PATH)); 283 284 // Validate that `active_network_config` correctly retrieves the testnet configuration 285 let active_config = config.active_network_config(); 286 assert!(active_config.is_some(), "Active network configuration should not be None."); 287 let active_config = active_config.unwrap(); 288 assert_eq!(active_config.database, NETWORK_CONFIGS[1].1); // Testnet database 289 assert_eq!(active_config.endpoint.to_string(), NETWORK_CONFIGS[1].2); 290 assert_eq!(&active_config.rpc.rpc_listen.to_string(), NETWORK_CONFIGS[1].3); 291 292 // Validate all network configurations values (localnet, testnet, mainnet) 293 for &(network, expected_db, expected_endpoint, expected_rpc) in NETWORK_CONFIGS { 294 let network_config = config.get_network_config(network); 295 296 if let Some(config) = network_config { 297 assert_eq!(config.database, expected_db); 298 assert_eq!(config.endpoint.to_string(), expected_endpoint); 299 assert_eq!(config.rpc.rpc_listen.to_string(), expected_rpc); 300 } else { 301 assert!(network_config.is_none(), "{network} configuration is missing"); 302 } 303 } 304 305 // Validate (path, network).try_into() 306 let config_path_buf = &PathBuf::from(CONFIG_PATH); 307 let mainnet_string = &String::from("mainnet"); 308 let mainnet_config: ExplorerNetworkConfig = (config_path_buf, mainnet_string) 309 .try_into() 310 .expect("Failed to load explorer network config"); 311 assert_eq!(mainnet_config.database, NETWORK_CONFIGS[2].1); // Mainnet database 312 assert_eq!(mainnet_config.endpoint.to_string(), NETWORK_CONFIGS[2].2); 313 assert_eq!(&mainnet_config.rpc.rpc_listen.to_string(), NETWORK_CONFIGS[2].3); 314 } 315 }