Skip to content

Commit dc6e2f0

Browse files
authored
Merge pull request #2501 from KomodoPlatform/staging
chore(release): propagate `delete_wallet` RPC (staging→dev)
2 parents 99896d0 + d62c20b commit dc6e2f0

File tree

10 files changed

+404
-69
lines changed

10 files changed

+404
-69
lines changed

docs/DEV_ENVIRONMENT.md

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,34 @@
6666
CC=/opt/homebrew/opt/llvm/bin/clang AR=/opt/homebrew/opt/llvm/bin/llvm-ar wasm-pack test --firefox --headless mm2src/mm2_main
6767
```
6868
Please note `CC` and `AR` must be specified in the same line as `wasm-pack test mm2src/mm2_main`.
69-
#### Running specific WASM tests with Cargo</br>
70-
- Install `wasm-bindgen-cli`: </br>
71-
Make sure you have wasm-bindgen-cli installed with a version that matches the one specified in your Cargo.toml file.
72-
You can install it using Cargo with the following command:
73-
```
74-
cargo install -f wasm-bindgen-cli --version <wasm-bindgen-version>
75-
```
76-
- Run
77-
```
78-
cargo test --target wasm32-unknown-unknown --package coins --lib utxo::utxo_block_header_storage::wasm::indexeddb_block_header_storage
79-
```
69+
70+
#### Running specific WASM tests
71+
72+
There are two primary methods for running specific tests:
73+
74+
* **Method 1: Using `wasm-pack` (Recommended for browser-based tests)**
75+
76+
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.
77+
78+
General Example:
79+
```shell
80+
wasm-pack test --firefox --headless mm2src/mm2_main -- <test_name_to_run>
81+
```
82+
83+
> **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 ...`
84+
85+
* **Method 2: Using `cargo test` (For non-browser tests)**
86+
87+
This method uses the standard Cargo test runner with a wasm target and is useful for tests that do not require a browser environment.
88+
89+
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.
90+
```shell
91+
cargo install -f wasm-bindgen-cli --version <wasm-bindgen-version>
92+
```
93+
94+
b. **Run the test**: Append `--` to the `cargo test` command, followed by the test path.
95+
```shell
96+
cargo test --target wasm32-unknown-unknown --package coins --lib -- utxo::utxo_block_header_storage::wasm::indexeddb_block_header_storage
97+
```
8098
8199
PS If you notice that this guide is outdated, please submit a PR.

mm2src/coins/qrc20/qrc20_tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ fn test_validate_fee() {
429429
}
430430

431431
#[test]
432+
#[ignore]
432433
fn test_wait_for_tx_spend_malicious() {
433434
// priv_key of qXxsj5RtciAby9T7m98AgAATL4zTi4UwDG
434435
let priv_key = [

mm2src/mm2_main/src/lp_native_dex.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ impl From<AdexBehaviourError> for P2PInitError {
113113
}
114114
}
115115
}
116-
117116
#[derive(Clone, Debug, Display, EnumFromTrait, Serialize, SerializeErrorType)]
118117
#[serde(tag = "error_type", content = "error_data")]
119118
pub enum MmInitError {
@@ -534,6 +533,10 @@ fn p2p_precheck(ctx: &MmArc) -> P2PResult<()> {
534533
}
535534
}
536535

536+
if is_seed_node && !CryptoCtx::is_init(ctx).unwrap_or(false) {
537+
return precheck_err("Seed node requires a persistent identity to generate its P2P key.");
538+
}
539+
537540
Ok(())
538541
}
539542

mm2src/mm2_main/src/lp_wallet.rs

Lines changed: 103 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ cfg_wasm32! {
1414
use crate::lp_wallet::mnemonics_wasm_db::{WalletsDb, WalletsDBError};
1515
use mm2_core::mm_ctx::from_ctx;
1616
use mm2_db::indexed_db::{ConstructibleDb, DbLocked, InitDbResult};
17-
use mnemonics_wasm_db::{read_all_wallet_names, read_encrypted_passphrase_if_available, save_encrypted_passphrase};
17+
use mnemonics_wasm_db::{delete_wallet, read_all_wallet_names, read_encrypted_passphrase, save_encrypted_passphrase};
1818
use std::sync::Arc;
1919

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

2323
cfg_native! {
24-
use mnemonics_storage::{read_all_wallet_names, read_encrypted_passphrase_if_available, save_encrypted_passphrase, WalletsStorageError};
24+
use mnemonics_storage::{delete_wallet, read_all_wallet_names, read_encrypted_passphrase, save_encrypted_passphrase, WalletsStorageError};
2525
}
2626
#[cfg(not(target_arch = "wasm32"))] mod mnemonics_storage;
2727
#[cfg(target_arch = "wasm32")] mod mnemonics_wasm_db;
@@ -69,13 +69,16 @@ pub enum ReadPassphraseError {
6969
WalletsStorageError(String),
7070
#[display(fmt = "Error decrypting passphrase: {}", _0)]
7171
DecryptionError(String),
72+
#[display(fmt = "Internal error: {}", _0)]
73+
Internal(String),
7274
}
7375

7476
impl From<ReadPassphraseError> for WalletInitError {
7577
fn from(e: ReadPassphraseError) -> Self {
7678
match e {
7779
ReadPassphraseError::WalletsStorageError(e) => WalletInitError::WalletsStorageError(e),
7880
ReadPassphraseError::DecryptionError(e) => WalletInitError::MnemonicError(e),
81+
ReadPassphraseError::Internal(e) => WalletInitError::InternalError(e),
7982
}
8083
}
8184
}
@@ -121,25 +124,39 @@ async fn encrypt_and_save_passphrase(
121124
.mm_err(|e| WalletInitError::WalletsStorageError(e.to_string()))
122125
}
123126

124-
/// Reads and decrypts the passphrase from a file associated with the given wallet name, if available.
125-
///
126-
/// This function first checks if a passphrase is available. If a passphrase is found,
127-
/// since it is stored in an encrypted format, it decrypts it before returning. If no passphrase is found,
128-
/// it returns `None`.
129-
///
130-
/// # Returns
131-
/// `MmInitResult<String>` - The decrypted passphrase or an error if any operation fails.
127+
/// A convenience wrapper that calls [`try_load_wallet_passphrase`] for the currently active wallet.
128+
async fn try_load_active_wallet_passphrase(
129+
ctx: &MmArc,
130+
wallet_password: &str,
131+
) -> MmResult<Option<String>, ReadPassphraseError> {
132+
let wallet_name = ctx
133+
.wallet_name
134+
.get()
135+
.ok_or(ReadPassphraseError::Internal(
136+
"`wallet_name` not initialized yet!".to_string(),
137+
))?
138+
.clone()
139+
.ok_or_else(|| {
140+
ReadPassphraseError::Internal("Cannot read stored passphrase: no active wallet is set.".to_string())
141+
})?;
142+
143+
try_load_wallet_passphrase(ctx, &wallet_name, wallet_password).await
144+
}
145+
146+
/// Loads (reads from storage and decrypts) a passphrase for a specific wallet by name.
132147
///
133-
/// # Errors
134-
/// Returns specific `MmInitError` variants for different failure scenarios.
135-
async fn read_and_decrypt_passphrase_if_available(
148+
/// Returns `Ok(None)` if the passphrase is not found in storage. This is an expected
149+
/// outcome for a new wallet or when using a legacy config where the passphrase is not saved.
150+
async fn try_load_wallet_passphrase(
136151
ctx: &MmArc,
152+
wallet_name: &str,
137153
wallet_password: &str,
138154
) -> MmResult<Option<String>, ReadPassphraseError> {
139-
match read_encrypted_passphrase_if_available(ctx)
155+
let encrypted = read_encrypted_passphrase(ctx, wallet_name)
140156
.await
141-
.mm_err(|e| ReadPassphraseError::WalletsStorageError(e.to_string()))?
142-
{
157+
.mm_err(|e| ReadPassphraseError::WalletsStorageError(e.to_string()))?;
158+
159+
match encrypted {
143160
Some(encrypted_passphrase) => {
144161
let mnemonic = decrypt_mnemonic(&encrypted_passphrase, wallet_password)
145162
.mm_err(|e| ReadPassphraseError::DecryptionError(e.to_string()))?;
@@ -171,7 +188,7 @@ async fn retrieve_or_create_passphrase(
171188
wallet_name: &str,
172189
wallet_password: &str,
173190
) -> WalletInitResult<Option<String>> {
174-
match read_and_decrypt_passphrase_if_available(ctx, wallet_password).await? {
191+
match try_load_active_wallet_passphrase(ctx, wallet_password).await? {
175192
Some(passphrase_from_file) => {
176193
// If an existing passphrase is found, return it
177194
Ok(Some(passphrase_from_file))
@@ -202,7 +219,7 @@ async fn confirm_or_encrypt_and_store_passphrase(
202219
passphrase: &str,
203220
wallet_password: &str,
204221
) -> WalletInitResult<Option<String>> {
205-
match read_and_decrypt_passphrase_if_available(ctx, wallet_password).await? {
222+
match try_load_active_wallet_passphrase(ctx, wallet_password).await? {
206223
Some(passphrase_from_file) if passphrase == passphrase_from_file => {
207224
// If an existing passphrase is found and it matches the provided passphrase, return it
208225
Ok(Some(passphrase_from_file))
@@ -238,7 +255,7 @@ async fn decrypt_validate_or_save_passphrase(
238255
// Decrypt the provided encrypted passphrase
239256
let decrypted_passphrase = decrypt_mnemonic(&encrypted_passphrase_data, wallet_password)?;
240257

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

478495
impl From<ReadPassphraseError> for MnemonicRpcError {
479-
fn from(e: ReadPassphraseError) -> Self { MnemonicRpcError::WalletsStorageError(e.to_string()) }
496+
fn from(e: ReadPassphraseError) -> Self {
497+
match e {
498+
ReadPassphraseError::DecryptionError(e) => MnemonicRpcError::InvalidPassword(e),
499+
ReadPassphraseError::WalletsStorageError(e) => MnemonicRpcError::WalletsStorageError(e),
500+
ReadPassphraseError::Internal(e) => MnemonicRpcError::Internal(e),
501+
}
502+
}
480503
}
481504

482505
/// Retrieves the wallet mnemonic in the requested format.
@@ -513,15 +536,27 @@ impl From<ReadPassphraseError> for MnemonicRpcError {
513536
pub async fn get_mnemonic_rpc(ctx: MmArc, req: GetMnemonicRequest) -> MmResult<GetMnemonicResponse, MnemonicRpcError> {
514537
match req.mnemonic_format {
515538
MnemonicFormat::Encrypted => {
516-
let encrypted_mnemonic = read_encrypted_passphrase_if_available(&ctx)
539+
let wallet_name = ctx
540+
.wallet_name
541+
.get()
542+
.ok_or(MnemonicRpcError::Internal(
543+
"`wallet_name` not initialized yet!".to_string(),
544+
))?
545+
.as_ref()
546+
.ok_or_else(|| {
547+
MnemonicRpcError::Internal(
548+
"Cannot get encrypted mnemonic: This operation requires an active named wallet.".to_string(),
549+
)
550+
})?;
551+
let encrypted_mnemonic = read_encrypted_passphrase(&ctx, wallet_name)
517552
.await?
518553
.ok_or_else(|| MnemonicRpcError::InvalidRequest("Wallet mnemonic file not found".to_string()))?;
519554
Ok(GetMnemonicResponse {
520555
mnemonic: encrypted_mnemonic.into(),
521556
})
522557
},
523558
MnemonicFormat::PlainText(wallet_password) => {
524-
let plaintext_mnemonic = read_and_decrypt_passphrase_if_available(&ctx, &wallet_password)
559+
let plaintext_mnemonic = try_load_active_wallet_passphrase(&ctx, &wallet_password)
525560
.await?
526561
.ok_or_else(|| MnemonicRpcError::InvalidRequest("Wallet mnemonic file not found".to_string()))?;
527562
Ok(GetMnemonicResponse {
@@ -584,7 +619,7 @@ pub async fn change_mnemonic_password(ctx: MmArc, req: ChangeMnemonicPasswordReq
584619
.as_ref()
585620
.ok_or_else(|| MnemonicRpcError::Internal("`wallet_name` cannot be None!".to_string()))?;
586621
// read mnemonic for a wallet_name using current user's password.
587-
let mnemonic = read_and_decrypt_passphrase_if_available(&ctx, &req.current_password)
622+
let mnemonic = try_load_active_wallet_passphrase(&ctx, &req.current_password)
588623
.await?
589624
.ok_or(MmError::new(MnemonicRpcError::Internal(format!(
590625
"{wallet_name}: wallet mnemonic file not found"
@@ -596,3 +631,48 @@ pub async fn change_mnemonic_password(ctx: MmArc, req: ChangeMnemonicPasswordReq
596631

597632
Ok(())
598633
}
634+
635+
#[derive(Debug, Deserialize)]
636+
pub struct DeleteWalletRequest {
637+
/// The name of the wallet to be deleted.
638+
pub wallet_name: String,
639+
/// The password to confirm wallet deletion.
640+
pub password: String,
641+
}
642+
643+
/// Deletes a wallet. Requires password confirmation.
644+
/// The active wallet cannot be deleted.
645+
pub async fn delete_wallet_rpc(ctx: MmArc, req: DeleteWalletRequest) -> MmResult<(), MnemonicRpcError> {
646+
let active_wallet = ctx
647+
.wallet_name
648+
.get()
649+
.ok_or(MnemonicRpcError::Internal(
650+
"`wallet_name` not initialized yet!".to_string(),
651+
))?
652+
.as_ref();
653+
654+
if active_wallet == Some(&req.wallet_name) {
655+
return MmError::err(MnemonicRpcError::InvalidRequest(format!(
656+
"Cannot delete wallet '{}' as it is currently active.",
657+
req.wallet_name
658+
)));
659+
}
660+
661+
// Verify the password by attempting to decrypt the mnemonic.
662+
let maybe_mnemonic = try_load_wallet_passphrase(&ctx, &req.wallet_name, &req.password).await?;
663+
664+
match maybe_mnemonic {
665+
Some(_) => {
666+
// Password is correct, proceed with deletion.
667+
delete_wallet(&ctx, &req.wallet_name).await?;
668+
Ok(())
669+
},
670+
None => {
671+
// This case implies no mnemonic file was found for the given wallet.
672+
MmError::err(MnemonicRpcError::InvalidRequest(format!(
673+
"Wallet '{}' not found.",
674+
req.wallet_name
675+
)))
676+
},
677+
}
678+
}

mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,27 +57,22 @@ pub(super) async fn save_encrypted_passphrase(
5757

5858
/// Reads the encrypted passphrase data from the file associated with the given wallet name, if available.
5959
///
60-
/// This function is responsible for retrieving the encrypted passphrase data from a file, if it exists.
60+
/// This function is responsible for retrieving the encrypted passphrase data from a file for a specific wallet.
6161
/// The data is expected to be in the format of `EncryptedData`, which includes
6262
/// all necessary components for decryption, such as the encryption algorithm, key derivation
6363
///
6464
/// # Returns
65-
/// `io::Result<EncryptedPassphraseData>` - The encrypted passphrase data or an error if the
66-
/// reading process fails.
65+
/// `WalletsStorageResult<Option<EncryptedData>>` - The encrypted passphrase data or an error if the
66+
/// reading process fails. An `Ok(None)` is returned if the wallet file does not exist.
6767
///
6868
/// # Errors
69-
/// Returns an `io::Error` if the file cannot be read or the data cannot be deserialized into
69+
/// Returns a `WalletsStorageError` if the file cannot be read or the data cannot be deserialized into
7070
/// `EncryptedData`.
71-
pub(super) async fn read_encrypted_passphrase_if_available(ctx: &MmArc) -> WalletsStorageResult<Option<EncryptedData>> {
72-
let wallet_name = ctx
73-
.wallet_name
74-
.get()
75-
.ok_or(WalletsStorageError::Internal(
76-
"`wallet_name` not initialized yet!".to_string(),
77-
))?
78-
.clone()
79-
.ok_or_else(|| WalletsStorageError::Internal("`wallet_name` cannot be None!".to_string()))?;
80-
let wallet_path = wallet_file_path(ctx, &wallet_name).map_to_mm(WalletsStorageError::InvalidWalletName)?;
71+
pub(super) async fn read_encrypted_passphrase(
72+
ctx: &MmArc,
73+
wallet_name: &str,
74+
) -> WalletsStorageResult<Option<EncryptedData>> {
75+
let wallet_path = wallet_file_path(ctx, wallet_name).map_to_mm(WalletsStorageError::InvalidWalletName)?;
8176
mm2_io::fs::read_json(&wallet_path).await.mm_err(|e| {
8277
WalletsStorageError::FsReadError(format!(
8378
"Error reading passphrase from file {}: {}",
@@ -93,3 +88,11 @@ pub(super) async fn read_all_wallet_names(ctx: &MmArc) -> WalletsStorageResult<i
9388
.mm_err(|e| WalletsStorageError::FsReadError(format!("Error reading wallets directory: {}", e)))?;
9489
Ok(wallet_names)
9590
}
91+
92+
/// Deletes the wallet file associated with the given wallet name.
93+
pub(super) async fn delete_wallet(ctx: &MmArc, wallet_name: &str) -> WalletsStorageResult<()> {
94+
let wallet_path = wallet_file_path(ctx, wallet_name).map_to_mm(WalletsStorageError::InvalidWalletName)?;
95+
mm2_io::fs::remove_file_async(&wallet_path)
96+
.await
97+
.mm_err(|e| WalletsStorageError::FsWriteError(e.to_string()))
98+
}

mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -119,21 +119,16 @@ pub(super) async fn save_encrypted_passphrase(
119119
Ok(())
120120
}
121121

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

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

129-
let wallet_name = ctx
130-
.wallet_name
131-
.get()
132-
.ok_or(WalletsDBError::Internal(
133-
"`wallet_name` not initialized yet!".to_string(),
134-
))?
135-
.clone()
136-
.ok_or_else(|| WalletsDBError::Internal("`wallet_name` can't be None!".to_string()))?;
137132
table
138133
.get_item_by_unique_index("wallet_name", wallet_name)
139134
.await?
@@ -160,3 +155,14 @@ pub(super) async fn read_all_wallet_names(ctx: &MmArc) -> WalletsDBResult<impl I
160155

161156
Ok(wallet_names)
162157
}
158+
159+
pub(super) async fn delete_wallet(ctx: &MmArc, wallet_name: &str) -> WalletsDBResult<()> {
160+
let wallets_ctx = WalletsContext::from_ctx(ctx).map_to_mm(WalletsDBError::Internal)?;
161+
162+
let db = wallets_ctx.wallets_db().await?;
163+
let transaction = db.transaction().await?;
164+
let table = transaction.table::<MnemonicsTable>().await?;
165+
166+
table.delete_item_by_unique_index("wallet_name", wallet_name).await?;
167+
Ok(())
168+
}

mm2src/mm2_main/src/rpc/dispatcher/dispatcher.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::lp_stats::{add_node_to_version_stat, remove_node_from_version_stat, s
1111
stop_version_stat_collection, update_version_stat_collection};
1212
use crate::lp_swap::swap_v2_rpcs::{active_swaps_rpc, my_recent_swaps_rpc, my_swap_status_rpc};
1313
use crate::lp_swap::{get_locked_amount_rpc, max_maker_vol, recreate_swap_data, trade_preimage_rpc};
14-
use crate::lp_wallet::{change_mnemonic_password, get_mnemonic_rpc, get_wallet_names_rpc};
14+
use crate::lp_wallet::{change_mnemonic_password, delete_wallet_rpc, get_mnemonic_rpc, get_wallet_names_rpc};
1515
use crate::rpc::lp_commands::db_id::get_shared_db_id;
1616
use crate::rpc::lp_commands::one_inch::rpcs::{one_inch_v6_0_classic_swap_contract_rpc,
1717
one_inch_v6_0_classic_swap_create_rpc,
@@ -201,6 +201,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult<Re
201201
"get_token_allowance" => handle_mmrpc(ctx, request, get_token_allowance_rpc).await,
202202
"best_orders" => handle_mmrpc(ctx, request, best_orders_rpc_v2).await,
203203
"clear_nft_db" => handle_mmrpc(ctx, request, clear_nft_db).await,
204+
"delete_wallet" => handle_mmrpc(ctx, request, delete_wallet_rpc).await,
204205
"enable_bch_with_tokens" => handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::<BchCoin>).await,
205206
"enable_slp" => handle_mmrpc(ctx, request, enable_token::<SlpToken>).await,
206207
"enable_eth_with_tokens" => handle_mmrpc(ctx, request, enable_platform_coin_with_tokens::<EthCoin>).await,

0 commit comments

Comments
 (0)