/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2024 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use std::{ fmt, path::{Path, PathBuf}, str::FromStr, }; use serde::Deserialize; use structopt::StructOpt; use tracing::{debug, error}; use url::Url; use darkfi::{rpc::settings::RpcSettingsOpt, util::file::load_file, Error, Result}; /// Represents an explorer configuration #[derive(Clone, Debug, Deserialize, StructOpt)] pub struct ExplorerConfig { /// Current active network #[allow(dead_code)] // Part of the config file pub network: String, /// Supported network configurations pub network_config: NetworkConfigs, /// Path to the configuration if read from a file pub path: Option, } impl ExplorerConfig { /// Creates a new configuration from a given file path. /// If the file cannot be loaded or parsed, an error is returned. pub fn new(config_path: String) -> Result { // Load the configuration file from the specified path let config_content = load_file(Path::new(&config_path)).map_err(|err| { Error::ConfigError(format!( "Failed to read the configuration file {config_path}: {err:?}" )) })?; // Parse the loaded content into a configuration instance let mut config = toml::from_str::(&config_content).map_err(|e| { error!(target: "explorerd::config", "Failed parsing TOML config: {e}"); Error::ConfigError(format!("Failed to parse the configuration file {config_path}")) })?; // Set the configuration path config.path = Some(config_path); debug!(target: "explorerd::config", "Successfully loaded configuration: {config:?}"); Ok(config) } /// Returns the currently active network configuration. #[allow(dead_code)] // Test case currently using pub fn active_network_config(&self) -> Option { self.get_network_config(self.network.as_str()) } /// Returns the network configuration for specified network. pub fn get_network_config(&self, network: &str) -> Option { match network { "localnet" => self.network_config.localnet.clone(), "testnet" => self.network_config.testnet.clone(), "mainnet" => self.network_config.mainnet.clone(), _ => None, } } } /// Provides a default `ExplorerConfig` configuration using the `testnet` network. impl Default for ExplorerConfig { fn default() -> Self { Self { network: String::from("testnet"), network_config: NetworkConfigs::default(), path: None, } } } /// Attempts to convert a [`PathBuff`] to an [`ExplorerConfig`] by loading and parsing from specified file path. impl TryFrom<&PathBuf> for ExplorerConfig { type Error = Error; fn try_from(path: &PathBuf) -> Result { let path_str = path.to_str().ok_or_else(|| { Error::ConfigError("Unable to convert PathBuf to a valid UTF-8 path string".to_string()) })?; // Create configuration and return ExplorerConfig::new(path_str.to_string()) } } /// Deserializes a `&str` containing explorer content in TOML format into an [`ExplorerConfig`] instance. impl FromStr for ExplorerConfig { type Err = String; fn from_str(s: &str) -> std::result::Result { let config: ExplorerConfig = toml::from_str(s).map_err(|e| format!("Failed to parse ExplorerdConfig: {e}"))?; Ok(config) } } /// Represents network configurations for localnet, testnet, and mainnet. #[derive(Debug, Clone, Deserialize, StructOpt)] pub struct NetworkConfigs { /// Local network configuration pub localnet: Option, /// Testnet network configuration pub testnet: Option, /// Mainnet network configuration pub mainnet: Option, } /// Provides a default `NetworkConfigs` configuration using the `testnet` network. impl Default for NetworkConfigs { fn default() -> Self { NetworkConfigs { localnet: None, testnet: Some(ExplorerNetworkConfig::default()), mainnet: None, } } } /// Deserializes a `&str` containing network configs content in TOML format into an [`NetworkConfigs`] instance. impl FromStr for NetworkConfigs { type Err = String; fn from_str(s: &str) -> std::result::Result { let config: NetworkConfigs = toml::from_str(s).map_err(|e| format!("Failed to parse NetworkConfigs: {e}"))?; Ok(config) } } /// Struct representing the configuration for an explorer network. #[derive(Clone, Deserialize, StructOpt)] #[structopt()] #[serde(default)] pub struct ExplorerNetworkConfig { #[structopt(flatten)] /// JSON-RPC settings used to set up a server that the explorer listens on for incoming RPC requests. pub rpc: RpcSettingsOpt, #[structopt(long, default_value = "~/.local/share/darkfi/explorerd/testnet")] /// Path to the explorer's database. pub database: String, #[structopt(short, long, default_value = "tcp://127.0.0.1:8340")] /// Endpoint of the DarkFi node JSON-RPC server to sync with. pub endpoint: Url, } /// Attempts to convert a tuple `(PathBuf, &str)` representing a configuration file path /// and network name into an `ExplorerNetworkConfig`. impl TryFrom<(&PathBuf, &String)> for ExplorerNetworkConfig { type Error = Error; fn try_from(path_and_network: (&PathBuf, &String)) -> Result { // Load the ExplorerConfig from the given file path let config: ExplorerConfig = path_and_network.0.try_into()?; // Retrieve the network configuration for the specified network match config.get_network_config(path_and_network.1) { Some(config) => Ok(config), None => Err(Error::ConfigError(format!( "Failed to retrieve network configuration for network: {}", path_and_network.1 ))), } } } /// Provides a default `ExplorerNetworkConfig` instance using `structopt` default values defined /// in the `ExplorerNetworkConfig` struct. impl Default for ExplorerNetworkConfig { fn default() -> Self { Self::from_iter(&[""]) } } /// Provides a user-friendly debug view of the `ExplorerdNetworkConfig` configuration. impl fmt::Debug for ExplorerNetworkConfig { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut debug_struct = f.debug_struct("ExplorerdConfig"); debug_struct .field("rpc_listen", &self.rpc.rpc_listen.to_string().trim_end_matches('/')) .field("db_path", &self.database) .field("endpoint", &self.endpoint.to_string().trim_end_matches('/')); debug_struct.finish() } } /// Deserializes a `&str` containing network config content in TOML format into an [`ExplorerNetworkConfig`] instance. impl FromStr for ExplorerNetworkConfig { type Err = String; fn from_str(s: &str) -> std::result::Result { let config: ExplorerNetworkConfig = toml::from_str(s) .map_err(|e| format!("Failed to parse ExplorerdNetworkConfig: {e}"))?; Ok(config) } } #[cfg(test)] /// Contains test cases for validating the functionality and correctness of the `ExplorerConfig` /// and related components using a configuration loaded from a TOML file. mod tests { use std::path::Path; use super::*; use crate::test_utils::init_logger; /// Validates the functionality of initializing and interacting with `ExplorerConfig` /// loaded from a TOML file, ensuring correctness of the network-specific configurations. #[test] fn test_explorerd_config_from_file() { // Constants for expected configurations const CONFIG_PATH: &str = "explorerd_config.toml"; const ACTIVE_NETWORK: &str = "testnet"; const NETWORK_CONFIGS: &[(&str, &str, &str, &str)] = &[ ( "localnet", "~/.local/share/darkfi/explorerd/localnet", "tcp://127.0.0.1:8240/", "tcp://127.0.0.1:14567/", ), ( "testnet", "~/.local/share/darkfi/explorerd/testnet", "tcp://127.0.0.1:8340/", "tcp://127.0.0.1:14667/", ), ( "mainnet", "~/.local/share/darkfi/explorerd/mainnet", "tcp://127.0.0.1:8440/", "tcp://127.0.0.1:14767/", ), ]; init_logger(simplelog::LevelFilter::Info, vec!["sled", "runtime", "net"]); // Ensure the configuration file exists assert!(Path::new(CONFIG_PATH).exists()); // Load the configuration let config = ExplorerConfig::new(CONFIG_PATH.to_string()) .expect("Failed to load configuration from file"); // Validate the expected network assert_eq!(config.network, ACTIVE_NETWORK); // Validate the path is correctly set assert_eq!(config.path.as_deref(), Some(CONFIG_PATH)); // Validate that `active_network_config` correctly retrieves the testnet configuration let active_config = config.active_network_config(); assert!(active_config.is_some(), "Active network configuration should not be None."); let active_config = active_config.unwrap(); assert_eq!(active_config.database, NETWORK_CONFIGS[1].1); // Testnet database assert_eq!(active_config.endpoint.to_string(), NETWORK_CONFIGS[1].2); assert_eq!(&active_config.rpc.rpc_listen.to_string(), NETWORK_CONFIGS[1].3); // Validate all network configurations values (localnet, testnet, mainnet) for &(network, expected_db, expected_endpoint, expected_rpc) in NETWORK_CONFIGS { let network_config = config.get_network_config(network); if let Some(config) = network_config { assert_eq!(config.database, expected_db); assert_eq!(config.endpoint.to_string(), expected_endpoint); assert_eq!(config.rpc.rpc_listen.to_string(), expected_rpc); } else { assert!(network_config.is_none(), "{network} configuration is missing"); } } // Validate (path, network).try_into() let config_path_buf = &PathBuf::from(CONFIG_PATH); let mainnet_string = &String::from("mainnet"); let mainnet_config: ExplorerNetworkConfig = (config_path_buf, mainnet_string) .try_into() .expect("Failed to load explorer network config"); assert_eq!(mainnet_config.database, NETWORK_CONFIGS[2].1); // Mainnet database assert_eq!(mainnet_config.endpoint.to_string(), NETWORK_CONFIGS[2].2); assert_eq!(&mainnet_config.rpc.rpc_listen.to_string(), NETWORK_CONFIGS[2].3); } }