481: Min buy amount r=da-kami a=da-kami



Co-authored-by: Daniel Karzel <daniel@comit.network>
This commit is contained in:
bors[bot] 2021-05-11 04:08:59 +00:00 committed by GitHub
commit 227c383d76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 407 additions and 117 deletions

View File

@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
By default we wait for finality of the redeem transaction; this can be disabled by setting `--do-not-await-finality`.
- Resume-only mode for the ASB.
When started with `--resume-only` the ASB does not accept new, incoming swap requests but only finishes swaps that are resumed upon startup.
- A minimum accepted Bitcoin amount for the ASB similar to the maximum amount already present.
For the CLI the minimum amount is enforced by waiting until at least the minimum is available as max-giveable amount.
### Fixed
@ -41,6 +43,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2. Buy amount sent by CLI exceeds maximum buy amount accepted by ASB
3. ASB is running in resume-only mode and does not accept incoming swap requests
### Changed
- The ASB's `--max-buy` and `ask-spread` parameter were removed in favour of entries in the config file.
The initial setup includes setting these two values now.
## [0.5.0] - 2021-04-17
### Changed

View File

@ -40,7 +40,7 @@ proptest = "1"
rand = "0.7"
rand_chacha = "0.2"
reqwest = { version = "0.11", features = [ "rustls-tls", "stream", "socks" ], default-features = false }
rust_decimal = "1"
rust_decimal = { version = "1", features = [ "serde-float" ] }
rust_decimal_macros = "1"
serde = { version = "1", features = [ "derive" ] }
serde_cbor = "0.11"

View File

@ -1,7 +1,5 @@
use crate::bitcoin::Amount;
use bitcoin::util::amount::ParseAmountError;
use bitcoin::{Address, Denomination};
use rust_decimal::Decimal;
use bitcoin::Address;
use std::path::PathBuf;
use uuid::Uuid;
@ -28,15 +26,6 @@ pub struct Arguments {
pub enum Command {
#[structopt(about = "Main command to run the ASB.")]
Start {
#[structopt(long = "max-buy-btc", help = "The maximum amount of BTC the ASB is willing to buy.", default_value = "0.005", parse(try_from_str = parse_btc))]
max_buy: Amount,
#[structopt(
long = "ask-spread",
help = "The spread in percent that should be applied to the asking price.",
default_value = "0.02"
)]
ask_spread: Decimal,
#[structopt(
long = "resume-only",
help = "For maintenance only. When set, no new swap requests will be accepted, but existing unfinished swaps will be resumed."
@ -124,7 +113,3 @@ pub struct RecoverCommandParams {
)]
pub force: bool,
}
fn parse_btc(s: &str) -> Result<Amount, ParseAmountError> {
Amount::from_str_in(s, Denomination::Bitcoin)
}

View File

@ -1,10 +1,12 @@
use crate::fs::{ensure_directory_exists, system_config_dir, system_data_dir};
use crate::tor::{DEFAULT_CONTROL_PORT, DEFAULT_SOCKS5_PORT};
use anyhow::{Context, Result};
use anyhow::{bail, Context, Result};
use config::ConfigError;
use dialoguer::theme::ColorfulTheme;
use dialoguer::Input;
use libp2p::core::Multiaddr;
use rust_decimal::prelude::FromPrimitive;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::ffi::OsStr;
use std::fs;
@ -18,6 +20,10 @@ const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://electrum.blockstream.info:60002";
const DEFAULT_MONERO_WALLET_RPC_TESTNET_URL: &str = "http://127.0.0.1:38083/json_rpc";
const DEFAULT_BITCOIN_CONFIRMATION_TARGET: usize = 3;
const DEFAULT_MIN_BUY_AMOUNT: f64 = 0.002f64;
const DEFAULT_MAX_BUY_AMOUNT: f64 = 0.02f64;
const DEFAULT_SPREAD: f64 = 0.02f64;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
pub struct Config {
pub data: Data,
@ -25,6 +31,7 @@ pub struct Config {
pub bitcoin: Bitcoin,
pub monero: Monero,
pub tor: TorConf,
pub maker: Maker,
}
impl Config {
@ -72,6 +79,16 @@ pub struct TorConf {
pub socks5_port: u16,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Maker {
#[serde(with = "::bitcoin::util::amount::serde::as_btc")]
pub min_buy_btc: bitcoin::Amount,
#[serde(with = "::bitcoin::util::amount::serde::as_btc")]
pub max_buy_btc: bitcoin::Amount,
pub ask_spread: Decimal,
}
impl Default for TorConf {
fn default() -> Self {
Self {
@ -123,10 +140,11 @@ where
F: Fn() -> Result<Config>,
{
info!("Config file not found, running initial setup...");
ensure_directory_exists(config_path.as_path())?;
let initial_config = config_file()?;
let toml = toml::to_string(&initial_config)?;
ensure_directory_exists(config_path.as_path())?;
fs::write(&config_path, toml)?;
info!(
@ -185,6 +203,27 @@ pub fn query_user_for_initial_testnet_config() -> Result<Config> {
.default(DEFAULT_SOCKS5_PORT.to_owned())
.interact_text()?;
let min_buy = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter minimum Bitcoin amount you are willing to accept per swap or hit enter to use default.")
.default(DEFAULT_MIN_BUY_AMOUNT)
.interact_text()?;
let min_buy = bitcoin::Amount::from_btc(min_buy)?;
let max_buy = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter maximum Bitcoin amount you are willing to accept per swap or hit enter to use default.")
.default(DEFAULT_MAX_BUY_AMOUNT)
.interact_text()?;
let max_buy = bitcoin::Amount::from_btc(max_buy)?;
let ask_spread = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter spread (in percent; value between 0.x and 1.0) to be used on top of the market rate or hit enter to use default.")
.default(DEFAULT_SPREAD)
.interact_text()?;
if !(0.0..=1.0).contains(&ask_spread) {
bail!(format!("Invalid spread {}. For the spread value floating point number in interval [0..1] are allowed.", ask_spread))
}
let ask_spread = Decimal::from_f64(ask_spread).context("Unable to parse spread")?;
println!();
Ok(Config {
@ -203,6 +242,11 @@ pub fn query_user_for_initial_testnet_config() -> Result<Config> {
control_port: tor_control_port,
socks5_port: tor_socks5_port,
},
maker: Maker {
min_buy_btc: min_buy,
max_buy_btc: max_buy,
ask_spread,
},
})
}
@ -236,6 +280,11 @@ mod tests {
wallet_rpc_url: Url::from_str(DEFAULT_MONERO_WALLET_RPC_TESTNET_URL).unwrap(),
},
tor: Default::default(),
maker: Maker {
min_buy_btc: bitcoin::Amount::from_btc(DEFAULT_MIN_BUY_AMOUNT).unwrap(),
max_buy_btc: bitcoin::Amount::from_btc(DEFAULT_MAX_BUY_AMOUNT).unwrap(),
ask_spread: Decimal::from_f64(DEFAULT_SPREAD).unwrap(),
},
};
initial_setup(config_path.clone(), || Ok(expected.clone())).unwrap();

View File

@ -79,11 +79,7 @@ async fn main() -> Result<()> {
let env_config = env::Testnet::get_config();
match opt.cmd {
Command::Start {
max_buy,
ask_spread,
resume_only,
} => {
Command::Start { resume_only } => {
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
let monero_wallet = init_monero_wallet(&config, env_config).await?;
@ -122,12 +118,13 @@ async fn main() -> Result<()> {
let current_balance = monero_wallet.get_balance().await?;
let lock_fee = monero_wallet.static_tx_fee_estimate();
let kraken_rate = KrakenRate::new(ask_spread, kraken_price_updates);
let kraken_rate = KrakenRate::new(config.maker.ask_spread, kraken_price_updates);
let mut swarm = swarm::alice(
&seed,
current_balance,
lock_fee,
max_buy,
config.maker.min_buy_btc,
config.maker.max_buy_btc,
kraken_rate.clone(),
resume_only,
)?;
@ -144,7 +141,8 @@ async fn main() -> Result<()> {
Arc::new(monero_wallet),
Arc::new(db),
kraken_rate,
max_buy,
config.maker.min_buy_btc,
config.maker.max_buy_btc,
)
.unwrap();

View File

@ -20,7 +20,7 @@ use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use structopt::StructOpt;
use swap::bitcoin::{Amount, TxLock};
use swap::bitcoin::TxLock;
use swap::cli::command::{Arguments, Command, MoneroParams};
use swap::database::Database;
use swap::env::{Config, GetConfig};
@ -94,23 +94,19 @@ async fn main() -> Result<()> {
EventLoop::new(swap_id, swarm, alice_peer_id, bitcoin_wallet.clone())?;
let event_loop = tokio::spawn(event_loop.run());
let send_bitcoin = determine_btc_to_swap(
let max_givable = || bitcoin_wallet.max_giveable(TxLock::script_size());
let (send_bitcoin, fees) = determine_btc_to_swap(
event_loop_handle.request_quote(),
bitcoin_wallet.balance(),
max_givable().await?,
bitcoin_wallet.new_address(),
async {
while bitcoin_wallet.balance().await? == Amount::ZERO {
bitcoin_wallet.sync().await?;
tokio::time::sleep(Duration::from_secs(1)).await;
}
bitcoin_wallet.balance().await
},
bitcoin_wallet.max_giveable(TxLock::script_size()),
|| bitcoin_wallet.balance(),
max_givable,
|| bitcoin_wallet.sync(),
)
.await?;
info!("Swapping {} with {} fees", send_bitcoin, fees);
db.insert_peer_id(swap_id, alice_peer_id).await?;
let swap = Swap::new(
@ -332,51 +328,83 @@ async fn init_monero_wallet(
Ok((monero_wallet, monero_wallet_rpc_process))
}
async fn determine_btc_to_swap(
request_quote: impl Future<Output = Result<BidQuote>>,
initial_balance: impl Future<Output = Result<bitcoin::Amount>>,
async fn determine_btc_to_swap<FB, TB, FMG, TMG, FS, TS>(
bid_quote: impl Future<Output = Result<BidQuote>>,
mut current_maximum_giveable: bitcoin::Amount,
get_new_address: impl Future<Output = Result<bitcoin::Address>>,
wait_for_deposit: impl Future<Output = Result<bitcoin::Amount>>,
max_giveable: impl Future<Output = Result<bitcoin::Amount>>,
) -> Result<bitcoin::Amount> {
balance: FB,
max_giveable: FMG,
sync: FS,
) -> Result<(bitcoin::Amount, bitcoin::Amount)>
where
TB: Future<Output = Result<bitcoin::Amount>>,
FB: Fn() -> TB,
TMG: Future<Output = Result<bitcoin::Amount>>,
FMG: Fn() -> TMG,
TS: Future<Output = Result<()>>,
FS: Fn() -> TS,
{
debug!("Requesting quote");
let bid_quote = request_quote.await?;
let bid_quote = bid_quote.await?;
info!("Received quote: 1 XMR ~ {}", bid_quote.price);
// TODO: Also wait for more funds if balance < dust
let initial_balance = initial_balance.await?;
let max_giveable = if current_maximum_giveable == bitcoin::Amount::ZERO
|| current_maximum_giveable < bid_quote.min_quantity
{
let deposit_address = get_new_address.await?;
let minimum_amount = bid_quote.min_quantity;
let maximum_amount = bid_quote.max_quantity;
let balance = if initial_balance == Amount::ZERO {
info!(
"Please deposit the BTC you want to swap to {} (max {})",
get_new_address.await?,
bid_quote.max_quantity
%deposit_address,
%current_maximum_giveable,
%minimum_amount,
%maximum_amount,
"Please deposit BTC you want to swap to",
);
let new_balance = wait_for_deposit
.await
.context("Failed to wait for Bitcoin deposit")?;
loop {
sync().await?;
info!("Received {}", new_balance);
new_balance
let new_max_givable = max_giveable().await?;
if new_max_givable != current_maximum_giveable {
current_maximum_giveable = new_max_givable;
let new_balance = balance().await?;
tracing::info!(
%new_balance,
%current_maximum_giveable,
"Received BTC",
);
if current_maximum_giveable >= bid_quote.min_quantity {
break;
} else {
tracing::info!(
%minimum_amount,
%deposit_address,
"Please deposit more, not enough BTC to trigger swap with",
);
}
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
current_maximum_giveable
} else {
info!("Found {} in wallet", initial_balance);
initial_balance
current_maximum_giveable
};
let max_giveable = max_giveable
.await
.context("Failed to compute max 'giveable' Bitcoin amount")?;
let balance = balance().await?;
let fees = balance - max_giveable;
let max_accepted = bid_quote.max_quantity;
let btc_swap_amount = min(max_giveable, max_accepted);
info!("Swapping {} with {} fees", btc_swap_amount, fees);
Ok(btc_swap_amount)
Ok((btc_swap_amount, fees))
}
#[cfg(test)]
@ -390,74 +418,162 @@ mod tests {
async fn given_no_balance_and_transfers_less_than_max_swaps_max_giveable() {
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let amount = determine_btc_to_swap(
let (amount, fees) = determine_btc_to_swap(
async { Ok(quote_with_max(0.01)) },
async { Ok(Amount::ZERO) },
Amount::ZERO,
get_dummy_address(),
async { Ok(Amount::from_btc(0.0001)?) },
async { Ok(Amount::from_btc(0.00009)?) },
|| async { Ok(Amount::from_btc(0.001)?) },
|| async { Ok(Amount::from_btc(0.0009)?) },
|| async { Ok(()) },
)
.await
.unwrap();
assert_eq!(amount, Amount::from_btc(0.00009).unwrap())
let expected_amount = Amount::from_btc(0.0009).unwrap();
let expected_fees = Amount::from_btc(0.0001).unwrap();
assert_eq!((amount, fees), (expected_amount, expected_fees))
}
#[tokio::test]
async fn given_no_balance_and_transfers_more_then_swaps_max_quantity_from_quote() {
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let amount = determine_btc_to_swap(
let (amount, fees) = determine_btc_to_swap(
async { Ok(quote_with_max(0.01)) },
async { Ok(Amount::ZERO) },
Amount::ZERO,
get_dummy_address(),
async { Ok(Amount::from_btc(0.1)?) },
async { Ok(Amount::from_btc(0.09)?) },
|| async { Ok(Amount::from_btc(0.1001)?) },
|| async { Ok(Amount::from_btc(0.1)?) },
|| async { Ok(()) },
)
.await
.unwrap();
assert_eq!(amount, Amount::from_btc(0.01).unwrap())
let expected_amount = Amount::from_btc(0.01).unwrap();
let expected_fees = Amount::from_btc(0.0001).unwrap();
assert_eq!((amount, fees), (expected_amount, expected_fees))
}
#[tokio::test]
async fn given_initial_balance_below_max_quantity_swaps_max_givable() {
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let amount = determine_btc_to_swap(
let (amount, fees) = determine_btc_to_swap(
async { Ok(quote_with_max(0.01)) },
async { Ok(Amount::from_btc(0.005)?) },
Amount::from_btc(0.0049).unwrap(),
async { panic!("should not request new address when initial balance is > 0") },
async { panic!("should not wait for deposit when initial balance > 0") },
async { Ok(Amount::from_btc(0.0049)?) },
|| async { Ok(Amount::from_btc(0.005)?) },
|| async { panic!("should not wait for deposit when initial balance > 0") },
|| async { Ok(()) },
)
.await
.unwrap();
assert_eq!(amount, Amount::from_btc(0.0049).unwrap())
let expected_amount = Amount::from_btc(0.0049).unwrap();
let expected_fees = Amount::from_btc(0.0001).unwrap();
assert_eq!((amount, fees), (expected_amount, expected_fees))
}
#[tokio::test]
async fn given_initial_balance_above_max_quantity_swaps_max_quantity() {
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let amount = determine_btc_to_swap(
let (amount, fees) = determine_btc_to_swap(
async { Ok(quote_with_max(0.01)) },
async { Ok(Amount::from_btc(0.1)?) },
Amount::from_btc(0.1).unwrap(),
async { panic!("should not request new address when initial balance is > 0") },
async { panic!("should not wait for deposit when initial balance > 0") },
async { Ok(Amount::from_btc(0.09)?) },
|| async { Ok(Amount::from_btc(0.1001)?) },
|| async { panic!("should not wait for deposit when initial balance > 0") },
|| async { Ok(()) },
)
.await
.unwrap();
assert_eq!(amount, Amount::from_btc(0.01).unwrap())
let expected_amount = Amount::from_btc(0.01).unwrap();
let expected_fees = Amount::from_btc(0.0001).unwrap();
assert_eq!((amount, fees), (expected_amount, expected_fees))
}
#[tokio::test]
async fn given_no_initial_balance_then_min_wait_for_sufficient_deposit() {
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let (amount, fees) = determine_btc_to_swap(
async { Ok(quote_with_min(0.01)) },
Amount::ZERO,
get_dummy_address(),
|| async { Ok(Amount::from_btc(0.0101)?) },
|| async { Ok(Amount::from_btc(0.01)?) },
|| async { Ok(()) },
)
.await
.unwrap();
let expected_amount = Amount::from_btc(0.01).unwrap();
let expected_fees = Amount::from_btc(0.0001).unwrap();
assert_eq!((amount, fees), (expected_amount, expected_fees))
}
#[tokio::test]
async fn given_balance_less_then_min_wait_for_sufficient_deposit() {
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let (amount, fees) = determine_btc_to_swap(
async { Ok(quote_with_min(0.01)) },
Amount::from_btc(0.0001).unwrap(),
get_dummy_address(),
|| async { Ok(Amount::from_btc(0.0101)?) },
|| async { Ok(Amount::from_btc(0.01)?) },
|| async { Ok(()) },
)
.await
.unwrap();
let expected_amount = Amount::from_btc(0.01).unwrap();
let expected_fees = Amount::from_btc(0.0001).unwrap();
assert_eq!((amount, fees), (expected_amount, expected_fees))
}
#[tokio::test]
async fn given_no_initial_balance_and_transfers_less_than_min_keep_waiting() {
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
let error = tokio::time::timeout(
Duration::from_secs(1),
determine_btc_to_swap(
async { Ok(quote_with_min(0.1)) },
Amount::ZERO,
get_dummy_address(),
|| async { Ok(Amount::from_btc(0.0101)?) },
|| async { Ok(Amount::from_btc(0.01)?) },
|| async { Ok(()) },
),
)
.await
.unwrap_err();
assert!(matches!(error, tokio::time::error::Elapsed { .. }))
}
fn quote_with_max(btc: f64) -> BidQuote {
BidQuote {
price: Amount::from_btc(0.001).unwrap(),
max_quantity: Amount::from_btc(btc).unwrap(),
min_quantity: Amount::ZERO,
}
}
fn quote_with_min(btc: f64) -> BidQuote {
BidQuote {
price: Amount::from_btc(0.001).unwrap(),
max_quantity: Amount::max_value(),
min_quantity: Amount::from_btc(btc).unwrap(),
}
}

View File

@ -30,6 +30,9 @@ pub struct BidQuote {
/// The price at which the maker is willing to buy at.
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
pub price: bitcoin::Amount,
/// The minimum quantity the maker is willing to buy.
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
pub min_quantity: bitcoin::Amount,
/// The maximum quantity the maker is willing to buy.
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
pub max_quantity: bitcoin::Amount,

View File

@ -43,7 +43,13 @@ pub enum Response {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Error {
NoSwapsAccepted,
MaxBuyAmountExceeded {
AmountBelowMinimum {
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
min: bitcoin::Amount,
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
buy: bitcoin::Amount,
},
AmountAboveMaximum {
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
max: bitcoin::Amount,
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
@ -74,8 +80,16 @@ mod tests {
let serialized = serde_json::to_string(&Response::Error(Error::NoSwapsAccepted)).unwrap();
assert_eq!(error, serialized);
let error = r#"{"Error":{"MaxBuyAmountExceeded":{"max":0,"buy":0}}}"#.to_string();
let serialized = serde_json::to_string(&Response::Error(Error::MaxBuyAmountExceeded {
let error = r#"{"Error":{"AmountBelowMinimum":{"min":0,"buy":0}}}"#.to_string();
let serialized = serde_json::to_string(&Response::Error(Error::AmountBelowMinimum {
min: Default::default(),
buy: Default::default(),
}))
.unwrap();
assert_eq!(error, serialized);
let error = r#"{"Error":{"AmountAboveMaximum":{"max":0,"buy":0}}}"#.to_string();
let serialized = serde_json::to_string(&Response::Error(Error::AmountAboveMaximum {
max: Default::default(),
buy: Default::default(),
}))

View File

@ -12,6 +12,7 @@ pub fn alice<LR>(
seed: &Seed,
balance: monero::Amount,
lock_fee: monero::Amount,
min_buy: bitcoin::Amount,
max_buy: bitcoin::Amount,
latest_rate: LR,
resume_only: bool,
@ -21,7 +22,14 @@ where
{
with_clear_net(
seed,
alice::Behaviour::new(balance, lock_fee, max_buy, latest_rate, resume_only),
alice::Behaviour::new(
balance,
lock_fee,
min_buy,
max_buy,
latest_rate,
resume_only,
),
)
}

View File

@ -84,6 +84,7 @@ where
pub fn new(
balance: monero::Amount,
lock_fee: monero::Amount,
min_buy: bitcoin::Amount,
max_buy: bitcoin::Amount,
latest_rate: LR,
resume_only: bool,
@ -93,6 +94,7 @@ where
spot_price: spot_price::Behaviour::new(
balance,
lock_fee,
min_buy,
max_buy,
latest_rate,
resume_only,

View File

@ -43,6 +43,7 @@ where
monero_wallet: Arc<monero::Wallet>,
db: Arc<Database>,
latest_rate: LR,
min_buy: bitcoin::Amount,
max_buy: bitcoin::Amount,
swap_sender: mpsc::Sender<Swap>,
@ -74,6 +75,7 @@ where
monero_wallet: Arc<monero::Wallet>,
db: Arc<Database>,
latest_rate: LR,
min_buy: bitcoin::Amount,
max_buy: bitcoin::Amount,
) -> Result<(Self, mpsc::Receiver<Swap>)> {
let swap_channel = MpscChannels::default();
@ -86,6 +88,7 @@ where
db,
latest_rate,
swap_sender: swap_channel.sender,
min_buy,
max_buy,
recv_encrypted_signature: Default::default(),
inflight_encrypted_signatures: Default::default(),
@ -206,7 +209,9 @@ where
}
SwarmEvent::Behaviour(OutEvent::SwapRequestDeclined { peer, error }) => {
match error {
Error::ResumeOnlyMode | Error::MaxBuyAmountExceeded { .. } => {
Error::ResumeOnlyMode
| Error::AmountBelowMinimum { .. }
| Error::AmountAboveMaximum { .. } => {
tracing::warn!(%peer, "Ignoring spot price request because: {}", error);
}
Error::BalanceTooLow { .. }
@ -228,7 +233,7 @@ where
}
}
let quote = match self.make_quote(self.max_buy).await {
let quote = match self.make_quote(self.min_buy, self.max_buy).await {
Ok(quote) => quote,
Err(e) => {
tracing::warn!(%peer, "Failed to make quote: {:#}", e);
@ -355,7 +360,11 @@ where
}
}
async fn make_quote(&mut self, max_buy: bitcoin::Amount) -> Result<BidQuote> {
async fn make_quote(
&mut self,
min_buy: bitcoin::Amount,
max_buy: bitcoin::Amount,
) -> Result<BidQuote> {
let rate = self
.latest_rate
.latest_rate()
@ -363,6 +372,7 @@ where
Ok(BidQuote {
price: rate.ask().context("Failed to compute asking price")?,
min_quantity: min_buy,
max_quantity: max_buy,
})
}

View File

@ -44,6 +44,8 @@ where
#[behaviour(ignore)]
lock_fee: monero::Amount,
#[behaviour(ignore)]
min_buy: bitcoin::Amount,
#[behaviour(ignore)]
max_buy: bitcoin::Amount,
#[behaviour(ignore)]
latest_rate: LR,
@ -62,6 +64,7 @@ where
pub fn new(
balance: monero::Amount,
lock_fee: monero::Amount,
min_buy: bitcoin::Amount,
max_buy: bitcoin::Amount,
latest_rate: LR,
resume_only: bool,
@ -75,6 +78,7 @@ where
events: Default::default(),
balance,
lock_fee,
min_buy,
max_buy,
latest_rate,
resume_only,
@ -156,8 +160,17 @@ where
}
let btc = request.btc;
if btc < self.min_buy {
self.decline(peer, channel, Error::AmountBelowMinimum {
min: self.min_buy,
buy: btc,
});
return;
}
if btc > self.max_buy {
self.decline(peer, channel, Error::MaxBuyAmountExceeded {
self.decline(peer, channel, Error::AmountAboveMaximum {
max: self.max_buy,
buy: btc,
});
@ -183,7 +196,10 @@ where
let xmr_lock_fees = self.lock_fee;
if xmr_balance < xmr + xmr_lock_fees {
self.decline(peer, channel, Error::BalanceTooLow { buy: btc });
self.decline(peer, channel, Error::BalanceTooLow {
balance: xmr_balance,
buy: btc,
});
return;
}
@ -215,13 +231,21 @@ impl From<OutEvent> for alice::OutEvent {
pub enum Error {
#[error("ASB is running in resume-only mode")]
ResumeOnlyMode,
#[error("Maximum buy {max} exceeded {buy}")]
MaxBuyAmountExceeded {
#[error("Amount {buy} below minimum {min}")]
AmountBelowMinimum {
min: bitcoin::Amount,
buy: bitcoin::Amount,
},
#[error("Amount {buy} above maximum {max}")]
AmountAboveMaximum {
max: bitcoin::Amount,
buy: bitcoin::Amount,
},
#[error("This seller's XMR balance is currently too low to fulfill the swap request to buy {buy}, please try again later")]
BalanceTooLow { buy: bitcoin::Amount },
#[error("Balance {balance} too low to fulfill swapping {buy}")]
BalanceTooLow {
balance: monero::Amount,
buy: bitcoin::Amount,
},
#[error("Failed to fetch latest rate")]
LatestRateFetchFailed(#[source] Box<dyn std::error::Error + Send + 'static>),
@ -234,11 +258,15 @@ impl Error {
pub fn to_error_response(&self) -> spot_price::Error {
match self {
Error::ResumeOnlyMode => spot_price::Error::NoSwapsAccepted,
Error::MaxBuyAmountExceeded { max, buy } => spot_price::Error::MaxBuyAmountExceeded {
Error::AmountBelowMinimum { min, buy } => spot_price::Error::AmountBelowMinimum {
min: *min,
buy: *buy,
},
Error::AmountAboveMaximum { max, buy } => spot_price::Error::AmountAboveMaximum {
max: *max,
buy: *buy,
},
Error::BalanceTooLow { buy } => spot_price::Error::BalanceTooLow { buy: *buy },
Error::BalanceTooLow { buy, .. } => spot_price::Error::BalanceTooLow { buy: *buy },
Error::LatestRateFetchFailed(_) | Error::SellQuoteCalculationFailed(_) => {
spot_price::Error::Other
}
@ -262,6 +290,7 @@ mod tests {
Self {
balance: monero::Amount::from_monero(1.0).unwrap(),
lock_fee: monero::Amount::ZERO,
min_buy: bitcoin::Amount::from_btc(0.001).unwrap(),
max_buy: bitcoin::Amount::from_btc(0.01).unwrap(),
rate: TestRate::default(), // 0.01
resume_only: false,
@ -296,7 +325,10 @@ mod tests {
test.send_request(request);
test.assert_error(
alice::spot_price::Error::BalanceTooLow { buy: btc_to_swap },
alice::spot_price::Error::BalanceTooLow {
balance: monero::Amount::ZERO,
buy: btc_to_swap,
},
bob::spot_price::Error::BalanceTooLow { buy: btc_to_swap },
)
.await;
@ -323,7 +355,10 @@ mod tests {
test.send_request(request);
test.assert_error(
alice::spot_price::Error::BalanceTooLow { buy: btc_to_swap },
alice::spot_price::Error::BalanceTooLow {
balance: monero::Amount::ZERO,
buy: btc_to_swap,
},
bob::spot_price::Error::BalanceTooLow { buy: btc_to_swap },
)
.await;
@ -331,8 +366,12 @@ mod tests {
#[tokio::test]
async fn given_alice_has_insufficient_balance_because_of_lock_fee_then_returns_error() {
let balance = monero::Amount::from_monero(1.0).unwrap();
let mut test = SpotPriceTest::setup(
AliceBehaviourValues::default().with_lock_fee(monero::Amount::from_piconero(1)),
AliceBehaviourValues::default()
.with_balance(balance)
.with_lock_fee(monero::Amount::from_piconero(1)),
)
.await;
@ -342,14 +381,42 @@ mod tests {
test.send_request(request);
test.assert_error(
alice::spot_price::Error::BalanceTooLow { buy: btc_to_swap },
alice::spot_price::Error::BalanceTooLow {
balance,
buy: btc_to_swap,
},
bob::spot_price::Error::BalanceTooLow { buy: btc_to_swap },
)
.await;
}
#[tokio::test]
async fn given_max_buy_exceeded_then_returns_error() {
async fn given_below_min_buy_then_returns_error() {
let min_buy = bitcoin::Amount::from_btc(0.001).unwrap();
let mut test =
SpotPriceTest::setup(AliceBehaviourValues::default().with_min_buy(min_buy)).await;
let btc_to_swap = bitcoin::Amount::from_btc(0.0001).unwrap();
let request = spot_price::Request { btc: btc_to_swap };
test.send_request(request);
test.assert_error(
alice::spot_price::Error::AmountBelowMinimum {
buy: btc_to_swap,
min: min_buy,
},
bob::spot_price::Error::AmountBelowMinimum {
buy: btc_to_swap,
min: min_buy,
},
)
.await;
}
#[tokio::test]
async fn given_above_max_buy_then_returns_error() {
let max_buy = bitcoin::Amount::from_btc(0.001).unwrap();
let mut test =
@ -361,11 +428,11 @@ mod tests {
test.send_request(request);
test.assert_error(
alice::spot_price::Error::MaxBuyAmountExceeded {
alice::spot_price::Error::AmountAboveMaximum {
buy: btc_to_swap,
max: max_buy,
},
bob::spot_price::Error::MaxBuyAmountExceeded {
bob::spot_price::Error::AmountAboveMaximum {
buy: btc_to_swap,
max: max_buy,
},
@ -442,6 +509,7 @@ mod tests {
Behaviour::new(
values.balance,
values.lock_fee,
values.min_buy,
values.max_buy,
values.rate.clone(),
values.resume_only,
@ -508,12 +576,25 @@ mod tests {
// TODO: Somehow make PartialEq work on Alice's spot_price::Error
match (alice_assert, error) {
(
alice::spot_price::Error::BalanceTooLow { .. },
alice::spot_price::Error::BalanceTooLow { .. },
alice::spot_price::Error::BalanceTooLow {
balance: balance1,
buy: buy1,
},
alice::spot_price::Error::BalanceTooLow {
balance: balance2,
buy: buy2,
},
) => {
assert_eq!(balance1, balance2);
assert_eq!(buy1, buy2);
}
(
alice::spot_price::Error::AmountBelowMinimum { .. },
alice::spot_price::Error::AmountBelowMinimum { .. },
)
| (
alice::spot_price::Error::MaxBuyAmountExceeded { .. },
alice::spot_price::Error::MaxBuyAmountExceeded { .. },
alice::spot_price::Error::AmountAboveMaximum { .. },
alice::spot_price::Error::AmountAboveMaximum { .. },
)
| (
alice::spot_price::Error::LatestRateFetchFailed(_),
@ -555,6 +636,7 @@ mod tests {
struct AliceBehaviourValues {
pub balance: monero::Amount,
pub lock_fee: monero::Amount,
pub min_buy: bitcoin::Amount,
pub max_buy: bitcoin::Amount,
pub rate: TestRate, // 0.01
pub resume_only: bool,
@ -571,6 +653,11 @@ mod tests {
self
}
pub fn with_min_buy(mut self, min_buy: bitcoin::Amount) -> AliceBehaviourValues {
self.min_buy = min_buy;
self
}
pub fn with_max_buy(mut self, max_buy: bitcoin::Amount) -> AliceBehaviourValues {
self.max_buy = max_buy;
self

View File

@ -41,8 +41,13 @@ crate::impl_from_rr_event!(SpotPriceOutEvent, OutEvent, PROTOCOL);
pub enum Error {
#[error("Seller currently does not accept incoming swap requests, please try again later")]
NoSwapsAccepted,
#[error("Seller refused to buy {buy} because the minimum configured buy limit is {min}")]
AmountBelowMinimum {
min: bitcoin::Amount,
buy: bitcoin::Amount,
},
#[error("Seller refused to buy {buy} because the maximum configured buy limit is {max}")]
MaxBuyAmountExceeded {
AmountAboveMaximum {
max: bitcoin::Amount,
buy: bitcoin::Amount,
},
@ -59,8 +64,11 @@ impl From<spot_price::Error> for Error {
fn from(error: spot_price::Error) -> Self {
match error {
spot_price::Error::NoSwapsAccepted => Error::NoSwapsAccepted,
spot_price::Error::MaxBuyAmountExceeded { max, buy } => {
Error::MaxBuyAmountExceeded { max, buy }
spot_price::Error::AmountBelowMinimum { min, buy } => {
Error::AmountBelowMinimum { min, buy }
}
spot_price::Error::AmountAboveMaximum { max, buy } => {
Error::AmountAboveMaximum { max, buy }
}
spot_price::Error::BalanceTooLow { buy } => Error::BalanceTooLow { buy },
spot_price::Error::Other => Error::Other,

View File

@ -226,6 +226,7 @@ async fn start_alice(
let current_balance = monero_wallet.get_balance().await.unwrap();
let lock_fee = monero_wallet.static_tx_fee_estimate();
let min_buy = bitcoin::Amount::from_sat(u64::MIN);
let max_buy = bitcoin::Amount::from_sat(u64::MAX);
let latest_rate = FixedRate::default();
let resume_only = false;
@ -234,6 +235,7 @@ async fn start_alice(
&seed,
current_balance,
lock_fee,
min_buy,
max_buy,
latest_rate,
resume_only,
@ -248,7 +250,8 @@ async fn start_alice(
monero_wallet,
db,
FixedRate::default(),
bitcoin::Amount::ONE_BTC,
min_buy,
max_buy,
)
.unwrap();