Skip to content

feat: wallet export #240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions src/export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use crate::utils::display_string_discreetly;
use anyhow::{Context, Result, anyhow};
use rpassword::prompt_password;
use std::path::Path;

/// Decrypts a wallet using provided password
fn decrypt_mnemonic(wallet_path: &Path, password: &str) -> Result<String> {
let phrase_bytes = eth_keystore::decrypt_key(wallet_path, password)
.map_err(|e| anyhow!("Failed to decrypt keystore: {}", e))?;

String::from_utf8(phrase_bytes).context("Invalid UTF-8 in mnemonic phrase")
}

/// Prints the wallet at the given path as mnemonic phrase as a discrete string
pub fn export_wallet_cli(wallet_path: &Path) -> Result<()> {
let prompt = "Please enter your wallet password to export your wallet: ";
let password = prompt_password(prompt)?;
let phrase = decrypt_mnemonic(wallet_path, &password)?;

// Display phrase in alternate screen
display_string_discreetly(
&phrase,
"### Do not share or lose this mnemonic phrase! Press any key to complete. ###",
)?;

Ok(())
}

#[cfg(test)]
mod tests {
use crate::{
export::decrypt_mnemonic,
utils::test_utils::{TEST_MNEMONIC, TEST_PASSWORD, with_tmp_dir_and_wallet},
};

#[test]
fn decrypt_wallet() {
with_tmp_dir_and_wallet(|_dir, wallet_path| {
let decrypted_mnemonic = decrypt_mnemonic(wallet_path, TEST_PASSWORD).unwrap();
assert_eq!(decrypted_mnemonic, TEST_MNEMONIC)
});
}
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use std::path::PathBuf;

use url::Url;

pub mod account;
pub mod balance;
pub mod export;
pub mod format;
pub mod import;
pub mod list;
Expand Down
12 changes: 12 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use forc_wallet::{
CliContext,
account::{self, Account, Accounts},
balance::{self, Balance},
export::export_wallet_cli,
import::{Import, import_wallet_cli},
list::{List, list_wallet_cli},
network::DEFAULT as DEFAULT_NODE_URL,
Expand Down Expand Up @@ -51,6 +52,10 @@ enum Command {
/// If a '--fore' is specified, will automatically removes the existing wallet at the same
/// path.
Import(Import),
/// Export a wallet as a mnemonic phrase.
///
/// If a `--path` is specified, the wallet will be read from that location.
Export,
/// Lists all accounts derived for the wallet so far.
///
/// Note that this only includes accounts that have been previously derived
Expand Down Expand Up @@ -86,6 +91,12 @@ EXAMPLES:
# Import a new wallet from a mnemonic phrase.
forc wallet import

# Export wallet as a mnemonic phrase from default path.
forc wallet import

# Export wallet as a mnemonic phrase read from the given path.
forc wallet import --path /path/to/wallet

# Import a new wallet from a mnemonic phrase and automatically replace the existing wallet if it's at the same path.
forc wallet import --force

Expand Down Expand Up @@ -150,6 +161,7 @@ async fn run() -> Result<()> {
Command::New(new) => new_wallet_cli(&ctx, new).await?,
Command::List(list) => list_wallet_cli(&ctx, list).await?,
Command::Import(import) => import_wallet_cli(&ctx, import).await?,
Command::Export => export_wallet_cli(&ctx.wallet_path)?,
Command::Accounts(accounts) => account::print_accounts_cli(&ctx, accounts).await?,
Command::Account(account) => account::cli(&ctx, account).await?,
Command::Sign(sign) => sign::cli(&ctx, sign)?,
Expand Down
Loading