953: Adjust quote based on Bitcoin balance r=lescuer97 a=lescuer97

Fixes #939 #963 
Please comment in the new method for getting a Monero value, I had to allow clippy::cast_precision_loss to convert to a f64, for our use case I don't really thing we will lose much precision. 

Please comment on the implementation of the check. 

Co-authored-by: leonardo <leoescuer@protonmail.com>
pull/966/head
bors[bot] 2 years ago committed by GitHub
commit c76abd48c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -319,13 +319,46 @@ where
min_buy: bitcoin::Amount,
max_buy: bitcoin::Amount,
) -> Result<BidQuote> {
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,
})

@ -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::<ZeroQuoteReceived>() {
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<Amount>,
call_counter: usize,

@ -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<Self> {
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};

@ -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.

Loading…
Cancel
Save