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
30#[derive(Serialize, Deserialize, Debug)]
31pub struct KeyPair {
32    pub peer_id: String,
33    pub secret: String,
34}
35
36/// Given a `Command`, wrap it to enable generating the actual string that would
37/// create this command.
38/// Example:
39/// ```rust
40/// use std::process::Command;
41/// use forc_node::util::HumanReadableCommand;
42///
43/// let mut command = Command::new("fuel-core");
44/// command.arg("run");
45/// let command = HumanReadableCommand::from(&command);
46/// let formatted = format!("{command}");
47/// assert_eq!(&formatted, "fuel-core run");
48/// ```
49pub struct HumanReadableCommand<'a>(&'a Command);
50
51impl From<DbConfig> for PathBuf {
52    fn from(value: DbConfig) -> Self {
53        let user_db_dir = user_forc_directory().join(DB_FOLDER);
54        match value {
55            DbConfig::Local => user_db_dir.join(LOCAL_CONFIG_FOLDER_NAME),
56            DbConfig::Testnet => user_db_dir.join(TESTNET_CONFIG_FOLDER_NAME),
57            DbConfig::Ignition => user_db_dir.join(IGNITION_CONFIG_FOLDER_NAME),
58        }
59    }
60}
61
62impl Display for HumanReadableCommand<'_> {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        let dbg_out = format!("{:?}", self.0);
65        // This is in the ""command-name" "param-name" "param-val"" format.
66        let parsed = dbg_out
67            .replace("\" \"", " ") // replace " " between items with space
68            .replace("\"", ""); // remove remaining quotes at start/end
69        write!(f, "{parsed}")
70    }
71}
72
73impl<'a> From<&'a Command> for HumanReadableCommand<'a> {
74    fn from(value: &'a Command) -> Self {
75        Self(value)
76    }
77}
78
79impl KeyPair {
80    pub fn random() -> Self {
81        let mut rng = StdRng::from_entropy();
82        let secret = SecretKey::random(&mut rng);
83
84        let mut bytes = *secret.deref();
85        let p2p_secret = secp256k1::SecretKey::try_from_bytes(&mut bytes)
86            .expect("Should be a valid private key");
87        let p2p_keypair = secp256k1::Keypair::from(p2p_secret);
88        let libp2p_keypair = Keypair::from(p2p_keypair);
89        let peer_id = PeerId::from_public_key(&libp2p_keypair.public());
90        Self {
91            peer_id: format!("{peer_id}"),
92            secret: format!("{secret}"),
93        }
94    }
95}
96
97pub(crate) fn ask_user_yes_no_question(question: &str) -> anyhow::Result<bool> {
98    let answer = Confirm::with_theme(&ColorfulTheme::default())
99        .with_prompt(question)
100        .default(false)
101        .show_default(false)
102        .interact()?;
103    Ok(answer)
104}
105
106pub(crate) fn ask_user_discreetly(question: &str) -> anyhow::Result<String> {
107    let discrete = Password::with_theme(&ColorfulTheme::default())
108        .with_prompt(question)
109        .interact()?;
110    Ok(discrete)
111}
112
113pub(crate) fn ask_user_string(question: &str) -> anyhow::Result<String> {
114    let response = Input::with_theme(&ColorfulTheme::default())
115        .with_prompt(question)
116        .interact_text()?;
117    Ok(response)
118}
119
120/// Print a string to an alternate screen, so the string isn't printed to the terminal.
121pub(crate) fn display_string_discreetly(
122    discreet_string: &str,
123    continue_message: &str,
124) -> Result<()> {
125    use termion::screen::IntoAlternateScreen;
126    let mut screen = std::io::stdout().into_alternate_screen()?;
127    writeln!(screen, "{discreet_string}")?;
128    screen.flush()?;
129    println!("{continue_message}");
130    wait_for_keypress();
131    Ok(())
132}
133
134pub(crate) fn wait_for_keypress() {
135    let mut single_key = [0u8];
136    std::io::stdin().read_exact(&mut single_key).unwrap();
137}
138
139/// Ask if the user has a keypair generated and if so, collect the details.
140/// If not, bails out with a help message about how to generate a keypair.
141pub(crate) fn ask_user_keypair() -> Result<KeyPair> {
142    let has_keypair = ask_user_yes_no_question("Do you have a keypair in hand?")?;
143    if has_keypair {
144        // ask the keypair
145        let peer_id = ask_user_string("Peer Id:")?;
146        let secret = ask_user_discreetly("Secret:")?;
147        Ok(KeyPair { peer_id, secret })
148    } else {
149        println!("Generating new keypair...");
150        let pair = KeyPair::random();
151        display_string_discreetly(
152            &format!(
153                "Generated keypair:\n PeerID: {}, secret: {}",
154                pair.peer_id, pair.secret
155            ),
156            "### Do not share or lose this private key! Press any key to complete. ###",
157        )?;
158        Ok(pair)
159    }
160}
161
162/// Checks the local fuel-core's version that `forc-node` will be running.
163pub(crate) fn get_fuel_core_version() -> anyhow::Result<Version> {
164    let version_cmd = Command::new("fuel-core")
165        .arg("--version")
166        .stdout(Stdio::piped())
167        .output()
168        .expect("failed to run fuel-core, make sure that it is installed.");
169
170    let version_output = String::from_utf8_lossy(&version_cmd.stdout).to_string();
171
172    // Version output is `fuel-core <SEMVER VERSION>`. We should split it to only
173    // get the version part of it before parsing as semver.
174    let version = version_output
175        .split_whitespace()
176        .last()
177        .ok_or_else(|| anyhow!("fuel-core version parse failed"))?;
178    let version_semver = Version::parse(version)?;
179
180    Ok(version_semver)
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use std::process::Command;
187
188    #[test]
189    fn test_basic_command() {
190        let mut command = Command::new("fuel-core");
191        command.arg("run");
192        let human_readable = HumanReadableCommand(&command);
193        assert_eq!(format!("{human_readable}"), "fuel-core run");
194    }
195
196    #[test]
197    fn test_command_with_multiple_args() {
198        let mut command = Command::new("fuel-core");
199        command.arg("run");
200        command.arg("--config");
201        command.arg("config.toml");
202        let human_readable = HumanReadableCommand(&command);
203        assert_eq!(
204            format!("{human_readable}"),
205            "fuel-core run --config config.toml"
206        );
207    }
208
209    #[test]
210    fn test_command_no_args() {
211        let command = Command::new("fuel-core");
212        let human_readable = HumanReadableCommand(&command);
213        assert_eq!(format!("{human_readable}"), "fuel-core");
214    }
215
216    #[test]
217    fn test_command_with_path() {
218        let mut command = Command::new("fuel-core");
219        command.arg("--config");
220        command.arg("/path/to/config.toml");
221        let human_readable = HumanReadableCommand(&command);
222        assert_eq!(
223            format!("{human_readable}"),
224            "fuel-core --config /path/to/config.toml"
225        );
226    }
227}