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(wallets): add get_wallet_names rpc #2202

Merged
merged 11 commits into from
Sep 25, 2024
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) }
}
shamardy marked this conversation as resolved.
Show resolved Hide resolved
12 changes: 10 additions & 2 deletions mm2src/mm2_core/src/mm_ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,10 +287,12 @@ 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 wallet_file_path(&self, wallet_name: &str) -> PathBuf {
let db_root = path_to_db_root(self.conf["dbdir"].as_str());
db_root.join(wallet_name.to_string() + ".dat")
self.db_root().join(wallet_name.to_string() + ".dat")
}

/// MM database path.
Expand Down Expand Up @@ -737,6 +739,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(),
))?;
Comment on lines +544 to +546
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you explain please, how you get Ok result in non-login mode with null activated_wallet, if you return Err when wallet_name None?

No-Login Mode Response from pr info

{
    "mmrpc": "2.0",
    "result": {
        "wallet_names": ["Test 1", "Test 2"],
        "activated_wallet": null
    },
    "id": null
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok_or is for Constructible not Option

/// Wallet name for this mm2 instance. Optional for backwards compatibility.
pub wallet_name: Constructible<Option<String>>,

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, exactly

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just ran into the same issue :) Maybe we should make it more explicit with some helpful comments?

(Unresolving the topic to make it visible - feel free to re-resolve the topic once you see it)

Copy link
Collaborator Author

@shamardy shamardy Sep 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment here 704305d


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
Loading