diff --git a/CHANGELOG.md b/CHANGELOG.md index 131db87b3..40960e690 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.17] - 2025-04-18 - GUI: The user will now be asked to approve the swap offer again before the Bitcoin lock transaction is published. Makers should take care to only assume a swap has been accepted by the taker if the Bitcoin lock transaction is detected (`Advancing state state=bitcoin lock transaction in mempool ...`). Swaps that have been safely aborted will not be displayed in the GUI anymore. @@ -30,7 +32,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.0-rc.12] - 2025-01-14 - GUI: Fixed a bug where the CLI wasn't passed the correct Monero node. -- ## [1.0.0-rc.11] - 2024-12-22 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/dev-docs/asb/README.md b/dev-docs/asb/README.md index 38d467e03..2770dcb7b 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 diff --git a/swap/src/asb/event_loop.rs b/swap/src/asb/event_loop.rs index f9d9c3db5..160acc9cf 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; @@ -511,17 +511,51 @@ where result } + /// 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 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()) + .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 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() + .await? + .iter() + .filter_map(|(_, state)| match state { + State::Alice(AliceState::BtcLockTransactionSeen { state3 }) + | State::Alice(AliceState::BtcLocked { state3 }) => Some(state3.xmr + MONERO_FEE), + _ => None, + }) + .fold(Amount::ZERO, |acc, amount| acc + amount); + + 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> { - /// 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() @@ -529,26 +563,19 @@ where .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); - - let max_bitcoin_for_monero = - xmr_balance - .max_bitcoin_for_price(ask_price) - .ok_or_else(|| { - Arc::new(anyhow!( - "Bitcoin price ({}) x Monero ({}) overflow", - ask_price, - xmr_balance - )) - })?; - - tracing::trace!(%ask_price, %xmr_balance, %max_bitcoin_for_monero, "Computed quote"); + let unreserved_xmr_balance = self.unreserved_monero_balance().await?; + + 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, + unreserved_xmr_balance + )) + })?; + + tracing::trace!(%ask_price, %unreserved_xmr_balance, %max_bitcoin_for_monero, "Computed quote"); if min_buy > max_bitcoin_for_monero { tracing::trace!( 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 {