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)]
99 pub remove_existing_chain: bool,
100 #[arg(long)]
103 pub skip_load: Option<bool>,
104 #[arg(long)]
106 pub req_window: Option<usize>,
107 #[arg(long)]
110 pub tipset_sample_size: Option<u8>,
111 #[arg(long)]
113 pub target_peer_count: Option<u32>,
114 #[arg(long)]
116 pub encrypt_keystore: Option<bool>,
117 #[arg(long)]
119 pub chain: Option<NetworkChain>,
120 #[arg(long)]
123 pub auto_download_snapshot: bool,
124 #[arg(long, default_value = "auto")]
126 pub color: LoggingColor,
127 #[arg(long)]
129 pub tokio_console: bool,
130 #[arg(long)]
132 pub loki: bool,
133 #[arg(long, default_value = "http://127.0.0.1:3100")]
135 pub loki_endpoint: String,
136 #[arg(long)]
138 pub log_dir: Option<PathBuf>,
139 #[arg(long)]
141 pub exit_after_init: bool,
142 #[arg(long)]
144 pub save_token: Option<PathBuf>,
145 #[arg(long)]
147 pub no_gc: bool,
148 #[arg(long)]
150 pub stateless: bool,
151 #[arg(long)]
153 pub dry_run: bool,
154 #[arg(long)]
156 pub skip_load_actors: bool,
157}
158
159impl CliOpts {
160 pub fn to_config(&self) -> anyhow::Result<(Config, Option<ConfigPath>)> {
161 let (path, mut cfg) = read_config(self.config.as_ref(), self.chain.clone())?;
162
163 if let Some(genesis_file) = &self.genesis {
164 cfg.client.genesis_file = Some(genesis_file.to_owned());
165 }
166 if self.rpc.unwrap_or(cfg.client.enable_rpc) {
167 cfg.client.enable_rpc = true;
168 cfg.client.rpc_filter_list = self.rpc_filter_list.clone();
169 if let Some(rpc_address) = self.rpc_address {
170 cfg.client.rpc_address = rpc_address;
171 }
172 } else {
173 cfg.client.enable_rpc = false;
174 }
175
176 if self.no_healthcheck {
177 cfg.client.enable_health_check = false;
178 } else {
179 cfg.client.enable_health_check = true;
180 if let Some(healthcheck_address) = self.healthcheck_address {
181 cfg.client.healthcheck_address = healthcheck_address;
182 }
183 }
184
185 if self.no_metrics {
186 cfg.client.enable_metrics_endpoint = false;
187 } else {
188 cfg.client.enable_metrics_endpoint = true;
189 if let Some(metrics_address) = self.metrics_address {
190 cfg.client.metrics_address = metrics_address;
191 }
192 }
193
194 if let Some(addresses) = &self.p2p_listen_address {
195 cfg.network.listening_multiaddrs.clone_from(addresses);
196 }
197
198 if let Some(snapshot_path) = &self.import_snapshot {
199 cfg.client.snapshot_path = Some(snapshot_path.into());
200 cfg.client.import_mode = self.import_mode;
201 }
202
203 cfg.client.snapshot_height = self.height;
204 cfg.client.snapshot_head = self.head.map(|head| head as i64);
205 if let Some(skip_load) = self.skip_load {
206 cfg.client.skip_load = skip_load;
207 }
208
209 cfg.network.kademlia = self.kademlia.unwrap_or(cfg.network.kademlia);
210 cfg.network.mdns = self.mdns.unwrap_or(cfg.network.mdns);
211 if let Some(target_peer_count) = self.target_peer_count {
212 cfg.network.target_peer_count = target_peer_count;
213 }
214 if let Some(encrypt_keystore) = self.encrypt_keystore {
217 cfg.client.encrypt_keystore = encrypt_keystore;
218 }
219
220 cfg.client.load_actors = !self.skip_load_actors;
221
222 Ok((cfg, path))
223 }
224}
225
226#[derive(Default, Debug, Parser)]
228pub struct CliRpcOpts {
229 #[arg(long)]
231 pub token: Option<String>,
232}
233
234#[derive(Debug, PartialEq)]
235pub enum ConfigPath {
236 Cli(PathBuf),
237 Env(PathBuf),
238 Project(PathBuf),
239}
240
241impl ConfigPath {
242 pub fn to_path_buf(&self) -> &PathBuf {
243 match self {
244 ConfigPath::Cli(path) => path,
245 ConfigPath::Env(path) => path,
246 ConfigPath::Project(path) => path,
247 }
248 }
249}
250
251pub fn find_config_path(config: Option<&PathBuf>) -> Option<ConfigPath> {
252 if let Some(s) = config {
253 return Some(ConfigPath::Cli(s.to_owned()));
254 }
255 if let Ok(s) = std::env::var("FOREST_CONFIG_PATH") {
256 return Some(ConfigPath::Env(PathBuf::from(s)));
257 }
258 if let Some(dir) = ProjectDirs::from("com", "ChainSafe", "Forest") {
259 let path = dir.config_dir().join("config.toml");
260 if path.exists() {
261 return Some(ConfigPath::Project(path));
262 }
263 }
264 None
265}
266
267fn find_unknown_keys<'a>(
268 tables: Vec<&'a str>,
269 x: &'a toml::Value,
270 y: &'a toml::Value,
271 result: &mut Vec<(Vec<&'a str>, &'a str)>,
272) {
273 if let (toml::Value::Table(x_map), toml::Value::Table(y_map)) = (x, y) {
274 let x_set: HashSet<_> = x_map.keys().collect();
275 let y_set: HashSet<_> = y_map.keys().collect();
276 for k in x_set.difference(&y_set) {
277 result.push((tables.clone(), k));
278 }
279 for (x_key, x_value) in x_map.iter() {
280 if let Some(y_value) = y_map.get(x_key) {
281 let mut copy = tables.clone();
282 copy.push(x_key);
283 find_unknown_keys(copy, x_value, y_value, result);
284 }
285 }
286 }
287 if let (toml::Value::Array(x_vec), toml::Value::Array(y_vec)) = (x, y) {
288 for (x_value, y_value) in x_vec.iter().zip(y_vec.iter()) {
289 find_unknown_keys(tables.clone(), x_value, y_value, result);
290 }
291 }
292}
293
294pub fn check_for_unknown_keys(path: &Path, config: &Config) {
295 let file = std::fs::read_to_string(path).unwrap();
299 let value = toml::Value::Table(file.parse::<toml::Table>().unwrap());
300
301 let config_file = toml::to_string(config).unwrap();
302 let config_value = toml::Value::Table(config_file.parse::<toml::Table>().unwrap());
303
304 let mut result = vec![];
305 find_unknown_keys(vec![], &value, &config_value, &mut result);
306 for (tables, k) in result.iter() {
307 if tables.is_empty() {
308 error!("Unknown key `{k}` in top-level table");
309 } else {
310 error!("Unknown key `{k}` in [{}]", tables.join("."));
311 }
312 }
313 if !result.is_empty() {
314 let path = path.display();
315 cli_error_and_die(
316 format!("Error checking {path}. Verify that all keys are valid"),
317 1,
318 )
319 }
320}
321
322pub fn cli_error_and_die(msg: impl AsRef<str>, code: i32) -> ! {
325 error!("{}", msg.as_ref());
326 std::process::exit(code);
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn test_check_for_unknown_keys() {
335 let config = Config::default();
336 let config_content = toml::to_string(&config).unwrap();
337 let temp_file = tempfile::Builder::new().tempfile().unwrap();
338 std::fs::write(temp_file.path(), config_content).unwrap();
339 check_for_unknown_keys(temp_file.path(), &config);
340 }
341
342 #[test]
343 fn find_unknown_keys_must_work() {
344 let x: toml::Value = toml::from_str(
345 r#"
346 folklore = true
347 foo = "foo"
348 [myth]
349 author = 'H. P. Lovecraft'
350 entities = [
351 { name = 'Cthulhu' },
352 { name = 'Azathoth' },
353 { baz = 'Dagon' },
354 ]
355 bar = "bar"
356 "#,
357 )
358 .unwrap();
359
360 let y: toml::Value = toml::from_str(
361 r#"
362 folklore = true
363 [myth]
364 author = 'H. P. Lovecraft'
365 entities = [
366 { name = 'Cthulhu' },
367 { name = 'Azathoth' },
368 { name = 'Dagon' },
369 ]
370 "#,
371 )
372 .unwrap();
373
374 let mut result = vec![];
376 find_unknown_keys(vec![], &y, &y, &mut result);
377 assert!(result.is_empty());
378
379 let mut result = vec![];
381 find_unknown_keys(vec![], &x, &y, &mut result);
382 assert_eq!(
383 result,
384 vec![
385 (vec![], "foo"),
386 (vec!["myth"], "bar"),
387 (vec!["myth", "entities"], "baz"),
388 ]
389 );
390 }
391
392 #[test]
393 fn combination_of_import_snapshot_and_import_chain_should_fail() {
394 let options = CliOpts::default();
396 assert!(options.to_config().is_ok());
397
398 let options = CliOpts {
400 import_snapshot: Some("snapshot.car".into()),
401 ..Default::default()
402 };
403 assert!(options.to_config().is_ok());
404 }
405}