Skip to content

Commit

Permalink
feat(wallets): add get_wallet_names rpc (#2202)
Browse files Browse the repository at this point in the history
This commit introduces the `get_wallet_names` RPC method, which allows clients to retrieve information about all wallet names and the currently active one.
  • Loading branch information
shamardy authored Sep 25, 2024
1 parent 5c324f2 commit ab23c11
Show file tree
Hide file tree
Showing 11 changed files with 343 additions and 59 deletions.
9 changes: 7 additions & 2 deletions mm2src/mm2_core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use derive_more::Display;
use rand::{thread_rng, Rng};
#[cfg(target_arch = "wasm32")] use derive_more::Display;
#[cfg(target_arch = "wasm32")] use rand::{thread_rng, Rng};

pub mod data_asker;
pub mod event_dispatcher;
pub mod mm_ctx;

#[cfg(target_arch = "wasm32")]
#[derive(Clone, Copy, Display, PartialEq, Default)]
pub enum DbNamespaceId {
#[display(fmt = "MAIN")]
Expand All @@ -14,9 +15,13 @@ pub enum DbNamespaceId {
Test(u64),
}

#[cfg(target_arch = "wasm32")]
impl DbNamespaceId {
pub fn for_test() -> DbNamespaceId {
let mut rng = thread_rng();
DbNamespaceId::Test(rng.gen())
}

#[inline(always)]
pub fn for_test_with_id(id: u64) -> DbNamespaceId { DbNamespaceId::Test(id) }
}
9 changes: 9 additions & 0 deletions mm2src/mm2_core/src/mm_ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,9 @@ impl MmCtx {
#[cfg(not(target_arch = "wasm32"))]
pub fn db_root(&self) -> PathBuf { path_to_db_root(self.conf["dbdir"].as_str()) }

#[cfg(not(target_arch = "wasm32"))]
pub fn db_root(&self) -> PathBuf { path_to_db_root(self.conf["dbdir"].as_str()) }

#[cfg(not(target_arch = "wasm32"))]
pub fn wallet_file_path(&self, wallet_name: &str) -> PathBuf {
self.db_root().join(wallet_name.to_string() + ".dat")
Expand Down Expand Up @@ -753,6 +756,12 @@ impl MmCtxBuilder {
self
}

#[cfg(target_arch = "wasm32")]
pub fn with_test_db_namespace_with_id(mut self, id: u64) -> Self {
self.db_namespace = DbNamespaceId::for_test_with_id(id);
self
}

pub fn into_mm_arc(self) -> MmArc {
// NB: We avoid recreating LogState
// in order not to interfere with the integration tests checking LogState drop on shutdown.
Expand Down
59 changes: 51 additions & 8 deletions mm2src/mm2_io/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,19 +192,52 @@ where
json::from_slice(&content).map_to_mm(FsJsonError::Deserializing)
}

/// Read the `dir_path` entries trying to deserialize each as the `T` type.
async fn filter_files_by_extension(dir_path: &Path, extension: &str) -> IoResult<Vec<PathBuf>> {
let ext = Some(OsStr::new(extension).to_ascii_lowercase());
let entries = read_dir_async(dir_path)
.await?
.into_iter()
.filter(|path| path.extension().map(|ext| ext.to_ascii_lowercase()) == ext)
.collect();
Ok(entries)
}

/// Helper function to extract file names or stems based on the provided extraction function.
fn extract_file_identifiers<'a, F>(entries: Vec<PathBuf>, extractor: F) -> impl Iterator<Item = String> + 'a
where
F: Fn(&Path) -> Option<&OsStr> + 'a,
{
entries
.into_iter()
.filter_map(move |path| extractor(&path).and_then(OsStr::to_str).map(ToOwned::to_owned))
}

/// Lists files by the specified extension from the given directory path.
/// If include_extension is true, returns full file names; otherwise, returns file stems.
pub async fn list_files_by_extension(
dir_path: &Path,
extension: &str,
include_extension: bool,
) -> IoResult<impl Iterator<Item = String>> {
let entries = filter_files_by_extension(dir_path, extension).await?;
let extractor = if include_extension {
Path::file_name
} else {
Path::file_stem
};
Ok(extract_file_identifiers(entries, extractor))
}

/// Read the `dir_path` entries trying to deserialize each as the `T` type,
/// filtering by the specified extension.
/// Please note that files that couldn't be deserialized are skipped.
pub async fn read_dir_json<T>(dir_path: &Path) -> FsJsonResult<Vec<T>>
pub async fn read_files_with_extension<T>(dir_path: &Path, extension: &str) -> FsJsonResult<Vec<T>>
where
T: DeserializeOwned,
{
let json_ext = Some(OsStr::new("json"));
let entries: Vec<_> = read_dir_async(dir_path)
let entries = filter_files_by_extension(dir_path, extension)
.await
.mm_err(FsJsonError::IoReading)?
.into_iter()
.filter(|path| path.extension() == json_ext)
.collect();
.mm_err(FsJsonError::IoReading)?;
let type_name = std::any::type_name::<T>();

let mut result = Vec::new();
Expand Down Expand Up @@ -233,6 +266,16 @@ where
Ok(result)
}

/// Read the `dir_path` entries trying to deserialize each as the `T` type from JSON files.
/// Please note that files that couldn't be deserialized are skipped.
#[inline(always)]
pub async fn read_dir_json<T>(dir_path: &Path) -> FsJsonResult<Vec<T>>
where
T: DeserializeOwned,
{
read_files_with_extension(dir_path, "json").await
}

pub async fn write_json<T>(t: &T, path: &Path, use_tmp_file: bool) -> FsJsonResult<()>
where
T: Serialize,
Expand Down
57 changes: 54 additions & 3 deletions mm2src/mm2_main/src/lp_wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@ use common::HttpStatusCode;
use crypto::{decrypt_mnemonic, encrypt_mnemonic, generate_mnemonic, CryptoCtx, CryptoInitError, EncryptedData,
MnemonicError};
use http::StatusCode;
use itertools::Itertools;
use mm2_core::mm_ctx::MmArc;
use mm2_err_handle::prelude::*;
use serde::de::DeserializeOwned;
use serde_json::{self as json};
use serde_json::{self as json, Value as Json};

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_encrypted_passphrase_if_available, save_encrypted_passphrase};
use mnemonics_wasm_db::{read_all_wallet_names, read_encrypted_passphrase_if_available, save_encrypted_passphrase};
use std::sync::Arc;

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

cfg_native! {
use mnemonics_storage::{read_encrypted_passphrase_if_available, save_encrypted_passphrase, WalletsStorageError};
use mnemonics_storage::{read_all_wallet_names, read_encrypted_passphrase_if_available, save_encrypted_passphrase, WalletsStorageError};
}

#[cfg(not(target_arch = "wasm32"))] mod mnemonics_storage;
Expand Down Expand Up @@ -499,3 +500,53 @@ pub async fn get_mnemonic_rpc(ctx: MmArc, req: GetMnemonicRequest) -> MmResult<G
},
}
}

/// The response to `get_wallet_names_rpc`, returns all created wallet names and the currently activated wallet name.
#[derive(Serialize)]
pub struct GetWalletNamesResponse {
wallet_names: Vec<String>,
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> {
// 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.ok_or(GetWalletsError::Internal(
"`wallet_name` not initialized yet!".to_string(),
))?;

Ok(GetWalletNamesResponse {
wallet_names: wallets,
activated_wallet: activated_wallet.clone(),
})
}
9 changes: 8 additions & 1 deletion mm2src/mm2_main/src/lp_wallet/mnemonics_storage.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crypto::EncryptedData;
use mm2_core::mm_ctx::MmArc;
use mm2_err_handle::prelude::*;
use mm2_io::fs::ensure_file_is_writable;
use mm2_io::fs::{ensure_file_is_writable, list_files_by_extension};

type WalletsStorageResult<T> = Result<T, MmError<WalletsStorageError>>;

Expand Down Expand Up @@ -61,3 +61,10 @@ pub(super) async fn read_encrypted_passphrase_if_available(ctx: &MmArc) -> Walle
))
})
}

pub(super) async fn read_all_wallet_names(ctx: &MmArc) -> WalletsStorageResult<impl Iterator<Item = String>> {
let wallet_names = list_files_by_extension(&ctx.db_root(), "dat", false)
.await
.mm_err(|e| WalletsStorageError::FsReadError(format!("Error reading wallets directory: {}", e)))?;
Ok(wallet_names)
}
13 changes: 13 additions & 0 deletions mm2src/mm2_main/src/lp_wallet/mnemonics_wasm_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,16 @@ pub(super) async fn read_encrypted_passphrase_if_available(ctx: &MmArc) -> Walle
})
.transpose()
}

pub(super) async fn read_all_wallet_names(ctx: &MmArc) -> WalletsDBResult<impl Iterator<Item = String>> {
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 all_items = table.get_all_items().await?;
let wallet_names = all_items.into_iter().map(|(_, item)| item.wallet_name);

Ok(wallet_names)
}
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 @@ -5,7 +5,7 @@ use crate::lp_native_dex::init_metamask::{cancel_connect_metamask, connect_metam
use crate::lp_ordermatch::{best_orders_rpc_v2, orderbook_rpc_v2, start_simple_market_maker_bot,
stop_simple_market_maker_bot};
use crate::lp_swap::swap_v2_rpcs::{active_swaps_rpc, my_recent_swaps_rpc, my_swap_status_rpc};
use crate::lp_wallet::get_mnemonic_rpc;
use crate::lp_wallet::{get_mnemonic_rpc, get_wallet_names_rpc};
use crate::rpc::rate_limiter::{process_rate_limit, RateLimitContext};
use crate::{lp_stats::{add_node_to_version_stat, remove_node_from_version_stat, start_version_stat_collection,
stop_version_stat_collection, update_version_stat_collection},
Expand Down Expand Up @@ -190,6 +190,7 @@ async fn dispatcher_v2(request: MmRpcRequest, ctx: MmArc) -> DispatcherResult<Re
"get_raw_transaction" => handle_mmrpc(ctx, request, get_raw_transaction).await,
"get_shared_db_id" => handle_mmrpc(ctx, request, get_shared_db_id).await,
"get_staking_infos" => handle_mmrpc(ctx, request, get_staking_infos).await,
"get_wallet_names" => handle_mmrpc(ctx, request, get_wallet_names_rpc).await,
"max_maker_vol" => handle_mmrpc(ctx, request, max_maker_vol).await,
"my_recent_swaps" => handle_mmrpc(ctx, request, my_recent_swaps_rpc).await,
"my_swap_status" => handle_mmrpc(ctx, request, my_swap_status_rpc).await,
Expand Down
70 changes: 52 additions & 18 deletions mm2src/mm2_main/src/wasm_tests.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
use crate::lp_init;
use common::executor::{spawn, Timer};
use common::executor::{spawn, spawn_abortable, spawn_local_abortable, AbortOnDropHandle, Timer};
use common::log::wasm_log::register_wasm_log;
use mm2_core::mm_ctx::MmArc;
use mm2_number::BigDecimal;
use mm2_rpc::data::legacy::OrderbookResponse;
use mm2_test_helpers::electrums::{doc_electrums, marty_electrums};
use mm2_test_helpers::for_tests::{check_recent_swaps, enable_electrum_json, enable_utxo_v2_electrum,
enable_z_coin_light, morty_conf, pirate_conf, rick_conf, start_swaps,
test_qrc20_history_impl, wait_for_swaps_finish_and_check_status, MarketMakerIt,
Mm2InitPrivKeyPolicy, Mm2TestConf, Mm2TestConfForSwap, ARRR, MORTY,
enable_z_coin_light, get_wallet_names, morty_conf, pirate_conf, rick_conf,
start_swaps, test_qrc20_history_impl, wait_for_swaps_finish_and_check_status,
MarketMakerIt, Mm2InitPrivKeyPolicy, Mm2TestConf, Mm2TestConfForSwap, ARRR, MORTY,
PIRATE_ELECTRUMS, PIRATE_LIGHTWALLETD_URLS, RICK};
use mm2_test_helpers::get_passphrase;
use mm2_test_helpers::structs::{Bip44Chain, EnableCoinBalance, HDAccountAddressId};
use serde_json::json;
use wasm_bindgen_test::wasm_bindgen_test;

const PIRATE_TEST_BALANCE_SEED: &str = "pirate test seed";
const STOP_TIMEOUT_MS: u64 = 1000;

/// Starts the WASM version of MM.
fn wasm_start(ctx: MmArc) {
Expand All @@ -26,13 +27,7 @@ fn wasm_start(ctx: MmArc) {

/// This function runs Alice and Bob nodes, activates coins, starts swaps,
/// and then immediately stops the nodes to check if `MmArc` is dropped in a short period.
async fn test_mm2_stops_impl(
pairs: &[(&'static str, &'static str)],
maker_price: f64,
taker_price: f64,
volume: f64,
stop_timeout_ms: u64,
) {
async fn test_mm2_stops_impl(pairs: &[(&'static str, &'static str)], maker_price: f64, taker_price: f64, volume: f64) {
let coins = json!([rick_conf(), morty_conf()]);

let bob_passphrase = get_passphrase!(".env.seed", "BOB_PASSPHRASE").unwrap();
Expand Down Expand Up @@ -69,20 +64,18 @@ async fn test_mm2_stops_impl(
start_swaps(&mut mm_bob, &mut mm_alice, pairs, maker_price, taker_price, volume).await;

mm_alice
.stop_and_wait_for_ctx_is_dropped(stop_timeout_ms)
.stop_and_wait_for_ctx_is_dropped(STOP_TIMEOUT_MS)
.await
.unwrap();
mm_bob.stop_and_wait_for_ctx_is_dropped(stop_timeout_ms).await.unwrap();
mm_bob.stop_and_wait_for_ctx_is_dropped(STOP_TIMEOUT_MS).await.unwrap();
}

#[wasm_bindgen_test]
async fn test_mm2_stops_immediately() {
const STOP_TIMEOUT_MS: u64 = 1000;

register_wasm_log();

let pairs: &[_] = &[("RICK", "MORTY")];
test_mm2_stops_impl(pairs, 1., 1., 0.0001, STOP_TIMEOUT_MS).await;
test_mm2_stops_impl(pairs, 1., 1., 0.0001).await;
}

#[wasm_bindgen_test]
Expand Down Expand Up @@ -147,8 +140,6 @@ async fn trade_base_rel_electrum(
assert_eq!(0, bob_orderbook.asks.len(), "{} {} asks must be empty", base, rel);
}

const STOP_TIMEOUT_MS: u64 = 1000;

mm_bob.stop_and_wait_for_ctx_is_dropped(STOP_TIMEOUT_MS).await.unwrap();
mm_alice
.stop_and_wait_for_ctx_is_dropped(STOP_TIMEOUT_MS)
Expand Down Expand Up @@ -266,3 +257,46 @@ async fn activate_z_coin_light() {
};
assert_eq!(balance.balance.spendable, BigDecimal::default());
}

#[wasm_bindgen_test]
async fn test_get_wallet_names() {
const DB_NAMESPACE_NUM: u64 = 1;

let coins = json!([]);

// Initialize the first wallet with a specific name
let wallet_1 = Mm2TestConf::seednode_with_wallet_name(&coins, "wallet_1", "pass");
let mm_wallet_1 =
MarketMakerIt::start_with_db(wallet_1.conf, wallet_1.rpc_password, Some(wasm_start), DB_NAMESPACE_NUM)
.await
.unwrap();

// Retrieve and verify the wallet names for the first wallet
let get_wallet_names_1 = get_wallet_names(&mm_wallet_1).await;
assert_eq!(get_wallet_names_1.wallet_names, vec!["wallet_1"]);
assert_eq!(get_wallet_names_1.activated_wallet.unwrap(), "wallet_1");

// Stop the first wallet before starting the second one
mm_wallet_1
.stop_and_wait_for_ctx_is_dropped(STOP_TIMEOUT_MS)
.await
.unwrap();

// Initialize the second wallet with a different name
let wallet_2 = Mm2TestConf::seednode_with_wallet_name(&coins, "wallet_2", "pass");
let mm_wallet_2 =
MarketMakerIt::start_with_db(wallet_2.conf, wallet_2.rpc_password, Some(wasm_start), DB_NAMESPACE_NUM)
.await
.unwrap();

// Retrieve and verify the wallet names for the second wallet
let get_wallet_names_2 = get_wallet_names(&mm_wallet_2).await;
assert_eq!(get_wallet_names_2.wallet_names, vec!["wallet_1", "wallet_2"]);
assert_eq!(get_wallet_names_2.activated_wallet.unwrap(), "wallet_2");

// Stop the second wallet
mm_wallet_2
.stop_and_wait_for_ctx_is_dropped(STOP_TIMEOUT_MS)
.await
.unwrap();
}
Loading

0 comments on commit ab23c11

Please sign in to comment.