Introduce a minimum buy amount

Introduces a minimum buy Bitcoin amount 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.
This commit is contained in:
Daniel Karzel 2021-05-07 19:06:58 +10:00
parent 6d3cf0af91
commit 652aae9590
No known key found for this signature in database
GPG Key ID: 30C3FC2E438ADB6E
12 changed files with 319 additions and 81 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
@ -43,7 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- The ASB's `--max-buy` and `ask-spread` parameter were removed in favour of entries in the config file.
- 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

View File

@ -20,6 +20,7 @@ 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;
@ -81,6 +82,8 @@ pub struct TorConf {
#[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,
@ -200,6 +203,12 @@ 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)
@ -234,6 +243,7 @@ pub fn query_user_for_initial_testnet_config() -> Result<Config> {
socks5_port: tor_socks5_port,
},
maker: Maker {
min_buy_btc: min_buy,
max_buy_btc: max_buy,
ask_spread,
},
@ -271,6 +281,7 @@ mod tests {
},
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(),
},

View File

@ -123,6 +123,7 @@ async fn main() -> Result<()> {
&seed,
current_balance,
lock_fee,
config.maker.min_buy_btc,
config.maker.max_buy_btc,
kraken_rate.clone(),
resume_only,
@ -140,6 +141,7 @@ async fn main() -> Result<()> {
Arc::new(monero_wallet),
Arc::new(db),
kraken_rate,
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,
});
@ -218,8 +231,13 @@ 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,
},
@ -240,7 +258,11 @@ 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,
},
@ -268,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,
@ -368,7 +391,32 @@ mod tests {
}
#[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 =
@ -380,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,
},
@ -461,6 +509,7 @@ mod tests {
Behaviour::new(
values.balance,
values.lock_fee,
values.min_buy,
values.max_buy,
values.rate.clone(),
values.resume_only,
@ -540,8 +589,12 @@ mod tests {
assert_eq!(buy1, buy2);
}
(
alice::spot_price::Error::MaxBuyAmountExceeded { .. },
alice::spot_price::Error::MaxBuyAmountExceeded { .. },
alice::spot_price::Error::AmountBelowMinimum { .. },
alice::spot_price::Error::AmountBelowMinimum { .. },
)
| (
alice::spot_price::Error::AmountAboveMaximum { .. },
alice::spot_price::Error::AmountAboveMaximum { .. },
)
| (
alice::spot_price::Error::LatestRateFetchFailed(_),
@ -583,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,
@ -599,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();