From 5f496b56372d3fc8e90a029b36463b694ded55af Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Tue, 7 Jan 2025 12:54:32 +0100 Subject: [PATCH 01/13] Subtract monero that is reversed for ongoing swaps from the quote volume --- swap/src/asb/event_loop.rs | 38 ++++++++++++++++++++++++++++---------- swap/src/monero.rs | 13 +++++++++++-- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/swap/src/asb/event_loop.rs b/swap/src/asb/event_loop.rs index e880c8703..8d9cd073a 100644 --- a/swap/src/asb/event_loop.rs +++ b/swap/src/asb/event_loop.rs @@ -479,16 +479,34 @@ where // use unlocked monero balance for quote let xmr_balance = Amount::from_piconero(balance.unlocked_balance); - let max_bitcoin_for_monero = - xmr_balance - .max_bitcoin_for_price(ask_price) - .ok_or_else(|| { - anyhow!( - "Bitcoin price ({}) x Monero ({}) overflow", - ask_price, - xmr_balance - ) - })?; + // From our full balance we need to subtract any Monero that is 'reserved' for ongoing swaps + // (where the Bitcoin has been (or is being) locked but we haven't sent the Monero yet). + + let reserved: Amount = self + .db + .all() + .await? + .iter() + .filter_map(|(_, state)| match state { + State::Alice(AliceState::BtcLockTransactionSeen { state3 }) + | State::Alice(AliceState::BtcLocked { state3 }) => Some(state3.xmr), + _ => None, + }) + .fold(Amount::ZERO, |acc, amount| acc + amount); + + let free_monero_balance = xmr_balance + .checked_sub(reserved) + .context("reserved more monero than we've got")?; + + let max_bitcoin_for_monero = free_monero_balance + .max_bitcoin_for_price(ask_price) + .ok_or_else(|| { + anyhow!( + "Bitcoin price ({}) x Monero ({}) overflow", + ask_price, + xmr_balance + ) + })?; tracing::trace!(%ask_price, %xmr_balance, %max_bitcoin_for_monero, "Computed quote"); diff --git a/swap/src/monero.rs b/swap/src/monero.rs index d96c21446..d399699da 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -9,7 +9,7 @@ pub use wallet::Wallet; pub use wallet_rpc::{WalletRpc, WalletRpcProcess}; use crate::bitcoin; -use anyhow::Result; +use anyhow::{bail, Result}; use rand::{CryptoRng, RngCore}; use rust_decimal::prelude::*; use rust_decimal::Decimal; @@ -164,6 +164,15 @@ impl Amount { .ok_or_else(|| OverflowError(amount.to_string()))?; Ok(Amount(piconeros)) } + + /// Subtract but throw an error on underflow. + pub fn checked_sub(self, rhs: Amount) -> Result { + if self.0 < rhs.0 { + bail!("checked sub would underflow"); + } + + Ok(Amount::from_piconero(self.0 - rhs.0)) + } } impl Add for Amount { @@ -174,7 +183,7 @@ impl Add for Amount { } } -impl Sub for Amount { +impl Sub for Amount { type Output = Amount; fn sub(self, rhs: Self) -> Self::Output { From 89e4a63dbda1165ee39a9044e326d47d06b02c1f Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Tue, 7 Jan 2025 12:55:40 +0100 Subject: [PATCH 02/13] Also reserve monero tx fee --- swap/src/asb/event_loop.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/swap/src/asb/event_loop.rs b/swap/src/asb/event_loop.rs index 8d9cd073a..a021c1137 100644 --- a/swap/src/asb/event_loop.rs +++ b/swap/src/asb/event_loop.rs @@ -1,5 +1,5 @@ use crate::asb::{Behaviour, OutEvent, Rate}; -use crate::monero::Amount; +use crate::monero::{Amount, MONERO_FEE}; use crate::network::cooperative_xmr_redeem_after_punish::CooperativeXmrRedeemRejectReason; use crate::network::cooperative_xmr_redeem_after_punish::Response::{Fullfilled, Rejected}; use crate::network::quote::BidQuote; @@ -489,7 +489,7 @@ where .iter() .filter_map(|(_, state)| match state { State::Alice(AliceState::BtcLockTransactionSeen { state3 }) - | State::Alice(AliceState::BtcLocked { state3 }) => Some(state3.xmr), + | State::Alice(AliceState::BtcLocked { state3 }) => Some(state3.xmr + MONERO_FEE), _ => None, }) .fold(Amount::ZERO, |acc, amount| acc + amount); From b3badc7d9b81ddeceaf6786bd8900afd3b843324 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Tue, 7 Jan 2025 13:14:48 +0100 Subject: [PATCH 03/13] dprint fmt --- dev-docs/asb/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-docs/asb/README.md b/dev-docs/asb/README.md index d53ee2c70..9c2f43211 100644 --- a/dev-docs/asb/README.md +++ b/dev-docs/asb/README.md @@ -32,6 +32,7 @@ Consider joining the designated [Matrix chat](https://matrix.to/#/%23unstoppable ### Using Docker Running the ASB and its required services (Bitcoin node, Monero node, wallet RPC) can be complex to set up manually. We provide a Docker Compose solution that handles all of this automatically. See our [docker-compose repository](https://github.com/UnstoppableSwap/asb-docker-compose) for setup instructions and configuration details. + ## ASB Details The ASB is a long running daemon that acts as the trading partner to the swap CLI. From 0e36b894ebbab8cecf9c1dace9a4ee40c1e915ee Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Tue, 14 Jan 2025 15:05:56 +0100 Subject: [PATCH 04/13] Add todo for better XMR management --- swap/src/asb/event_loop.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/swap/src/asb/event_loop.rs b/swap/src/asb/event_loop.rs index a021c1137..5aac68ae6 100644 --- a/swap/src/asb/event_loop.rs +++ b/swap/src/asb/event_loop.rs @@ -481,6 +481,10 @@ where // From our full balance we need to subtract any Monero that is 'reserved' for ongoing swaps // (where the Bitcoin has been (or is being) locked but we haven't sent the Monero yet). + // + // TODO: Better manage monero funds. Currently we store all Monero in one UTXO, then the change + // address is blocked for 10 blocks before we can use it again. + // We should distribute the monero funds into multiple UTXOs, to avoid blocking large amounts of the total balance. let reserved: Amount = self .db From fa9195e4744e0cafe14519f354af6e1430568130 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Tue, 14 Jan 2025 15:07:55 +0100 Subject: [PATCH 05/13] fix dprint lint --- dev-docs/asb/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dev-docs/asb/README.md b/dev-docs/asb/README.md index 9c2f43211..7cc6adc25 100644 --- a/dev-docs/asb/README.md +++ b/dev-docs/asb/README.md @@ -31,7 +31,9 @@ Consider joining the designated [Matrix chat](https://matrix.to/#/%23unstoppable ### Using Docker -Running the ASB and its required services (Bitcoin node, Monero node, wallet RPC) can be complex to set up manually. We provide a Docker Compose solution that handles all of this automatically. See our [docker-compose repository](https://github.com/UnstoppableSwap/asb-docker-compose) for setup instructions and configuration details. +Running the ASB and its required services (Bitcoin node, Monero node, wallet RPC) can be complex to set up manually. +We provide a Docker Compose solution that handles all of this automatically. +See our [docker-compose repository](https://github.com/UnstoppableSwap/asb-docker-compose) for setup instructions and configuration details. ## ASB Details From 52b96b02fe948e2f7c4432a5c05814c222bf3ca5 Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Mon, 20 Jan 2025 19:01:13 +0100 Subject: [PATCH 06/13] Warn instead of fail, default to 0 quote when reserved funds exceed monero balance --- swap/src/asb/event_loop.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/swap/src/asb/event_loop.rs b/swap/src/asb/event_loop.rs index 5aac68ae6..b180aaabf 100644 --- a/swap/src/asb/event_loop.rs +++ b/swap/src/asb/event_loop.rs @@ -498,9 +498,10 @@ where }) .fold(Amount::ZERO, |acc, amount| acc + amount); - let free_monero_balance = xmr_balance - .checked_sub(reserved) - .context("reserved more monero than we've got")?; + let free_monero_balance = xmr_balance.checked_sub(reserved).unwrap_or_else(|_| { + tracing::warn!("Monero funds needed for ongoing swaps exceed current balance."); + Amount::ZERO + }); let max_bitcoin_for_monero = free_monero_balance .max_bitcoin_for_price(ask_price) From 0102f61459660a1e9c6b40480581fd4edabeb40f Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Tue, 21 Jan 2025 14:15:03 +0100 Subject: [PATCH 07/13] Add more information to warning --- swap/src/asb/event_loop.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swap/src/asb/event_loop.rs b/swap/src/asb/event_loop.rs index b180aaabf..5ec421930 100644 --- a/swap/src/asb/event_loop.rs +++ b/swap/src/asb/event_loop.rs @@ -499,7 +499,7 @@ where .fold(Amount::ZERO, |acc, amount| acc + amount); let free_monero_balance = xmr_balance.checked_sub(reserved).unwrap_or_else(|_| { - tracing::warn!("Monero funds needed for ongoing swaps exceed current balance."); + tracing::warn!(%xmr_balance, missing=%(reserved - xmr_balance), "Monero balance is too low for ongoing swaps, need more funds to complete all ongoing swaps."); Amount::ZERO }); From bc9c1bc46f654a1612dbb3a0594715fd4c6e39fd Mon Sep 17 00:00:00 2001 From: einliterflasche Date: Tue, 21 Jan 2025 14:20:07 +0100 Subject: [PATCH 08/13] Add changelog entry --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b08008922..2517fba1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- ASB: The maker will take Monero funds needed for ongoing swaps into consideration when making a quote. A warning will be displayed if the Monero funds do not cover all ongoing swaps. + ## [1.0.0-rc.11] - 2024-12-22 - ASB: The `history` command will now display additional information about each swap such as the amounts involved, the current state and the txid of the Bitcoin lock transaction. From 14a5b4c348a109d2524657ffeba306422458ea44 Mon Sep 17 00:00:00 2001 From: Binarybaron Date: Sat, 26 Apr 2025 17:24:19 +0200 Subject: [PATCH 09/13] feat(monero-sys): Initial commit. Regtest integration test. Wrapper around basic Wallet functions, depends on monero#9464 --- Cargo.toml | 2 +- monero-harness/src/lib.rs | 24 +++ monero-sys/.gitignore | 9 ++ monero-sys/.gitmodules | 3 + monero-sys/CLAUDE.md | 70 +++++++++ monero-sys/Cargo.toml | 28 ++++ monero-sys/README.md | 19 +++ monero-sys/build.rs | 171 ++++++++++++++++++++ monero-sys/monero | 1 + monero-sys/src/bridge.h | 33 ++++ monero-sys/src/bridge.rs | 123 +++++++++++++++ monero-sys/src/lib.rs | 262 +++++++++++++++++++++++++++++++ monero-sys/tests/harness_test.rs | 196 +++++++++++++++++++++++ monero-sys/tests/simple.rs | 88 +++++++++++ 14 files changed, 1028 insertions(+), 1 deletion(-) create mode 100644 monero-sys/.gitignore create mode 100644 monero-sys/.gitmodules create mode 100644 monero-sys/CLAUDE.md create mode 100644 monero-sys/Cargo.toml create mode 100644 monero-sys/README.md create mode 100644 monero-sys/build.rs create mode 160000 monero-sys/monero create mode 100644 monero-sys/src/bridge.h create mode 100644 monero-sys/src/bridge.rs create mode 100644 monero-sys/src/lib.rs create mode 100644 monero-sys/tests/harness_test.rs create mode 100644 monero-sys/tests/simple.rs diff --git a/Cargo.toml b/Cargo.toml index d46abd08d..d0d2ed9ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = [ "monero-harness", "monero-rpc", "swap", "monero-wallet", "src-tauri" ] +members = [ "monero-harness", "monero-rpc", "swap", "monero-wallet", "src-tauri", "monero-sys" ] [patch.crates-io] # patch until new release https://github.com/thomaseizinger/rust-jsonrpc-client/pull/51 diff --git a/monero-harness/src/lib.rs b/monero-harness/src/lib.rs index 61c12a275..c621caf56 100644 --- a/monero-harness/src/lib.rs +++ b/monero-harness/src/lib.rs @@ -171,6 +171,29 @@ impl<'c> Monero { Ok(()) } + /// Funds a specific wallet address with XMR + /// + /// This function is useful when you want to fund an address that isn't managed by + /// a wallet in the testcontainer setup, like an external wallet address. + pub async fn fund_address(&self, address: &str, amount: u64) -> Result<()> { + let monerod = &self.monerod; + + // Make sure miner has funds by generating blocks + monerod + .client() + .generateblocks(120, address.to_string()) + .await?; + + // Mine more blocks to confirm the transaction + monerod + .client() + .generateblocks(10, address.to_string()) + .await?; + + tracing::info!("Successfully funded address with {} piconero", amount); + Ok(()) + } + pub async fn start_miner(&self) -> Result<()> { let miner_wallet = self.wallet("miner")?; let miner_address = miner_wallet.address().await?.address; @@ -246,6 +269,7 @@ impl<'c> Monerod { pub fn client(&self) -> &monerod::Client { &self.client } + /// Spawns a task to mine blocks in a regular interval to the provided /// address diff --git a/monero-sys/.gitignore b/monero-sys/.gitignore new file mode 100644 index 000000000..d5ebb44ee --- /dev/null +++ b/monero-sys/.gitignore @@ -0,0 +1,9 @@ +# IDE specific files +.vscode/ +.idea/ + +# Cargo specific +.cargo/ +/target/ +**/*.rs.bk +Cargo.lock diff --git a/monero-sys/.gitmodules b/monero-sys/.gitmodules new file mode 100644 index 000000000..fc6562c22 --- /dev/null +++ b/monero-sys/.gitmodules @@ -0,0 +1,3 @@ +[submodule "monero"] + path = monero + url = https://github.com/monero-project/monero diff --git a/monero-sys/CLAUDE.md b/monero-sys/CLAUDE.md new file mode 100644 index 000000000..9641e2fc9 --- /dev/null +++ b/monero-sys/CLAUDE.md @@ -0,0 +1,70 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build/Test Commands +- Build: `cargo build` +- Test all: `cargo test` + +## Development Notes +- In src/bridge.rs we mirror some functions from monero/src/wallet/api/wallet2_api.h. CXX automatically generates the bindings from the header file. +- When you want to add a new function to the bridge, you need to copy its definition from the monero/src/wallet/api/wallet2_api.h header file into the bridge.rs file, and then add it some wrapping logic in src/lib.rs. + +## Code Conventions +- Rust 2021 edition +- Use `unsafe` only for FFI interactions with Monero C++ code +- The cmake build target we need is wallet_api. We need to link libwallet.a and libwallet_api.a. + +## Important Development Guidelines +- Always verify method signatures in the Monero C++ headers before adding them to the Rust bridge +- Check wallet2_api.h for the correct function names, parameters, and return types +- When implementing new wrapper functions: + 1. First locate the function in the original C++ header file (wallet2_api.h) + 2. Copy the exact method signature to bridge.rs + 3. Implement the Rust wrapper in lib.rs + 4. Run the build to ensure everything compiles correctly +- Remember that some C++ methods may be overloaded or have different names than expected + +## Bridge Architecture + +The bridge between Rust and the Monero C++ code works as follows: + +1. **CXX Interface (bridge.rs)**: + - Defines the FFI interface using the `cxx::bridge` macro + - Declares C++ types and functions that will be accessed from Rust + - Special considerations in the interface: + - Static C++ methods are wrapped as free functions in bridge.h + - String returns use std::unique_ptr to bridge the language boundary + +2. **C++ Adapter (bridge.h)**: + - Contains helper functions to work around CXX limitations + - Provides wrappers for static methods (like `getWalletManager()`) + - Handles string returns with `std::unique_ptr` + +3. **Rust Wrapper (lib.rs)**: + - Provides idiomatic Rust interfaces to the C++ code + - Uses wrapper types (WalletManager, Wallet) with safer interfaces + - Handles memory management and safety concerns: + - Raw pointers are never exposed to users of the library + - Implements `Send` and `Sync` for wrapper types + - Uses `Pin` for C++ objects that require stable memory addresses + +4. **Build Process (build.rs)**: + - Compiles the Monero C++ code with CMake targeting wallet_api + - Sets up appropriate include paths and library linking + - Configures CXX to build the bridge between Rust and C++ + - Links numerous static and dynamic libraries required by Monero + +5. **Memory Safety Model**: + - Raw pointers are wrapped in safe Rust types + - `unsafe` is only used at the FFI boundary + - Proper deref implementations for wrapper types + - The `OnceLock` pattern ensures WalletManager is a singleton + +6. **Adding New Functionality**: + 1. Find the desired function in wallet2_api.h + 2. Add its declaration to the `unsafe extern "C++"` block in bridge.rs + 3. Create a corresponding Rust wrapper method in lib.rs + 4. For functions returning strings or with other CXX limitations, add helper functions in bridge.h + +This architecture ensures memory safety while providing idiomatic access to the Monero wallet functionality from Rust. \ No newline at end of file diff --git a/monero-sys/Cargo.toml b/monero-sys/Cargo.toml new file mode 100644 index 000000000..721e36992 --- /dev/null +++ b/monero-sys/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "monero-wallet-sys" +version = "0.1.0" +edition = "2021" + +[dependencies] +cxx = "1.0.137" +tracing = "0.1" +uuid = { version = "1.16.0", features = ["v4"] } + +[build-dependencies] +pkg-config = "0.3" +cmake = "0.1" +cxx-build = "1.0.137" + +[dev-dependencies] +tempfile = "3.19.1" +tracing-subscriber = "0.3" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] } +anyhow = "1" +testcontainers = "0.15" +reqwest = { version = "0.12", default-features = false, features = ["json"] } +monero-harness = { path = "../monero-harness" } + +[patch.crates-io] +# Match the patches from the core workspace +jsonrpc_client = { git = "https://github.com/delta1/rust-jsonrpc-client.git", rev = "3b6081697cd616c952acb9c2f02d546357d35506" } +monero = { git = "https://github.com/comit-network/monero-rs", rev = "818f38b" } diff --git a/monero-sys/README.md b/monero-sys/README.md new file mode 100644 index 000000000..2ce543d07 --- /dev/null +++ b/monero-sys/README.md @@ -0,0 +1,19 @@ +# monero-wallet-sys + +This is a (statically-linked) wrapper around [`wallet2_api.h`](./monero/src/wallet/api/wallet2_api.h) from the official Monero codebase. + +Since we statically link the Monero codebase, we need to build it. +That requires build dependencies, for a complete and up-to-date list see the Monero [README](./monero/README.md#dependencies). +Missing dependencies will currently result in obscure CMake or linker errors. +If you get obscure linker CMake or linker errors, check whether you correctly installed the dependencies. + +Since we build the Monero codebase from source, building this crate for the first time might take a while. + +## Contributing + +Make sure to load the Monero submodule: + +```bash +git submodule update --init --recursive +``` + diff --git a/monero-sys/build.rs b/monero-sys/build.rs new file mode 100644 index 000000000..e603877b9 --- /dev/null +++ b/monero-sys/build.rs @@ -0,0 +1,171 @@ +use cmake::Config; + +fn main() { + // Only rerun this when the bridge.rs or static_bridge.h file changes. + println!("cargo:rerun-if-changed=src/bridge.rs"); + println!("cargo:rerun-if-changed=src/bridge.h"); + + // Build with the monero library all dependencies required + let mut config = Config::new("monero"); + let output_directory = config + .build_target("wallet_api") + .define("CMAKE_RELEASE_TYPE", "Release") + .define("STATIC", "ON") + .build_arg("-j") + .build(); + + let monero_build_dir = output_directory.join("build"); + + println!( + "cargo:warning=Build directory: {}", + output_directory.display() + ); + + // Add output directories to the link search path + println!( + "cargo:rustc-link-search=native={}", + monero_build_dir.join("lib").display() + ); + + // Add additional link search paths for libraries in different directories + println!( + "cargo:rustc-link-search=native={}", + monero_build_dir.join("contrib/epee/src").display() + ); + println!( + "cargo:rustc-link-search=native={}", + monero_build_dir.join("external/easylogging++").display() + ); + println!( + "cargo:rustc-link-search=native={}", + monero_build_dir + .join("external/db_drivers/liblmdb") + .display() + ); + println!( + "cargo:rustc-link-search=native={}", + monero_build_dir.join("external/randomx").display() + ); + println!( + "cargo:rustc-link-search=native={}", + monero_build_dir.join("src").display() + ); + println!( + "cargo:rustc-link-search=native={}", + monero_build_dir.join("src/crypto").display() + ); + println!( + "cargo:rustc-link-search=native={}", + monero_build_dir.join("src/net").display() + ); + println!( + "cargo:rustc-link-search=native={}", + monero_build_dir.join("src/ringct").display() + ); + println!( + "cargo:rustc-link-search=native={}", + monero_build_dir.join("src/checkpoints").display() + ); + println!( + "cargo:rustc-link-search=native={}", + monero_build_dir.join("src/multisig").display() + ); + println!( + "cargo:rustc-link-search=native={}", + monero_build_dir.join("src/cryptonote_basic").display() + ); + println!( + "cargo:rustc-link-search=native={}", + monero_build_dir.join("src/common").display() + ); + println!( + "cargo:rustc-link-search=native={}", + monero_build_dir.join("src/cryptonote_core").display() + ); + println!( + "cargo:rustc-link-search=native={}", + monero_build_dir.join("src/hardforks").display() + ); + println!( + "cargo:rustc-link-search=native={}", + monero_build_dir.join("src/blockchain_db").display() + ); + println!( + "cargo:rustc-link-search=native={}", + monero_build_dir.join("src/device").display() + ); + println!( + "cargo:rustc-link-search=native={}", + monero_build_dir.join("src/device_trezor").display() + ); + println!( + "cargo:rustc-link-search=native={}", + monero_build_dir.join("src/mnemonics").display() + ); + println!( + "cargo:rustc-link-search=native={}", + monero_build_dir.join("src/rpc").display() + ); + + #[cfg(target_os = "macos")] + { + // add homebrew search paths/ + println!("cargo:rustc-link-search=native=/opt/homebrew/lib"); + } + + // Link libwallet and libwallet_api statically + println!("cargo:rustc-link-lib=static=wallet"); + println!("cargo:rustc-link-lib=static=wallet_api"); + + // Link additional required libraries + println!("cargo:rustc-link-lib=static=epee"); + println!("cargo:rustc-link-lib=static=easylogging"); + println!("cargo:rustc-link-lib=static=lmdb"); + println!("cargo:rustc-link-lib=static=randomx"); + println!("cargo:rustc-link-lib=static=cncrypto"); + println!("cargo:rustc-link-lib=static=net"); + println!("cargo:rustc-link-lib=static=ringct"); + println!("cargo:rustc-link-lib=static=ringct_basic"); + println!("cargo:rustc-link-lib=static=checkpoints"); + println!("cargo:rustc-link-lib=static=multisig"); + println!("cargo:rustc-link-lib=static=version"); + println!("cargo:rustc-link-lib=static=cryptonote_basic"); + println!("cargo:rustc-link-lib=static=cryptonote_format_utils_basic"); + println!("cargo:rustc-link-lib=static=common"); + println!("cargo:rustc-link-lib=static=cryptonote_core"); + println!("cargo:rustc-link-lib=static=hardforks"); + println!("cargo:rustc-link-lib=static=blockchain_db"); + println!("cargo:rustc-link-lib=static=device"); + println!("cargo:rustc-link-lib=static=device_trezor"); + println!("cargo:rustc-link-lib=static=mnemonics"); + println!("cargo:rustc-link-lib=static=rpc_base"); + + // Link required system libraries dynamically + println!("cargo:rustc-link-lib=dylib=hidapi"); + println!("cargo:rustc-link-lib=dylib=usb-1.0"); + println!("cargo:rustc-link-lib=dylib=unbound"); + println!("cargo:rustc-link-lib=dylib=boost_serialization"); + println!("cargo:rustc-link-lib=dylib=protobuf"); + println!("cargo:rustc-link-lib=dylib=sodium"); + println!("cargo:rustc-link-lib=dylib=boost_filesystem"); + println!("cargo:rustc-link-lib=dylib=boost_thread"); + println!("cargo:rustc-link-lib=dylib=boost_chrono"); + println!("cargo:rustc-link-lib=dylib=absl_base"); + println!("cargo:rustc-link-lib=dylib=absl_log_sink"); + println!("cargo:rustc-link-lib=dylib=absl_strings"); + println!("cargo:rustc-link-lib=dylib=absl_log_entry"); + println!("cargo:rustc-link-lib=dylib=absl_log_severity"); + println!("cargo:rustc-link-lib=dylib=absl_log_internal_message"); + println!("cargo:rustc-link-lib=dylib=absl_raw_logging_internal"); + println!("cargo:rustc-link-lib=dylib=absl_log_internal_check_op"); + println!("cargo:rustc-link-lib=dylib=absl_log_internal_nullguard"); + println!("cargo:rustc-link-lib=dylib=ssl"); + println!("cargo:rustc-link-lib=dylib=crypto"); + + // Build the CXX bridge + let mut build = cxx_build::bridge("src/bridge.rs"); + build + .flag_if_supported("-std=c++17") + .include("monero/src") + .compile("monero-wallet-sys"); +} diff --git a/monero-sys/monero b/monero-sys/monero new file mode 160000 index 000000000..3ed468970 --- /dev/null +++ b/monero-sys/monero @@ -0,0 +1 @@ +Subproject commit 3ed468970276543910676988b160f16d25908c9e diff --git a/monero-sys/src/bridge.h b/monero-sys/src/bridge.h new file mode 100644 index 000000000..40a44e323 --- /dev/null +++ b/monero-sys/src/bridge.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include "../monero/src/wallet/api/wallet2_api.h" + +namespace Monero +{ + /** + * CXX doesn't support static methods as yet, so we define free functions here that simply + * call the appropriate static methods. + */ + inline WalletManager *getWalletManager() + { + // This causes the wallet to print some logging to stdout + // This is useful for debugging + // TODO: Only enable in debug releases or expose setLogLevel as a FFI function + WalletManagerFactory::setLogLevel(2); + + return WalletManagerFactory::getWalletManager(); + } + + /** + * CXX also doesn't support returning strings by value from C++ to Rust, so we wrap those + * in a unique_ptr. + */ + inline std::unique_ptr address(const Wallet &wallet, uint32_t account_index, uint32_t address_index) + { + auto addr = wallet.address(account_index, address_index); + return std::make_unique(addr); + } + +} \ No newline at end of file diff --git a/monero-sys/src/bridge.rs b/monero-sys/src/bridge.rs new file mode 100644 index 000000000..14484df02 --- /dev/null +++ b/monero-sys/src/bridge.rs @@ -0,0 +1,123 @@ +#[cxx::bridge(namespace = "Monero")] +pub mod ffi { + /// The type of the network. + enum NetworkType { + #[rust_name = "Mainnet"] + MAINNET, + #[rust_name = "Testnet"] + TESTNET, + #[rust_name = "Stagenet"] + STAGENET, + } + + unsafe extern "C++" { + include!("monero-wallet-sys/monero/src/wallet/api/wallet2_api.h"); + include!("monero-wallet-sys/src/bridge.h"); + + /// A manager for multiple wallets. + type WalletManager; + + /// A single wallet. + type Wallet; + + /// The type of the network. + type NetworkType; + + /// An unsigned transaction. + type UnsignedTransaction; + + /// A pending transaction. + type PendingTransaction; + + /// Get the wallet manager. + unsafe fn getWalletManager() -> *mut WalletManager; + + /// Create a new wallet. + unsafe fn createWallet( + self: Pin<&mut WalletManager>, + path: &CxxString, + password: &CxxString, + language: &CxxString, + network_type: NetworkType, + kdf_rounds: u64, + ) -> *mut Wallet; + + /// Recover a wallet from a mnemonic seed (electrum seed). + unsafe fn recoveryWallet( + self: Pin<&mut WalletManager>, + path: &CxxString, + password: &CxxString, + mnemonic: &CxxString, + network_type: NetworkType, + restore_height: u64, + kdf_rounds: u64, + seed_offset: &CxxString, + ) -> *mut Wallet; + + /// Get the current blockchain height. + unsafe fn blockchainHeight(self: Pin<&mut WalletManager>) -> u64; + + /// Set the address of the remote node ("daemon"). + unsafe fn setDaemonAddress(self: Pin<&mut WalletManager>, address: &CxxString); + + /// Check if the wallet manager is connected to the configured daemon. + unsafe fn connected(self: Pin<&mut WalletManager>, version: *mut u32) -> bool; + + /// Get the status of the wallet and an error string if there is one. + unsafe fn statusWithErrorString( + self: &Wallet, + status: &mut i32, + error_string: Pin<&mut CxxString>, + ); + + /// Address for the given account and address index. + /// address(0, 0) is the main address. + unsafe fn address( + wallet: &Wallet, + account_index: u32, + address_index: u32, + ) -> UniquePtr; + + /// Initialize the wallet by connecting to the specified remote node (daemon). + #[allow(clippy::too_many_arguments)] + unsafe fn init( + self: Pin<&mut Wallet>, + daemon_address: &CxxString, + upper_transaction_size_limit: u64, + daemon_username: &CxxString, + daemon_password: &CxxString, + use_ssl: bool, + light_wallet: bool, + proxy_address: &CxxString, + ) -> bool; + + /// Refresh the wallet once. + unsafe fn refresh(self: Pin<&mut Wallet>) -> bool; + + /// Start the background refresh thread (refreshes every 10 seconds). + unsafe fn startRefresh(self: Pin<&mut Wallet>); + + /// Refresh the wallet asynchronously. + unsafe fn refreshAsync(self: Pin<&mut Wallet>); + + /// Get the current blockchain height. + unsafe fn blockChainHeight(self: &Wallet) -> u64; + + /// Get the daemon's blockchain height. + unsafe fn daemonBlockChainHeight(self: &Wallet) -> u64; + + /// Check if wallet was ever synchronized. + unsafe fn synchronized(self: &Wallet) -> bool; + + /// Get the status of a pending transaction. + unsafe fn status(self: &PendingTransaction) -> i32; + + /// Get the total balance across all accounts in atomic units. + unsafe fn balanceAll(self: &Wallet) -> u64; + + /// Get the total unlocked balance across all accounts in atomic units. + unsafe fn unlockedBalanceAll(self: &Wallet) -> u64; + + unsafe fn setAllowMismatchedDaemonVersion(self: Pin<&mut Wallet>, allow_mismatch: bool); + } +} diff --git a/monero-sys/src/lib.rs b/monero-sys/src/lib.rs new file mode 100644 index 000000000..d720cca35 --- /dev/null +++ b/monero-sys/src/lib.rs @@ -0,0 +1,262 @@ +mod bridge; + +use std::{ + ops::{Deref, DerefMut}, + pin::Pin, + sync::{Arc, Mutex, OnceLock}, +}; + +use cxx::let_cxx_string; + +use bridge::ffi; + +pub use bridge::ffi::NetworkType; + +static WALLET_MANAGER: OnceLock>> = OnceLock::new(); + +pub struct WalletManager { + inner: RawWalletManager, +} + +pub struct RawWalletManager(*mut ffi::WalletManager); + +unsafe impl Send for RawWalletManager {} +unsafe impl Sync for RawWalletManager {} + +impl WalletManager { + /// Get the wallet manager instance. + pub fn get() -> Arc> { + WALLET_MANAGER + .get_or_init(|| { + let manager = unsafe { ffi::getWalletManager() }; + + Arc::new(Mutex::new(Self { + inner: RawWalletManager(manager), + })) + }) + .clone() + } + + /// Create a new wallet. + pub fn create_wallet( + &mut self, + path: &str, + password: &str, + language: &str, + network_type: ffi::NetworkType, + kdf_rounds: u64, + ) -> Wallet { + let_cxx_string!(path = path); + let_cxx_string!(password = password); + let_cxx_string!(language = language); + + let wallet_pointer = unsafe { + self.inner + .pinned() + .createWallet(&path, &password, &language, network_type, kdf_rounds) + }; + + Wallet::new(wallet_pointer) + } + + /// Recover a wallet from a mnemonic seed (electrum seed). + /// + /// # Arguments + /// + /// * `path` - Name of wallet file to be created + /// * `password` - Password of wallet file + /// * `mnemonic` - Mnemonic seed (25 words electrum seed) + /// * `network_type` - Network type (MAINNET, TESTNET, STAGENET) + /// * `restore_height` - Restore from start height (0 to start from the beginning) + /// * `kdf_rounds` - Number of rounds for key derivation function + /// * `seed_offset` - Optional passphrase used to derive the seed + /// + /// # Returns + /// + /// A new Wallet instance. Call wallet.status() to check if recovered successfully. + pub fn recover_wallet( + &mut self, + path: &str, + password: &str, + mnemonic: &str, + network_type: ffi::NetworkType, + restore_height: u64, + kdf_rounds: u64, + seed_offset: &str, + ) -> Wallet { + let_cxx_string!(path = path); + let_cxx_string!(password = password); + let_cxx_string!(mnemonic = mnemonic); + let_cxx_string!(seed_offset = seed_offset); + + let wallet_pointer = unsafe { + self.inner + .pinned() + .recoveryWallet(&path, &password, &mnemonic, network_type, restore_height, kdf_rounds, &seed_offset) + }; + + Wallet::new(wallet_pointer) + } + + /// Set the address of the remote node ("daemon"). + pub fn set_daemon_address(&mut self, address: &str) { + tracing::debug!(%address, "Connecting wallet manager to remote node"); + + let_cxx_string!(address = address); + unsafe { + self.inner.pinned().setDaemonAddress(&address); + } + } + + /// Check if the wallet manager is connected to the configured daemon. + pub fn connected(&mut self) -> bool { + unsafe { self.inner.pinned().connected(std::ptr::null_mut()) } + } +} + +impl RawWalletManager { + /// Get a pinned reference to the inner (c++) wallet manager. + /// This is a convenience function necessary because + /// the ffi interface mostly takes a Pin<&mut T> but + /// we haven't figured out how to hold that in the struct. + pub fn pinned(&mut self) -> Pin<&mut ffi::WalletManager> { + unsafe { + Pin::new_unchecked( + self.0 + .as_mut() + .expect("wallet manager pointer not to be null"), + ) + } + } +} + +/// A single Monero wallet. +pub struct Wallet { + inner: RawWallet, +} + +pub struct RawWallet(*mut ffi::Wallet); + +unsafe impl Send for RawWallet {} +unsafe impl Sync for RawWallet {} + +impl Wallet { + /// Create a new wallet from a raw C++ wallet pointer. + fn new(inner: *mut ffi::Wallet) -> Self { + Self { + inner: RawWallet(inner), + } + } + + /// Get the address for the given account and address index. + /// address(0, 0) is the main address. + pub fn address(&self, account_index: u32, address_index: u32) -> String { + let address = unsafe { ffi::address(&self.inner, account_index, address_index) }; + address.to_string() + } + + /// Initialize the wallet by connecting to the specified remote node (daemon). + pub fn init(&mut self, daemon_address: &str, ssl: bool) -> bool { + let_cxx_string!(daemon_address = daemon_address); + let_cxx_string!(daemon_username = ""); + let_cxx_string!(daemon_password = ""); + let_cxx_string!(proxy_address = ""); + unsafe { + self.inner.pinned().init( + &daemon_address, + 0, + &daemon_username, + &daemon_password, + ssl, + false, + &proxy_address, + ) + } + } + + /// Start the background refresh thread (refreshes every 10 seconds). + pub fn start_refresh(&mut self) { + unsafe { self.inner.pinned().startRefresh() } + } + + /// Refresh the wallet asynchronously. + pub fn refresh_async(&mut self) { + unsafe { self.inner.pinned().refreshAsync() } + } + + /// Refresh the wallet once. + pub fn refresh(&mut self) -> bool { + unsafe { self.inner.pinned().refresh() } + } + + /// Get the current blockchain height. + pub fn blockchain_height(&self) -> u64 { + unsafe { self.inner.blockChainHeight() } + } + + /// Get the daemon's blockchain height. + /// + /// Returns the height of the blockchain from the connected daemon. + /// Returns 0 if there's an error communicating with the daemon. + pub fn daemon_blockchain_height(&self) -> u64 { + unsafe { self.inner.daemonBlockChainHeight() } + } + + /// Get the total balance across all accounts in atomic units. + pub fn balance_all(&self) -> u64 { + unsafe { self.inner.balanceAll() } + } + + /// Get the total unlocked balance across all accounts in atomic units. + pub fn unlocked_balance_all(&self) -> u64 { + unsafe { self.inner.unlockedBalanceAll() } + } + + /// Check if wallet was ever synchronized. + /// + /// Returns true if the wallet has been synchronized at least once, + /// false otherwise. + pub fn synchronized(&self) -> bool { + unsafe { self.inner.synchronized() } + } + + /// Set the allow mismatched daemon version flag. + pub fn set_allow_mismatched_daemon_version(&mut self, allow_mismatch: bool) { + unsafe { self.inner.pinned().setAllowMismatchedDaemonVersion(allow_mismatch) } + } +} + +impl RawWallet { + /// Get a pinned reference to the inner (c++) wallet. + pub fn pinned(&mut self) -> Pin<&mut ffi::Wallet> { + unsafe { Pin::new_unchecked(self.0.as_mut().expect("wallet pointer not to be null")) } + } +} + +impl Deref for Wallet { + type Target = RawWallet; + + fn deref(&self) -> &RawWallet { + &self.inner + } +} + +impl DerefMut for Wallet { + fn deref_mut(&mut self) -> &mut RawWallet { + &mut self.inner + } +} + +impl Deref for RawWallet { + type Target = ffi::Wallet; + + fn deref(&self) -> &ffi::Wallet { + unsafe { self.0.as_ref().expect("wallet pointer not to be null") } + } +} + +impl DerefMut for RawWallet { + fn deref_mut(&mut self) -> &mut ffi::Wallet { + unsafe { self.0.as_mut().expect("wallet pointer not to be null") } + } +} diff --git a/monero-sys/tests/harness_test.rs b/monero-sys/tests/harness_test.rs new file mode 100644 index 000000000..2e016937e --- /dev/null +++ b/monero-sys/tests/harness_test.rs @@ -0,0 +1,196 @@ +use monero_harness::{image::Monerod, Monero}; +use monero_wallet_sys::{NetworkType, WalletManager}; +use std::{time::Duration, sync::OnceLock}; +use tempfile::{tempdir, TempDir}; +use testcontainers::{clients::Cli, Container}; +use tokio::time::sleep; +use tracing::info; +use uuid::Uuid; + +const KDF_ROUNDS: u64 = 1; +const PASSWORD: &str = "test"; +const SEED_OFFSET: &str = ""; + +// Amount to fund the wallet with (in piconero) +const FUND_AMOUNT: u64 = 1_000_000_000_000; + +// Global temporary directory for all wallet files +static GLOBAL_TEMP_DIR: OnceLock = OnceLock::new(); + +#[tokio::test] +async fn test_monero_wrapper_with_harness() { + tracing_subscriber::fmt() + .with_env_filter("warn,test=debug,monero_harness=debug,monero_rpc=debug,harness_test=debug") + .with_test_writer() + .init(); + + // Step 1: Create a wallet with monero-wrapper using the global temp directory + let wallet_path = get_temp_wallet_path(); + let (address, wallet_seed) = create_monero_wrapper_wallet(&wallet_path).await; + + info!("Created monero-wrapper wallet with address: {}", address); + info!("Wallet seed: {}", wallet_seed); + + // Step 2: Set up monero-harness and fund the address + let tc = Cli::default(); + let (monero, monerod_container, _wallet_containers) = + Monero::new(&tc, vec![]).await.expect("Failed to create Monero containers"); + + let daemon_address = get_daemon_address(&monerod_container); + + // Initialize miner + info!("Initializing miner wallet"); + monero.init_miner().await.expect("Failed to initialize miner"); + + // Start mining continuously to generate blocks + info!("Starting continuous mining"); + monero.start_miner().await.expect("Failed to start miner"); + + // Fund the address created by monero-wrapper + info!("Funding the test wallet address: {}", address); + init_to_wallet_address(&monero, &address, FUND_AMOUNT).await + .expect("Failed to fund wallet address"); + + // Step 3: Connect the wrapper wallet to the daemon and check balance + info!("Connecting to daemon at: {}", daemon_address); + + let wallet_balance = connect_and_check_balance(wallet_seed, daemon_address).await; + + // Step 4: Verify the balance + info!("Wallet balance: {}", wallet_balance); + assert!(wallet_balance > 0, "Wallet balance should be greater than 0"); + + info!("Test passed! Wallet successfully received and detected funds"); +} + +async fn create_monero_wrapper_wallet(wallet_path: &str) -> (String, String) { + // Get wallet manager + let wallet_manager_mutex = WalletManager::get(); + let mut wallet_manager = wallet_manager_mutex + .lock() + .expect("Failed to lock wallet manager"); + + // Define a fixed seed to use for reproducible tests + let seed = "echo ourselves ruined oven masterful wives enough addicted future cottage illness adopt lucky movement tiger taboo imbalance antics iceberg hobby oval aloof tuesday uttered oval"; + + // Create wallet from the seed - we'll use 'recover' since we have a seed + let wallet = wallet_manager.recover_wallet( + wallet_path, + PASSWORD, + seed, + // Regtest uses Mainnet addresses + NetworkType::Mainnet, + 1, + KDF_ROUNDS, + SEED_OFFSET, + ); + + // Get the main address + let address = wallet.address(0, 0); + + (address, seed.to_string()) +} + +async fn init_to_wallet_address(monero: &Monero, address: &str, amount: u64) -> anyhow::Result<()> { + info!("Funding address {} with {} piconero", address, amount); + + // Generate some blocks to ensure miner has funds + monero.fund_address(address, amount).await?; + + info!("Successfully funded address with {} piconero", amount); + Ok(()) +} + +fn get_temp_wallet_path() -> String { + // Get or initialize the global temp directory + let temp_dir = GLOBAL_TEMP_DIR.get_or_init(|| { + // Create a directory that won't be deleted until the program exits + info!("Creating global temporary directory for wallet files"); + tempdir().expect("Failed to create global temporary directory") + }); + + // Generate a unique wallet filename using UUID + let uuid = Uuid::new_v4(); // This is the correct method to generate a random UUID + let wallet_filename = format!("wallet_{}", uuid); + let wallet_path = temp_dir.path().join(wallet_filename); + + info!("Generated wallet path: {}", wallet_path.display()); + wallet_path.to_str().unwrap().to_string() +} + +/// As we are not running the monero-wrapper inside the Docker network, we need to connect to the locally exposed port +/// Docker maps the port from inside the container (18081) to a random port on the host +/// This function extracts the port and constructs the address as "localhost:" +fn get_daemon_address(monerod_container: &Container<'_, Monerod>) -> String { + let local_daemon_rpc_port = monerod_container.ports().map_to_host_port_ipv4(monero_harness::image::RPC_PORT); + let local_daemon_rpc_port = local_daemon_rpc_port.expect("monerod should have a mapping to the host for the default RPC port"); + + format!("localhost:{}", local_daemon_rpc_port) +} + +async fn connect_and_check_balance(seed: String, daemon_address: String) -> u64 { + // Get wallet manager + let wallet_manager_mutex = WalletManager::get(); + let mut wallet_manager = wallet_manager_mutex + .lock() + .expect("Failed to lock wallet manager"); + + // Set daemon address + wallet_manager.set_daemon_address(&daemon_address); + + // Check connection + let connected = wallet_manager.connected(); + info!("Connected to daemon: {}", connected); + assert!(connected, "Should be connected to daemon"); + + // Get a unique wallet path from the global temp directory + let wallet_path = get_temp_wallet_path(); + tracing::info!("Recovering wallet from seed to {}", wallet_path); + + // Recover wallet from seed + let mut wallet = wallet_manager.recover_wallet( + &wallet_path, + PASSWORD, + &seed, + NetworkType::Mainnet, // Regtest uses Mainnet addresses + 1, // Restore height (start from beginning) + KDF_ROUNDS, + SEED_OFFSET, + ); + + // Initialize wallet + wallet.init(&daemon_address, false); + + tracing::info!("Starting testing of wallet with address: {}", wallet.address(0, 0)); + + // We need to allow mismatched daemon versions for the Regtest network + // to be accepted by wallet2 + wallet.set_allow_mismatched_daemon_version(true); + + // Start background refresh + wallet.start_refresh(); + + // Wait for wallet to sync + info!("Waiting for wallet to sync..."); + while !wallet.synchronized() { + let wallet_height = wallet.blockchain_height(); + let daemon_height = wallet.daemon_blockchain_height(); + + info!( + "Wallet height: {}, Daemon height: {}", + wallet_height, daemon_height + ); + + sleep(Duration::from_secs(1)).await; + } + + // Manual refresh to ensure we have the latest state + info!("Performing final refresh"); + let refresh_result = wallet.refresh(); + info!("Final refresh result: {}", refresh_result); + + // Check balance + let balance = wallet.balance_all(); + info!("Final balance check: {}", balance); + balance +} \ No newline at end of file diff --git a/monero-sys/tests/simple.rs b/monero-sys/tests/simple.rs new file mode 100644 index 000000000..163b6fbf5 --- /dev/null +++ b/monero-sys/tests/simple.rs @@ -0,0 +1,88 @@ +use monero_wallet_sys::{NetworkType, WalletManager}; + +const KDF_ROUNDS: u64 = 1; +const PASSWORD: &str = "test"; + +// No seed offset for now. This is the default +// TODO: Let lib.rs take an Option<_> and if None pass in an empty string +const SEED_OFFSET: &str = ""; + +const STAGENET_REMOTE_NODE: &str = "node.sethforprivacy.com:38089"; +const STAGENET_WALLET_SEED: &str = "echo ourselves ruined oven masterful wives enough addicted future cottage illness adopt lucky movement tiger taboo imbalance antics iceberg hobby oval aloof tuesday uttered oval"; +const STAGENET_WALLET_RESTORE_HEIGHT: u64 = 1728128; + +#[test] +fn main() { + tracing_subscriber::fmt::init(); + + let wallet_manager_mutex = WalletManager::get(); + let mut wallet_manager = wallet_manager_mutex + .lock() + .expect("failed to lock wallet manager"); + + tracing::info!("Setting daemon address"); + wallet_manager.set_daemon_address(STAGENET_REMOTE_NODE); + + tracing::info!("Connected: {}", wallet_manager.connected()); + + let temp_dir = tempfile::tempdir().unwrap(); + + let wallet_name = "recovered_wallet"; + let wallet_path = temp_dir.path().join(wallet_name); + + tracing::info!("Recovering wallet from seed"); + let mut wallet = wallet_manager.recover_wallet( + wallet_path.to_str().unwrap(), + PASSWORD, + STAGENET_WALLET_SEED, + NetworkType::Stagenet, + STAGENET_WALLET_RESTORE_HEIGHT, + KDF_ROUNDS, + SEED_OFFSET, + ); + + // TODO: Here we'd need to call status() I believe to check if the creation was successful + + wallet.init(STAGENET_REMOTE_NODE, true); + + tracing::info!("Primary address: {}", wallet.address(0, 0)); + + // Start background refresh + tracing::info!("Starting background refresh"); + wallet.start_refresh(); + + // Wait for a while to let the wallet sync, checking sync status + tracing::info!("Waiting for wallet to sync..."); + + // TODO: lib.rs should provide an async method that does this for us + while !wallet.synchronized() { + let wallet_height = wallet.blockchain_height(); + let daemon_height = wallet.daemon_blockchain_height(); + let is_synced = wallet.synchronized(); + + // Calculate sync percentage if daemon height is available + let sync_percentage = if daemon_height > 0 && daemon_height >= wallet_height { + (wallet_height as f64 / daemon_height as f64 * 100.0).round() + } else { + 0.0 + }; + + tracing::info!( + "Wallet height: {}, Daemon height: {}, Sync: {}%, Synchronized: {} (iteration {})", + wallet_height, daemon_height, sync_percentage, is_synced, i + ); + + std::thread::sleep(std::time::Duration::from_secs(3)); + } + + tracing::info!("Wallet is synchronized!"); + + // Manual refresh one more time + tracing::info!(result=%wallet.refresh(), "Manual refresh"); + + let balance = wallet.balance_all(); + tracing::info!("Balance: {}", balance); + + let unlocked_balance = wallet.unlocked_balance_all(); + tracing::info!("Unlocked balance: {}", unlocked_balance); +} From f621aaa3e6dee66a24998f023e8af66d4519bfa5 Mon Sep 17 00:00:00 2001 From: Binarybaron Date: Mon, 28 Apr 2025 12:40:03 +0200 Subject: [PATCH 10/13] merge master, extract logic into unreserved_monero_balance --- swap/src/asb/event_loop.rs | 60 +++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/swap/src/asb/event_loop.rs b/swap/src/asb/event_loop.rs index 6588b5896..19dea8e87 100644 --- a/swap/src/asb/event_loop.rs +++ b/swap/src/asb/event_loop.rs @@ -511,39 +511,30 @@ where result } - /// Computes a quote and returns the result wrapped in Arcs. - async fn make_quote( - &mut self, - min_buy: bitcoin::Amount, - max_buy: bitcoin::Amount, - ) -> Result, Arc> { + /// Returns the unreserved Monero balance + /// Meaning the Monero balance that is not reserved for ongoing swaps and can be used for new swaps + async fn unreserved_monero_balance(&self) -> Result> { /// This is how long we maximally wait for the wallet lock /// -- else the quote will be out of date and we will return /// an error. const MAX_WAIT_DURATION: Duration = Duration::from_secs(60); - let ask_price = self - .latest_rate - .latest_rate() - .map_err(|e| Arc::new(anyhow!(e).context("Failed to get latest rate")))? - .ask() - .map_err(|e| Arc::new(e.context("Failed to compute asking price")))?; - let balance = timeout(MAX_WAIT_DURATION, self.monero_wallet.lock()) .await .context("Timeout while waiting for lock on monero wallet while making quote")? .get_balance() .await .map_err(|e| Arc::new(e.context("Failed to get Monero balance")))?; - let xmr_balance = Amount::from_piconero(balance.unlocked_balance); - // From our full balance we need to subtract any Monero that is 'reserved' for ongoing swaps - // (where the Bitcoin has been (or is being) locked but we haven't sent the Monero yet). - // - // TODO: Better manage monero funds. Currently we store all Monero in one UTXO, then the change - // address is blocked for 10 blocks before we can use it again. - // We should distribute the monero funds into multiple UTXOs, to avoid blocking large amounts of the total balance. + let balance = Amount::from_piconero(balance.unlocked_balance); + // From our full balance we need to subtract any Monero that is 'reserved' for ongoing swaps + // Those swaps where the Bitcoin has been locked (may be unconfirmed or confirmed) but we haven't locked the Monero yet + // This is a naive approach because: + // - Suppose we have two UTXOs each 5 XMR + // - We have a pending swap for 6 XMR + // The code will assume we only have reserved 6 XMR but once we lock the 6 XMR our two outputs will be spent + // and it'll take 10 blocks before we can use our new output (4 XMR change) again let reserved: Amount = self .db .all() @@ -556,22 +547,37 @@ where }) .fold(Amount::ZERO, |acc, amount| acc + amount); - let free_monero_balance = xmr_balance.checked_sub(reserved).unwrap_or_else(|_| { - tracing::warn!(%xmr_balance, missing=%(reserved - xmr_balance), "Monero balance is too low for ongoing swaps, need more funds to complete all ongoing swaps."); - Amount::ZERO - }); + let free_monero_balance = balance.checked_sub(reserved).unwrap_or(Amount::ZERO); + + Ok(free_monero_balance) + } + + /// Computes a quote and returns the result wrapped in Arcs. + async fn make_quote( + &mut self, + min_buy: bitcoin::Amount, + max_buy: bitcoin::Amount, + ) -> Result, Arc> { + let ask_price = self + .latest_rate + .latest_rate() + .map_err(|e| Arc::new(anyhow!(e).context("Failed to get latest rate")))? + .ask() + .map_err(|e| Arc::new(e.context("Failed to compute asking price")))?; + + let unreserved_xmr_balance = self.unreserved_monero_balance().await?; - let max_bitcoin_for_monero = free_monero_balance + let max_bitcoin_for_monero = unreserved_xmr_balance .max_bitcoin_for_price(ask_price) .ok_or_else(|| { Arc::new(anyhow!( "Bitcoin price ({}) x Monero ({}) overflow", ask_price, - xmr_balance + unreserved_xmr_balance )) })?; - tracing::trace!(%ask_price, %xmr_balance, %max_bitcoin_for_monero, "Computed quote"); + tracing::trace!(%ask_price, %unreserved_xmr_balance, %max_bitcoin_for_monero, "Computed quote"); if min_buy > max_bitcoin_for_monero { tracing::trace!( From 0e29a7e218c6b69134874f4ed7d5a45fb74d6103 Mon Sep 17 00:00:00 2001 From: Binarybaron Date: Mon, 28 Apr 2025 12:42:20 +0200 Subject: [PATCH 11/13] fmt --- swap/src/asb/event_loop.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/swap/src/asb/event_loop.rs b/swap/src/asb/event_loop.rs index 19dea8e87..160acc9cf 100644 --- a/swap/src/asb/event_loop.rs +++ b/swap/src/asb/event_loop.rs @@ -514,9 +514,7 @@ where /// Returns the unreserved Monero balance /// Meaning the Monero balance that is not reserved for ongoing swaps and can be used for new swaps async fn unreserved_monero_balance(&self) -> Result> { - /// This is how long we maximally wait for the wallet lock - /// -- else the quote will be out of date and we will return - /// an error. + /// This is how long we maximally wait to get access to the wallet const MAX_WAIT_DURATION: Duration = Duration::from_secs(60); let balance = timeout(MAX_WAIT_DURATION, self.monero_wallet.lock()) From e59df2fce3b12370c712634de732fb1c084bd15a Mon Sep 17 00:00:00 2001 From: Binarybaron Date: Mon, 28 Apr 2025 12:54:55 +0200 Subject: [PATCH 12/13] remove shit --- monero-sys/.gitignore | 9 -- monero-sys/.gitmodules | 3 - monero-sys/CLAUDE.md | 70 --------- monero-sys/Cargo.toml | 28 ---- monero-sys/README.md | 19 --- monero-sys/build.rs | 171 -------------------- monero-sys/monero | 1 - monero-sys/src/bridge.h | 33 ---- monero-sys/src/bridge.rs | 123 --------------- monero-sys/src/lib.rs | 262 ------------------------------- monero-sys/tests/harness_test.rs | 196 ----------------------- monero-sys/tests/simple.rs | 88 ----------- 12 files changed, 1003 deletions(-) delete mode 100644 monero-sys/.gitignore delete mode 100644 monero-sys/.gitmodules delete mode 100644 monero-sys/CLAUDE.md delete mode 100644 monero-sys/Cargo.toml delete mode 100644 monero-sys/README.md delete mode 100644 monero-sys/build.rs delete mode 160000 monero-sys/monero delete mode 100644 monero-sys/src/bridge.h delete mode 100644 monero-sys/src/bridge.rs delete mode 100644 monero-sys/src/lib.rs delete mode 100644 monero-sys/tests/harness_test.rs delete mode 100644 monero-sys/tests/simple.rs diff --git a/monero-sys/.gitignore b/monero-sys/.gitignore deleted file mode 100644 index d5ebb44ee..000000000 --- a/monero-sys/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -# IDE specific files -.vscode/ -.idea/ - -# Cargo specific -.cargo/ -/target/ -**/*.rs.bk -Cargo.lock diff --git a/monero-sys/.gitmodules b/monero-sys/.gitmodules deleted file mode 100644 index fc6562c22..000000000 --- a/monero-sys/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "monero"] - path = monero - url = https://github.com/monero-project/monero diff --git a/monero-sys/CLAUDE.md b/monero-sys/CLAUDE.md deleted file mode 100644 index 9641e2fc9..000000000 --- a/monero-sys/CLAUDE.md +++ /dev/null @@ -1,70 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Build/Test Commands -- Build: `cargo build` -- Test all: `cargo test` - -## Development Notes -- In src/bridge.rs we mirror some functions from monero/src/wallet/api/wallet2_api.h. CXX automatically generates the bindings from the header file. -- When you want to add a new function to the bridge, you need to copy its definition from the monero/src/wallet/api/wallet2_api.h header file into the bridge.rs file, and then add it some wrapping logic in src/lib.rs. - -## Code Conventions -- Rust 2021 edition -- Use `unsafe` only for FFI interactions with Monero C++ code -- The cmake build target we need is wallet_api. We need to link libwallet.a and libwallet_api.a. - -## Important Development Guidelines -- Always verify method signatures in the Monero C++ headers before adding them to the Rust bridge -- Check wallet2_api.h for the correct function names, parameters, and return types -- When implementing new wrapper functions: - 1. First locate the function in the original C++ header file (wallet2_api.h) - 2. Copy the exact method signature to bridge.rs - 3. Implement the Rust wrapper in lib.rs - 4. Run the build to ensure everything compiles correctly -- Remember that some C++ methods may be overloaded or have different names than expected - -## Bridge Architecture - -The bridge between Rust and the Monero C++ code works as follows: - -1. **CXX Interface (bridge.rs)**: - - Defines the FFI interface using the `cxx::bridge` macro - - Declares C++ types and functions that will be accessed from Rust - - Special considerations in the interface: - - Static C++ methods are wrapped as free functions in bridge.h - - String returns use std::unique_ptr to bridge the language boundary - -2. **C++ Adapter (bridge.h)**: - - Contains helper functions to work around CXX limitations - - Provides wrappers for static methods (like `getWalletManager()`) - - Handles string returns with `std::unique_ptr` - -3. **Rust Wrapper (lib.rs)**: - - Provides idiomatic Rust interfaces to the C++ code - - Uses wrapper types (WalletManager, Wallet) with safer interfaces - - Handles memory management and safety concerns: - - Raw pointers are never exposed to users of the library - - Implements `Send` and `Sync` for wrapper types - - Uses `Pin` for C++ objects that require stable memory addresses - -4. **Build Process (build.rs)**: - - Compiles the Monero C++ code with CMake targeting wallet_api - - Sets up appropriate include paths and library linking - - Configures CXX to build the bridge between Rust and C++ - - Links numerous static and dynamic libraries required by Monero - -5. **Memory Safety Model**: - - Raw pointers are wrapped in safe Rust types - - `unsafe` is only used at the FFI boundary - - Proper deref implementations for wrapper types - - The `OnceLock` pattern ensures WalletManager is a singleton - -6. **Adding New Functionality**: - 1. Find the desired function in wallet2_api.h - 2. Add its declaration to the `unsafe extern "C++"` block in bridge.rs - 3. Create a corresponding Rust wrapper method in lib.rs - 4. For functions returning strings or with other CXX limitations, add helper functions in bridge.h - -This architecture ensures memory safety while providing idiomatic access to the Monero wallet functionality from Rust. \ No newline at end of file diff --git a/monero-sys/Cargo.toml b/monero-sys/Cargo.toml deleted file mode 100644 index 721e36992..000000000 --- a/monero-sys/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "monero-wallet-sys" -version = "0.1.0" -edition = "2021" - -[dependencies] -cxx = "1.0.137" -tracing = "0.1" -uuid = { version = "1.16.0", features = ["v4"] } - -[build-dependencies] -pkg-config = "0.3" -cmake = "0.1" -cxx-build = "1.0.137" - -[dev-dependencies] -tempfile = "3.19.1" -tracing-subscriber = "0.3" -tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] } -anyhow = "1" -testcontainers = "0.15" -reqwest = { version = "0.12", default-features = false, features = ["json"] } -monero-harness = { path = "../monero-harness" } - -[patch.crates-io] -# Match the patches from the core workspace -jsonrpc_client = { git = "https://github.com/delta1/rust-jsonrpc-client.git", rev = "3b6081697cd616c952acb9c2f02d546357d35506" } -monero = { git = "https://github.com/comit-network/monero-rs", rev = "818f38b" } diff --git a/monero-sys/README.md b/monero-sys/README.md deleted file mode 100644 index 2ce543d07..000000000 --- a/monero-sys/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# monero-wallet-sys - -This is a (statically-linked) wrapper around [`wallet2_api.h`](./monero/src/wallet/api/wallet2_api.h) from the official Monero codebase. - -Since we statically link the Monero codebase, we need to build it. -That requires build dependencies, for a complete and up-to-date list see the Monero [README](./monero/README.md#dependencies). -Missing dependencies will currently result in obscure CMake or linker errors. -If you get obscure linker CMake or linker errors, check whether you correctly installed the dependencies. - -Since we build the Monero codebase from source, building this crate for the first time might take a while. - -## Contributing - -Make sure to load the Monero submodule: - -```bash -git submodule update --init --recursive -``` - diff --git a/monero-sys/build.rs b/monero-sys/build.rs deleted file mode 100644 index e603877b9..000000000 --- a/monero-sys/build.rs +++ /dev/null @@ -1,171 +0,0 @@ -use cmake::Config; - -fn main() { - // Only rerun this when the bridge.rs or static_bridge.h file changes. - println!("cargo:rerun-if-changed=src/bridge.rs"); - println!("cargo:rerun-if-changed=src/bridge.h"); - - // Build with the monero library all dependencies required - let mut config = Config::new("monero"); - let output_directory = config - .build_target("wallet_api") - .define("CMAKE_RELEASE_TYPE", "Release") - .define("STATIC", "ON") - .build_arg("-j") - .build(); - - let monero_build_dir = output_directory.join("build"); - - println!( - "cargo:warning=Build directory: {}", - output_directory.display() - ); - - // Add output directories to the link search path - println!( - "cargo:rustc-link-search=native={}", - monero_build_dir.join("lib").display() - ); - - // Add additional link search paths for libraries in different directories - println!( - "cargo:rustc-link-search=native={}", - monero_build_dir.join("contrib/epee/src").display() - ); - println!( - "cargo:rustc-link-search=native={}", - monero_build_dir.join("external/easylogging++").display() - ); - println!( - "cargo:rustc-link-search=native={}", - monero_build_dir - .join("external/db_drivers/liblmdb") - .display() - ); - println!( - "cargo:rustc-link-search=native={}", - monero_build_dir.join("external/randomx").display() - ); - println!( - "cargo:rustc-link-search=native={}", - monero_build_dir.join("src").display() - ); - println!( - "cargo:rustc-link-search=native={}", - monero_build_dir.join("src/crypto").display() - ); - println!( - "cargo:rustc-link-search=native={}", - monero_build_dir.join("src/net").display() - ); - println!( - "cargo:rustc-link-search=native={}", - monero_build_dir.join("src/ringct").display() - ); - println!( - "cargo:rustc-link-search=native={}", - monero_build_dir.join("src/checkpoints").display() - ); - println!( - "cargo:rustc-link-search=native={}", - monero_build_dir.join("src/multisig").display() - ); - println!( - "cargo:rustc-link-search=native={}", - monero_build_dir.join("src/cryptonote_basic").display() - ); - println!( - "cargo:rustc-link-search=native={}", - monero_build_dir.join("src/common").display() - ); - println!( - "cargo:rustc-link-search=native={}", - monero_build_dir.join("src/cryptonote_core").display() - ); - println!( - "cargo:rustc-link-search=native={}", - monero_build_dir.join("src/hardforks").display() - ); - println!( - "cargo:rustc-link-search=native={}", - monero_build_dir.join("src/blockchain_db").display() - ); - println!( - "cargo:rustc-link-search=native={}", - monero_build_dir.join("src/device").display() - ); - println!( - "cargo:rustc-link-search=native={}", - monero_build_dir.join("src/device_trezor").display() - ); - println!( - "cargo:rustc-link-search=native={}", - monero_build_dir.join("src/mnemonics").display() - ); - println!( - "cargo:rustc-link-search=native={}", - monero_build_dir.join("src/rpc").display() - ); - - #[cfg(target_os = "macos")] - { - // add homebrew search paths/ - println!("cargo:rustc-link-search=native=/opt/homebrew/lib"); - } - - // Link libwallet and libwallet_api statically - println!("cargo:rustc-link-lib=static=wallet"); - println!("cargo:rustc-link-lib=static=wallet_api"); - - // Link additional required libraries - println!("cargo:rustc-link-lib=static=epee"); - println!("cargo:rustc-link-lib=static=easylogging"); - println!("cargo:rustc-link-lib=static=lmdb"); - println!("cargo:rustc-link-lib=static=randomx"); - println!("cargo:rustc-link-lib=static=cncrypto"); - println!("cargo:rustc-link-lib=static=net"); - println!("cargo:rustc-link-lib=static=ringct"); - println!("cargo:rustc-link-lib=static=ringct_basic"); - println!("cargo:rustc-link-lib=static=checkpoints"); - println!("cargo:rustc-link-lib=static=multisig"); - println!("cargo:rustc-link-lib=static=version"); - println!("cargo:rustc-link-lib=static=cryptonote_basic"); - println!("cargo:rustc-link-lib=static=cryptonote_format_utils_basic"); - println!("cargo:rustc-link-lib=static=common"); - println!("cargo:rustc-link-lib=static=cryptonote_core"); - println!("cargo:rustc-link-lib=static=hardforks"); - println!("cargo:rustc-link-lib=static=blockchain_db"); - println!("cargo:rustc-link-lib=static=device"); - println!("cargo:rustc-link-lib=static=device_trezor"); - println!("cargo:rustc-link-lib=static=mnemonics"); - println!("cargo:rustc-link-lib=static=rpc_base"); - - // Link required system libraries dynamically - println!("cargo:rustc-link-lib=dylib=hidapi"); - println!("cargo:rustc-link-lib=dylib=usb-1.0"); - println!("cargo:rustc-link-lib=dylib=unbound"); - println!("cargo:rustc-link-lib=dylib=boost_serialization"); - println!("cargo:rustc-link-lib=dylib=protobuf"); - println!("cargo:rustc-link-lib=dylib=sodium"); - println!("cargo:rustc-link-lib=dylib=boost_filesystem"); - println!("cargo:rustc-link-lib=dylib=boost_thread"); - println!("cargo:rustc-link-lib=dylib=boost_chrono"); - println!("cargo:rustc-link-lib=dylib=absl_base"); - println!("cargo:rustc-link-lib=dylib=absl_log_sink"); - println!("cargo:rustc-link-lib=dylib=absl_strings"); - println!("cargo:rustc-link-lib=dylib=absl_log_entry"); - println!("cargo:rustc-link-lib=dylib=absl_log_severity"); - println!("cargo:rustc-link-lib=dylib=absl_log_internal_message"); - println!("cargo:rustc-link-lib=dylib=absl_raw_logging_internal"); - println!("cargo:rustc-link-lib=dylib=absl_log_internal_check_op"); - println!("cargo:rustc-link-lib=dylib=absl_log_internal_nullguard"); - println!("cargo:rustc-link-lib=dylib=ssl"); - println!("cargo:rustc-link-lib=dylib=crypto"); - - // Build the CXX bridge - let mut build = cxx_build::bridge("src/bridge.rs"); - build - .flag_if_supported("-std=c++17") - .include("monero/src") - .compile("monero-wallet-sys"); -} diff --git a/monero-sys/monero b/monero-sys/monero deleted file mode 160000 index 3ed468970..000000000 --- a/monero-sys/monero +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3ed468970276543910676988b160f16d25908c9e diff --git a/monero-sys/src/bridge.h b/monero-sys/src/bridge.h deleted file mode 100644 index 40a44e323..000000000 --- a/monero-sys/src/bridge.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include - -#include "../monero/src/wallet/api/wallet2_api.h" - -namespace Monero -{ - /** - * CXX doesn't support static methods as yet, so we define free functions here that simply - * call the appropriate static methods. - */ - inline WalletManager *getWalletManager() - { - // This causes the wallet to print some logging to stdout - // This is useful for debugging - // TODO: Only enable in debug releases or expose setLogLevel as a FFI function - WalletManagerFactory::setLogLevel(2); - - return WalletManagerFactory::getWalletManager(); - } - - /** - * CXX also doesn't support returning strings by value from C++ to Rust, so we wrap those - * in a unique_ptr. - */ - inline std::unique_ptr address(const Wallet &wallet, uint32_t account_index, uint32_t address_index) - { - auto addr = wallet.address(account_index, address_index); - return std::make_unique(addr); - } - -} \ No newline at end of file diff --git a/monero-sys/src/bridge.rs b/monero-sys/src/bridge.rs deleted file mode 100644 index 14484df02..000000000 --- a/monero-sys/src/bridge.rs +++ /dev/null @@ -1,123 +0,0 @@ -#[cxx::bridge(namespace = "Monero")] -pub mod ffi { - /// The type of the network. - enum NetworkType { - #[rust_name = "Mainnet"] - MAINNET, - #[rust_name = "Testnet"] - TESTNET, - #[rust_name = "Stagenet"] - STAGENET, - } - - unsafe extern "C++" { - include!("monero-wallet-sys/monero/src/wallet/api/wallet2_api.h"); - include!("monero-wallet-sys/src/bridge.h"); - - /// A manager for multiple wallets. - type WalletManager; - - /// A single wallet. - type Wallet; - - /// The type of the network. - type NetworkType; - - /// An unsigned transaction. - type UnsignedTransaction; - - /// A pending transaction. - type PendingTransaction; - - /// Get the wallet manager. - unsafe fn getWalletManager() -> *mut WalletManager; - - /// Create a new wallet. - unsafe fn createWallet( - self: Pin<&mut WalletManager>, - path: &CxxString, - password: &CxxString, - language: &CxxString, - network_type: NetworkType, - kdf_rounds: u64, - ) -> *mut Wallet; - - /// Recover a wallet from a mnemonic seed (electrum seed). - unsafe fn recoveryWallet( - self: Pin<&mut WalletManager>, - path: &CxxString, - password: &CxxString, - mnemonic: &CxxString, - network_type: NetworkType, - restore_height: u64, - kdf_rounds: u64, - seed_offset: &CxxString, - ) -> *mut Wallet; - - /// Get the current blockchain height. - unsafe fn blockchainHeight(self: Pin<&mut WalletManager>) -> u64; - - /// Set the address of the remote node ("daemon"). - unsafe fn setDaemonAddress(self: Pin<&mut WalletManager>, address: &CxxString); - - /// Check if the wallet manager is connected to the configured daemon. - unsafe fn connected(self: Pin<&mut WalletManager>, version: *mut u32) -> bool; - - /// Get the status of the wallet and an error string if there is one. - unsafe fn statusWithErrorString( - self: &Wallet, - status: &mut i32, - error_string: Pin<&mut CxxString>, - ); - - /// Address for the given account and address index. - /// address(0, 0) is the main address. - unsafe fn address( - wallet: &Wallet, - account_index: u32, - address_index: u32, - ) -> UniquePtr; - - /// Initialize the wallet by connecting to the specified remote node (daemon). - #[allow(clippy::too_many_arguments)] - unsafe fn init( - self: Pin<&mut Wallet>, - daemon_address: &CxxString, - upper_transaction_size_limit: u64, - daemon_username: &CxxString, - daemon_password: &CxxString, - use_ssl: bool, - light_wallet: bool, - proxy_address: &CxxString, - ) -> bool; - - /// Refresh the wallet once. - unsafe fn refresh(self: Pin<&mut Wallet>) -> bool; - - /// Start the background refresh thread (refreshes every 10 seconds). - unsafe fn startRefresh(self: Pin<&mut Wallet>); - - /// Refresh the wallet asynchronously. - unsafe fn refreshAsync(self: Pin<&mut Wallet>); - - /// Get the current blockchain height. - unsafe fn blockChainHeight(self: &Wallet) -> u64; - - /// Get the daemon's blockchain height. - unsafe fn daemonBlockChainHeight(self: &Wallet) -> u64; - - /// Check if wallet was ever synchronized. - unsafe fn synchronized(self: &Wallet) -> bool; - - /// Get the status of a pending transaction. - unsafe fn status(self: &PendingTransaction) -> i32; - - /// Get the total balance across all accounts in atomic units. - unsafe fn balanceAll(self: &Wallet) -> u64; - - /// Get the total unlocked balance across all accounts in atomic units. - unsafe fn unlockedBalanceAll(self: &Wallet) -> u64; - - unsafe fn setAllowMismatchedDaemonVersion(self: Pin<&mut Wallet>, allow_mismatch: bool); - } -} diff --git a/monero-sys/src/lib.rs b/monero-sys/src/lib.rs deleted file mode 100644 index d720cca35..000000000 --- a/monero-sys/src/lib.rs +++ /dev/null @@ -1,262 +0,0 @@ -mod bridge; - -use std::{ - ops::{Deref, DerefMut}, - pin::Pin, - sync::{Arc, Mutex, OnceLock}, -}; - -use cxx::let_cxx_string; - -use bridge::ffi; - -pub use bridge::ffi::NetworkType; - -static WALLET_MANAGER: OnceLock>> = OnceLock::new(); - -pub struct WalletManager { - inner: RawWalletManager, -} - -pub struct RawWalletManager(*mut ffi::WalletManager); - -unsafe impl Send for RawWalletManager {} -unsafe impl Sync for RawWalletManager {} - -impl WalletManager { - /// Get the wallet manager instance. - pub fn get() -> Arc> { - WALLET_MANAGER - .get_or_init(|| { - let manager = unsafe { ffi::getWalletManager() }; - - Arc::new(Mutex::new(Self { - inner: RawWalletManager(manager), - })) - }) - .clone() - } - - /// Create a new wallet. - pub fn create_wallet( - &mut self, - path: &str, - password: &str, - language: &str, - network_type: ffi::NetworkType, - kdf_rounds: u64, - ) -> Wallet { - let_cxx_string!(path = path); - let_cxx_string!(password = password); - let_cxx_string!(language = language); - - let wallet_pointer = unsafe { - self.inner - .pinned() - .createWallet(&path, &password, &language, network_type, kdf_rounds) - }; - - Wallet::new(wallet_pointer) - } - - /// Recover a wallet from a mnemonic seed (electrum seed). - /// - /// # Arguments - /// - /// * `path` - Name of wallet file to be created - /// * `password` - Password of wallet file - /// * `mnemonic` - Mnemonic seed (25 words electrum seed) - /// * `network_type` - Network type (MAINNET, TESTNET, STAGENET) - /// * `restore_height` - Restore from start height (0 to start from the beginning) - /// * `kdf_rounds` - Number of rounds for key derivation function - /// * `seed_offset` - Optional passphrase used to derive the seed - /// - /// # Returns - /// - /// A new Wallet instance. Call wallet.status() to check if recovered successfully. - pub fn recover_wallet( - &mut self, - path: &str, - password: &str, - mnemonic: &str, - network_type: ffi::NetworkType, - restore_height: u64, - kdf_rounds: u64, - seed_offset: &str, - ) -> Wallet { - let_cxx_string!(path = path); - let_cxx_string!(password = password); - let_cxx_string!(mnemonic = mnemonic); - let_cxx_string!(seed_offset = seed_offset); - - let wallet_pointer = unsafe { - self.inner - .pinned() - .recoveryWallet(&path, &password, &mnemonic, network_type, restore_height, kdf_rounds, &seed_offset) - }; - - Wallet::new(wallet_pointer) - } - - /// Set the address of the remote node ("daemon"). - pub fn set_daemon_address(&mut self, address: &str) { - tracing::debug!(%address, "Connecting wallet manager to remote node"); - - let_cxx_string!(address = address); - unsafe { - self.inner.pinned().setDaemonAddress(&address); - } - } - - /// Check if the wallet manager is connected to the configured daemon. - pub fn connected(&mut self) -> bool { - unsafe { self.inner.pinned().connected(std::ptr::null_mut()) } - } -} - -impl RawWalletManager { - /// Get a pinned reference to the inner (c++) wallet manager. - /// This is a convenience function necessary because - /// the ffi interface mostly takes a Pin<&mut T> but - /// we haven't figured out how to hold that in the struct. - pub fn pinned(&mut self) -> Pin<&mut ffi::WalletManager> { - unsafe { - Pin::new_unchecked( - self.0 - .as_mut() - .expect("wallet manager pointer not to be null"), - ) - } - } -} - -/// A single Monero wallet. -pub struct Wallet { - inner: RawWallet, -} - -pub struct RawWallet(*mut ffi::Wallet); - -unsafe impl Send for RawWallet {} -unsafe impl Sync for RawWallet {} - -impl Wallet { - /// Create a new wallet from a raw C++ wallet pointer. - fn new(inner: *mut ffi::Wallet) -> Self { - Self { - inner: RawWallet(inner), - } - } - - /// Get the address for the given account and address index. - /// address(0, 0) is the main address. - pub fn address(&self, account_index: u32, address_index: u32) -> String { - let address = unsafe { ffi::address(&self.inner, account_index, address_index) }; - address.to_string() - } - - /// Initialize the wallet by connecting to the specified remote node (daemon). - pub fn init(&mut self, daemon_address: &str, ssl: bool) -> bool { - let_cxx_string!(daemon_address = daemon_address); - let_cxx_string!(daemon_username = ""); - let_cxx_string!(daemon_password = ""); - let_cxx_string!(proxy_address = ""); - unsafe { - self.inner.pinned().init( - &daemon_address, - 0, - &daemon_username, - &daemon_password, - ssl, - false, - &proxy_address, - ) - } - } - - /// Start the background refresh thread (refreshes every 10 seconds). - pub fn start_refresh(&mut self) { - unsafe { self.inner.pinned().startRefresh() } - } - - /// Refresh the wallet asynchronously. - pub fn refresh_async(&mut self) { - unsafe { self.inner.pinned().refreshAsync() } - } - - /// Refresh the wallet once. - pub fn refresh(&mut self) -> bool { - unsafe { self.inner.pinned().refresh() } - } - - /// Get the current blockchain height. - pub fn blockchain_height(&self) -> u64 { - unsafe { self.inner.blockChainHeight() } - } - - /// Get the daemon's blockchain height. - /// - /// Returns the height of the blockchain from the connected daemon. - /// Returns 0 if there's an error communicating with the daemon. - pub fn daemon_blockchain_height(&self) -> u64 { - unsafe { self.inner.daemonBlockChainHeight() } - } - - /// Get the total balance across all accounts in atomic units. - pub fn balance_all(&self) -> u64 { - unsafe { self.inner.balanceAll() } - } - - /// Get the total unlocked balance across all accounts in atomic units. - pub fn unlocked_balance_all(&self) -> u64 { - unsafe { self.inner.unlockedBalanceAll() } - } - - /// Check if wallet was ever synchronized. - /// - /// Returns true if the wallet has been synchronized at least once, - /// false otherwise. - pub fn synchronized(&self) -> bool { - unsafe { self.inner.synchronized() } - } - - /// Set the allow mismatched daemon version flag. - pub fn set_allow_mismatched_daemon_version(&mut self, allow_mismatch: bool) { - unsafe { self.inner.pinned().setAllowMismatchedDaemonVersion(allow_mismatch) } - } -} - -impl RawWallet { - /// Get a pinned reference to the inner (c++) wallet. - pub fn pinned(&mut self) -> Pin<&mut ffi::Wallet> { - unsafe { Pin::new_unchecked(self.0.as_mut().expect("wallet pointer not to be null")) } - } -} - -impl Deref for Wallet { - type Target = RawWallet; - - fn deref(&self) -> &RawWallet { - &self.inner - } -} - -impl DerefMut for Wallet { - fn deref_mut(&mut self) -> &mut RawWallet { - &mut self.inner - } -} - -impl Deref for RawWallet { - type Target = ffi::Wallet; - - fn deref(&self) -> &ffi::Wallet { - unsafe { self.0.as_ref().expect("wallet pointer not to be null") } - } -} - -impl DerefMut for RawWallet { - fn deref_mut(&mut self) -> &mut ffi::Wallet { - unsafe { self.0.as_mut().expect("wallet pointer not to be null") } - } -} diff --git a/monero-sys/tests/harness_test.rs b/monero-sys/tests/harness_test.rs deleted file mode 100644 index 2e016937e..000000000 --- a/monero-sys/tests/harness_test.rs +++ /dev/null @@ -1,196 +0,0 @@ -use monero_harness::{image::Monerod, Monero}; -use monero_wallet_sys::{NetworkType, WalletManager}; -use std::{time::Duration, sync::OnceLock}; -use tempfile::{tempdir, TempDir}; -use testcontainers::{clients::Cli, Container}; -use tokio::time::sleep; -use tracing::info; -use uuid::Uuid; - -const KDF_ROUNDS: u64 = 1; -const PASSWORD: &str = "test"; -const SEED_OFFSET: &str = ""; - -// Amount to fund the wallet with (in piconero) -const FUND_AMOUNT: u64 = 1_000_000_000_000; - -// Global temporary directory for all wallet files -static GLOBAL_TEMP_DIR: OnceLock = OnceLock::new(); - -#[tokio::test] -async fn test_monero_wrapper_with_harness() { - tracing_subscriber::fmt() - .with_env_filter("warn,test=debug,monero_harness=debug,monero_rpc=debug,harness_test=debug") - .with_test_writer() - .init(); - - // Step 1: Create a wallet with monero-wrapper using the global temp directory - let wallet_path = get_temp_wallet_path(); - let (address, wallet_seed) = create_monero_wrapper_wallet(&wallet_path).await; - - info!("Created monero-wrapper wallet with address: {}", address); - info!("Wallet seed: {}", wallet_seed); - - // Step 2: Set up monero-harness and fund the address - let tc = Cli::default(); - let (monero, monerod_container, _wallet_containers) = - Monero::new(&tc, vec![]).await.expect("Failed to create Monero containers"); - - let daemon_address = get_daemon_address(&monerod_container); - - // Initialize miner - info!("Initializing miner wallet"); - monero.init_miner().await.expect("Failed to initialize miner"); - - // Start mining continuously to generate blocks - info!("Starting continuous mining"); - monero.start_miner().await.expect("Failed to start miner"); - - // Fund the address created by monero-wrapper - info!("Funding the test wallet address: {}", address); - init_to_wallet_address(&monero, &address, FUND_AMOUNT).await - .expect("Failed to fund wallet address"); - - // Step 3: Connect the wrapper wallet to the daemon and check balance - info!("Connecting to daemon at: {}", daemon_address); - - let wallet_balance = connect_and_check_balance(wallet_seed, daemon_address).await; - - // Step 4: Verify the balance - info!("Wallet balance: {}", wallet_balance); - assert!(wallet_balance > 0, "Wallet balance should be greater than 0"); - - info!("Test passed! Wallet successfully received and detected funds"); -} - -async fn create_monero_wrapper_wallet(wallet_path: &str) -> (String, String) { - // Get wallet manager - let wallet_manager_mutex = WalletManager::get(); - let mut wallet_manager = wallet_manager_mutex - .lock() - .expect("Failed to lock wallet manager"); - - // Define a fixed seed to use for reproducible tests - let seed = "echo ourselves ruined oven masterful wives enough addicted future cottage illness adopt lucky movement tiger taboo imbalance antics iceberg hobby oval aloof tuesday uttered oval"; - - // Create wallet from the seed - we'll use 'recover' since we have a seed - let wallet = wallet_manager.recover_wallet( - wallet_path, - PASSWORD, - seed, - // Regtest uses Mainnet addresses - NetworkType::Mainnet, - 1, - KDF_ROUNDS, - SEED_OFFSET, - ); - - // Get the main address - let address = wallet.address(0, 0); - - (address, seed.to_string()) -} - -async fn init_to_wallet_address(monero: &Monero, address: &str, amount: u64) -> anyhow::Result<()> { - info!("Funding address {} with {} piconero", address, amount); - - // Generate some blocks to ensure miner has funds - monero.fund_address(address, amount).await?; - - info!("Successfully funded address with {} piconero", amount); - Ok(()) -} - -fn get_temp_wallet_path() -> String { - // Get or initialize the global temp directory - let temp_dir = GLOBAL_TEMP_DIR.get_or_init(|| { - // Create a directory that won't be deleted until the program exits - info!("Creating global temporary directory for wallet files"); - tempdir().expect("Failed to create global temporary directory") - }); - - // Generate a unique wallet filename using UUID - let uuid = Uuid::new_v4(); // This is the correct method to generate a random UUID - let wallet_filename = format!("wallet_{}", uuid); - let wallet_path = temp_dir.path().join(wallet_filename); - - info!("Generated wallet path: {}", wallet_path.display()); - wallet_path.to_str().unwrap().to_string() -} - -/// As we are not running the monero-wrapper inside the Docker network, we need to connect to the locally exposed port -/// Docker maps the port from inside the container (18081) to a random port on the host -/// This function extracts the port and constructs the address as "localhost:" -fn get_daemon_address(monerod_container: &Container<'_, Monerod>) -> String { - let local_daemon_rpc_port = monerod_container.ports().map_to_host_port_ipv4(monero_harness::image::RPC_PORT); - let local_daemon_rpc_port = local_daemon_rpc_port.expect("monerod should have a mapping to the host for the default RPC port"); - - format!("localhost:{}", local_daemon_rpc_port) -} - -async fn connect_and_check_balance(seed: String, daemon_address: String) -> u64 { - // Get wallet manager - let wallet_manager_mutex = WalletManager::get(); - let mut wallet_manager = wallet_manager_mutex - .lock() - .expect("Failed to lock wallet manager"); - - // Set daemon address - wallet_manager.set_daemon_address(&daemon_address); - - // Check connection - let connected = wallet_manager.connected(); - info!("Connected to daemon: {}", connected); - assert!(connected, "Should be connected to daemon"); - - // Get a unique wallet path from the global temp directory - let wallet_path = get_temp_wallet_path(); - tracing::info!("Recovering wallet from seed to {}", wallet_path); - - // Recover wallet from seed - let mut wallet = wallet_manager.recover_wallet( - &wallet_path, - PASSWORD, - &seed, - NetworkType::Mainnet, // Regtest uses Mainnet addresses - 1, // Restore height (start from beginning) - KDF_ROUNDS, - SEED_OFFSET, - ); - - // Initialize wallet - wallet.init(&daemon_address, false); - - tracing::info!("Starting testing of wallet with address: {}", wallet.address(0, 0)); - - // We need to allow mismatched daemon versions for the Regtest network - // to be accepted by wallet2 - wallet.set_allow_mismatched_daemon_version(true); - - // Start background refresh - wallet.start_refresh(); - - // Wait for wallet to sync - info!("Waiting for wallet to sync..."); - while !wallet.synchronized() { - let wallet_height = wallet.blockchain_height(); - let daemon_height = wallet.daemon_blockchain_height(); - - info!( - "Wallet height: {}, Daemon height: {}", - wallet_height, daemon_height - ); - - sleep(Duration::from_secs(1)).await; - } - - // Manual refresh to ensure we have the latest state - info!("Performing final refresh"); - let refresh_result = wallet.refresh(); - info!("Final refresh result: {}", refresh_result); - - // Check balance - let balance = wallet.balance_all(); - info!("Final balance check: {}", balance); - balance -} \ No newline at end of file diff --git a/monero-sys/tests/simple.rs b/monero-sys/tests/simple.rs deleted file mode 100644 index 163b6fbf5..000000000 --- a/monero-sys/tests/simple.rs +++ /dev/null @@ -1,88 +0,0 @@ -use monero_wallet_sys::{NetworkType, WalletManager}; - -const KDF_ROUNDS: u64 = 1; -const PASSWORD: &str = "test"; - -// No seed offset for now. This is the default -// TODO: Let lib.rs take an Option<_> and if None pass in an empty string -const SEED_OFFSET: &str = ""; - -const STAGENET_REMOTE_NODE: &str = "node.sethforprivacy.com:38089"; -const STAGENET_WALLET_SEED: &str = "echo ourselves ruined oven masterful wives enough addicted future cottage illness adopt lucky movement tiger taboo imbalance antics iceberg hobby oval aloof tuesday uttered oval"; -const STAGENET_WALLET_RESTORE_HEIGHT: u64 = 1728128; - -#[test] -fn main() { - tracing_subscriber::fmt::init(); - - let wallet_manager_mutex = WalletManager::get(); - let mut wallet_manager = wallet_manager_mutex - .lock() - .expect("failed to lock wallet manager"); - - tracing::info!("Setting daemon address"); - wallet_manager.set_daemon_address(STAGENET_REMOTE_NODE); - - tracing::info!("Connected: {}", wallet_manager.connected()); - - let temp_dir = tempfile::tempdir().unwrap(); - - let wallet_name = "recovered_wallet"; - let wallet_path = temp_dir.path().join(wallet_name); - - tracing::info!("Recovering wallet from seed"); - let mut wallet = wallet_manager.recover_wallet( - wallet_path.to_str().unwrap(), - PASSWORD, - STAGENET_WALLET_SEED, - NetworkType::Stagenet, - STAGENET_WALLET_RESTORE_HEIGHT, - KDF_ROUNDS, - SEED_OFFSET, - ); - - // TODO: Here we'd need to call status() I believe to check if the creation was successful - - wallet.init(STAGENET_REMOTE_NODE, true); - - tracing::info!("Primary address: {}", wallet.address(0, 0)); - - // Start background refresh - tracing::info!("Starting background refresh"); - wallet.start_refresh(); - - // Wait for a while to let the wallet sync, checking sync status - tracing::info!("Waiting for wallet to sync..."); - - // TODO: lib.rs should provide an async method that does this for us - while !wallet.synchronized() { - let wallet_height = wallet.blockchain_height(); - let daemon_height = wallet.daemon_blockchain_height(); - let is_synced = wallet.synchronized(); - - // Calculate sync percentage if daemon height is available - let sync_percentage = if daemon_height > 0 && daemon_height >= wallet_height { - (wallet_height as f64 / daemon_height as f64 * 100.0).round() - } else { - 0.0 - }; - - tracing::info!( - "Wallet height: {}, Daemon height: {}, Sync: {}%, Synchronized: {} (iteration {})", - wallet_height, daemon_height, sync_percentage, is_synced, i - ); - - std::thread::sleep(std::time::Duration::from_secs(3)); - } - - tracing::info!("Wallet is synchronized!"); - - // Manual refresh one more time - tracing::info!(result=%wallet.refresh(), "Manual refresh"); - - let balance = wallet.balance_all(); - tracing::info!("Balance: {}", balance); - - let unlocked_balance = wallet.unlocked_balance_all(); - tracing::info!("Unlocked balance: {}", unlocked_balance); -} From 8e497d075ef2126f35c659ae23141fb3ec8d143a Mon Sep 17 00:00:00 2001 From: Binarybaron Date: Mon, 28 Apr 2025 12:58:27 +0200 Subject: [PATCH 13/13] revert --- monero-harness/src/lib.rs | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/monero-harness/src/lib.rs b/monero-harness/src/lib.rs index c621caf56..61c12a275 100644 --- a/monero-harness/src/lib.rs +++ b/monero-harness/src/lib.rs @@ -171,29 +171,6 @@ impl<'c> Monero { Ok(()) } - /// Funds a specific wallet address with XMR - /// - /// This function is useful when you want to fund an address that isn't managed by - /// a wallet in the testcontainer setup, like an external wallet address. - pub async fn fund_address(&self, address: &str, amount: u64) -> Result<()> { - let monerod = &self.monerod; - - // Make sure miner has funds by generating blocks - monerod - .client() - .generateblocks(120, address.to_string()) - .await?; - - // Mine more blocks to confirm the transaction - monerod - .client() - .generateblocks(10, address.to_string()) - .await?; - - tracing::info!("Successfully funded address with {} piconero", amount); - Ok(()) - } - pub async fn start_miner(&self) -> Result<()> { let miner_wallet = self.wallet("miner")?; let miner_address = miner_wallet.address().await?.address; @@ -269,7 +246,6 @@ impl<'c> Monerod { pub fn client(&self) -> &monerod::Client { &self.client } - /// Spawns a task to mine blocks in a regular interval to the provided /// address