Skip to content
Merged
40 changes: 29 additions & 11 deletions docs/DEV_ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,34 @@
CC=/opt/homebrew/opt/llvm/bin/clang AR=/opt/homebrew/opt/llvm/bin/llvm-ar wasm-pack test --firefox --headless mm2src/mm2_main
```
Please note `CC` and `AR` must be specified in the same line as `wasm-pack test mm2src/mm2_main`.
#### Running specific WASM tests with Cargo</br>
- Install `wasm-bindgen-cli`: </br>
Make sure you have wasm-bindgen-cli installed with a version that matches the one specified in your Cargo.toml file.
You can install it using Cargo with the following command:
```
cargo install -f wasm-bindgen-cli --version <wasm-bindgen-version>
```
- Run
```
cargo test --target wasm32-unknown-unknown --package coins --lib utxo::utxo_block_header_storage::wasm::indexeddb_block_header_storage
```

#### Running specific WASM tests

There are two primary methods for running specific tests:

* **Method 1: Using `wasm-pack` (Recommended for browser-based tests)**

To filter tests, append `--` to the `wasm-pack test` command, followed by the name of the test you want to run. This will execute only the tests whose names contain the provided string.

General Example:
```shell
wasm-pack test --firefox --headless mm2src/mm2_main -- <test_name_to_run>
```

> **Note for macOS users:** You must prepend the `CC` and `AR` environment variables to the command if they weren't already exported, just as you would when running all tests. For example: `CC=... AR=... wasm-pack test ...`

* **Method 2: Using `cargo test` (For non-browser tests)**

This method uses the standard Cargo test runner with a wasm target and is useful for tests that do not require a browser environment.

a. **Install `wasm-bindgen-cli`**: Make sure you have `wasm-bindgen-cli` installed with a version that matches the one specified in your `Cargo.toml` file.
```shell
cargo install -f wasm-bindgen-cli --version <wasm-bindgen-version>
```

b. **Run the test**: Append `--` to the `cargo test` command, followed by the test path.
```shell
cargo test --target wasm32-unknown-unknown --package coins --lib -- utxo::utxo_block_header_storage::wasm::indexeddb_block_header_storage
```

PS If you notice that this guide is outdated, please submit a PR.
1 change: 1 addition & 0 deletions mm2src/coins/qrc20/qrc20_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ fn test_validate_fee() {
}

#[test]
#[ignore]
fn test_wait_for_tx_spend_malicious() {
// priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG
let priv_key = [
Expand Down
5 changes: 4 additions & 1 deletion mm2src/mm2_main/src/lp_native_dex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ impl From<AdexBehaviourError> for P2PInitError {
}
}
}

#[derive(Clone, Debug, Display, EnumFromTrait, Serialize, SerializeErrorType)]
#[serde(tag = "error_type", content = "error_data")]
pub enum MmInitError {
Expand Down Expand Up @@ -534,6 +533,10 @@ fn p2p_precheck(ctx: &MmArc) -> P2PResult<()> {
}
}

if is_seed_node && !CryptoCtx::is_init(ctx).unwrap_or(false) {
return precheck_err("Seed node requires a persistent identity to generate its P2P key.");
}

Ok(())
}

Expand Down
126 changes: 103 additions & 23 deletions mm2src/mm2_main/src/lp_wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ cfg_wasm32! {
use crate::lp_wallet::mnemonics_wasm_db::{WalletsDb, WalletsDBError};
use mm2_core::mm_ctx::from_ctx;
use mm2_db::indexed_db::{ConstructibleDb, DbLocked, InitDbResult};
use mnemonics_wasm_db::{read_all_wallet_names, read_encrypted_passphrase_if_available, save_encrypted_passphrase};
use mnemonics_wasm_db::{delete_wallet, read_all_wallet_names, read_encrypted_passphrase, save_encrypted_passphrase};
use std::sync::Arc;

type WalletsDbLocked<'a> = DbLocked<'a, WalletsDb>;
}

cfg_native! {
use mnemonics_storage::{read_all_wallet_names, read_encrypted_passphrase_if_available, save_encrypted_passphrase, WalletsStorageError};
use mnemonics_storage::{delete_wallet, read_all_wallet_names, read_encrypted_passphrase, save_encrypted_passphrase, WalletsStorageError};
}
#[cfg(not(target_arch = "wasm32"))] mod mnemonics_storage;
#[cfg(target_arch = "wasm32")] mod mnemonics_wasm_db;
Expand Down Expand Up @@ -69,13 +69,16 @@ pub enum ReadPassphraseError {
WalletsStorageError(String),
#[display(fmt = "Error decrypting passphrase: {}", _0)]
DecryptionError(String),
#[display(fmt = "Internal error: {}", _0)]
Internal(String),
}

impl From<ReadPassphraseError> for WalletInitError {
fn from(e: ReadPassphraseError) -> Self {
match e {
ReadPassphraseError::WalletsStorageError(e) => WalletInitError::WalletsStorageError(e),
ReadPassphraseError::DecryptionError(e) => WalletInitError::MnemonicError(e),
ReadPassphraseError::Internal(e) => WalletInitError::InternalError(e),
}
}
}
Expand Down Expand Up @@ -121,25 +124,39 @@ async fn encrypt_and_save_passphrase(
.mm_err(|e| WalletInitError::WalletsStorageError(e.to_string()))
}

/// Reads and decrypts the passphrase from a file associated with the given wallet name, if available.
///
/// This function first checks if a passphrase is available. If a passphrase is found,
/// since it is stored in an encrypted format, it decrypts it before returning. If no passphrase is found,
/// it returns `None`.
///
/// # Returns
/// `MmInitResult<String>` - The decrypted passphrase or an error if any operation fails.
/// A convenience wrapper that calls [`try_load_wallet_passphrase`] for the currently active wallet.
async fn try_load_active_wallet_passphrase(
ctx: &MmArc,
wallet_password: &str,
) -> MmResult<Option<String>, ReadPassphraseError> {
let wallet_name = ctx
.wallet_name
.get()
.ok_or(ReadPassphraseError::Internal(
"`wallet_name` not initialized yet!".to_string(),
))?
.clone()
.ok_or_else(|| {
ReadPassphraseError::Internal("Cannot read stored passphrase: no active wallet is set.".to_string())
})?;

try_load_wallet_passphrase(ctx, &wallet_name, wallet_password).await
}

/// Loads (reads from storage and decrypts) a passphrase for a specific wallet by name.
///
/// # Errors
/// Returns specific `MmInitError` variants for different failure scenarios.
async fn read_and_decrypt_passphrase_if_available(
/// Returns `Ok(None)` if the passphrase is not found in storage. This is an expected
/// outcome for a new wallet or when using a legacy config where the passphrase is not saved.
async fn try_load_wallet_passphrase(
ctx: &MmArc,
wallet_name: &str,
wallet_password: &str,
) -> MmResult<Option<String>, ReadPassphraseError> {
match read_encrypted_passphrase_if_available(ctx)
let encrypted = read_encrypted_passphrase(ctx, wallet_name)
.await
.mm_err(|e| ReadPassphraseError::WalletsStorageError(e.to_string()))?
{
.mm_err(|e| ReadPassphraseError::WalletsStorageError(e.to_string()))?;

match encrypted {
Some(encrypted_passphrase) => {
let mnemonic = decrypt_mnemonic(&encrypted_passphrase, wallet_password)
.mm_err(|e| ReadPassphraseError::DecryptionError(e.to_string()))?;
Expand Down Expand Up @@ -171,7 +188,7 @@ async fn retrieve_or_create_passphrase(
wallet_name: &str,
wallet_password: &str,
) -> WalletInitResult<Option<String>> {
match read_and_decrypt_passphrase_if_available(ctx, wallet_password).await? {
match try_load_active_wallet_passphrase(ctx, wallet_password).await? {
Some(passphrase_from_file) => {
// If an existing passphrase is found, return it
Ok(Some(passphrase_from_file))
Expand Down Expand Up @@ -202,7 +219,7 @@ async fn confirm_or_encrypt_and_store_passphrase(
passphrase: &str,
wallet_password: &str,
) -> WalletInitResult<Option<String>> {
match read_and_decrypt_passphrase_if_available(ctx, wallet_password).await? {
match try_load_active_wallet_passphrase(ctx, wallet_password).await? {
Some(passphrase_from_file) if passphrase == passphrase_from_file => {
// If an existing passphrase is found and it matches the provided passphrase, return it
Ok(Some(passphrase_from_file))
Expand Down Expand Up @@ -238,7 +255,7 @@ async fn decrypt_validate_or_save_passphrase(
// Decrypt the provided encrypted passphrase
let decrypted_passphrase = decrypt_mnemonic(&encrypted_passphrase_data, wallet_password)?;

match read_and_decrypt_passphrase_if_available(ctx, wallet_password).await? {
match try_load_active_wallet_passphrase(ctx, wallet_password).await? {
Some(passphrase_from_file) if decrypted_passphrase == passphrase_from_file => {
// If an existing passphrase is found and it matches the decrypted passphrase, return it
Ok(Some(decrypted_passphrase))
Expand Down Expand Up @@ -476,7 +493,13 @@ impl From<WalletsDBError> for MnemonicRpcError {
}

impl From<ReadPassphraseError> for MnemonicRpcError {
fn from(e: ReadPassphraseError) -> Self { MnemonicRpcError::WalletsStorageError(e.to_string()) }
fn from(e: ReadPassphraseError) -> Self {
match e {
ReadPassphraseError::DecryptionError(e) => MnemonicRpcError::InvalidPassword(e),
ReadPassphraseError::WalletsStorageError(e) => MnemonicRpcError::WalletsStorageError(e),
ReadPassphraseError::Internal(e) => MnemonicRpcError::Internal(e),
}
}
}

/// Retrieves the wallet mnemonic in the requested format.
Expand Down Expand Up @@ -513,15 +536,27 @@ impl From<ReadPassphraseError> for MnemonicRpcError {
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)
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(
"Cannot get encrypted mnemonic: This operation requires an active named wallet.".to_string(),
)
})?;
let encrypted_mnemonic = read_encrypted_passphrase(&ctx, wallet_name)
.await?
.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)
let plaintext_mnemonic = try_load_active_wallet_passphrase(&ctx, &wallet_password)
.await?
.ok_or_else(|| MnemonicRpcError::InvalidRequest("Wallet mnemonic file not found".to_string()))?;
Ok(GetMnemonicResponse {
Expand Down Expand Up @@ -584,7 +619,7 @@ pub async fn change_mnemonic_password(ctx: MmArc, req: ChangeMnemonicPasswordReq
.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)
let mnemonic = try_load_active_wallet_passphrase(&ctx, &req.current_password)
.await?
.ok_or(MmError::new(MnemonicRpcError::Internal(format!(
"{wallet_name}: wallet mnemonic file not found"
Expand All @@ -596,3 +631,48 @@ pub async fn change_mnemonic_password(ctx: MmArc, req: ChangeMnemonicPasswordReq

Ok(())
}

#[derive(Debug, Deserialize)]
pub struct DeleteWalletRequest {
/// The name of the wallet to be deleted.
pub wallet_name: String,
/// The password to confirm wallet deletion.
pub password: String,
}

/// Deletes a wallet. Requires password confirmation.
/// The active wallet cannot be deleted.
pub async fn delete_wallet_rpc(ctx: MmArc, req: DeleteWalletRequest) -> MmResult<(), MnemonicRpcError> {
let active_wallet = ctx
.wallet_name
.get()
.ok_or(MnemonicRpcError::Internal(
"`wallet_name` not initialized yet!".to_string(),
))?
Comment thread
mariocynicys marked this conversation as resolved.
.as_ref();

if active_wallet == Some(&req.wallet_name) {
return MmError::err(MnemonicRpcError::InvalidRequest(format!(
"Cannot delete wallet '{}' as it is currently active.",
req.wallet_name
)));
}

// Verify the password by attempting to decrypt the mnemonic.
let maybe_mnemonic = try_load_wallet_passphrase(&ctx, &req.wallet_name, &req.password).await?;

match maybe_mnemonic {
Some(_) => {
// Password is correct, proceed with deletion.
delete_wallet(&ctx, &req.wallet_name).await?;
Ok(())
},
None => {
// This case implies no mnemonic file was found for the given wallet.
MmError::err(MnemonicRpcError::InvalidRequest(format!(
"Wallet '{}' not found.",
req.wallet_name
)))
},
}
}
31 changes: 17 additions & 14 deletions mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,27 +57,22 @@ pub(super) async fn save_encrypted_passphrase(

/// Reads the encrypted passphrase data from the file associated with the given wallet name, if available.
///
/// This function is responsible for retrieving the encrypted passphrase data from a file, if it exists.
/// This function is responsible for retrieving the encrypted passphrase data from a file for a specific wallet.
/// The data is expected to be in the format of `EncryptedData`, which includes
/// all necessary components for decryption, such as the encryption algorithm, key derivation
///
/// # Returns
/// `io::Result<EncryptedPassphraseData>` - The encrypted passphrase data or an error if the
/// reading process fails.
/// `WalletsStorageResult<Option<EncryptedData>>` - The encrypted passphrase data or an error if the
/// reading process fails. An `Ok(None)` is returned if the wallet file does not exist.
///
/// # Errors
/// Returns an `io::Error` if the file cannot be read or the data cannot be deserialized into
/// Returns a `WalletsStorageError` if the file cannot be read or the data cannot be deserialized into
/// `EncryptedData`.
pub(super) async fn read_encrypted_passphrase_if_available(ctx: &MmArc) -> WalletsStorageResult<Option<EncryptedData>> {
let wallet_name = ctx
.wallet_name
.get()
.ok_or(WalletsStorageError::Internal(
"`wallet_name` not initialized yet!".to_string(),
))?
.clone()
.ok_or_else(|| WalletsStorageError::Internal("`wallet_name` cannot be None!".to_string()))?;
let wallet_path = wallet_file_path(ctx, &wallet_name).map_to_mm(WalletsStorageError::InvalidWalletName)?;
pub(super) async fn read_encrypted_passphrase(
ctx: &MmArc,
wallet_name: &str,
) -> WalletsStorageResult<Option<EncryptedData>> {
let wallet_path = wallet_file_path(ctx, wallet_name).map_to_mm(WalletsStorageError::InvalidWalletName)?;
mm2_io::fs::read_json(&wallet_path).await.mm_err(|e| {
WalletsStorageError::FsReadError(format!(
"Error reading passphrase from file {}: {}",
Expand All @@ -93,3 +88,11 @@ pub(super) async fn read_all_wallet_names(ctx: &MmArc) -> WalletsStorageResult<i
.mm_err(|e| WalletsStorageError::FsReadError(format!("Error reading wallets directory: {}", e)))?;
Ok(wallet_names)
}

/// Deletes the wallet file associated with the given wallet name.
pub(super) async fn delete_wallet(ctx: &MmArc, wallet_name: &str) -> WalletsStorageResult<()> {
let wallet_path = wallet_file_path(ctx, wallet_name).map_to_mm(WalletsStorageError::InvalidWalletName)?;
mm2_io::fs::remove_file_async(&wallet_path)
.await
.mm_err(|e| WalletsStorageError::FsWriteError(e.to_string()))
}
24 changes: 15 additions & 9 deletions mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,21 +119,16 @@ pub(super) async fn save_encrypted_passphrase(
Ok(())
}

pub(super) async fn read_encrypted_passphrase_if_available(ctx: &MmArc) -> WalletsDBResult<Option<EncryptedData>> {
pub(super) async fn read_encrypted_passphrase(
ctx: &MmArc,
wallet_name: &str,
) -> WalletsDBResult<Option<EncryptedData>> {
let wallets_ctx = WalletsContext::from_ctx(ctx).map_to_mm(WalletsDBError::Internal)?;

let db = wallets_ctx.wallets_db().await?;
let transaction = db.transaction().await?;
let table = transaction.table::<MnemonicsTable>().await?;

let wallet_name = ctx
.wallet_name
.get()
.ok_or(WalletsDBError::Internal(
"`wallet_name` not initialized yet!".to_string(),
))?
.clone()
.ok_or_else(|| WalletsDBError::Internal("`wallet_name` can't be None!".to_string()))?;
table
.get_item_by_unique_index("wallet_name", wallet_name)
.await?
Expand All @@ -160,3 +155,14 @@ pub(super) async fn read_all_wallet_names(ctx: &MmArc) -> WalletsDBResult<impl I

Ok(wallet_names)
}

pub(super) async fn delete_wallet(ctx: &MmArc, wallet_name: &str) -> WalletsDBResult<()> {
let wallets_ctx = WalletsContext::from_ctx(ctx).map_to_mm(WalletsDBError::Internal)?;

let db = wallets_ctx.wallets_db().await?;
let transaction = db.transaction().await?;
let table = transaction.table::<MnemonicsTable>().await?;

table.delete_item_by_unique_index("wallet_name", wallet_name).await?;
Ok(())
}
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 @@ -11,7 +11,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::{change_mnemonic_password, get_mnemonic_rpc, get_wallet_names_rpc};
use crate::lp_wallet::{change_mnemonic_password, delete_wallet_rpc, 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 @@ -201,6 +201,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult<Re
"get_token_allowance" => handle_mmrpc(ctx, request, get_token_allowance_rpc).await,
"best_orders" => handle_mmrpc(ctx, request, best_orders_rpc_v2).await,
"clear_nft_db" => handle_mmrpc(ctx, request, clear_nft_db).await,
"delete_wallet" => handle_mmrpc(ctx, request, delete_wallet_rpc).await,
"enable_bch_with_tokens" => handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::<BchCoin>).await,
"enable_slp" => handle_mmrpc(ctx, request, enable_token::<SlpToken>).await,
"enable_eth_with_tokens" => handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::<EthCoin>).await,
Expand Down
Loading
Loading