forc_node/
util.rs

1use crate::consts::{
2    DB_FOLDER, IGNITION_CONFIG_FOLDER_NAME, LOCAL_CONFIG_FOLDER_NAME, TESTNET_CONFIG_FOLDER_NAME,
3};
4use anyhow::{anyhow, Result};
5use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password};
6use forc_util::user_forc_directory;
7use fuel_crypto::{
8    rand::{prelude::StdRng, SeedableRng},
9    SecretKey,
10};
11use libp2p_identity::{secp256k1, Keypair, PeerId};
12use semver::Version;
13use serde::{Deserialize, Serialize};
14use std::{
15    fmt::Display,
16    path::PathBuf,
17    process::{Command, Stdio},
18};
19use std::{
20    io::{Read, Write},
21    ops::Deref,
22};
23
24pub enum DbConfig {
25    Local,
26    Testnet,
27    Ignition,
28}
29
30impl From<DbConfig> for PathBuf {
31    fn from(value: DbConfig) -> Self {
32        let user_db_dir = user_forc_directory().join(DB_FOLDER);
33        match value {
34            DbConfig::Local => user_db_dir.join(LOCAL_CONFIG_FOLDER_NAME),
35            DbConfig::Testnet => user_db_dir.join(TESTNET_CONFIG_FOLDER_NAME),
36            DbConfig::Ignition => user_db_dir.join(IGNITION_CONFIG_FOLDER_NAME),
37        }
38    }
39}
40
41/// Given a `Command`, wrap it to enable generating the actual string that would
42/// create this command.
43/// Example:
44/// ```rust
45/// use std::process::Command;
46/// use forc_node::util::HumanReadableCommand;
47///
48/// let mut command = Command::new("fuel-core");
49/// command.arg("run");
50/// let command = HumanReadableCommand::from(&command);
51/// let formatted = format!("{command}");
52/// assert_eq!(&formatted, "fuel-core run");
53/// ```
54pub struct HumanReadableCommand<'a>(&'a Command);
55
56impl Display for HumanReadableCommand<'_> {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        let dbg_out = format!("{:?}", self.0);
59        // This is in the ""command-name" "param-name" "param-val"" format.
60        let parsed = dbg_out
61            .replace("\" \"", " ") // replace " " between items with space
62            .replace("\"", ""); // remove remaining quotes at start/end
63        write!(f, "{parsed}")
64    }
65}
66
67impl<'a> From<&'a Command> for HumanReadableCommand<'a> {
68    fn from(value: &'a Command) -> Self {
69        Self(value)
70    }
71}
72
73/// Display a fuel_core::service::Config in a human-readable format
74pub struct HumanReadableConfig<'a>(pub &'a fuel_core::service::Config);
75
76impl Display for HumanReadableConfig<'_> {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        writeln!(f, "Fuel Core Configuration:")?;
79        writeln!(f, "  GraphQL Address: {}", self.0.graphql_config.addr)?;
80        writeln!(f, "  Continue on Error: {}", self.0.continue_on_error)?;
81        writeln!(f, "  Debug Mode: {}", self.0.debug)?;
82        writeln!(f, "  UTXO Validation: {}", self.0.utxo_validation)?;
83        writeln!(f, "  Snapshot Reader: {:?}", self.0.snapshot_reader)?;
84        writeln!(
85            f,
86            "  Database Type: {:?}",
87            self.0.combined_db_config.database_type
88        )?;
89        writeln!(
90            f,
91            "  Database Path: {}",
92            self.0.combined_db_config.database_path.display()
93        )?;
94        Ok(())
95    }
96}
97
98impl<'a> From<&'a fuel_core::service::Config> for HumanReadableConfig<'a> {
99    fn from(value: &'a fuel_core::service::Config) -> Self {
100        Self(value)
101    }
102}
103
104#[derive(Serialize, Deserialize, Debug)]
105pub struct KeyPair {
106    pub peer_id: String,
107    pub secret: String,
108}
109
110impl KeyPair {
111    pub fn random() -> Self {
112        let mut rng = StdRng::from_entropy();
113        let secret = SecretKey::random(&mut rng);
114
115        let mut bytes = *secret.deref();
116        let p2p_secret = secp256k1::SecretKey::try_from_bytes(&mut bytes)
117            .expect("Should be a valid private key");
118        let p2p_keypair = secp256k1::Keypair::from(p2p_secret);
119        let libp2p_keypair = Keypair::from(p2p_keypair);
120        let peer_id = PeerId::from_public_key(&libp2p_keypair.public());
121        Self {
122            peer_id: format!("{peer_id}"),
123            secret: format!("{secret}"),
124        }
125    }
126}
127
128pub(crate) fn ask_user_yes_no_question(question: &str) -> anyhow::Result<bool> {
129    let answer = Confirm::with_theme(&ColorfulTheme::default())
130        .with_prompt(question)
131        .default(false)
132        .show_default(false)
133        .interact()?;
134    Ok(answer)
135}
136
137pub(crate) fn ask_user_discreetly(question: &str) -> anyhow::Result<String> {
138    let discrete = Password::with_theme(&ColorfulTheme::default())
139        .with_prompt(question)
140        .interact()?;
141    Ok(discrete)
142}
143
144pub(crate) fn ask_user_string(question: &str) -> anyhow::Result<String> {
145    let response = Input::with_theme(&ColorfulTheme::default())
146        .with_prompt(question)
147        .interact_text()?;
148    Ok(response)
149}
150
151/// Print a string to an alternate screen, so the string isn't printed to the terminal.
152pub(crate) fn display_string_discreetly(
153    discreet_string: &str,
154    continue_message: &str,
155) -> Result<()> {
156    use termion::screen::IntoAlternateScreen;
157    let mut screen = std::io::stdout().into_alternate_screen()?;
158    writeln!(screen, "{discreet_string}")?;
159    screen.flush()?;
160    println!("{continue_message}");
161    wait_for_keypress();
162    Ok(())
163}
164
165pub(crate) fn wait_for_keypress() {
166    let mut single_key = [0u8];
167    std::io::stdin().read_exact(&mut single_key).unwrap();
168}
169
170/// Ask if the user has a keypair generated and if so, collect the details.
171/// If not, bails out with a help message about how to generate a keypair.
172pub(crate) fn ask_user_keypair() -> Result<KeyPair> {
173    let has_keypair = ask_user_yes_no_question("Do you have a keypair in hand?")?;
174    if has_keypair {
175        // ask the keypair
176        let peer_id = ask_user_string("Peer Id:")?;
177        let secret = ask_user_discreetly("Secret:")?;
178        Ok(KeyPair { peer_id, secret })
179    } else {
180        println!("Generating new keypair...");
181        let pair = KeyPair::random();
182        display_string_discreetly(
183            &format!(
184                "Generated keypair:\n PeerID: {}, secret: {}",
185                pair.peer_id, pair.secret
186            ),
187            "### Do not share or lose this private key! Press any key to complete. ###",
188        )?;
189        Ok(pair)
190    }
191}
192
193/// Checks the local fuel-core's version that `forc-node` will be running.
194pub fn get_fuel_core_version() -> anyhow::Result<Version> {
195    let version_cmd = Command::new("fuel-core")
196        .arg("--version")
197        .stdout(Stdio::piped())
198        .output()
199        .expect("failed to run fuel-core, make sure that it is installed.");
200
201    let version_output = String::from_utf8_lossy(&version_cmd.stdout).to_string();
202
203    // Version output is `fuel-core <SEMVER VERSION>`. We should split it to only
204    // get the version part of it before parsing as semver.
205    let version = version_output
206        .split_whitespace()
207        .last()
208        .ok_or_else(|| anyhow!("fuel-core version parse failed"))?;
209    let version_semver = Version::parse(version)?;
210
211    Ok(version_semver)
212}
213
214#[cfg(unix)]
215pub fn check_open_fds_limit(max_files: u64) -> Result<(), Box<dyn std::error::Error>> {
216    use std::mem;
217
218    unsafe {
219        let mut fd_limit = mem::zeroed();
220        let mut err = libc::getrlimit(libc::RLIMIT_NOFILE, &mut fd_limit);
221        if err != 0 {
222            return Err("check_open_fds_limit failed".into());
223        }
224        if fd_limit.rlim_cur >= max_files {
225            return Ok(());
226        }
227
228        let prev_limit = fd_limit.rlim_cur;
229        fd_limit.rlim_cur = max_files;
230        if fd_limit.rlim_max < max_files {
231            // If the process is not started by privileged user, this will fail.
232            fd_limit.rlim_max = max_files;
233        }
234        err = libc::setrlimit(libc::RLIMIT_NOFILE, &fd_limit);
235        if err == 0 {
236            return Ok(());
237        }
238        Err(format!(
239            "the maximum number of open file descriptors is too \
240             small, got {prev_limit}, expect greater or equal to {max_files}"
241        )
242        .into())
243    }
244}
245
246#[cfg(not(unix))]
247pub fn check_open_fds_limit(_max_files: u64) -> Result<(), Box<dyn std::error::Error>> {
248    Ok(())
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use fuel_core::service::Config;
255
256    #[test]
257    fn test_human_readable_config() {
258        let config = Config::local_node();
259        let human_readable = HumanReadableConfig(&config);
260        let formatted = format!("{human_readable}");
261        let expected = format!(
262            r#"Fuel Core Configuration:
263  GraphQL Address: {}
264  Continue on Error: {}
265  Debug Mode: {}
266  UTXO Validation: {}
267  Snapshot Reader: {:?}
268  Database Type: {:?}
269  Database Path: {}
270"#,
271            config.graphql_config.addr,
272            config.continue_on_error,
273            config.debug,
274            config.utxo_validation,
275            config.snapshot_reader,
276            config.combined_db_config.database_type,
277            config.combined_db_config.database_path.display()
278        );
279        assert_eq!(formatted, expected);
280    }
281}