From 9891eed4f412897bd6f25ea87b2ebe6c1aad2414 Mon Sep 17 00:00:00 2001 From: fxrstor <132809543+fxrstor@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:12:52 -0400 Subject: [PATCH] add monero_wallet integration tests --- .github/workflows/ci.yml | 8 + Cargo.lock | 114 ++++++++--- Cargo.toml | 2 +- {monero-wallet => monero_wallet}/Cargo.toml | 11 +- .../src/compat.rs | 0 {monero-wallet => monero_wallet}/src/lib.rs | 0 .../src/listener.rs | 2 +- .../src/wallets.rs | 0 monero_wallet/tests/harness/mod.rs | 178 +++++++++++++++++ monero_wallet/tests/swap_wallet_tests.rs | 77 +++++++ .../tests/tauri_wallet_listener_tests.rs | 109 ++++++++++ monero_wallet/tests/transaction_tests.rs | 107 ++++++++++ monero_wallet/tests/wallet_lifecycle.rs | 189 ++++++++++++++++++ swap/Cargo.toml | 2 +- 14 files changed, 762 insertions(+), 37 deletions(-) rename {monero-wallet => monero_wallet}/Cargo.toml (78%) rename {monero-wallet => monero_wallet}/src/compat.rs (100%) rename {monero-wallet => monero_wallet}/src/lib.rs (100%) rename {monero-wallet => monero_wallet}/src/listener.rs (99%) rename {monero-wallet => monero_wallet}/src/wallets.rs (100%) create mode 100644 monero_wallet/tests/harness/mod.rs create mode 100644 monero_wallet/tests/swap_wallet_tests.rs create mode 100644 monero_wallet/tests/tauri_wallet_listener_tests.rs create mode 100644 monero_wallet/tests/transaction_tests.rs create mode 100644 monero_wallet/tests/wallet_lifecycle.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b1ecea7b7..73b61efbf3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -212,6 +212,14 @@ jobs: test_name: transfers_wrong_key - package: monero-tests test_name: sweep + - package: monero_wallet + test_name: swap_wallet_tests + - package: monero_wallet + test_name: tauri_wallet_listener_tests + - package: monero_wallet + test_name: transaction_tests + - package: monero_wallet + test_name: wallet_lifecycle runs-on: ubuntu-22.04 if: github.event_name == 'push' || !github.event.pull_request.draft diff --git a/Cargo.lock b/Cargo.lock index 1f6bb787c0..fd3568a35b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6484,7 +6484,7 @@ dependencies = [ "curve25519-dalek-ng", "hex", "monero-address", - "monero-wallet 0.1.0 (git+https://github.com/kayabaNerve/monero-oxide.git)", + "monero-wallet", "serde", "typeshare", ] @@ -6598,7 +6598,7 @@ dependencies = [ "monero-oxide-ext", "monero-simple-request-rpc", "monero-sys", - "monero-wallet 0.1.0 (git+https://github.com/kayabaNerve/monero-oxide.git)", + "monero-wallet", "monero-wallet-ng", "rand 0.8.5", "testcontainers", @@ -6609,30 +6609,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "monero-wallet" -version = "0.1.0" -dependencies = [ - "anyhow", - "backoff", - "curve25519-dalek", - "hex", - "monero-address", - "monero-daemon-rpc", - "monero-oxide-ext", - "monero-simple-request-rpc", - "monero-sys", - "monero-wallet 0.1.0 (git+https://github.com/kayabaNerve/monero-oxide.git)", - "monero-wallet-ng", - "swap-core", - "throttle", - "tokio", - "tracing", - "tracing-subscriber", - "uuid", - "zeroize", -] - [[package]] name = "monero-wallet" version = "0.1.0" @@ -6666,7 +6642,7 @@ dependencies = [ "monero-interface", "monero-oxide", "monero-simple-request-rpc", - "monero-wallet 0.1.0 (git+https://github.com/kayabaNerve/monero-oxide.git)", + "monero-wallet", "rand 0.8.5", "serde", "serde_json", @@ -6676,6 +6652,37 @@ dependencies = [ "zeroize", ] +[[package]] +name = "monero_wallet" +version = "0.1.0" +dependencies = [ + "anyhow", + "backoff", + "curve25519-dalek", + "hex", + "monero-address", + "monero-daemon-rpc", + "monero-harness", + "monero-oxide-ext", + "monero-simple-request-rpc", + "monero-sys", + "monero-wallet", + "monero-wallet-ng", + "paste", + "rand 0.8.5", + "rustls 0.23.37", + "serial_test", + "swap-core", + "tempfile", + "testcontainers", + "throttle", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", + "zeroize", +] + [[package]] name = "moxcms" version = "0.8.1" @@ -9247,6 +9254,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b63583a1dd0647d1484228529ab4ecaa874048d2956f117362aa5f5826456230" +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" version = "0.1.29" @@ -9339,6 +9355,12 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + [[package]] name = "seahash" version = "4.1.0" @@ -9772,6 +9794,32 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serial_test" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" +dependencies = [ + "futures-executor", + "futures-util", + "log", + "once_cell", + "parking_lot 0.12.5", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "serialize-to-javascript" version = "0.1.2" @@ -10619,8 +10667,8 @@ dependencies = [ "monero-rpc-pool", "monero-seed", "monero-sys", - "monero-wallet 0.1.0", - "monero-wallet 0.1.0 (git+https://github.com/kayabaNerve/monero-oxide.git)", + "monero-wallet", + "monero_wallet", "pem", "proptest", "rand 0.8.5", @@ -10742,7 +10790,7 @@ dependencies = [ "hex", "monero-address", "monero-oxide-ext", - "monero-wallet 0.1.0 (git+https://github.com/kayabaNerve/monero-oxide.git)", + "monero-wallet", "proptest", "rand 0.8.5", "rand_chacha 0.3.1", @@ -10765,7 +10813,7 @@ name = "swap-db" version = "4.0.0" dependencies = [ "bitcoin 0.32.8", - "monero-wallet 0.1.0 (git+https://github.com/kayabaNerve/monero-oxide.git)", + "monero-wallet", "serde", "serde_json", "strum 0.26.3", @@ -10839,7 +10887,7 @@ dependencies = [ "libp2p", "monero-address", "monero-oxide-ext", - "monero-wallet 0.1.0 (git+https://github.com/kayabaNerve/monero-oxide.git)", + "monero-wallet", "rand 0.8.5", "rand_chacha 0.3.1", "rust_decimal", @@ -10929,7 +10977,7 @@ dependencies = [ "libp2p", "monero-address", "monero-oxide-ext", - "monero-wallet 0.1.0 (git+https://github.com/kayabaNerve/monero-oxide.git)", + "monero-wallet", "serde", "serde_json", "thiserror 1.0.69", diff --git a/Cargo.toml b/Cargo.toml index d4574e625c..c677f75d92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ "monero-rpc-pool", "monero-sys", "monero-tests", - "monero-wallet", + "monero_wallet", "monero-wallet-ng", "src-tauri", "swap", diff --git a/monero-wallet/Cargo.toml b/monero_wallet/Cargo.toml similarity index 78% rename from monero-wallet/Cargo.toml rename to monero_wallet/Cargo.toml index af55440f39..5a1558579a 100644 --- a/monero-wallet/Cargo.toml +++ b/monero_wallet/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "monero-wallet" +name = "monero_wallet" version = "0.1.0" edition = "2024" @@ -33,3 +33,12 @@ monero-wallet-ng = { path = "../monero-wallet-ng" } curve25519-dalek = { workspace = true } hex = { workspace = true } zeroize = { workspace = true } +rustls = { version = "0.23", features = ["ring"] } + +[dev-dependencies] +monero-harness = { path = "../monero-harness" } +testcontainers = "0.15" +serial_test = "3" +tempfile = "3" +rand = { workspace = true } +paste = "1" diff --git a/monero-wallet/src/compat.rs b/monero_wallet/src/compat.rs similarity index 100% rename from monero-wallet/src/compat.rs rename to monero_wallet/src/compat.rs diff --git a/monero-wallet/src/lib.rs b/monero_wallet/src/lib.rs similarity index 100% rename from monero-wallet/src/lib.rs rename to monero_wallet/src/lib.rs diff --git a/monero-wallet/src/listener.rs b/monero_wallet/src/listener.rs similarity index 99% rename from monero-wallet/src/listener.rs rename to monero_wallet/src/listener.rs index 060f9ba3b3..e7e593e255 100644 --- a/monero-wallet/src/listener.rs +++ b/monero_wallet/src/listener.rs @@ -16,7 +16,7 @@ pub trait MoneroTauriHandle: Send + Sync { fn sync_progress(&self, current_block: u64, target_block: u64, progress_percentage: f32); } -pub(crate) struct TauriWalletListener { +pub struct TauriWalletListener { balance_throttle: Throttle<()>, history_throttle: Throttle<()>, sync_throttle: Throttle<()>, diff --git a/monero-wallet/src/wallets.rs b/monero_wallet/src/wallets.rs similarity index 100% rename from monero-wallet/src/wallets.rs rename to monero_wallet/src/wallets.rs diff --git a/monero_wallet/tests/harness/mod.rs b/monero_wallet/tests/harness/mod.rs new file mode 100644 index 0000000000..7a4d464c74 --- /dev/null +++ b/monero_wallet/tests/harness/mod.rs @@ -0,0 +1,178 @@ +#![allow(dead_code)] + +use anyhow::{Context, Result}; +use monero_address::Network; +use monero_harness::{image, Monero}; +use monero_sys::{Daemon, WalletHandle}; +use std::future::Future; +use std::path::PathBuf; +use std::sync::{Arc, Once, OnceLock}; +use tempfile::TempDir; +use testcontainers::clients::Cli; +use testcontainers::Container; +use tokio::time::{sleep, Duration}; + +pub const WALLET_NAME: &str = "test_wallet"; +pub const SETTLE_DURATION: Duration = Duration::from_secs(5); +pub const CONFIRM_BLOCKS: usize = 3; + +static INIT_RUSTLS: Once = Once::new(); +static INIT_TRACING: Once = Once::new(); +static CLI: OnceLock = OnceLock::new(); + +pub fn docker_client() -> &'static Cli { + CLI.get_or_init(Cli::default) +} + +fn init_rustls() { + INIT_RUSTLS.call_once(|| { + rustls::crypto::ring::default_provider() + .install_default() + .expect("failed to install rustls ring crypto provider"); + }); +} + +fn init_tracing() { + INIT_TRACING.call_once(|| { + let _ = tracing_subscriber::fmt() + .with_env_filter("info,monero_wallet=debug,monero_sys=debug") + .with_test_writer() + .try_init(); + }); +} + +pub struct TestContext { + pub monero: Monero, + pub daemon: Daemon, + _monerod: Container<'static, image::Monerod>, + pub wallet_dir: TempDir, +} + +impl TestContext { + async fn new() -> Result { + let (monero, monerod, _) = Monero::new(docker_client(), vec![WALLET_NAME]) + .await + .context("spawning monero containers")?; + + let monerod_port = monerod + .ports() + .map_to_host_port_ipv4(image::RPC_PORT) + .context("monerod RPC port should be mapped")?; + + let daemon = Daemon { + hostname: "127.0.0.1".to_string(), + port: monerod_port, + ssl: false, + }; + + let wallet_dir = TempDir::new().context("creating wallet temp dir")?; + + Ok(Self { + monero, + daemon, + _monerod: monerod, + wallet_dir, + }) + } + + pub fn wallet_path(&self) -> PathBuf { + self.wallet_dir.path().join(WALLET_NAME) + } + + pub async fn open_regtest_wallet(&self, wallet_path: PathBuf) -> Result { + let wallet = WalletHandle::open_or_create( + wallet_path.display().to_string(), + self.daemon.clone(), + Network::Mainnet, + false, + ) + .await + .context("opening wallet handle")?; + + wallet.unsafe_prepare_for_regtest().await; + Ok(wallet) + } + + pub async fn generate_blocks(&self, n: usize) -> Result<()> { + for _ in 0..n { + self.monero + .generate_blocks() + .await + .context("generate block")?; + } + + sleep(Duration::from_millis(150)).await; + Ok(()) + } + + pub async fn sync_wallet(&self, wallet: &WalletHandle) -> Result<()> { + tokio::time::timeout(Duration::from_secs(90), async { + wallet.wait_until_synced(monero_sys::no_listener()).await + }) + .await + .context("wallet sync timed out after 90 s")? + .context("wait_until_synced returned an error") + } + + pub async fn wait_for_unlocked_balance( + &self, + wallet: &WalletHandle, + expected_pico: u64, + timeout_secs: u64, + ) -> Result<()> { + let deadline = std::time::Instant::now() + Duration::from_secs(timeout_secs); + + loop { + let unlocked = wallet + .unlocked_balance() + .await + .context("reading unlocked balance")?; + + if unlocked.as_pico() >= expected_pico { + return Ok(()); + } + + if std::time::Instant::now() >= deadline { + anyhow::bail!( + "timed out waiting for unlocked balance ≥ {} pico (current: {})", + expected_pico, + unlocked.as_pico() + ); + } + + sleep(Duration::from_millis(100)).await; + } + } +} + +pub async fn setup_test(test: F) -> Result<()> +where + F: FnOnce(Arc) -> Fut + Send + 'static, + Fut: Future> + Send + 'static, +{ + init_rustls(); + init_tracing(); + + let ctx = Arc::new(TestContext::new().await.context("spawning test context")?); + ctx.monero.init_miner().await.context("init miner")?; + ctx.monero.start_miner().await.context("start miner")?; + + let result = test(ctx.clone()).await; + + sleep(SETTLE_DURATION).await; + + result +} + +#[macro_export] +macro_rules! integration_test { + ($name:ident) => { + paste::paste! { + #[tokio::test] + #[serial_test::serial] + async fn $name() -> anyhow::Result<()> { + crate::harness::setup_test([<$name _case>]).await + } + } + }; +} diff --git a/monero_wallet/tests/swap_wallet_tests.rs b/monero_wallet/tests/swap_wallet_tests.rs new file mode 100644 index 0000000000..51967fd377 --- /dev/null +++ b/monero_wallet/tests/swap_wallet_tests.rs @@ -0,0 +1,77 @@ +mod harness; + +use anyhow::{Context, Result}; +use harness::{TestContext, CONFIRM_BLOCKS, WALLET_NAME}; +use monero_address::{AddressType, MoneroAddress, Network}; +use monero_oxide_ext::{PrivateKey, PublicKey}; +use rand::rngs::OsRng; +use swap_core::monero::primitives::{PrivateViewKey, TxHash}; +use uuid::Uuid; +use std::sync::Arc; +use monero_wallet::Wallets; + +integration_test!(test_swap_wallet_detects_incoming_balance); + +async fn test_swap_wallet_detects_incoming_balance_case(ctx: Arc) -> Result<()> { + let mut rng = OsRng; + + let spend_key_prim = PrivateViewKey::new_random(&mut rng); + let view_key_prim = PrivateViewKey::new_random(&mut rng); + + let spend_key: PrivateKey = spend_key_prim.into(); + let view_key_for_addr: PrivateKey = view_key_prim.clone().into(); + + let address = MoneroAddress::new( + Network::Mainnet, + AddressType::Legacy, + PublicKey::from_private_key(&spend_key).decompress(), + PublicKey::from_private_key(&view_key_for_addr).decompress(), + ); + + let amount = 1_000_000_000_000u64; + let receipt = ctx + .monero + .wallet("miner")? + .transfer(&address, amount) + .await + .context("funding swap address")?; + + let tx_hash = TxHash(receipt.txid); + + ctx.generate_blocks(CONFIRM_BLOCKS).await?; + + let wallets = Wallets::new( + ctx.wallet_dir.path().to_path_buf(), + WALLET_NAME.to_string(), + ctx.daemon.clone(), + Network::Mainnet, + true, + None, + None, + ) + .await + .context("creating Wallets")?; + + let main_wallet = wallets.main_wallet().await; + main_wallet.refresh_blocking().await?; + let restore_height = main_wallet.blockchain_height().await?.saturating_sub(15); + + let swap_wallet = wallets + .swap_wallet_spendable(Uuid::new_v4(), spend_key, view_key_prim, tx_hash) + .await + .context("creating spendable swap wallet")?; + + swap_wallet.set_restore_height(restore_height).await?; + swap_wallet.refresh_blocking().await?; + + ctx.wait_for_unlocked_balance(&swap_wallet, amount, 120).await?; + + let balance = swap_wallet.total_balance().await?; + assert_eq!( + balance.as_pico(), + amount, + "swap wallet balance mismatch" + ); + + Ok(()) +} diff --git a/monero_wallet/tests/tauri_wallet_listener_tests.rs b/monero_wallet/tests/tauri_wallet_listener_tests.rs new file mode 100644 index 0000000000..4d4a1dbc6b --- /dev/null +++ b/monero_wallet/tests/tauri_wallet_listener_tests.rs @@ -0,0 +1,109 @@ +mod harness; + +use anyhow::{Context, Result}; +use harness::TestContext; +use monero_sys::{TransactionInfo, WalletEventListener}; +use monero_wallet::{MoneroTauriHandle, TauriWalletListener}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use swap_core::monero::Amount; +use tokio::sync::Notify; + +struct RecordingHandle { + balance_updates: Mutex>, + history_updates: Mutex>>, + sync_updates: Mutex>, + + balance_notified: Notify, + history_notified: Notify, + sync_notified: Notify, +} + +impl RecordingHandle { + fn new() -> Self { + Self { + balance_updates: Mutex::new(Vec::new()), + history_updates: Mutex::new(Vec::new()), + sync_updates: Mutex::new(Vec::new()), + balance_notified: Notify::new(), + history_notified: Notify::new(), + sync_notified: Notify::new(), + } + } +} + +impl MoneroTauriHandle for RecordingHandle { + fn balance_change(&self, total: Amount, unlocked: Amount) { + self.balance_updates.lock().unwrap().push((total, unlocked)); + self.balance_notified.notify_one(); + } + + fn history_update(&self, txs: Vec) { + self.history_updates.lock().unwrap().push(txs); + self.history_notified.notify_one(); + } + + fn sync_progress(&self, current: u64, target: u64, pct: f32) { + self.sync_updates.lock().unwrap().push((current, target, pct)); + self.sync_notified.notify_one(); + } +} + +async fn wait_for(notify: &Notify, what: &str) -> Result<()> { + tokio::time::timeout(Duration::from_secs(8), notify.notified()) + .await + .with_context(|| format!("timed out waiting for {what}"))?; + Ok(()) +} + +async fn make_listener( + context: &TestContext, +) -> Result<(Arc, TauriWalletListener, monero_sys::WalletHandle)> { + let handle = Arc::new(RecordingHandle::new()); + let wallet = context.open_regtest_wallet(context.wallet_path()).await?; + let listener = TauriWalletListener::new( + handle.clone() as Arc, + wallet.clone().into(), + ) + .await; + + Ok((handle, listener, wallet)) +} + +integration_test!(test_tauri_listener_emits_balance_and_history_updates); + +async fn test_tauri_listener_emits_balance_and_history_updates_case(ctx: Arc) -> Result<()> { + let (handle, listener, _wallet) = make_listener(&ctx).await?; + + listener.on_money_received("txid", 1_000_000); + listener.on_money_spent("txid2", 500_000); + + wait_for(&handle.balance_notified, "balance update").await?; + wait_for(&handle.history_notified, "history update").await?; + + { + let balance_guards = handle.balance_updates.lock().unwrap(); + assert!(!balance_guards.is_empty(), "expected at least one balance update"); + let history_guards = handle.history_updates.lock().unwrap(); + assert!(!history_guards.is_empty(), "expected at least one history update"); + } + + Ok(()) +} + +integration_test!(test_tauri_listener_emits_sync_progress_on_new_block); + +async fn test_tauri_listener_emits_sync_progress_on_new_block_case(ctx: Arc) -> Result<()> { + let (handle, listener, _wallet) = make_listener(&ctx).await?; + + listener.on_new_block(100); + + wait_for(&handle.sync_notified, "sync progress").await?; + + { + let sync = handle.sync_updates.lock().unwrap(); + assert!(!sync.is_empty(), "expected at least one sync update"); + } + + Ok(()) +} diff --git a/monero_wallet/tests/transaction_tests.rs b/monero_wallet/tests/transaction_tests.rs new file mode 100644 index 0000000000..01b40a822e --- /dev/null +++ b/monero_wallet/tests/transaction_tests.rs @@ -0,0 +1,107 @@ +mod harness; + +use anyhow::{Context, Result}; +use harness::{TestContext, CONFIRM_BLOCKS}; +use monero_address::{MoneroAddress}; +use monero_sys::{TransactionDirection, WalletHandle}; +use swap_core::monero::Amount; +use tempfile::TempDir; +use std::sync::Arc; + +async fn open_wallet_with_address( + ctx: &TestContext, +) -> Result<(WalletHandle, MoneroAddress)> { + let wallet = ctx.open_regtest_wallet(ctx.wallet_path()).await?; + let address = wallet.main_address().await?; + Ok((wallet, address)) +} + +async fn fund_wallet(ctx: &TestContext, wallet: &WalletHandle, amount: u64) -> Result<()> { + let address = wallet.main_address().await?; + + ctx.monero.wallet("miner")? + .transfer(&address, amount) + .await + .context("funding wallet")?; + + ctx.generate_blocks(CONFIRM_BLOCKS).await?; + ctx.sync_wallet(wallet).await?; + + Ok(()) +} + + +integration_test!(test_receive_funds_into_wallet); + +async fn test_receive_funds_into_wallet_case(ctx: Arc) -> Result<()> { + let (main_wallet, _) = open_wallet_with_address(&ctx).await?; + + let amount = 1_000_000_000_000u64; + fund_wallet(&ctx, &main_wallet, amount).await?; + ctx.wait_for_unlocked_balance(&main_wallet, amount, 60).await?; + + let unlocked = main_wallet.unlocked_balance().await?; + assert_eq!(unlocked.as_pico(), amount); + + Ok(()) + +} + + +integration_test!(test_records_incoming_transaction_in_history); + +async fn test_records_incoming_transaction_in_history_case(ctx: Arc) -> Result<()> { + let (main_wallet, _) = open_wallet_with_address(&ctx).await?; + + let amount = 1_000_000_000_000u64; + fund_wallet(&ctx, &main_wallet, amount).await?; + + let transactions = main_wallet.history().await?; + assert!( + !transactions.is_empty(), + "transaction history is empty after receiving funds" + ); + + let tx = transactions + .iter() + .find(|t| t.direction == TransactionDirection::In && t.amount.as_pico() == amount) + .context("expected incoming transaction not found in history")?; + + assert_eq!(tx.direction, TransactionDirection::In); + assert_eq!(tx.amount.as_pico(), amount); + + Ok(()) +} + +integration_test!(test_transfers_funds_between_wallets); + +async fn test_transfers_funds_between_wallets_case(ctx: Arc) -> Result<()> { + let (alice, _alice_address) = open_wallet_with_address(&ctx).await?; + + let fund_amount = 1_000_000_000_000u64; + fund_wallet(&ctx, &alice, fund_amount).await?; + ctx.wait_for_unlocked_balance(&alice, fund_amount, 120).await?; + + let bob_dir = TempDir::new().context("creating Bob's wallet dir")?; + let bob = ctx + .open_regtest_wallet(bob_dir.path().join("bob_wallet")) + .await + .context("creating Bob's wallet")?; + + let bob_address = bob.main_address().await?; + let send_amount = 100_000_000_000u64; + + alice + .transfer_single_destination(&bob_address, Amount::from_pico(send_amount)) + .await + .context("Alice -> Bob transfer")?; + + ctx.generate_blocks(CONFIRM_BLOCKS).await?; + ctx.sync_wallet(&bob).await?; + ctx.wait_for_unlocked_balance(&bob, send_amount, 120).await?; + + let bob_unlocked = bob.unlocked_balance().await?; + assert_eq!(bob_unlocked.as_pico(), send_amount); + + Ok(()) +} diff --git a/monero_wallet/tests/wallet_lifecycle.rs b/monero_wallet/tests/wallet_lifecycle.rs new file mode 100644 index 0000000000..6fbc34e3f2 --- /dev/null +++ b/monero_wallet/tests/wallet_lifecycle.rs @@ -0,0 +1,189 @@ +mod harness; + +use anyhow::{Context, Result}; +use harness::{TestContext, WALLET_NAME}; +use monero_address::Network; +use monero_harness::{image, Monero}; +use monero_sys::{Daemon, WalletHandle}; +use monero_wallet::Wallets; +use std::sync::Arc; +use std::time::Duration; + +integration_test!(test_creates_new_wallet); + +async fn test_creates_new_wallet_case(ctx: Arc) -> Result<()> { + let wallet = ctx.open_regtest_wallet(ctx.wallet_path()).await?; + let address = wallet.main_address().await?; + + assert_eq!(address.network(), Network::Mainnet); + assert!( + address.to_string().starts_with('4'), + "unexpected address prefix: {address}" + ); + + Ok(()) +} + +integration_test!(test_reopens_existing_wallet); + +async fn test_reopens_existing_wallet_case(ctx: Arc) -> Result<()> { + let wallet_path = ctx.wallet_path(); + + let initial_address = { + let wallet = ctx.open_regtest_wallet(ctx.wallet_path()).await?; + wallet.main_address().await? + }; + + let reopened = ctx + .open_regtest_wallet(wallet_path) + .await + .context("re-opening existing wallet")?; + + let address = reopened.main_address().await?; + assert_eq!(address.network(), Network::Mainnet); + assert_eq!(address.to_string(), initial_address.to_string()); + + Ok(()) +} + +integration_test!(test_restores_wallet_from_seed); + +async fn test_restores_wallet_from_seed_case(ctx: Arc) -> Result<()> { + let (seed, original_address) = { + let wallet = ctx.open_regtest_wallet(ctx.wallet_path()).await?; + let seed = wallet.seed().await?; + let address = wallet.main_address().await?; + drop(wallet); + (seed, address) + }; + + let restore_dir = tempfile::TempDir::new().context("creating restore dir")?; + let restore_path = restore_dir.path().join("restored_wallet").display().to_string(); + + let restored = WalletHandle::open_or_create_from_seed( + restore_path, + seed, + Network::Mainnet, + 0, + false, + ctx.daemon.clone(), + ) + .await + .context("restoring wallet from seed")?; + + restored.unsafe_prepare_for_regtest().await; + + assert_eq!( + restored.main_address().await?, + original_address, + "restored address differs from original" + ); + + Ok(()) +} + +integration_test!(test_records_wallet_in_recent_wallets); + +async fn test_records_wallet_in_recent_wallets_case(ctx: Arc) -> Result<()> { + let db_dir = tempfile::TempDir::new().context("creating db dir")?; + let db = Arc::new(monero_sys::Database::new(db_dir.path().to_path_buf()).await?); + + let wallets = Wallets::new( + ctx.wallet_dir.path().to_path_buf(), + WALLET_NAME.to_string(), + ctx.daemon.clone(), + Network::Mainnet, + true, + None, + Some(db.clone()), + ) + .await + .context("creating wallets")?; + + let main_wallet = wallets.main_wallet().await; + main_wallet.refresh_blocking().await?; + let _ = main_wallet.main_address().await?; + drop(main_wallet); + + let recent = wallets.get_recent_wallets().await?; + assert!(!recent.is_empty()); + assert!( + recent.iter().any(|p| p.contains(WALLET_NAME)), + "expected wallet name '{WALLET_NAME}' not found in recent list" + ); + + Ok(()) +} + +integration_test!(test_change_monero_node_to_same_daemon); + +async fn test_change_monero_node_to_same_daemon_case(ctx: Arc) -> Result<()> { + let wallet = ctx.open_regtest_wallet(ctx.wallet_path()).await?; + + wallet.refresh_blocking().await?; + let height_before = wallet.blockchain_height().await?; + + let daemon = ctx.daemon.clone(); + wallet + .call(move |w| w.set_daemon(&daemon)) + .await??; + + wallet.refresh_blocking().await?; + let height_after = wallet.blockchain_height().await?; + + assert!(height_after >= height_before); + + Ok(()) +} + +integration_test!(test_change_monero_node_to_different_daemon_and_resyncs); + +async fn test_change_monero_node_to_different_daemon_and_resyncs_case(ctx: Arc) -> Result<()> { + let wallet = ctx.open_regtest_wallet(ctx.wallet_path()).await?; + + ctx.generate_blocks(12).await?; + wallet.refresh_blocking().await?; + let height_a = wallet.blockchain_height().await?; + + let cli_b = harness::docker_client(); + let (monero_b, monerod_b, wallet_b) = + Monero::new(cli_b, vec!["secondary_wallet"]).await?; + + let port_b = monerod_b + .ports() + .map_to_host_port_ipv4(image::RPC_PORT) + .ok_or_else(|| anyhow::anyhow!("failed to map secondary monerod RPC port"))?; + + let daemon_b = Daemon { + hostname: "127.0.0.1".to_string(), + port: port_b, + ssl: false, + }; + + let daemon = daemon_b.clone(); + wallet + .call(move |w| w.set_daemon(&daemon)) + .await??; + + tokio::time::timeout(Duration::from_secs(45), async { + loop { + wallet.refresh_blocking().await?; + let h = wallet.blockchain_height().await?; + if h < height_a { + return Ok::<(), anyhow::Error>(()); + } + tokio::time::sleep(Duration::from_millis(250)).await; + } + }) + .await + .context("wallet did not switch to new daemon within 45 s")??; + + let height_b = wallet.blockchain_height().await?; + assert!(height_b < height_a); + + drop(wallet); + drop(wallet_b); + drop(monerod_b); + drop(monero_b); + Ok(()) +} diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 2148cf2376..eacab6608f 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -19,7 +19,7 @@ bitcoin = { workspace = true } # Wallets bitcoin-wallet = { path = "../bitcoin-wallet" } -monero-wallet = { path = "../monero-wallet" } +monero_wallet = { path = "../monero_wallet" } # Tor arti-client = { workspace = true, features = ["static-sqlite", "tokio", "rustls", "onion-service-service", "hs-pow-full", "ephemeral-keystore"] }