1use anyhow::{Context, Ok, Result, anyhow, bail};
2use eth_keystore::EthKeystore;
3use forc_tracing::println_warning;
4use home::home_dir;
5use std::{
6 fs,
7 io::{BufRead, Read, Write},
8 path::{Path, PathBuf},
9};
10
11pub const DEFAULT_DERIVATION_PATH_PREFIX: &str = "m/44'/1179993420'";
12
13pub fn user_fuel_dir() -> PathBuf {
15 const USER_FUEL_DIR: &str = ".fuel";
16 let home_dir = home_dir().expect("failed to retrieve user home directory");
17 home_dir.join(USER_FUEL_DIR)
18}
19
20pub fn user_fuel_wallets_dir() -> PathBuf {
22 const WALLETS_DIR: &str = "wallets";
23 user_fuel_dir().join(WALLETS_DIR)
24}
25
26pub fn user_fuel_wallets_accounts_dir() -> PathBuf {
28 const ACCOUNTS_DIR: &str = "accounts";
29 user_fuel_wallets_dir().join(ACCOUNTS_DIR)
30}
31
32pub fn default_wallet_path() -> PathBuf {
34 const DEFAULT_WALLET_FILE_NAME: &str = ".wallet";
35 user_fuel_wallets_dir().join(DEFAULT_WALLET_FILE_NAME)
36}
37
38pub fn load_wallet(wallet_path: &Path) -> Result<EthKeystore> {
40 let file = fs::File::open(wallet_path).map_err(|e| {
41 anyhow!(
42 "Failed to load a wallet from {wallet_path:?}: {e}.\n\
43 Please be sure to initialize a wallet before creating an account.\n\
44 To initialize a wallet, use `forc-wallet new`"
45 )
46 })?;
47 let reader = std::io::BufReader::new(file);
48 serde_json::from_reader(reader).map_err(|e| {
49 anyhow!(
50 "Failed to deserialize keystore from {wallet_path:?}: {e}.\n\
51 Please ensure that {wallet_path:?} is a valid wallet file."
52 )
53 })
54}
55
56pub(crate) fn wait_for_keypress() {
57 let mut single_key = [0u8];
58 std::io::stdin().read_exact(&mut single_key).unwrap();
59}
60
61pub(crate) fn get_derivation_path(account_index: usize) -> String {
63 format!("{DEFAULT_DERIVATION_PATH_PREFIX}/{account_index}'/0/0")
64}
65
66pub(crate) fn request_new_password() -> String {
67 let password =
68 rpassword::prompt_password("Please enter a password to encrypt this private key: ")
69 .unwrap();
70
71 let confirmation = rpassword::prompt_password("Please confirm your password: ").unwrap();
72
73 if password != confirmation {
74 println_warning("Passwords do not match -- try again!");
75 std::process::exit(1);
76 }
77 password
78}
79
80pub(crate) fn display_string_discreetly(
82 discreet_string: &str,
83 continue_message: &str,
84) -> Result<()> {
85 use termion::screen::IntoAlternateScreen;
86 let mut screen = std::io::stdout().into_alternate_screen()?;
87 writeln!(screen, "{discreet_string}")?;
88 screen.flush()?;
89 println!("{continue_message}");
90 wait_for_keypress();
91 Ok(())
92}
93
94pub(crate) fn write_wallet_from_mnemonic_and_password(
101 wallet_path: &Path,
102 mnemonic: &str,
103 password: &str,
104) -> Result<()> {
105 if wallet_path.exists() {
108 bail!(
109 "File or directory already exists at {wallet_path:?}. \
110 Remove the existing file, or provide a different path."
111 );
112 }
113
114 let wallet_dir = wallet_path
116 .parent()
117 .ok_or_else(|| anyhow!("failed to retrieve parent directory of {wallet_path:?}"))?;
118 std::fs::create_dir_all(wallet_dir)?;
119
120 let wallet_file_name = wallet_path
122 .file_name()
123 .and_then(|os_str| os_str.to_str())
124 .ok_or_else(|| anyhow!("failed to retrieve file name from {wallet_path:?}"))?;
125
126 eth_keystore::encrypt_key(
128 wallet_dir,
129 &mut rand::thread_rng(),
130 mnemonic,
131 password,
132 Some(wallet_file_name),
133 )
134 .with_context(|| format!("failed to create keystore at {wallet_path:?}"))
135 .map(|_| ())
136}
137
138pub(crate) fn ensure_no_wallet_exists(
142 wallet_path: &Path,
143 force: bool,
144 mut reader: impl BufRead,
145) -> Result<()> {
146 let remove_wallet = || {
147 if wallet_path.is_dir() {
148 fs::remove_dir_all(wallet_path).unwrap();
149 } else {
150 fs::remove_file(wallet_path).unwrap();
151 }
152 };
153
154 if wallet_path.exists() && fs::metadata(wallet_path)?.len() > 0 {
155 if force {
156 println_warning(&format!(
157 "Because the `--force` argument was supplied, the wallet at {} will be removed.",
158 wallet_path.display(),
159 ));
160 remove_wallet();
161 } else {
162 println_warning(&format!(
163 "There is an existing wallet at {}. \
164 Do you wish to replace it with a new wallet? (y/N) ",
165 wallet_path.display(),
166 ));
167 let mut need_replace = String::new();
168 reader.read_line(&mut need_replace).unwrap();
169 if need_replace.trim() == "y" {
170 remove_wallet();
171 } else {
172 bail!(
173 "Failed to create a new wallet at {} \
174 because a wallet already exists at that location.",
175 wallet_path.display(),
176 );
177 }
178 }
179 }
180 Ok(())
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::utils::test_utils::{TEST_MNEMONIC, TEST_PASSWORD};
187 const INPUT_NOP: &[u8; 1] = b"\n";
189 const INPUT_YES: &[u8; 2] = b"y\n";
190 const INPUT_NO: &[u8; 2] = b"n\n";
191
192 enum WalletSerializedState {
195 Empty,
196 WithData(String),
197 }
198
199 fn serialize_wallet_to_file(wallet_path: &Path, state: WalletSerializedState) {
202 if !wallet_path.exists() {
204 fs::File::create(wallet_path).unwrap();
205 }
206
207 if let WalletSerializedState::WithData(data) = state {
209 fs::write(wallet_path, data).unwrap();
210 }
211 }
212
213 fn remove_wallet(wallet_path: &Path) {
214 if wallet_path.exists() {
215 fs::remove_file(wallet_path).unwrap();
216 }
217 }
218
219 #[test]
220 fn handle_absolute_path_argument() {
221 let tmp_dir = tempfile::TempDir::new().unwrap();
222 let tmp_dir_abs = tmp_dir.path().canonicalize().unwrap();
223 let wallet_path = tmp_dir_abs.join("wallet.json");
224 write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
225 .unwrap();
226 load_wallet(&wallet_path).unwrap();
227 }
228
229 #[test]
230 fn handle_relative_path_argument() {
231 let wallet_path = Path::new("test-wallet.json");
232 let panic = std::panic::catch_unwind(|| {
233 write_wallet_from_mnemonic_and_password(wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
234 .unwrap();
235 load_wallet(wallet_path).unwrap();
236 });
237 let _ = std::fs::remove_file(wallet_path);
238 if let Err(e) = panic {
239 std::panic::resume_unwind(e);
240 }
241 }
242
243 #[test]
244 fn derivation_path() {
245 let derivation_path = get_derivation_path(0);
246 assert_eq!(derivation_path, "m/44'/1179993420'/0'/0/0");
247 }
248 #[test]
249 fn encrypt_and_save_phrase() {
250 let tmp_dir = tempfile::TempDir::new().unwrap();
251 let wallet_path = tmp_dir.path().join("wallet.json");
252 write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
253 .unwrap();
254 let phrase_recovered = eth_keystore::decrypt_key(wallet_path, TEST_PASSWORD).unwrap();
255 let phrase = String::from_utf8(phrase_recovered).unwrap();
256 assert_eq!(phrase, TEST_MNEMONIC)
257 }
258
259 #[test]
260 fn write_wallet() {
261 let tmp_dir = tempfile::TempDir::new().unwrap();
262 let wallet_path = tmp_dir.path().join("wallet.json");
263 write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
264 .unwrap();
265 load_wallet(&wallet_path).unwrap();
266 }
267
268 #[test]
269 #[should_panic]
270 fn write_wallet_to_existing_file_should_fail() {
271 let tmp_dir = tempfile::TempDir::new().unwrap();
272 let wallet_path = tmp_dir.path().join("wallet.json");
273 write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
274 .unwrap();
275 write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
276 .unwrap();
277 }
278
279 #[test]
280 fn write_wallet_subdir() {
281 let tmp_dir = tempfile::TempDir::new().unwrap();
282 let wallet_path = tmp_dir.path().join("path").join("to").join("wallet.json");
283 write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
284 .unwrap();
285 load_wallet(&wallet_path).unwrap();
286 }
287
288 #[test]
289 fn test_ensure_no_wallet_exists_no_wallet() {
290 let tmp_dir = tempfile::TempDir::new().unwrap();
291 let wallet_path = tmp_dir.path().join("wallet.json");
292 remove_wallet(&wallet_path);
293 ensure_no_wallet_exists(&wallet_path, false, &INPUT_NOP[..]).unwrap();
294 }
295
296 #[test]
297 fn test_ensure_no_wallet_exists_exists_wallet() {
298 let tmp_dir = tempfile::TempDir::new().unwrap();
300 let wallet_path = tmp_dir.path().join("wallet.json");
301 serialize_wallet_to_file(&wallet_path, WalletSerializedState::Empty);
302 ensure_no_wallet_exists(&wallet_path, false, &INPUT_YES[..]).unwrap();
303
304 let tmp_dir = tempfile::TempDir::new().unwrap();
306 let wallet_path = tmp_dir.path().join("empty_wallet.json");
307 serialize_wallet_to_file(&wallet_path, WalletSerializedState::Empty);
308
309 ensure_no_wallet_exists(&wallet_path, false, &INPUT_YES[..]).unwrap();
311 assert!(wallet_path.exists(), "Empty file should remain untouched");
312 }
313
314 #[test]
315 fn test_ensure_no_wallet_exists_nonempty_file() {
316 let tmp_dir = tempfile::TempDir::new().unwrap();
317 let wallet_path = tmp_dir.path().join("nonempty_wallet.json");
318
319 serialize_wallet_to_file(
321 &wallet_path,
322 WalletSerializedState::WithData("some wallet content".to_string()),
323 );
324
325 ensure_no_wallet_exists(&wallet_path, true, &INPUT_NO[..]).unwrap();
327 assert!(
328 !wallet_path.exists(),
329 "File should be removed with --force flag"
330 );
331
332 serialize_wallet_to_file(
334 &wallet_path,
335 WalletSerializedState::WithData("some wallet content".to_string()),
336 );
337 ensure_no_wallet_exists(&wallet_path, false, &INPUT_YES[..]).unwrap();
338 assert!(
339 !wallet_path.exists(),
340 "File should be removed after user confirmation"
341 );
342
343 serialize_wallet_to_file(
345 &wallet_path,
346 WalletSerializedState::WithData("some wallet content".to_string()),
347 );
348 let result = ensure_no_wallet_exists(&wallet_path, false, &INPUT_NO[..]);
349 assert!(
350 result.is_err(),
351 "Should error when user rejects file removal"
352 );
353 assert!(
354 wallet_path.exists(),
355 "File should remain when user rejects removal"
356 );
357 }
358}
359
360#[cfg(test)]
361pub(crate) mod test_utils {
362 use fuels::accounts::provider::Provider;
363 use serde_json::json;
364 use wiremock::{
365 Mock, MockServer, ResponseTemplate,
366 matchers::{method, path},
367 };
368
369 use super::*;
370 use std::{panic, path::Path};
371
372 pub(crate) const TEST_MNEMONIC: &str = "rapid mechanic escape victory bacon switch soda math embrace frozen novel document wait motor thrive ski addict ripple bid magnet horse merge brisk exile";
373 pub(crate) const TEST_PASSWORD: &str = "1234";
374
375 pub(crate) fn with_tmp_dir_and_wallet<F>(f: F)
377 where
378 F: FnOnce(&Path, &Path) + panic::UnwindSafe,
379 {
380 let tmp_dir = tempfile::TempDir::new().unwrap();
381 let wallet_path = tmp_dir.path().join("wallet.json");
382 write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
383 .unwrap();
384 f(tmp_dir.path(), &wallet_path);
385 }
386
387 pub(crate) async fn mock_provider() -> Provider {
390 let mock_server = MockServer::start().await;
391
392 let node_info_res_body = json!({
398 "data": {
399 "nodeInfo": {
400 "utxoValidation": true,
401 "vmBacktrace": false,
402 "maxTx": "160000",
403 "maxGas": "30000000000",
404 "maxSize": "131072000",
405 "maxDepth": "32",
406 "nodeVersion": "0.41.9",
407 "indexation": {
408 "balances": false,
409 "coinsToSpend": false,
410 "assetMetadata": false
411 },
412 "txPoolStats": {
413 "txCount": "0",
414 "totalGas": "0",
415 "totalSize": "0"
416 }
417 }
418 }
419 });
420
421 let node_info_response = ResponseTemplate::new(200).set_body_json(node_info_res_body);
422
423 Mock::given(method("POST"))
424 .and(path("/v1/graphql"))
425 .respond_with(node_info_response)
426 .mount(&mock_server)
427 .await;
428
429 Provider::connect(mock_server.uri())
430 .await
431 .expect("mock provider")
432 }
433}