diff --git a/node-gui/src/main_window/mod.rs b/node-gui/src/main_window/mod.rs index 6109d23d6..fc6ef69e7 100644 --- a/node-gui/src/main_window/mod.rs +++ b/node-gui/src/main_window/mod.rs @@ -832,7 +832,6 @@ impl MainWindow { ActiveDialog::WalletRecover { wallet_type } => { let wallet_type = *wallet_type; - // FIXME match wallet_type { WalletType::Hot | WalletType::Cold => wallet_mnemonic_dialog( None, diff --git a/test/functional/test_framework/wallet_cli_controller.py b/test/functional/test_framework/wallet_cli_controller.py index 477c42c75..8d7b1e9d3 100644 --- a/test/functional/test_framework/wallet_cli_controller.py +++ b/test/functional/test_framework/wallet_cli_controller.py @@ -177,7 +177,7 @@ async def open_wallet(self, name: str, password: Optional[str] = None, force_cha async def recover_wallet(self, mnemonic: str, name: str = "recovered_wallet") -> str: wallet_file = os.path.join(self.node.datadir, name) - return await self._write_command(f"wallet-create \"{wallet_file}\" store-seed-phrase \"{mnemonic}\"\n") + return await self._write_command(f"wallet-recover \"{wallet_file}\" store-seed-phrase \"{mnemonic}\"\n") async def close_wallet(self) -> str: return await self._write_command("wallet-close\n") diff --git a/wallet/wallet-cli-commands/src/command_handler/mod.rs b/wallet/wallet-cli-commands/src/command_handler/mod.rs index 129effdf3..496271fef 100644 --- a/wallet/wallet-cli-commands/src/command_handler/mod.rs +++ b/wallet/wallet-cli-commands/src/command_handler/mod.rs @@ -199,6 +199,62 @@ where }) } + WalletManagementCommand::RecoverWallet { + wallet_path, + mnemonic, + whether_to_store_seed_phrase, + passphrase, + hardware_wallet, + } => { + let hardware_wallet = hardware_wallet.and_then(|t| match t { + #[cfg(feature = "trezor")] + CLIHardwareWalletType::Trezor => Some(HardwareWalletType::Trezor), + CLIHardwareWalletType::None => None, + }); + + let newly_generated_mnemonic = self + .wallet() + .await? + .recover_wallet( + wallet_path, + whether_to_store_seed_phrase.to_bool(), + mnemonic, + passphrase, + hardware_wallet, + ) + .await?; + + self.wallet.update_wallet::().await; + + let msg = match newly_generated_mnemonic.mnemonic { + MnemonicInfo::NewlyGenerated { + mnemonic, + passphrase, + } => { + let passphrase = if let Some(passphrase) = passphrase { + format!("passphrase: {passphrase}\n") + } else { + String::new() + }; + format!( + "New wallet created successfully\nYour mnemonic: {}\n{passphrase}\ + Please write it somewhere safe to be able to restore your wallet. \ + It's recommended that you attempt to recover the wallet now as practice\ + to check that you arrive at the same addresses, \ + to ensure that you have done everything correctly. + ", + mnemonic + ) + } + MnemonicInfo::UserProvided => "New wallet created successfully".to_owned(), + }; + + Ok(ConsoleCommand::SetStatus { + status: self.repl_status().await?, + print_message: msg, + }) + } + WalletManagementCommand::OpenWallet { wallet_path, encryption_password, diff --git a/wallet/wallet-cli-commands/src/lib.rs b/wallet/wallet-cli-commands/src/lib.rs index ef64250b4..c21a09a96 100644 --- a/wallet/wallet-cli-commands/src/lib.rs +++ b/wallet/wallet-cli-commands/src/lib.rs @@ -69,6 +69,31 @@ pub enum WalletManagementCommand { hardware_wallet: Option, }, + #[clap(name = "wallet-recover")] + RecoverWallet { + /// File path of the wallet file + wallet_path: PathBuf, + + /// If 'store-seed-phrase', the seed-phrase will be stored in the wallet file. + /// If 'do-not-store-seed-phrase', the seed-phrase will only be printed on the screen. + /// Not storing the seed-phrase can be seen as a security measure + /// to ensure sufficient secrecy in case that seed-phrase is reused + /// elsewhere if this wallet is compromised. + whether_to_store_seed_phrase: CliStoreSeedPhrase, + + /// Mnemonic phrase (12, 15, or 24 words as a single quoted argument). If not specified, a new mnemonic phrase is generated and printed. + mnemonic: Option, + + /// Passphrase along the mnemonic + #[arg(long = "passphrase")] + passphrase: Option, + + /// Create a wallet using a connected hardware wallet. Only the public keys will be kept in + /// the software wallet + #[arg(long, conflicts_with_all(["mnemonic", "passphrase"]))] + hardware_wallet: Option, + }, + #[clap(name = "wallet-open")] OpenWallet { /// File path of the wallet file diff --git a/wallet/wallet-cli-lib/tests/cli_test_framework.rs b/wallet/wallet-cli-lib/tests/cli_test_framework.rs index 13af81d10..0b88cd840 100644 --- a/wallet/wallet-cli-lib/tests/cli_test_framework.rs +++ b/wallet/wallet-cli-lib/tests/cli_test_framework.rs @@ -198,7 +198,7 @@ impl CliTestFramework { .unwrap() .to_owned(); let cmd = format!( - "wallet-create \"{}\" store-seed-phrase \"{}\"", + "wallet-recover \"{}\" store-seed-phrase \"{}\"", file_name, MNEMONIC ); assert_eq!(self.exec(&cmd), "New wallet created successfully"); diff --git a/wallet/wallet-rpc-client/src/handles_client/mod.rs b/wallet/wallet-rpc-client/src/handles_client/mod.rs index 45f69347c..a2ed7b683 100644 --- a/wallet/wallet-rpc-client/src/handles_client/mod.rs +++ b/wallet/wallet-rpc-client/src/handles_client/mod.rs @@ -156,7 +156,51 @@ where } }; - let scan_blockchain = args.user_supplied_menmonic(); + let scan_blockchain = false; + self.wallet_rpc + .create_wallet(path, args, false, scan_blockchain) + .await + .map(Into::into) + .map_err(WalletRpcHandlesClientError::WalletRpcError) + } + + async fn recover_wallet( + &self, + path: PathBuf, + store_seed_phrase: bool, + mnemonic: Option, + passphrase: Option, + hardware_wallet: Option, + ) -> Result { + let store_seed_phrase = if store_seed_phrase { + StoreSeedPhrase::Store + } else { + StoreSeedPhrase::DoNotStore + }; + + let args = match hardware_wallet { + None => WalletTypeArgs::Software { + mnemonic, + passphrase, + store_seed_phrase, + }, + #[cfg(feature = "trezor")] + Some(HardwareWalletType::Trezor) => { + ensure!( + mnemonic.is_none() + && passphrase.is_none() + && store_seed_phrase == StoreSeedPhrase::DoNotStore, + RpcError::HardwareWalletWithMnemonic + ); + WalletTypeArgs::Trezor + } + #[cfg(not(feature = "trezor"))] + Some(_) => { + return Err(RpcError::::InvalidHardwareWallet)?; + } + }; + + let scan_blockchain = true; self.wallet_rpc .create_wallet(path, args, false, scan_blockchain) .await diff --git a/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs b/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs index 764688938..c6717d5e2 100644 --- a/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs +++ b/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs @@ -95,6 +95,26 @@ impl WalletInterface for ClientWalletRpc { .map_err(WalletRpcError::ResponseError) } + async fn recover_wallet( + &self, + path: PathBuf, + store_seed_phrase: bool, + mnemonic: Option, + passphrase: Option, + hardware_wallet: Option, + ) -> Result { + ColdWalletRpcClient::recover_wallet( + &self.http_client, + path.to_string_lossy().to_string(), + store_seed_phrase, + mnemonic, + passphrase, + hardware_wallet, + ) + .await + .map_err(WalletRpcError::ResponseError) + } + async fn open_wallet( &self, path: PathBuf, diff --git a/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs b/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs index e26d289ef..42adff6a4 100644 --- a/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs +++ b/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs @@ -74,6 +74,15 @@ pub trait WalletInterface { hardware_wallet: Option, ) -> Result; + async fn recover_wallet( + &self, + path: PathBuf, + store_seed_phrase: bool, + mnemonic: Option, + passphrase: Option, + hardware_wallet: Option, + ) -> Result; + async fn open_wallet( &self, path: PathBuf, diff --git a/wallet/wallet-rpc-daemon/docs/RPC.md b/wallet/wallet-rpc-daemon/docs/RPC.md index 45f339e89..8a5134e6f 100644 --- a/wallet/wallet-rpc-daemon/docs/RPC.md +++ b/wallet/wallet-rpc-daemon/docs/RPC.md @@ -1864,7 +1864,42 @@ string ### Method `wallet_create` -Create new wallet +Create a new wallet, this will skip scanning the blockchain + + +Parameters: +``` +{ + "path": string, + "store_seed_phrase": bool, + "mnemonic": EITHER OF + 1) string + 2) null, + "passphrase": EITHER OF + 1) string + 2) null, + "hardware_wallet": null, +} +``` + +Returns: +``` +{ "mnemonic": EITHER OF + 1) { "type": "UserProvided" } + 2) { + "type": "NewlyGenerated", + "content": { + "mnemonic": string, + "passphrase": EITHER OF + 1) string + 2) null, + }, + } } +``` + +### Method `wallet_recover` + +Recover new wallet, this will rescan the blockchain upon creation Parameters: diff --git a/wallet/wallet-rpc-lib/src/rpc/interface.rs b/wallet/wallet-rpc-lib/src/rpc/interface.rs index 83dac77c3..b5a5037b5 100644 --- a/wallet/wallet-rpc-lib/src/rpc/interface.rs +++ b/wallet/wallet-rpc-lib/src/rpc/interface.rs @@ -62,7 +62,7 @@ trait ColdWalletRpc { #[method(name = "version")] async fn version(&self) -> rpc::RpcResult; - /// Create new wallet + /// Create a new wallet, this will skip scanning the blockchain #[method(name = "wallet_create")] async fn create_wallet( &self, @@ -73,6 +73,17 @@ trait ColdWalletRpc { hardware_wallet: Option, ) -> rpc::RpcResult; + /// Recover new wallet, this will rescan the blockchain upon creation + #[method(name = "wallet_recover")] + async fn recover_wallet( + &self, + path: String, + store_seed_phrase: bool, + mnemonic: Option, + passphrase: Option, + hardware_wallet: Option, + ) -> rpc::RpcResult; + /// Open an exiting wallet by specifying the file location of the wallet file #[method(name = "wallet_open")] async fn open_wallet( diff --git a/wallet/wallet-rpc-lib/src/rpc/server_impl.rs b/wallet/wallet-rpc-lib/src/rpc/server_impl.rs index f6f1fe056..7a35b3665 100644 --- a/wallet/wallet-rpc-lib/src/rpc/server_impl.rs +++ b/wallet/wallet-rpc-lib/src/rpc/server_impl.rs @@ -29,7 +29,6 @@ use common::{ use crypto::key::PrivateKey; use p2p_types::{bannable_address::BannableAddress, socket_address::SocketAddress, PeerId}; use serialization::{hex::HexEncode, json_encoded::JsonEncoded}; -#[cfg(feature = "trezor")] use utils::ensure; use utils_networking::IpOrSocketAddress; use wallet::{account::TxInfo, version::get_version}; @@ -124,7 +123,54 @@ where } }; - let scan_blockchain = args.user_supplied_menmonic(); + let scan_blockchain = false; + rpc::handle_result( + self.create_wallet(path.into(), args, false, scan_blockchain) + .await + .map(Into::::into), + ) + } + + async fn recover_wallet( + &self, + path: String, + store_seed_phrase: bool, + mnemonic: Option, + passphrase: Option, + hardware_wallet: Option, + ) -> rpc::RpcResult { + let store_seed_phrase = if store_seed_phrase { + StoreSeedPhrase::Store + } else { + StoreSeedPhrase::DoNotStore + }; + + let args = match hardware_wallet { + None => { + ensure!(mnemonic.is_some(), RpcError::::EmptyMnemonic); + WalletTypeArgs::Software { + mnemonic, + passphrase, + store_seed_phrase, + } + } + #[cfg(feature = "trezor")] + Some(HardwareWalletType::Trezor) => { + ensure!( + mnemonic.is_none() + && passphrase.is_none() + && store_seed_phrase == StoreSeedPhrase::DoNotStore, + RpcError::::HardwareWalletWithMnemonic + ); + WalletTypeArgs::Trezor + } + #[cfg(not(feature = "trezor"))] + Some(_) => { + return Err(RpcError::::InvalidHardwareWallet)?; + } + }; + + let scan_blockchain = true; rpc::handle_result( self.create_wallet(path.into(), args, false, scan_blockchain) .await diff --git a/wallet/wallet-rpc-lib/src/rpc/types.rs b/wallet/wallet-rpc-lib/src/rpc/types.rs index 8e87c9d5c..9d10ce11f 100644 --- a/wallet/wallet-rpc-lib/src/rpc/types.rs +++ b/wallet/wallet-rpc-lib/src/rpc/types.rs @@ -90,6 +90,9 @@ pub enum RpcError { #[error("Invalid mnemonic: {0}")] InvalidMnemonic(wallet_controller::mnemonic::Error), + #[error("Cannont recover a software wallet without providing a mnemonic")] + EmptyMnemonic, + #[error("Cannot specify a mnemonic or passphrase when using a hardware wallet")] HardwareWalletWithMnemonic,