/ bin / explorer / explorerd / src / config.rs
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  }