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
41pub 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 let parsed = dbg_out
61 .replace("\" \"", " ") .replace("\"", ""); 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
73pub 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
151pub(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
170pub(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 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
193pub 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 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 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}