use crate::consts::{
DB_FOLDER, IGNITION_CONFIG_FOLDER_NAME, LOCAL_CONFIG_FOLDER_NAME, TESTNET_CONFIG_FOLDER_NAME,
};
use anyhow::{anyhow, Result};
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password};
use forc_util::user_forc_directory;
use fuel_crypto::{
rand::{prelude::StdRng, SeedableRng},
SecretKey,
};
use libp2p_identity::{secp256k1, Keypair, PeerId};
use semver::Version;
use serde::{Deserialize, Serialize};
use std::{
fmt::Display,
path::PathBuf,
process::{Command, Stdio},
};
use std::{
io::{Read, Write},
ops::Deref,
};
pub enum DbConfig {
Local,
Testnet,
Ignition,
}
impl From<DbConfig> for PathBuf {
fn from(value: DbConfig) -> Self {
let user_db_dir = user_forc_directory().join(DB_FOLDER);
match value {
DbConfig::Local => user_db_dir.join(LOCAL_CONFIG_FOLDER_NAME),
DbConfig::Testnet => user_db_dir.join(TESTNET_CONFIG_FOLDER_NAME),
DbConfig::Ignition => user_db_dir.join(IGNITION_CONFIG_FOLDER_NAME),
}
}
}
pub struct HumanReadableCommand<'a>(&'a Command);
impl Display for HumanReadableCommand<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let dbg_out = format!("{:?}", self.0);
let parsed = dbg_out
.replace("\" \"", " ") .replace("\"", ""); write!(f, "{parsed}")
}
}
impl<'a> From<&'a Command> for HumanReadableCommand<'a> {
fn from(value: &'a Command) -> Self {
Self(value)
}
}
pub struct HumanReadableConfig<'a>(pub &'a fuel_core::service::Config);
impl Display for HumanReadableConfig<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Fuel Core Configuration:")?;
writeln!(f, " GraphQL Address: {}", self.0.graphql_config.addr)?;
writeln!(f, " Continue on Error: {}", self.0.continue_on_error)?;
writeln!(f, " Debug Mode: {}", self.0.debug)?;
writeln!(f, " UTXO Validation: {}", self.0.utxo_validation)?;
writeln!(f, " Snapshot Reader: {:?}", self.0.snapshot_reader)?;
writeln!(
f,
" Database Type: {:?}",
self.0.combined_db_config.database_type
)?;
writeln!(
f,
" Database Path: {}",
self.0.combined_db_config.database_path.display()
)?;
Ok(())
}
}
impl<'a> From<&'a fuel_core::service::Config> for HumanReadableConfig<'a> {
fn from(value: &'a fuel_core::service::Config) -> Self {
Self(value)
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct KeyPair {
pub peer_id: String,
pub secret: String,
}
impl KeyPair {
pub fn random() -> Self {
let mut rng = StdRng::from_entropy();
let secret = SecretKey::random(&mut rng);
let mut bytes = *secret.deref();
let p2p_secret = secp256k1::SecretKey::try_from_bytes(&mut bytes)
.expect("Should be a valid private key");
let p2p_keypair = secp256k1::Keypair::from(p2p_secret);
let libp2p_keypair = Keypair::from(p2p_keypair);
let peer_id = PeerId::from_public_key(&libp2p_keypair.public());
Self {
peer_id: format!("{peer_id}"),
secret: format!("{secret}"),
}
}
}
pub(crate) fn ask_user_yes_no_question(question: &str) -> anyhow::Result<bool> {
let answer = Confirm::with_theme(&ColorfulTheme::default())
.with_prompt(question)
.default(false)
.show_default(false)
.interact()?;
Ok(answer)
}
pub(crate) fn ask_user_discreetly(question: &str) -> anyhow::Result<String> {
let discrete = Password::with_theme(&ColorfulTheme::default())
.with_prompt(question)
.interact()?;
Ok(discrete)
}
pub(crate) fn ask_user_string(question: &str) -> anyhow::Result<String> {
let response = Input::with_theme(&ColorfulTheme::default())
.with_prompt(question)
.interact_text()?;
Ok(response)
}
pub(crate) fn display_string_discreetly(
discreet_string: &str,
continue_message: &str,
) -> Result<()> {
use termion::screen::IntoAlternateScreen;
let mut screen = std::io::stdout().into_alternate_screen()?;
writeln!(screen, "{discreet_string}")?;
screen.flush()?;
println!("{continue_message}");
wait_for_keypress();
Ok(())
}
pub(crate) fn wait_for_keypress() {
let mut single_key = [0u8];
std::io::stdin().read_exact(&mut single_key).unwrap();
}
pub(crate) fn ask_user_keypair() -> Result<KeyPair> {
let has_keypair = ask_user_yes_no_question("Do you have a keypair in hand?")?;
if has_keypair {
let peer_id = ask_user_string("Peer Id:")?;
let secret = ask_user_discreetly("Secret:")?;
Ok(KeyPair { peer_id, secret })
} else {
println!("Generating new keypair...");
let pair = KeyPair::random();
display_string_discreetly(
&format!(
"Generated keypair:\n PeerID: {}, secret: {}",
pair.peer_id, pair.secret
),
"### Do not share or lose this private key! Press any key to complete. ###",
)?;
Ok(pair)
}
}
pub fn get_fuel_core_version() -> anyhow::Result<Version> {
let version_cmd = Command::new("fuel-core")
.arg("--version")
.stdout(Stdio::piped())
.output()
.expect("failed to run fuel-core, make sure that it is installed.");
let version_output = String::from_utf8_lossy(&version_cmd.stdout).to_string();
let version = version_output
.split_whitespace()
.last()
.ok_or_else(|| anyhow!("fuel-core version parse failed"))?;
let version_semver = Version::parse(version)?;
Ok(version_semver)
}
#[cfg(unix)]
pub fn check_open_fds_limit(max_files: u64) -> Result<(), Box<dyn std::error::Error>> {
use std::mem;
unsafe {
let mut fd_limit = mem::zeroed();
let mut err = libc::getrlimit(libc::RLIMIT_NOFILE, &mut fd_limit);
if err != 0 {
return Err("check_open_fds_limit failed".into());
}
if fd_limit.rlim_cur >= max_files {
return Ok(());
}
let prev_limit = fd_limit.rlim_cur;
fd_limit.rlim_cur = max_files;
if fd_limit.rlim_max < max_files {
fd_limit.rlim_max = max_files;
}
err = libc::setrlimit(libc::RLIMIT_NOFILE, &fd_limit);
if err == 0 {
return Ok(());
}
Err(format!(
"the maximum number of open file descriptors is too \
small, got {prev_limit}, expect greater or equal to {max_files}"
)
.into())
}
}
#[cfg(not(unix))]
pub fn check_open_fds_limit(_max_files: u64) -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use fuel_core::service::Config;
#[test]
fn test_human_readable_config() {
let config = Config::local_node();
let human_readable = HumanReadableConfig(&config);
let formatted = format!("{human_readable}");
let expected = format!(
r#"Fuel Core Configuration:
GraphQL Address: {}
Continue on Error: {}
Debug Mode: {}
UTXO Validation: {}
Snapshot Reader: {:?}
Database Type: {:?}
Database Path: {}
"#,
config.graphql_config.addr,
config.continue_on_error,
config.debug,
config.utxo_validation,
config.snapshot_reader,
config.combined_db_config.database_type,
config.combined_db_config.database_path.display()
);
assert_eq!(formatted, expected);
}
}