From d224c2910bbdddb8f0d82b53f468f09652d49cab Mon Sep 17 00:00:00 2001 From: leonardo Date: Thu, 24 Feb 2022 02:12:45 +0100 Subject: [PATCH] Adjust quote based on Bitcoin balance --- CHANGELOG.md | 7 +++++- swap/src/asb/event_loop.rs | 39 +++++++++++++++++++++++++++++--- swap/src/bin/swap.rs | 46 +++++++++++++++++++++++++++++++++++--- swap/src/monero.rs | 38 +++++++++++++++++++++++++++++++ swap/src/network/quote.rs | 4 ++++ 5 files changed, 127 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6722ea7..5421b912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Revert logs to use rfc3339 local time formatting. - Always write logs as JSON to files +### Added + +- Adjust quote based on Bitcoin balance. + If the max_buy_btc in the ASB config is higher than the available balance to trade it will return the max available balance discounting the locking fees for monero, in the case the balance is lower than the min_buy_btc config it will return 0 to the CLI. If the ASB returns a quote of 0 the CLI will not allow you continue with a trade. + ## [0.10.2] - 2021-12-25 ### Changed @@ -305,7 +310,7 @@ It is possible to migrate critical data from the old db to the sqlite but there - Fixed an issue where Alice would not verify if Bob's Bitcoin lock transaction is semantically correct, i.e. pays the agreed upon amount to an output owned by both of them. Fixing this required a **breaking change** on the network layer and hence old versions are not compatible with this version. -[Unreleased]: https://github.com/comit-network/xmr-btc-swap/compare/0.10.2...HEAD +[unreleased]: https://github.com/comit-network/xmr-btc-swap/compare/0.10.2...HEAD [0.10.2]: https://github.com/comit-network/xmr-btc-swap/compare/0.10.1...0.10.2 [0.10.1]: https://github.com/comit-network/xmr-btc-swap/compare/0.10.0...0.10.1 [0.10.0]: https://github.com/comit-network/xmr-btc-swap/compare/0.9.0...0.10.0 diff --git a/swap/src/asb/event_loop.rs b/swap/src/asb/event_loop.rs index a5ad678f..1a06d95f 100644 --- a/swap/src/asb/event_loop.rs +++ b/swap/src/asb/event_loop.rs @@ -319,13 +319,46 @@ where min_buy: bitcoin::Amount, max_buy: bitcoin::Amount, ) -> Result { - let rate = self + let ask_price = self .latest_rate .latest_rate() - .context("Failed to get latest rate")?; + .context("Failed to get latest rate")? + .ask() + .context("Failed to compute asking price")?; + + let max_bitcoin_for_monero = self + .monero_wallet + .get_balance() + .await? + .max_bitcoin_for_price(ask_price); + + if min_buy > max_bitcoin_for_monero { + tracing::warn!( + "Your Monero balance is too low to initiate a swap, as your minimum swap amount is {}. You could at most swap {}", + min_buy, max_bitcoin_for_monero + ); + + return Ok(BidQuote { + price: ask_price, + min_quantity: bitcoin::Amount::ZERO, + max_quantity: bitcoin::Amount::ZERO, + }); + } + + if max_buy > max_bitcoin_for_monero { + tracing::warn!( + "Your Monero balance is too low to initiate a swap with the maximum swap amount {} that you have specified in your config. You can at most swap {}", + max_buy, max_bitcoin_for_monero + ); + return Ok(BidQuote { + price: ask_price, + min_quantity: min_buy, + max_quantity: max_bitcoin_for_monero, + }); + } Ok(BidQuote { - price: rate.ask().context("Failed to compute asking price")?, + price: ask_price, min_quantity: min_buy, max_quantity: max_buy, }) diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index bac3ae94..81ec69e3 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -29,7 +29,7 @@ use swap::cli::{list_sellers, EventLoop, SellerStatus}; use swap::database::open_db; use swap::env::Config; use swap::libp2p_ext::MultiAddrExt; -use swap::network::quote::BidQuote; +use swap::network::quote::{BidQuote, ZeroQuoteReceived}; use swap::network::swarm; use swap::protocol::bob; use swap::protocol::bob::{BobState, Swap}; @@ -99,7 +99,7 @@ async fn main() -> Result<()> { let event_loop = tokio::spawn(event_loop.run()); let max_givable = || bitcoin_wallet.max_giveable(TxLock::script_size()); - let (amount, fees) = determine_btc_to_swap( + let (amount, fees) = match determine_btc_to_swap( json, event_loop_handle.request_quote(), bitcoin_wallet.new_address(), @@ -107,7 +107,16 @@ async fn main() -> Result<()> { max_givable, || bitcoin_wallet.sync(), ) - .await?; + .await + { + Ok(val) => val, + Err(error) => match error.downcast::() { + Ok(_) => { + bail!("Seller's XMR balance is currently too low to initiate a swap, please try again later") + } + Err(other) => bail!(other), + }, + }; tracing::info!(%amount, %fees, "Determined swap amount"); @@ -556,6 +565,11 @@ where { tracing::debug!("Requesting quote"); let bid_quote = bid_quote.await?; + + if bid_quote.max_quantity == bitcoin::Amount::ZERO { + bail!(ZeroQuoteReceived) + } + tracing::info!( price = %bid_quote.price, minimum_amount = %bid_quote.min_quantity, @@ -915,6 +929,32 @@ mod tests { ); } + #[tokio::test] + async fn given_bid_quote_max_amount_0_return_errorq() { + let givable = Arc::new(Mutex::new(MaxGiveable::new(vec![ + Amount::from_btc(0.0001).unwrap(), + Amount::from_btc(0.01).unwrap(), + ]))); + + let determination_error = determine_btc_to_swap( + true, + async { Ok(quote_with_max(0.00)) }, + get_dummy_address(), + || async { Ok(Amount::from_btc(0.0101)?) }, + || async { + let mut result = givable.lock().unwrap(); + result.give() + }, + || async { Ok(()) }, + ) + .await + .err() + .unwrap() + .to_string(); + + assert_eq!("Received quote of 0", determination_error); + } + struct MaxGiveable { amounts: Vec, call_counter: usize, diff --git a/swap/src/monero.rs b/swap/src/monero.rs index 86c97eba..bf3045ce 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -99,6 +99,20 @@ impl Amount { self.0 } + pub fn max_bitcoin_for_price(&self, ask_price: bitcoin::Amount) -> bitcoin::Amount { + let piconero_minus_fee = self.as_piconero().saturating_sub(MONERO_FEE.as_piconero()); + + if piconero_minus_fee == 0 { + return bitcoin::Amount::ZERO; + } + + // There needs to be an offset for difference in zeroes beetween Piconeros and + // Satoshis + let piconero_calc = (piconero_minus_fee * ask_price.as_sat()) / PICONERO_OFFSET; + + bitcoin::Amount::from_sat(piconero_calc) + } + pub fn from_monero(amount: f64) -> Result { let decimal = Decimal::try_from(amount)?; Self::from_decimal(decimal) @@ -360,6 +374,30 @@ mod tests { ); } + #[test] + fn geting_max_bitcoin_to_trade() { + let amount = Amount::parse_monero("10").unwrap(); + let bitcoin_price_sats = bitcoin::Amount::from_sat(382_900); + + let monero_max_from_bitcoin = amount.max_bitcoin_for_price(bitcoin_price_sats); + + assert_eq!( + bitcoin::Amount::from_sat(3_828_988), + monero_max_from_bitcoin + ); + } + + #[test] + fn geting_max_bitcoin_to_trade_with_balance_smaller_than_locking_fee() { + let monero = "0.00001"; + let amount = Amount::parse_monero(monero).unwrap(); + let bitcoin_price_sats = bitcoin::Amount::from_sat(382_900); + + let monero_max_from_bitcoin = amount.max_bitcoin_for_price(bitcoin_price_sats); + + assert_eq!(bitcoin::Amount::ZERO, monero_max_from_bitcoin); + } + use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; diff --git a/swap/src/network/quote.rs b/swap/src/network/quote.rs index 67640d65..caf99e09 100644 --- a/swap/src/network/quote.rs +++ b/swap/src/network/quote.rs @@ -37,6 +37,10 @@ pub struct BidQuote { pub max_quantity: bitcoin::Amount, } +#[derive(Clone, Copy, Debug, thiserror::Error)] +#[error("Received quote of 0")] +pub struct ZeroQuoteReceived; + /// Constructs a new instance of the `quote` behaviour to be used by the ASB. /// /// The ASB is always listening and only supports inbound connections, i.e.