1mod client;
5mod completion_cmd;
6mod config;
7
8use std::{
9 net::SocketAddr,
10 path::{Path, PathBuf},
11};
12
13use crate::networks::NetworkChain;
14use crate::utils::misc::LoggingColor;
15use crate::{cli_shared::read_config, daemon::db_util::ImportMode};
16use ahash::HashSet;
17use clap::Parser;
18use directories::ProjectDirs;
19use libp2p::Multiaddr;
20use tracing::error;
21
22pub use self::{client::*, completion_cmd::*, config::*};
23
24pub static HELP_MESSAGE: &str = "\
25{name} {version}
26{author}
27{about}
28
29USAGE:
30 {usage}
31
32SUBCOMMANDS:
33{subcommands}
34
35OPTIONS:
36{options}
37";
38
39#[derive(Default, Debug, Parser)]
41pub struct CliOpts {
42 #[arg(long)]
44 pub config: Option<PathBuf>,
45 #[arg(long)]
47 pub genesis: Option<PathBuf>,
48 #[arg(long)]
50 pub rpc: Option<bool>,
51 #[arg(long)]
53 pub no_metrics: bool,
54 #[arg(long)]
57 pub metrics_address: Option<SocketAddr>,
58 #[arg(long)]
60 pub rpc_address: Option<SocketAddr>,
61 #[arg(long)]
63 pub rpc_filter_list: Option<PathBuf>,
64 #[arg(long)]
66 pub no_healthcheck: bool,
67 #[arg(long)]
69 pub healthcheck_address: Option<SocketAddr>,
70 #[arg(long)]
72 pub p2p_listen_address: Option<Vec<Multiaddr>>,
73 #[arg(long)]
75 pub kademlia: Option<bool>,
76 #[arg(long)]
78 pub mdns: Option<bool>,
79 #[arg(long)]
82 pub height: Option<i64>,
83 #[arg(long)]
87 pub head: Option<u64>,
88 #[arg(long)]
90 pub import_snapshot: Option<String>,
91 #[arg(long, default_value = "auto")]
93 pub import_mode: ImportMode,
94 #[arg(long)]
96 pub halt_after_import: bool,
97 #[arg(long)]
100 pub skip_load: Option<bool>,
101 #[arg(long)]
103 pub req_window: Option<usize>,
104 #[arg(long)]
107 pub tipset_sample_size: Option<u8>,
108 #[arg(long)]
110 pub target_peer_count: Option<u32>,
111 #[arg(long)]
113 pub encrypt_keystore: Option<bool>,
114 #[arg(long)]
116 pub chain: Option<NetworkChain>,
117 #[arg(long)]
120 pub auto_download_snapshot: bool,
121 #[arg(long, default_value = "auto")]
123 pub color: LoggingColor,
124 #[arg(long)]
126 pub tokio_console: bool,
127 #[arg(long)]
129 pub loki: bool,
130 #[arg(long, default_value = "http://127.0.0.1:3100")]
132 pub loki_endpoint: String,
133 #[arg(long)]
135 pub log_dir: Option<PathBuf>,
136 #[arg(long)]
138 pub exit_after_init: bool,
139 #[arg(long)]
141 pub save_token: Option<PathBuf>,
142 #[arg(long)]
144 pub no_gc: bool,
145 #[arg(long)]
147 pub stateless: bool,
148 #[arg(long)]
150 pub dry_run: bool,
151 #[arg(long)]
153 pub skip_load_actors: bool,
154}
155
156impl CliOpts {
157 pub fn to_config(&self) -> Result<(Config, Option<ConfigPath>), anyhow::Error> {
158 let (path, mut cfg) = read_config(self.config.as_ref(), self.chain.clone())?;
159
160 if let Some(genesis_file) = &self.genesis {
161 cfg.client.genesis_file = Some(genesis_file.to_owned());
162 }
163 if self.rpc.unwrap_or(cfg.client.enable_rpc) {
164 cfg.client.enable_rpc = true;
165 cfg.client.rpc_filter_list = self.rpc_filter_list.clone();
166 if let Some(rpc_address) = self.rpc_address {
167 cfg.client.rpc_address = rpc_address;
168 }
169 } else {
170 cfg.client.enable_rpc = false;
171 }
172
173 if self.no_healthcheck {
174 cfg.client.enable_health_check = false;
175 } else {
176 cfg.client.enable_health_check = true;
177 if let Some(healthcheck_address) = self.healthcheck_address {
178 cfg.client.healthcheck_address = healthcheck_address;
179 }
180 }
181
182 if self.no_metrics {
183 cfg.client.enable_metrics_endpoint = false;
184 } else {
185 cfg.client.enable_metrics_endpoint = true;
186 if let Some(metrics_address) = self.metrics_address {
187 cfg.client.metrics_address = metrics_address;
188 }
189 }
190
191 if let Some(addresses) = &self.p2p_listen_address {
192 cfg.network.listening_multiaddrs.clone_from(addresses);
193 }
194
195 if let Some(snapshot_path) = &self.import_snapshot {
196 cfg.client.snapshot_path = Some(snapshot_path.into());
197 cfg.client.import_mode = self.import_mode;
198 }
199
200 cfg.client.snapshot_height = self.height;
201 cfg.client.snapshot_head = self.head.map(|head| head as i64);
202 if let Some(skip_load) = self.skip_load {
203 cfg.client.skip_load = skip_load;
204 }
205
206 cfg.network.kademlia = self.kademlia.unwrap_or(cfg.network.kademlia);
207 cfg.network.mdns = self.mdns.unwrap_or(cfg.network.mdns);
208 if let Some(target_peer_count) = self.target_peer_count {
209 cfg.network.target_peer_count = target_peer_count;
210 }
211 if let Some(encrypt_keystore) = self.encrypt_keystore {
214 cfg.client.encrypt_keystore = encrypt_keystore;
215 }
216
217 cfg.client.load_actors = !self.skip_load_actors;
218
219 Ok((cfg, path))
220 }
221}
222
223#[derive(Default, Debug, Parser)]
225pub struct CliRpcOpts {
226 #[arg(long)]
228 pub token: Option<String>,
229}
230
231#[derive(Debug, PartialEq)]
232pub enum ConfigPath {
233 Cli(PathBuf),
234 Env(PathBuf),
235 Project(PathBuf),
236}
237
238impl ConfigPath {
239 pub fn to_path_buf(&self) -> &PathBuf {
240 match self {
241 ConfigPath::Cli(path) => path,
242 ConfigPath::Env(path) => path,
243 ConfigPath::Project(path) => path,
244 }
245 }
246}
247
248pub fn find_config_path(config: Option<&PathBuf>) -> Option<ConfigPath> {
249 if let Some(s) = config {
250 return Some(ConfigPath::Cli(s.to_owned()));
251 }
252 if let Ok(s) = std::env::var("FOREST_CONFIG_PATH") {
253 return Some(ConfigPath::Env(PathBuf::from(s)));
254 }
255 if let Some(dir) = ProjectDirs::from("com", "ChainSafe", "Forest") {
256 let path = dir.config_dir().join("config.toml");
257 if path.exists() {
258 return Some(ConfigPath::Project(path));
259 }
260 }
261 None
262}
263
264fn find_unknown_keys<'a>(
265 tables: Vec<&'a str>,
266 x: &'a toml::Value,
267 y: &'a toml::Value,
268 result: &mut Vec<(Vec<&'a str>, &'a str)>,
269) {
270 if let (toml::Value::Table(x_map), toml::Value::Table(y_map)) = (x, y) {
271 let x_set: HashSet<_> = x_map.keys().collect();
272 let y_set: HashSet<_> = y_map.keys().collect();
273 for k in x_set.difference(&y_set) {
274 result.push((tables.clone(), k));
275 }
276 for (x_key, x_value) in x_map.iter() {
277 if let Some(y_value) = y_map.get(x_key) {
278 let mut copy = tables.clone();
279 copy.push(x_key);
280 find_unknown_keys(copy, x_value, y_value, result);
281 }
282 }
283 }
284 if let (toml::Value::Array(x_vec), toml::Value::Array(y_vec)) = (x, y) {
285 for (x_value, y_value) in x_vec.iter().zip(y_vec.iter()) {
286 find_unknown_keys(tables.clone(), x_value, y_value, result);
287 }
288 }
289}
290
291pub fn check_for_unknown_keys(path: &Path, config: &Config) {
292 let file = std::fs::read_to_string(path).unwrap();
296 let value = file.parse::<toml::Value>().unwrap();
297
298 let config_file = toml::to_string(config).unwrap();
299 let config_value = config_file.parse::<toml::Value>().unwrap();
300
301 let mut result = vec![];
302 find_unknown_keys(vec![], &value, &config_value, &mut result);
303 for (tables, k) in result.iter() {
304 if tables.is_empty() {
305 error!("Unknown key `{k}` in top-level table");
306 } else {
307 error!("Unknown key `{k}` in [{}]", tables.join("."));
308 }
309 }
310 if !result.is_empty() {
311 let path = path.display();
312 cli_error_and_die(
313 format!("Error checking {path}. Verify that all keys are valid"),
314 1,
315 )
316 }
317}
318
319pub fn cli_error_and_die(msg: impl AsRef<str>, code: i32) -> ! {
322 error!("{}", msg.as_ref());
323 std::process::exit(code);
324}
325
326#[cfg(test)]
327mod tests {
328
329 use super::*;
330
331 #[test]
332 fn find_unknown_keys_must_work() {
333 let x: toml::Value = toml::from_str(
334 r#"
335 folklore = true
336 foo = "foo"
337 [myth]
338 author = 'H. P. Lovecraft'
339 entities = [
340 { name = 'Cthulhu' },
341 { name = 'Azathoth' },
342 { baz = 'Dagon' },
343 ]
344 bar = "bar"
345 "#,
346 )
347 .unwrap();
348
349 let y: toml::Value = toml::from_str(
350 r#"
351 folklore = true
352 [myth]
353 author = 'H. P. Lovecraft'
354 entities = [
355 { name = 'Cthulhu' },
356 { name = 'Azathoth' },
357 { name = 'Dagon' },
358 ]
359 "#,
360 )
361 .unwrap();
362
363 let mut result = vec![];
365 find_unknown_keys(vec![], &y, &y, &mut result);
366 assert!(result.is_empty());
367
368 let mut result = vec![];
370 find_unknown_keys(vec![], &x, &y, &mut result);
371 assert_eq!(
372 result,
373 vec![
374 (vec![], "foo"),
375 (vec!["myth"], "bar"),
376 (vec!["myth", "entities"], "baz"),
377 ]
378 );
379 }
380
381 #[test]
382 fn combination_of_import_snapshot_and_import_chain_should_fail() {
383 let options = CliOpts::default();
385 assert!(options.to_config().is_ok());
386
387 let options = CliOpts {
389 import_snapshot: Some("snapshot.car".into()),
390 ..Default::default()
391 };
392 assert!(options.to_config().is_ok());
393 }
394}