Skip to content
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

feat(wallet): add change mnemonic password rpc #2317

Merged
merged 21 commits into from
Feb 12, 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
106 changes: 58 additions & 48 deletions mm2src/mm2_main/src/lp_wallet.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use common::HttpStatusCode;
use crypto::{decrypt_mnemonic, encrypt_mnemonic, generate_mnemonic, CryptoCtx, CryptoInitError, EncryptedData,
MnemonicError};
use enum_derives::EnumFromStringify;
use http::StatusCode;
use itertools::Itertools;
use mm2_core::mm_ctx::MmArc;
Expand All @@ -21,7 +22,6 @@ cfg_wasm32! {
cfg_native! {
use mnemonics_storage::{read_all_wallet_names, read_encrypted_passphrase_if_available, save_encrypted_passphrase, WalletsStorageError};
}

#[cfg(not(target_arch = "wasm32"))] mod mnemonics_storage;
#[cfg(target_arch = "wasm32")] mod mnemonics_wasm_db;

Expand Down Expand Up @@ -253,7 +253,7 @@ async fn process_wallet_with_name(

async fn process_passphrase_logic(
ctx: &MmArc,
wallet_name: Option<String>,
wallet_name: Option<&str>,
passphrase: Option<Passphrase>,
) -> WalletInitResult<Option<String>> {
match (wallet_name, passphrase) {
Expand All @@ -268,7 +268,7 @@ async fn process_passphrase_logic(

(Some(wallet_name), passphrase_option) => {
let wallet_password = deserialize_config_field::<String>(ctx, "wallet_password")?;
process_wallet_with_name(ctx, &wallet_name, passphrase_option, &wallet_password).await
process_wallet_with_name(ctx, wallet_name, passphrase_option, &wallet_password).await
},
}
}
Expand Down Expand Up @@ -307,8 +307,8 @@ pub(crate) async fn initialize_wallet_passphrase(ctx: &MmArc) -> WalletInitResul
ctx.wallet_name
.set(wallet_name.clone())
.map_to_mm(|_| WalletInitError::InternalError("Already Initialized".to_string()))?;
let passphrase = process_passphrase_logic(ctx, wallet_name, passphrase).await?;

let passphrase = process_passphrase_logic(ctx, wallet_name.as_deref(), passphrase).await?;
if let Some(passphrase) = passphrase {
initialize_crypto_context(ctx, &passphrase)?;
}
Expand Down Expand Up @@ -413,40 +413,43 @@ pub struct GetMnemonicResponse {
pub mnemonic: MnemonicForRpc,
}

#[derive(Debug, Display, Serialize, SerializeErrorType)]
#[derive(Debug, Display, Serialize, SerializeErrorType, EnumFromStringify)]
#[serde(tag = "error_type", content = "error_data")]
pub enum GetMnemonicError {
pub enum MnemonicRpcError {
#[display(fmt = "Invalid request error: {}", _0)]
InvalidRequest(String),
#[display(fmt = "Wallets storage error: {}", _0)]
WalletsStorageError(String),
#[display(fmt = "Internal error: {}", _0)]
Internal(String),
#[display(fmt = "Invalid password error: {}", _0)]
#[from_stringify("MnemonicError")]
InvalidPassword(String),
}

impl HttpStatusCode for GetMnemonicError {
impl HttpStatusCode for MnemonicRpcError {
fn status_code(&self) -> StatusCode {
match self {
GetMnemonicError::InvalidRequest(_) => StatusCode::BAD_REQUEST,
GetMnemonicError::WalletsStorageError(_) | GetMnemonicError::Internal(_) => {
MnemonicRpcError::InvalidRequest(_) | MnemonicRpcError::InvalidPassword(_) => StatusCode::BAD_REQUEST,
MnemonicRpcError::WalletsStorageError(_) | MnemonicRpcError::Internal(_) => {
StatusCode::INTERNAL_SERVER_ERROR
},
}
}
}

#[cfg(not(target_arch = "wasm32"))]
impl From<WalletsStorageError> for GetMnemonicError {
fn from(e: WalletsStorageError) -> Self { GetMnemonicError::WalletsStorageError(e.to_string()) }
impl From<WalletsStorageError> for MnemonicRpcError {
fn from(e: WalletsStorageError) -> Self { MnemonicRpcError::WalletsStorageError(e.to_string()) }
}

#[cfg(target_arch = "wasm32")]
impl From<WalletsDBError> for GetMnemonicError {
fn from(e: WalletsDBError) -> Self { GetMnemonicError::WalletsStorageError(e.to_string()) }
impl From<WalletsDBError> for MnemonicRpcError {
fn from(e: WalletsDBError) -> Self { MnemonicRpcError::WalletsStorageError(e.to_string()) }
}

impl From<ReadPassphraseError> for GetMnemonicError {
fn from(e: ReadPassphraseError) -> Self { GetMnemonicError::WalletsStorageError(e.to_string()) }
impl From<ReadPassphraseError> for MnemonicRpcError {
fn from(e: ReadPassphraseError) -> Self { MnemonicRpcError::WalletsStorageError(e.to_string()) }
}

/// Retrieves the wallet mnemonic in the requested format.
Expand All @@ -456,7 +459,7 @@ impl From<ReadPassphraseError> for GetMnemonicError {
/// A `Result` type containing:
///
/// * [`Ok`]([`GetMnemonicResponse`]) - The wallet mnemonic in the requested format.
/// * [`MmError`]<[`GetMnemonicError>`]> - Returns specific [`GetMnemonicError`] variants for different failure scenarios.
/// * [`MmError`]<[`MnemonicRpcError>`]> - Returns specific [`MnemonicRpcError`] variants for different failure scenarios.
///
/// # Errors
///
Expand All @@ -480,20 +483,20 @@ impl From<ReadPassphraseError> for GetMnemonicError {
/// Err(e) => println!("Error: {:?}", e),
/// }
/// ```
pub async fn get_mnemonic_rpc(ctx: MmArc, req: GetMnemonicRequest) -> MmResult<GetMnemonicResponse, GetMnemonicError> {
pub async fn get_mnemonic_rpc(ctx: MmArc, req: GetMnemonicRequest) -> MmResult<GetMnemonicResponse, MnemonicRpcError> {
match req.mnemonic_format {
MnemonicFormat::Encrypted => {
let encrypted_mnemonic = read_encrypted_passphrase_if_available(&ctx)
.await?
.ok_or_else(|| GetMnemonicError::InvalidRequest("Wallet mnemonic file not found".to_string()))?;
.ok_or_else(|| MnemonicRpcError::InvalidRequest("Wallet mnemonic file not found".to_string()))?;
Ok(GetMnemonicResponse {
mnemonic: encrypted_mnemonic.into(),
})
},
MnemonicFormat::PlainText(wallet_password) => {
let plaintext_mnemonic = read_and_decrypt_passphrase_if_available(&ctx, &wallet_password)
.await?
.ok_or_else(|| GetMnemonicError::InvalidRequest("Wallet mnemonic file not found".to_string()))?;
.ok_or_else(|| MnemonicRpcError::InvalidRequest("Wallet mnemonic file not found".to_string()))?;
Ok(GetMnemonicResponse {
mnemonic: plaintext_mnemonic.into(),
})
Expand All @@ -508,40 +511,13 @@ pub struct GetWalletNamesResponse {
activated_wallet: Option<String>,
}

#[derive(Debug, Display, Serialize, SerializeErrorType)]
#[serde(tag = "error_type", content = "error_data")]
pub enum GetWalletsError {
#[display(fmt = "Wallets storage error: {}", _0)]
WalletsStorageError(String),
#[display(fmt = "Internal error: {}", _0)]
Internal(String),
}

impl HttpStatusCode for GetWalletsError {
fn status_code(&self) -> StatusCode {
match self {
GetWalletsError::WalletsStorageError(_) | GetWalletsError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}

#[cfg(not(target_arch = "wasm32"))]
impl From<WalletsStorageError> for GetWalletsError {
fn from(e: WalletsStorageError) -> Self { GetWalletsError::WalletsStorageError(e.to_string()) }
}

#[cfg(target_arch = "wasm32")]
impl From<WalletsDBError> for GetWalletsError {
fn from(e: WalletsDBError) -> Self { GetWalletsError::WalletsStorageError(e.to_string()) }
}

/// Retrieves all created wallets and the currently activated wallet.
pub async fn get_wallet_names_rpc(ctx: MmArc, _req: Json) -> MmResult<GetWalletNamesResponse, GetWalletsError> {
pub async fn get_wallet_names_rpc(ctx: MmArc, _req: Json) -> MmResult<GetWalletNamesResponse, MnemonicRpcError> {
// We want to return wallet names in the same order for both native and wasm32 targets.
let wallets = read_all_wallet_names(&ctx).await?.sorted().collect();
// Note: `ok_or` is used here on `Constructible<Option<String>>` to handle the case where the wallet name is not set.
// `wallet_name` can be `None` in the case of no-login mode.
let activated_wallet = ctx.wallet_name.get().ok_or(GetWalletsError::Internal(
let activated_wallet = ctx.wallet_name.get().ok_or(MnemonicRpcError::Internal(
"`wallet_name` not initialized yet!".to_string(),
))?;

Expand All @@ -550,3 +526,37 @@ pub async fn get_wallet_names_rpc(ctx: MmArc, _req: Json) -> MmResult<GetWalletN
activated_wallet: activated_wallet.clone(),
})
}

/// `ChangeMnemonicPasswordReq ` represents a request to update the password for Menmonic.
/// It includes the current password and the new password to be set.
#[derive(Debug, Deserialize)]
pub struct ChangeMnemonicPasswordReq {
/// Current mnemonic password.
pub current_password: String,
/// New mnemonic password.
pub new_password: String,
}

/// RPC function to handle a request for changing mnemonic password.
pub async fn change_mnemonic_password(ctx: MmArc, req: ChangeMnemonicPasswordReq) -> MmResult<(), MnemonicRpcError> {
let wallet_name = ctx
.wallet_name
.get()
.ok_or(MnemonicRpcError::Internal(
"`wallet_name` not initialized yet!".to_string(),
))?
.as_ref()
.ok_or_else(|| MnemonicRpcError::Internal("`wallet_name` cannot be None!".to_string()))?;
// read mnemonic for a wallet_name using current user's password.
let mnemonic = read_and_decrypt_passphrase_if_available(&ctx, &req.current_password)
.await?
.ok_or(MmError::new(MnemonicRpcError::Internal(format!(
"{wallet_name}: wallet mnemonic file not found"
))))?;
// encrypt mnemonic with new passphrase.
let encrypted_data = encrypt_mnemonic(&mnemonic, &req.new_password)?;
// save new encrypted mnemonic data with new password
save_encrypted_passphrase(&ctx, wallet_name, &encrypted_data).await?;

Ok(())
}
4 changes: 3 additions & 1 deletion mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ pub(super) async fn save_encrypted_passphrase(
}
})?,
};
table.add_item(&mnemonics_table_item).await?;
table
.replace_item_by_unique_index("wallet_name", wallet_name, &mnemonics_table_item)
.await?;

Ok(())
}
Expand Down
3 changes: 2 additions & 1 deletion mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::lp_stats::{add_node_to_version_stat, remove_node_from_version_stat, s
stop_version_stat_collection, update_version_stat_collection};
use crate::lp_swap::swap_v2_rpcs::{active_swaps_rpc, my_recent_swaps_rpc, my_swap_status_rpc};
use crate::lp_swap::{get_locked_amount_rpc, max_maker_vol, recreate_swap_data, trade_preimage_rpc};
use crate::lp_wallet::{get_mnemonic_rpc, get_wallet_names_rpc};
use crate::lp_wallet::{change_mnemonic_password, get_mnemonic_rpc, get_wallet_names_rpc};
use crate::rpc::lp_commands::db_id::get_shared_db_id;
use crate::rpc::lp_commands::one_inch::rpcs::{one_inch_v6_0_classic_swap_contract_rpc,
one_inch_v6_0_classic_swap_create_rpc,
Expand Down Expand Up @@ -221,6 +221,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult<Re
"trade_preimage" => handle_mmrpc(ctx, request, trade_preimage_rpc).await,
"trezor_connection_status" => handle_mmrpc(ctx, request, trezor_connection_status).await,
"update_nft" => handle_mmrpc(ctx, request, update_nft).await,
"change_mnemonic_password" => handle_mmrpc(ctx, request, change_mnemonic_password).await,
"update_version_stat_collection" => handle_mmrpc(ctx, request, update_version_stat_collection).await,
"verify_message" => handle_mmrpc(ctx, request, verify_message).await,
"withdraw" => handle_mmrpc(ctx, request, withdraw).await,
Expand Down
70 changes: 70 additions & 0 deletions mm2src/mm2_main/tests/mm2_tests/mm2_tests_inner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5853,6 +5853,76 @@ fn test_get_wallet_names() {
block_on(mm_wallet_2.stop()).unwrap();
}

#[test]
#[cfg(not(target_arch = "wasm32"))]
fn test_change_mnemonic_password_rpc() {
let coins = json!([]);
// Initialize wallet with current_password.
let old_password = "helloworld";
let wallet_1 = Mm2TestConf::seednode_with_wallet_name(&coins, "wallet_1", old_password);
let mm = MarketMakerIt::start(wallet_1.conf, wallet_1.rpc_password, None).unwrap();

// Retrieve all wallet names(should succeed).
let get_wallet_names_1 = block_on(get_wallet_names(&mm));
assert_eq!(get_wallet_names_1.wallet_names, vec!["wallet_1"]);
assert_eq!(get_wallet_names_1.activated_wallet.unwrap(), "wallet_1");

// STAGE 1: send update mnemonic password using new rpc(must succeed).
let new_password_stage_1 = "worldhello";
let request = block_on(mm.rpc(&json!({
"userpass": mm.userpass,
"method": "change_mnemonic_password",
"mmrpc": "2.0",
"params": {
"current_password": old_password,
"new_password": new_password_stage_1
}
})))
.unwrap();
assert_eq!(
request.0,
StatusCode::OK,
"'change_mnemonic_password' failed: {}",
request.1
);

// STAGE 2: Try changing wallet password using old_password(Should fail!)
let request = block_on(mm.rpc(&json!({
"userpass": mm.userpass,
"method": "change_mnemonic_password",
"mmrpc": "2.0",
"params": {
"current_password": old_password,
"new_password": "password2"
}
})))
.unwrap();
assert_eq!(
request.0,
StatusCode::INTERNAL_SERVER_ERROR,
"'change_mnemonic_password' failed: {}",
request.1
);

// STAGE 3: try updating password again using new_password_stage_1 password(Should pass!)
let request = block_on(mm.rpc(&json!({
"userpass": mm.userpass,
"method": "change_mnemonic_password",
"mmrpc": "2.0",
"params": {
"current_password": new_password_stage_1,
"new_password": "password3"
}
})))
.unwrap();
assert_eq!(
request.0,
StatusCode::OK,
"'change_mnemonic_password' failed: {}",
request.1
);
}

#[test]
#[cfg(not(target_arch = "wasm32"))]
fn test_sign_raw_transaction_rick() {
Expand Down
Loading