Skip to content

Commit e0b0d8c

Browse files
zees-devkayagokalp
andauthored
Fix minor wallet issues (#220)
# Changelog - added [`tempfile`](https://crates.io/crates/tempfile) to create test dirs for testing - updated test functions and util functions accordingly (removed redundant helper functions and replaced references) - added `rust-toolchain.toml` with stable rust `1.80.0` - minor refactors to CI workflows to match the `rust-toolchain.toml` rust version - git ignoring IDE directory (`.vscode/`) - fixed `new_at_index` function to print checksum wallet address (currently printing bech32 address to stdout) - updated `ensure_no_wallet_exists` function to handle the following cases: - where wallet path is a directory - where wallet path is an empty file - this also addresses: #186 - added unit tests for `ensure_no_wallet_exists` --------- Co-authored-by: Kaya Gokalp <[email protected]>
1 parent c78954e commit e0b0d8c

File tree

8 files changed

+152
-114
lines changed

8 files changed

+152
-114
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ env:
1212
CARGO_TERM_COLOR: always
1313
RUSTFLAGS: -D warnings
1414
REGISTRY: ghcr.io
15-
RUST_VERSION_COV: nightly-2024-06-05
15+
RUST_VERSION: 1.82.0
16+
NIGHTLY_RUST_VERSION: nightly-2024-10-28
1617

1718
jobs:
1819
cancel-previous-runs:
@@ -29,17 +30,17 @@ jobs:
2930
permissions: # Write access to push changes to pages
3031
contents: write
3132
steps:
32-
- uses: actions/checkout@v3
33+
- uses: actions/checkout@v4
3334
- name: Install latest Rust
3435
uses: dtolnay/rust-toolchain@master
3536
with:
36-
toolchain: ${{ env.RUST_VERSION_COV }}
37+
toolchain: ${{ env.NIGHTLY_RUST_VERSION }}
3738

3839
- name: Install cargo-llvm-codecov
3940
uses: taiki-e/install-action@cargo-llvm-cov
4041

4142
- name: Code coverage report
42-
run: cargo +${{ env.RUST_VERSION_COV }} llvm-cov --all-features --lcov --branch --output-path lcov.info
43+
run: cargo +${{ env.NIGHTLY_RUST_VERSION }} llvm-cov --all-features --lcov --branch --output-path lcov.info
4344

4445
- name: Setup LCOV
4546
uses: hrishikesh-kadam/setup-lcov@v1

.github/workflows/markdown-lint.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ jobs:
1313
name: Markdown Lint
1414
runs-on: ubuntu-latest
1515
steps:
16-
- uses: actions/checkout@v3
17-
- uses: actions/setup-node@v3
16+
- uses: actions/checkout@v4
17+
- uses: actions/setup-node@v4
1818
with:
1919
node-version: 18
2020
- run: |

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1+
2+
# IDE
3+
.vscode
4+
5+
# Output
16
/target

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,6 @@ path = "src/lib.rs"
3939
[[bin]]
4040
name = "forc-wallet"
4141
path = "src/main.rs"
42+
43+
[dev-dependencies]
44+
tempfile = "3.13.0"

rust-toolchain.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[toolchain]
2+
profile = "default" # include rustfmt, clippy
3+
channel = "1.82.0"

src/account.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -453,18 +453,15 @@ pub(crate) fn derive_account(
453453
Ok(derive_account_unlocked(wallet_path, account_ix, password)?.lock())
454454
}
455455

456-
fn new_at_index(
457-
keystore: &EthKeystore,
458-
wallet_path: &Path,
459-
account_ix: usize,
460-
) -> Result<Bech32Address> {
456+
fn new_at_index(keystore: &EthKeystore, wallet_path: &Path, account_ix: usize) -> Result<String> {
461457
let prompt = format!("Please enter your wallet password to derive account {account_ix}: ");
462458
let password = rpassword::prompt_password(prompt)?;
463459
let account = derive_account(wallet_path, account_ix, &password)?;
464460
let account_addr = account.address();
465461
cache_address(&keystore.crypto.ciphertext, account_ix, account_addr)?;
466-
println!("Wallet address: {account_addr}");
467-
Ok(account_addr.clone())
462+
let checksum_addr = checksum_encode(&Address::from(account_addr).to_string())?;
463+
println!("Wallet address: {checksum_addr}");
464+
Ok(checksum_addr)
468465
}
469466

470467
pub fn new_at_index_cli(wallet_path: &Path, account_ix: usize) -> Result<()> {

src/utils.rs

Lines changed: 129 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,21 @@ pub(crate) fn ensure_no_wallet_exists(
142142
force: bool,
143143
mut reader: impl BufRead,
144144
) -> Result<()> {
145-
if wallet_path.exists() {
145+
let remove_wallet = || {
146+
if wallet_path.is_dir() {
147+
fs::remove_dir_all(wallet_path).unwrap();
148+
} else {
149+
fs::remove_file(wallet_path).unwrap();
150+
}
151+
};
152+
153+
if wallet_path.exists() && fs::metadata(wallet_path)?.len() > 0 {
146154
if force {
147155
println_warning(&format!(
148156
"Because the `--force` argument was supplied, the wallet at {} will be removed.",
149157
wallet_path.display(),
150158
));
151-
fs::remove_file(wallet_path).unwrap();
159+
remove_wallet();
152160
} else {
153161
println_warning(&format!(
154162
"There is an existing wallet at {}. \
@@ -158,7 +166,7 @@ pub(crate) fn ensure_no_wallet_exists(
158166
let mut need_replace = String::new();
159167
reader.read_line(&mut need_replace).unwrap();
160168
if need_replace.trim() == "y" {
161-
fs::remove_file(wallet_path).unwrap();
169+
remove_wallet();
162170
} else {
163171
bail!(
164172
"Failed to create a new wallet at {} \
@@ -174,32 +182,47 @@ pub(crate) fn ensure_no_wallet_exists(
174182
#[cfg(test)]
175183
mod tests {
176184
use super::*;
177-
use crate::utils::test_utils::{with_tmp_dir, TEST_MNEMONIC, TEST_PASSWORD};
185+
use crate::utils::test_utils::{TEST_MNEMONIC, TEST_PASSWORD};
178186
// simulate input
179187
const INPUT_NOP: &[u8; 1] = b"\n";
180188
const INPUT_YES: &[u8; 2] = b"y\n";
181189
const INPUT_NO: &[u8; 2] = b"n\n";
182190

183-
fn remove_wallet(wallet_path: &Path) {
184-
if wallet_path.exists() {
185-
fs::remove_file(wallet_path).unwrap();
186-
}
191+
/// Represents the possible serialized states of a wallet.
192+
/// Used primarily for simulating wallet creation and serialization processes.
193+
enum WalletSerializedState {
194+
Empty,
195+
WithData(String),
187196
}
188-
fn create_wallet(wallet_path: &Path) {
197+
198+
/// Simulates the serialization of a wallet to a file, optionally including dummy data.
199+
/// Primarily used to test if checks for wallet file existence are functioning correctly.
200+
fn serialize_wallet_to_file(wallet_path: &Path, state: WalletSerializedState) {
201+
// Create the wallet file if it does not exist.
189202
if !wallet_path.exists() {
190203
fs::File::create(wallet_path).unwrap();
191204
}
205+
206+
// Write content to the wallet file based on the specified state.
207+
if let WalletSerializedState::WithData(data) = state {
208+
fs::write(wallet_path, data).unwrap();
209+
}
210+
}
211+
212+
fn remove_wallet(wallet_path: &Path) {
213+
if wallet_path.exists() {
214+
fs::remove_file(wallet_path).unwrap();
215+
}
192216
}
193217

194218
#[test]
195219
fn handle_absolute_path_argument() {
196-
with_tmp_dir(|tmp_dir| {
197-
let tmp_dir_abs = tmp_dir.canonicalize().unwrap();
198-
let wallet_path = tmp_dir_abs.join("wallet.json");
199-
write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
200-
.unwrap();
201-
load_wallet(&wallet_path).unwrap();
202-
})
220+
let tmp_dir = tempfile::TempDir::new().unwrap();
221+
let tmp_dir_abs = tmp_dir.path().canonicalize().unwrap();
222+
let wallet_path = tmp_dir_abs.join("wallet.json");
223+
write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
224+
.unwrap();
225+
load_wallet(&wallet_path).unwrap();
203226
}
204227

205228
#[test]
@@ -223,88 +246,113 @@ mod tests {
223246
}
224247
#[test]
225248
fn encrypt_and_save_phrase() {
226-
with_tmp_dir(|tmp_dir| {
227-
let wallet_path = tmp_dir.join("wallet.json");
228-
write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
229-
.unwrap();
230-
let phrase_recovered = eth_keystore::decrypt_key(wallet_path, TEST_PASSWORD).unwrap();
231-
let phrase = String::from_utf8(phrase_recovered).unwrap();
232-
assert_eq!(phrase, TEST_MNEMONIC)
233-
});
249+
let tmp_dir = tempfile::TempDir::new().unwrap();
250+
let wallet_path = tmp_dir.path().join("wallet.json");
251+
write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
252+
.unwrap();
253+
let phrase_recovered = eth_keystore::decrypt_key(wallet_path, TEST_PASSWORD).unwrap();
254+
let phrase = String::from_utf8(phrase_recovered).unwrap();
255+
assert_eq!(phrase, TEST_MNEMONIC)
234256
}
235257

236258
#[test]
237259
fn write_wallet() {
238-
with_tmp_dir(|tmp_dir| {
239-
let wallet_path = tmp_dir.join("wallet.json");
240-
write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
241-
.unwrap();
242-
load_wallet(&wallet_path).unwrap();
243-
})
260+
let tmp_dir = tempfile::TempDir::new().unwrap();
261+
let wallet_path = tmp_dir.path().join("wallet.json");
262+
write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
263+
.unwrap();
264+
load_wallet(&wallet_path).unwrap();
244265
}
245266

246267
#[test]
247268
#[should_panic]
248269
fn write_wallet_to_existing_file_should_fail() {
249-
with_tmp_dir(|tmp_dir| {
250-
let wallet_path = tmp_dir.join("wallet.json");
251-
write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
252-
.unwrap();
253-
write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
254-
.unwrap();
255-
})
270+
let tmp_dir = tempfile::TempDir::new().unwrap();
271+
let wallet_path = tmp_dir.path().join("wallet.json");
272+
write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
273+
.unwrap();
274+
write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
275+
.unwrap();
256276
}
257277

258278
#[test]
259279
fn write_wallet_subdir() {
260-
with_tmp_dir(|tmp_dir| {
261-
let wallet_path = tmp_dir.join("path").join("to").join("wallet");
262-
write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
263-
.unwrap();
264-
load_wallet(&wallet_path).unwrap();
265-
})
280+
let tmp_dir = tempfile::TempDir::new().unwrap();
281+
let wallet_path = tmp_dir.path().join("path").join("to").join("wallet.json");
282+
write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
283+
.unwrap();
284+
load_wallet(&wallet_path).unwrap();
266285
}
267286

268287
#[test]
269288
fn test_ensure_no_wallet_exists_no_wallet() {
270-
with_tmp_dir(|tmp_dir| {
271-
let wallet_path = tmp_dir.join("wallet.json");
272-
remove_wallet(&wallet_path);
273-
ensure_no_wallet_exists(&wallet_path, false, &INPUT_NOP[..]).unwrap();
274-
});
275-
}
276-
277-
#[test]
278-
#[should_panic]
279-
fn test_ensure_no_wallet_exists_throws_err() {
280-
with_tmp_dir(|tmp_dir| {
281-
let wallet_path = tmp_dir.join("wallet.json");
282-
create_wallet(&wallet_path);
283-
ensure_no_wallet_exists(&wallet_path, false, &INPUT_NO[..]).unwrap();
284-
});
289+
let tmp_dir = tempfile::TempDir::new().unwrap();
290+
let wallet_path = tmp_dir.path().join("wallet.json");
291+
remove_wallet(&wallet_path);
292+
ensure_no_wallet_exists(&wallet_path, false, &INPUT_NOP[..]).unwrap();
285293
}
286294

287295
#[test]
288296
fn test_ensure_no_wallet_exists_exists_wallet() {
289297
// case: wallet path exist without --force and input[yes]
290-
with_tmp_dir(|tmp_dir| {
291-
let wallet_path = tmp_dir.join("wallet.json");
292-
create_wallet(&wallet_path);
293-
ensure_no_wallet_exists(&wallet_path, false, &INPUT_YES[..]).unwrap();
294-
});
298+
let tmp_dir = tempfile::TempDir::new().unwrap();
299+
let wallet_path = tmp_dir.path().join("wallet.json");
300+
serialize_wallet_to_file(&wallet_path, WalletSerializedState::Empty);
301+
ensure_no_wallet_exists(&wallet_path, false, &INPUT_YES[..]).unwrap();
302+
295303
// case: wallet path exist with --force
296-
with_tmp_dir(|tmp_dir| {
297-
let wallet_path = tmp_dir.join("wallet.json");
298-
create_wallet(&wallet_path);
299-
ensure_no_wallet_exists(&wallet_path, true, &INPUT_NOP[..]).unwrap();
300-
});
301-
// case: wallet path exist without --force and supply a different wallet path
302-
with_tmp_dir(|tmp_dir| {
303-
let wallet_path = tmp_dir.join("wallet.json");
304-
create_wallet(&wallet_path);
305-
let diff_wallet_path = tmp_dir.join("custom-wallet.json");
306-
ensure_no_wallet_exists(&diff_wallet_path, false, &INPUT_NOP[..]).unwrap();
307-
});
304+
let tmp_dir = tempfile::TempDir::new().unwrap();
305+
let wallet_path = tmp_dir.path().join("empty_wallet.json");
306+
serialize_wallet_to_file(&wallet_path, WalletSerializedState::Empty);
307+
308+
// Empty file should not trigger the replacement prompt
309+
ensure_no_wallet_exists(&wallet_path, false, &INPUT_YES[..]).unwrap();
310+
assert!(wallet_path.exists(), "Empty file should remain untouched");
311+
}
312+
313+
#[test]
314+
fn test_ensure_no_wallet_exists_nonempty_file() {
315+
let tmp_dir = tempfile::TempDir::new().unwrap();
316+
let wallet_path = tmp_dir.path().join("nonempty_wallet.json");
317+
318+
// Create non-empty file
319+
serialize_wallet_to_file(
320+
&wallet_path,
321+
WalletSerializedState::WithData("some wallet content".to_string()),
322+
);
323+
324+
// Test with --force flag
325+
ensure_no_wallet_exists(&wallet_path, true, &INPUT_NO[..]).unwrap();
326+
assert!(
327+
!wallet_path.exists(),
328+
"File should be removed with --force flag"
329+
);
330+
331+
// Test with user confirmation (yes)
332+
serialize_wallet_to_file(
333+
&wallet_path,
334+
WalletSerializedState::WithData("some wallet content".to_string()),
335+
);
336+
ensure_no_wallet_exists(&wallet_path, false, &INPUT_YES[..]).unwrap();
337+
assert!(
338+
!wallet_path.exists(),
339+
"File should be removed after user confirmation"
340+
);
341+
342+
// Test with user rejection (no)
343+
serialize_wallet_to_file(
344+
&wallet_path,
345+
WalletSerializedState::WithData("some wallet content".to_string()),
346+
);
347+
let result = ensure_no_wallet_exists(&wallet_path, false, &INPUT_NO[..]);
348+
assert!(
349+
result.is_err(),
350+
"Should error when user rejects file removal"
351+
);
352+
assert!(
353+
wallet_path.exists(),
354+
"File should remain when user rejects removal"
355+
);
308356
}
309357
}
310358

@@ -316,35 +364,15 @@ pub(crate) mod test_utils {
316364
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";
317365
pub(crate) const TEST_PASSWORD: &str = "1234";
318366

319-
/// Create a tmp folder and execute the given test function `f`
320-
pub(crate) fn with_tmp_dir<F>(f: F)
321-
where
322-
F: FnOnce(&Path) + panic::UnwindSafe,
323-
{
324-
let tmp_dir_name = format!("forc-wallet-test-{:x}", rand::random::<u64>());
325-
let tmp_dir = user_fuel_dir().join(".tmp").join(tmp_dir_name);
326-
std::fs::create_dir_all(&tmp_dir).unwrap();
327-
let panic = panic::catch_unwind(|| f(&tmp_dir));
328-
std::fs::remove_dir_all(&tmp_dir).unwrap();
329-
if let Err(e) = panic {
330-
panic::resume_unwind(e);
331-
}
332-
}
333-
334-
/// Saves a default test mnemonic to the disk
335-
pub(crate) fn save_dummy_wallet_file(wallet_path: &Path) {
336-
write_wallet_from_mnemonic_and_password(wallet_path, TEST_MNEMONIC, TEST_PASSWORD).unwrap();
337-
}
338-
339-
/// The same as `with_tmp_dir`, but also provides a test wallet.
367+
/// Creates temp dir with a temp/test wallet.
340368
pub(crate) fn with_tmp_dir_and_wallet<F>(f: F)
341369
where
342370
F: FnOnce(&Path, &Path) + panic::UnwindSafe,
343371
{
344-
with_tmp_dir(|dir| {
345-
let wallet_path = dir.join("wallet.json");
346-
save_dummy_wallet_file(&wallet_path);
347-
f(dir, &wallet_path);
348-
})
372+
let tmp_dir = tempfile::TempDir::new().unwrap();
373+
let wallet_path = tmp_dir.path().join("wallet.json");
374+
write_wallet_from_mnemonic_and_password(&wallet_path, TEST_MNEMONIC, TEST_PASSWORD)
375+
.unwrap();
376+
f(tmp_dir.path(), &wallet_path);
349377
}
350378
}

0 commit comments

Comments
 (0)