mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2024-10-01 01:45:40 -04:00
Merge #481
481: Min buy amount r=da-kami a=da-kami Co-authored-by: Daniel Karzel <daniel@comit.network>
This commit is contained in:
commit
227c383d76
@ -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`.
|
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.
|
- 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.
|
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
|
### 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
|
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
|
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
|
## [0.5.0] - 2021-04-17
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -40,7 +40,7 @@ proptest = "1"
|
|||||||
rand = "0.7"
|
rand = "0.7"
|
||||||
rand_chacha = "0.2"
|
rand_chacha = "0.2"
|
||||||
reqwest = { version = "0.11", features = [ "rustls-tls", "stream", "socks" ], default-features = false }
|
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"
|
rust_decimal_macros = "1"
|
||||||
serde = { version = "1", features = [ "derive" ] }
|
serde = { version = "1", features = [ "derive" ] }
|
||||||
serde_cbor = "0.11"
|
serde_cbor = "0.11"
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
use crate::bitcoin::Amount;
|
use crate::bitcoin::Amount;
|
||||||
use bitcoin::util::amount::ParseAmountError;
|
use bitcoin::Address;
|
||||||
use bitcoin::{Address, Denomination};
|
|
||||||
use rust_decimal::Decimal;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@ -28,15 +26,6 @@ pub struct Arguments {
|
|||||||
pub enum Command {
|
pub enum Command {
|
||||||
#[structopt(about = "Main command to run the ASB.")]
|
#[structopt(about = "Main command to run the ASB.")]
|
||||||
Start {
|
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(
|
#[structopt(
|
||||||
long = "resume-only",
|
long = "resume-only",
|
||||||
help = "For maintenance only. When set, no new swap requests will be accepted, but existing unfinished swaps will be resumed."
|
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,
|
pub force: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_btc(s: &str) -> Result<Amount, ParseAmountError> {
|
|
||||||
Amount::from_str_in(s, Denomination::Bitcoin)
|
|
||||||
}
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
use crate::fs::{ensure_directory_exists, system_config_dir, system_data_dir};
|
use crate::fs::{ensure_directory_exists, system_config_dir, system_data_dir};
|
||||||
use crate::tor::{DEFAULT_CONTROL_PORT, DEFAULT_SOCKS5_PORT};
|
use crate::tor::{DEFAULT_CONTROL_PORT, DEFAULT_SOCKS5_PORT};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use config::ConfigError;
|
use config::ConfigError;
|
||||||
use dialoguer::theme::ColorfulTheme;
|
use dialoguer::theme::ColorfulTheme;
|
||||||
use dialoguer::Input;
|
use dialoguer::Input;
|
||||||
use libp2p::core::Multiaddr;
|
use libp2p::core::Multiaddr;
|
||||||
|
use rust_decimal::prelude::FromPrimitive;
|
||||||
|
use rust_decimal::Decimal;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fs;
|
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_MONERO_WALLET_RPC_TESTNET_URL: &str = "http://127.0.0.1:38083/json_rpc";
|
||||||
const DEFAULT_BITCOIN_CONFIRMATION_TARGET: usize = 3;
|
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)]
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub data: Data,
|
pub data: Data,
|
||||||
@ -25,6 +31,7 @@ pub struct Config {
|
|||||||
pub bitcoin: Bitcoin,
|
pub bitcoin: Bitcoin,
|
||||||
pub monero: Monero,
|
pub monero: Monero,
|
||||||
pub tor: TorConf,
|
pub tor: TorConf,
|
||||||
|
pub maker: Maker,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@ -72,6 +79,16 @@ pub struct TorConf {
|
|||||||
pub socks5_port: u16,
|
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 {
|
impl Default for TorConf {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -123,10 +140,11 @@ where
|
|||||||
F: Fn() -> Result<Config>,
|
F: Fn() -> Result<Config>,
|
||||||
{
|
{
|
||||||
info!("Config file not found, running initial setup...");
|
info!("Config file not found, running initial setup...");
|
||||||
ensure_directory_exists(config_path.as_path())?;
|
|
||||||
let initial_config = config_file()?;
|
let initial_config = config_file()?;
|
||||||
|
|
||||||
let toml = toml::to_string(&initial_config)?;
|
let toml = toml::to_string(&initial_config)?;
|
||||||
|
|
||||||
|
ensure_directory_exists(config_path.as_path())?;
|
||||||
fs::write(&config_path, toml)?;
|
fs::write(&config_path, toml)?;
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
@ -185,6 +203,27 @@ pub fn query_user_for_initial_testnet_config() -> Result<Config> {
|
|||||||
.default(DEFAULT_SOCKS5_PORT.to_owned())
|
.default(DEFAULT_SOCKS5_PORT.to_owned())
|
||||||
.interact_text()?;
|
.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!();
|
println!();
|
||||||
|
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
@ -203,6 +242,11 @@ pub fn query_user_for_initial_testnet_config() -> Result<Config> {
|
|||||||
control_port: tor_control_port,
|
control_port: tor_control_port,
|
||||||
socks5_port: tor_socks5_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(),
|
wallet_rpc_url: Url::from_str(DEFAULT_MONERO_WALLET_RPC_TESTNET_URL).unwrap(),
|
||||||
},
|
},
|
||||||
tor: Default::default(),
|
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();
|
initial_setup(config_path.clone(), || Ok(expected.clone())).unwrap();
|
||||||
|
@ -79,11 +79,7 @@ async fn main() -> Result<()> {
|
|||||||
let env_config = env::Testnet::get_config();
|
let env_config = env::Testnet::get_config();
|
||||||
|
|
||||||
match opt.cmd {
|
match opt.cmd {
|
||||||
Command::Start {
|
Command::Start { resume_only } => {
|
||||||
max_buy,
|
|
||||||
ask_spread,
|
|
||||||
resume_only,
|
|
||||||
} => {
|
|
||||||
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
|
||||||
let monero_wallet = init_monero_wallet(&config, 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 current_balance = monero_wallet.get_balance().await?;
|
||||||
let lock_fee = monero_wallet.static_tx_fee_estimate();
|
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(
|
let mut swarm = swarm::alice(
|
||||||
&seed,
|
&seed,
|
||||||
current_balance,
|
current_balance,
|
||||||
lock_fee,
|
lock_fee,
|
||||||
max_buy,
|
config.maker.min_buy_btc,
|
||||||
|
config.maker.max_buy_btc,
|
||||||
kraken_rate.clone(),
|
kraken_rate.clone(),
|
||||||
resume_only,
|
resume_only,
|
||||||
)?;
|
)?;
|
||||||
@ -144,7 +141,8 @@ async fn main() -> Result<()> {
|
|||||||
Arc::new(monero_wallet),
|
Arc::new(monero_wallet),
|
||||||
Arc::new(db),
|
Arc::new(db),
|
||||||
kraken_rate,
|
kraken_rate,
|
||||||
max_buy,
|
config.maker.min_buy_btc,
|
||||||
|
config.maker.max_buy_btc,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ use std::path::PathBuf;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
use swap::bitcoin::{Amount, TxLock};
|
use swap::bitcoin::TxLock;
|
||||||
use swap::cli::command::{Arguments, Command, MoneroParams};
|
use swap::cli::command::{Arguments, Command, MoneroParams};
|
||||||
use swap::database::Database;
|
use swap::database::Database;
|
||||||
use swap::env::{Config, GetConfig};
|
use swap::env::{Config, GetConfig};
|
||||||
@ -94,23 +94,19 @@ async fn main() -> Result<()> {
|
|||||||
EventLoop::new(swap_id, swarm, alice_peer_id, bitcoin_wallet.clone())?;
|
EventLoop::new(swap_id, swarm, alice_peer_id, bitcoin_wallet.clone())?;
|
||||||
let event_loop = tokio::spawn(event_loop.run());
|
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(),
|
event_loop_handle.request_quote(),
|
||||||
bitcoin_wallet.balance(),
|
max_givable().await?,
|
||||||
bitcoin_wallet.new_address(),
|
bitcoin_wallet.new_address(),
|
||||||
async {
|
|| bitcoin_wallet.balance(),
|
||||||
while bitcoin_wallet.balance().await? == Amount::ZERO {
|
max_givable,
|
||||||
bitcoin_wallet.sync().await?;
|
|| bitcoin_wallet.sync(),
|
||||||
|
|
||||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
bitcoin_wallet.balance().await
|
|
||||||
},
|
|
||||||
bitcoin_wallet.max_giveable(TxLock::script_size()),
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
info!("Swapping {} with {} fees", send_bitcoin, fees);
|
||||||
|
|
||||||
db.insert_peer_id(swap_id, alice_peer_id).await?;
|
db.insert_peer_id(swap_id, alice_peer_id).await?;
|
||||||
|
|
||||||
let swap = Swap::new(
|
let swap = Swap::new(
|
||||||
@ -332,51 +328,83 @@ async fn init_monero_wallet(
|
|||||||
Ok((monero_wallet, monero_wallet_rpc_process))
|
Ok((monero_wallet, monero_wallet_rpc_process))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn determine_btc_to_swap(
|
async fn determine_btc_to_swap<FB, TB, FMG, TMG, FS, TS>(
|
||||||
request_quote: impl Future<Output = Result<BidQuote>>,
|
bid_quote: impl Future<Output = Result<BidQuote>>,
|
||||||
initial_balance: impl Future<Output = Result<bitcoin::Amount>>,
|
mut current_maximum_giveable: bitcoin::Amount,
|
||||||
get_new_address: impl Future<Output = Result<bitcoin::Address>>,
|
get_new_address: impl Future<Output = Result<bitcoin::Address>>,
|
||||||
wait_for_deposit: impl Future<Output = Result<bitcoin::Amount>>,
|
balance: FB,
|
||||||
max_giveable: impl Future<Output = Result<bitcoin::Amount>>,
|
max_giveable: FMG,
|
||||||
) -> Result<bitcoin::Amount> {
|
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");
|
debug!("Requesting quote");
|
||||||
|
let bid_quote = bid_quote.await?;
|
||||||
let bid_quote = request_quote.await?;
|
|
||||||
|
|
||||||
info!("Received quote: 1 XMR ~ {}", bid_quote.price);
|
info!("Received quote: 1 XMR ~ {}", bid_quote.price);
|
||||||
|
|
||||||
// TODO: Also wait for more funds if balance < dust
|
let max_giveable = if current_maximum_giveable == bitcoin::Amount::ZERO
|
||||||
let initial_balance = initial_balance.await?;
|
|| 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!(
|
info!(
|
||||||
"Please deposit the BTC you want to swap to {} (max {})",
|
%deposit_address,
|
||||||
get_new_address.await?,
|
%current_maximum_giveable,
|
||||||
bid_quote.max_quantity
|
%minimum_amount,
|
||||||
|
%maximum_amount,
|
||||||
|
"Please deposit BTC you want to swap to",
|
||||||
);
|
);
|
||||||
|
|
||||||
let new_balance = wait_for_deposit
|
loop {
|
||||||
.await
|
sync().await?;
|
||||||
.context("Failed to wait for Bitcoin deposit")?;
|
|
||||||
|
|
||||||
info!("Received {}", new_balance);
|
let new_max_givable = max_giveable().await?;
|
||||||
new_balance
|
|
||||||
|
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 {
|
} else {
|
||||||
info!("Found {} in wallet", initial_balance);
|
current_maximum_giveable
|
||||||
initial_balance
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let max_giveable = max_giveable
|
let balance = balance().await?;
|
||||||
.await
|
|
||||||
.context("Failed to compute max 'giveable' Bitcoin amount")?;
|
|
||||||
let fees = balance - max_giveable;
|
let fees = balance - max_giveable;
|
||||||
|
|
||||||
let max_accepted = bid_quote.max_quantity;
|
let max_accepted = bid_quote.max_quantity;
|
||||||
|
|
||||||
let btc_swap_amount = min(max_giveable, max_accepted);
|
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)]
|
#[cfg(test)]
|
||||||
@ -390,74 +418,162 @@ mod tests {
|
|||||||
async fn given_no_balance_and_transfers_less_than_max_swaps_max_giveable() {
|
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 _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(quote_with_max(0.01)) },
|
||||||
async { Ok(Amount::ZERO) },
|
Amount::ZERO,
|
||||||
get_dummy_address(),
|
get_dummy_address(),
|
||||||
async { Ok(Amount::from_btc(0.0001)?) },
|
|| async { Ok(Amount::from_btc(0.001)?) },
|
||||||
async { Ok(Amount::from_btc(0.00009)?) },
|
|| async { Ok(Amount::from_btc(0.0009)?) },
|
||||||
|
|| async { Ok(()) },
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.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]
|
#[tokio::test]
|
||||||
async fn given_no_balance_and_transfers_more_then_swaps_max_quantity_from_quote() {
|
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 _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(quote_with_max(0.01)) },
|
||||||
async { Ok(Amount::ZERO) },
|
Amount::ZERO,
|
||||||
get_dummy_address(),
|
get_dummy_address(),
|
||||||
async { Ok(Amount::from_btc(0.1)?) },
|
|| async { Ok(Amount::from_btc(0.1001)?) },
|
||||||
async { Ok(Amount::from_btc(0.09)?) },
|
|| async { Ok(Amount::from_btc(0.1)?) },
|
||||||
|
|| async { Ok(()) },
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.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]
|
#[tokio::test]
|
||||||
async fn given_initial_balance_below_max_quantity_swaps_max_givable() {
|
async fn given_initial_balance_below_max_quantity_swaps_max_givable() {
|
||||||
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
|
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(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 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.005)?) },
|
||||||
async { Ok(Amount::from_btc(0.0049)?) },
|
|| async { panic!("should not wait for deposit when initial balance > 0") },
|
||||||
|
|| async { Ok(()) },
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.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]
|
#[tokio::test]
|
||||||
async fn given_initial_balance_above_max_quantity_swaps_max_quantity() {
|
async fn given_initial_balance_above_max_quantity_swaps_max_quantity() {
|
||||||
let _guard = subscriber::set_default(tracing_subscriber::fmt().with_test_writer().finish());
|
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(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 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.1001)?) },
|
||||||
async { Ok(Amount::from_btc(0.09)?) },
|
|| async { panic!("should not wait for deposit when initial balance > 0") },
|
||||||
|
|| async { Ok(()) },
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.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 {
|
fn quote_with_max(btc: f64) -> BidQuote {
|
||||||
BidQuote {
|
BidQuote {
|
||||||
price: Amount::from_btc(0.001).unwrap(),
|
price: Amount::from_btc(0.001).unwrap(),
|
||||||
max_quantity: Amount::from_btc(btc).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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +30,9 @@ pub struct BidQuote {
|
|||||||
/// The price at which the maker is willing to buy at.
|
/// The price at which the maker is willing to buy at.
|
||||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||||
pub price: bitcoin::Amount,
|
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.
|
/// The maximum quantity the maker is willing to buy.
|
||||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||||
pub max_quantity: bitcoin::Amount,
|
pub max_quantity: bitcoin::Amount,
|
||||||
|
@ -43,7 +43,13 @@ pub enum Response {
|
|||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
NoSwapsAccepted,
|
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")]
|
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
||||||
max: bitcoin::Amount,
|
max: bitcoin::Amount,
|
||||||
#[serde(with = "::bitcoin::util::amount::serde::as_sat")]
|
#[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();
|
let serialized = serde_json::to_string(&Response::Error(Error::NoSwapsAccepted)).unwrap();
|
||||||
assert_eq!(error, serialized);
|
assert_eq!(error, serialized);
|
||||||
|
|
||||||
let error = r#"{"Error":{"MaxBuyAmountExceeded":{"max":0,"buy":0}}}"#.to_string();
|
let error = r#"{"Error":{"AmountBelowMinimum":{"min":0,"buy":0}}}"#.to_string();
|
||||||
let serialized = serde_json::to_string(&Response::Error(Error::MaxBuyAmountExceeded {
|
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(),
|
max: Default::default(),
|
||||||
buy: Default::default(),
|
buy: Default::default(),
|
||||||
}))
|
}))
|
||||||
|
@ -12,6 +12,7 @@ pub fn alice<LR>(
|
|||||||
seed: &Seed,
|
seed: &Seed,
|
||||||
balance: monero::Amount,
|
balance: monero::Amount,
|
||||||
lock_fee: monero::Amount,
|
lock_fee: monero::Amount,
|
||||||
|
min_buy: bitcoin::Amount,
|
||||||
max_buy: bitcoin::Amount,
|
max_buy: bitcoin::Amount,
|
||||||
latest_rate: LR,
|
latest_rate: LR,
|
||||||
resume_only: bool,
|
resume_only: bool,
|
||||||
@ -21,7 +22,14 @@ where
|
|||||||
{
|
{
|
||||||
with_clear_net(
|
with_clear_net(
|
||||||
seed,
|
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,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,6 +84,7 @@ where
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
balance: monero::Amount,
|
balance: monero::Amount,
|
||||||
lock_fee: monero::Amount,
|
lock_fee: monero::Amount,
|
||||||
|
min_buy: bitcoin::Amount,
|
||||||
max_buy: bitcoin::Amount,
|
max_buy: bitcoin::Amount,
|
||||||
latest_rate: LR,
|
latest_rate: LR,
|
||||||
resume_only: bool,
|
resume_only: bool,
|
||||||
@ -93,6 +94,7 @@ where
|
|||||||
spot_price: spot_price::Behaviour::new(
|
spot_price: spot_price::Behaviour::new(
|
||||||
balance,
|
balance,
|
||||||
lock_fee,
|
lock_fee,
|
||||||
|
min_buy,
|
||||||
max_buy,
|
max_buy,
|
||||||
latest_rate,
|
latest_rate,
|
||||||
resume_only,
|
resume_only,
|
||||||
|
@ -43,6 +43,7 @@ where
|
|||||||
monero_wallet: Arc<monero::Wallet>,
|
monero_wallet: Arc<monero::Wallet>,
|
||||||
db: Arc<Database>,
|
db: Arc<Database>,
|
||||||
latest_rate: LR,
|
latest_rate: LR,
|
||||||
|
min_buy: bitcoin::Amount,
|
||||||
max_buy: bitcoin::Amount,
|
max_buy: bitcoin::Amount,
|
||||||
|
|
||||||
swap_sender: mpsc::Sender<Swap>,
|
swap_sender: mpsc::Sender<Swap>,
|
||||||
@ -74,6 +75,7 @@ where
|
|||||||
monero_wallet: Arc<monero::Wallet>,
|
monero_wallet: Arc<monero::Wallet>,
|
||||||
db: Arc<Database>,
|
db: Arc<Database>,
|
||||||
latest_rate: LR,
|
latest_rate: LR,
|
||||||
|
min_buy: bitcoin::Amount,
|
||||||
max_buy: bitcoin::Amount,
|
max_buy: bitcoin::Amount,
|
||||||
) -> Result<(Self, mpsc::Receiver<Swap>)> {
|
) -> Result<(Self, mpsc::Receiver<Swap>)> {
|
||||||
let swap_channel = MpscChannels::default();
|
let swap_channel = MpscChannels::default();
|
||||||
@ -86,6 +88,7 @@ where
|
|||||||
db,
|
db,
|
||||||
latest_rate,
|
latest_rate,
|
||||||
swap_sender: swap_channel.sender,
|
swap_sender: swap_channel.sender,
|
||||||
|
min_buy,
|
||||||
max_buy,
|
max_buy,
|
||||||
recv_encrypted_signature: Default::default(),
|
recv_encrypted_signature: Default::default(),
|
||||||
inflight_encrypted_signatures: Default::default(),
|
inflight_encrypted_signatures: Default::default(),
|
||||||
@ -206,7 +209,9 @@ where
|
|||||||
}
|
}
|
||||||
SwarmEvent::Behaviour(OutEvent::SwapRequestDeclined { peer, error }) => {
|
SwarmEvent::Behaviour(OutEvent::SwapRequestDeclined { peer, error }) => {
|
||||||
match error {
|
match error {
|
||||||
Error::ResumeOnlyMode | Error::MaxBuyAmountExceeded { .. } => {
|
Error::ResumeOnlyMode
|
||||||
|
| Error::AmountBelowMinimum { .. }
|
||||||
|
| Error::AmountAboveMaximum { .. } => {
|
||||||
tracing::warn!(%peer, "Ignoring spot price request because: {}", error);
|
tracing::warn!(%peer, "Ignoring spot price request because: {}", error);
|
||||||
}
|
}
|
||||||
Error::BalanceTooLow { .. }
|
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,
|
Ok(quote) => quote,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(%peer, "Failed to make quote: {:#}", 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
|
let rate = self
|
||||||
.latest_rate
|
.latest_rate
|
||||||
.latest_rate()
|
.latest_rate()
|
||||||
@ -363,6 +372,7 @@ where
|
|||||||
|
|
||||||
Ok(BidQuote {
|
Ok(BidQuote {
|
||||||
price: rate.ask().context("Failed to compute asking price")?,
|
price: rate.ask().context("Failed to compute asking price")?,
|
||||||
|
min_quantity: min_buy,
|
||||||
max_quantity: max_buy,
|
max_quantity: max_buy,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,8 @@ where
|
|||||||
#[behaviour(ignore)]
|
#[behaviour(ignore)]
|
||||||
lock_fee: monero::Amount,
|
lock_fee: monero::Amount,
|
||||||
#[behaviour(ignore)]
|
#[behaviour(ignore)]
|
||||||
|
min_buy: bitcoin::Amount,
|
||||||
|
#[behaviour(ignore)]
|
||||||
max_buy: bitcoin::Amount,
|
max_buy: bitcoin::Amount,
|
||||||
#[behaviour(ignore)]
|
#[behaviour(ignore)]
|
||||||
latest_rate: LR,
|
latest_rate: LR,
|
||||||
@ -62,6 +64,7 @@ where
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
balance: monero::Amount,
|
balance: monero::Amount,
|
||||||
lock_fee: monero::Amount,
|
lock_fee: monero::Amount,
|
||||||
|
min_buy: bitcoin::Amount,
|
||||||
max_buy: bitcoin::Amount,
|
max_buy: bitcoin::Amount,
|
||||||
latest_rate: LR,
|
latest_rate: LR,
|
||||||
resume_only: bool,
|
resume_only: bool,
|
||||||
@ -75,6 +78,7 @@ where
|
|||||||
events: Default::default(),
|
events: Default::default(),
|
||||||
balance,
|
balance,
|
||||||
lock_fee,
|
lock_fee,
|
||||||
|
min_buy,
|
||||||
max_buy,
|
max_buy,
|
||||||
latest_rate,
|
latest_rate,
|
||||||
resume_only,
|
resume_only,
|
||||||
@ -156,8 +160,17 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
let btc = request.btc;
|
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 {
|
if btc > self.max_buy {
|
||||||
self.decline(peer, channel, Error::MaxBuyAmountExceeded {
|
self.decline(peer, channel, Error::AmountAboveMaximum {
|
||||||
max: self.max_buy,
|
max: self.max_buy,
|
||||||
buy: btc,
|
buy: btc,
|
||||||
});
|
});
|
||||||
@ -183,7 +196,10 @@ where
|
|||||||
let xmr_lock_fees = self.lock_fee;
|
let xmr_lock_fees = self.lock_fee;
|
||||||
|
|
||||||
if xmr_balance < xmr + xmr_lock_fees {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,13 +231,21 @@ impl From<OutEvent> for alice::OutEvent {
|
|||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("ASB is running in resume-only mode")]
|
#[error("ASB is running in resume-only mode")]
|
||||||
ResumeOnlyMode,
|
ResumeOnlyMode,
|
||||||
#[error("Maximum buy {max} exceeded {buy}")]
|
#[error("Amount {buy} below minimum {min}")]
|
||||||
MaxBuyAmountExceeded {
|
AmountBelowMinimum {
|
||||||
|
min: bitcoin::Amount,
|
||||||
|
buy: bitcoin::Amount,
|
||||||
|
},
|
||||||
|
#[error("Amount {buy} above maximum {max}")]
|
||||||
|
AmountAboveMaximum {
|
||||||
max: bitcoin::Amount,
|
max: bitcoin::Amount,
|
||||||
buy: 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")]
|
#[error("Balance {balance} too low to fulfill swapping {buy}")]
|
||||||
BalanceTooLow { buy: bitcoin::Amount },
|
BalanceTooLow {
|
||||||
|
balance: monero::Amount,
|
||||||
|
buy: bitcoin::Amount,
|
||||||
|
},
|
||||||
|
|
||||||
#[error("Failed to fetch latest rate")]
|
#[error("Failed to fetch latest rate")]
|
||||||
LatestRateFetchFailed(#[source] Box<dyn std::error::Error + Send + 'static>),
|
LatestRateFetchFailed(#[source] Box<dyn std::error::Error + Send + 'static>),
|
||||||
@ -234,11 +258,15 @@ impl Error {
|
|||||||
pub fn to_error_response(&self) -> spot_price::Error {
|
pub fn to_error_response(&self) -> spot_price::Error {
|
||||||
match self {
|
match self {
|
||||||
Error::ResumeOnlyMode => spot_price::Error::NoSwapsAccepted,
|
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,
|
max: *max,
|
||||||
buy: *buy,
|
buy: *buy,
|
||||||
},
|
},
|
||||||
Error::BalanceTooLow { buy } => spot_price::Error::BalanceTooLow { buy: *buy },
|
Error::BalanceTooLow { buy, .. } => spot_price::Error::BalanceTooLow { buy: *buy },
|
||||||
Error::LatestRateFetchFailed(_) | Error::SellQuoteCalculationFailed(_) => {
|
Error::LatestRateFetchFailed(_) | Error::SellQuoteCalculationFailed(_) => {
|
||||||
spot_price::Error::Other
|
spot_price::Error::Other
|
||||||
}
|
}
|
||||||
@ -262,6 +290,7 @@ mod tests {
|
|||||||
Self {
|
Self {
|
||||||
balance: monero::Amount::from_monero(1.0).unwrap(),
|
balance: monero::Amount::from_monero(1.0).unwrap(),
|
||||||
lock_fee: monero::Amount::ZERO,
|
lock_fee: monero::Amount::ZERO,
|
||||||
|
min_buy: bitcoin::Amount::from_btc(0.001).unwrap(),
|
||||||
max_buy: bitcoin::Amount::from_btc(0.01).unwrap(),
|
max_buy: bitcoin::Amount::from_btc(0.01).unwrap(),
|
||||||
rate: TestRate::default(), // 0.01
|
rate: TestRate::default(), // 0.01
|
||||||
resume_only: false,
|
resume_only: false,
|
||||||
@ -296,7 +325,10 @@ mod tests {
|
|||||||
|
|
||||||
test.send_request(request);
|
test.send_request(request);
|
||||||
test.assert_error(
|
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 },
|
bob::spot_price::Error::BalanceTooLow { buy: btc_to_swap },
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@ -323,7 +355,10 @@ mod tests {
|
|||||||
|
|
||||||
test.send_request(request);
|
test.send_request(request);
|
||||||
test.assert_error(
|
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 },
|
bob::spot_price::Error::BalanceTooLow { buy: btc_to_swap },
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@ -331,8 +366,12 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn given_alice_has_insufficient_balance_because_of_lock_fee_then_returns_error() {
|
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(
|
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;
|
.await;
|
||||||
|
|
||||||
@ -342,14 +381,42 @@ mod tests {
|
|||||||
|
|
||||||
test.send_request(request);
|
test.send_request(request);
|
||||||
test.assert_error(
|
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 },
|
bob::spot_price::Error::BalanceTooLow { buy: btc_to_swap },
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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 max_buy = bitcoin::Amount::from_btc(0.001).unwrap();
|
||||||
|
|
||||||
let mut test =
|
let mut test =
|
||||||
@ -361,11 +428,11 @@ mod tests {
|
|||||||
|
|
||||||
test.send_request(request);
|
test.send_request(request);
|
||||||
test.assert_error(
|
test.assert_error(
|
||||||
alice::spot_price::Error::MaxBuyAmountExceeded {
|
alice::spot_price::Error::AmountAboveMaximum {
|
||||||
buy: btc_to_swap,
|
buy: btc_to_swap,
|
||||||
max: max_buy,
|
max: max_buy,
|
||||||
},
|
},
|
||||||
bob::spot_price::Error::MaxBuyAmountExceeded {
|
bob::spot_price::Error::AmountAboveMaximum {
|
||||||
buy: btc_to_swap,
|
buy: btc_to_swap,
|
||||||
max: max_buy,
|
max: max_buy,
|
||||||
},
|
},
|
||||||
@ -442,6 +509,7 @@ mod tests {
|
|||||||
Behaviour::new(
|
Behaviour::new(
|
||||||
values.balance,
|
values.balance,
|
||||||
values.lock_fee,
|
values.lock_fee,
|
||||||
|
values.min_buy,
|
||||||
values.max_buy,
|
values.max_buy,
|
||||||
values.rate.clone(),
|
values.rate.clone(),
|
||||||
values.resume_only,
|
values.resume_only,
|
||||||
@ -508,12 +576,25 @@ mod tests {
|
|||||||
// TODO: Somehow make PartialEq work on Alice's spot_price::Error
|
// TODO: Somehow make PartialEq work on Alice's spot_price::Error
|
||||||
match (alice_assert, 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::AmountAboveMaximum { .. },
|
||||||
alice::spot_price::Error::MaxBuyAmountExceeded { .. },
|
alice::spot_price::Error::AmountAboveMaximum { .. },
|
||||||
)
|
)
|
||||||
| (
|
| (
|
||||||
alice::spot_price::Error::LatestRateFetchFailed(_),
|
alice::spot_price::Error::LatestRateFetchFailed(_),
|
||||||
@ -555,6 +636,7 @@ mod tests {
|
|||||||
struct AliceBehaviourValues {
|
struct AliceBehaviourValues {
|
||||||
pub balance: monero::Amount,
|
pub balance: monero::Amount,
|
||||||
pub lock_fee: monero::Amount,
|
pub lock_fee: monero::Amount,
|
||||||
|
pub min_buy: bitcoin::Amount,
|
||||||
pub max_buy: bitcoin::Amount,
|
pub max_buy: bitcoin::Amount,
|
||||||
pub rate: TestRate, // 0.01
|
pub rate: TestRate, // 0.01
|
||||||
pub resume_only: bool,
|
pub resume_only: bool,
|
||||||
@ -571,6 +653,11 @@ mod tests {
|
|||||||
self
|
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 {
|
pub fn with_max_buy(mut self, max_buy: bitcoin::Amount) -> AliceBehaviourValues {
|
||||||
self.max_buy = max_buy;
|
self.max_buy = max_buy;
|
||||||
self
|
self
|
||||||
|
@ -41,8 +41,13 @@ crate::impl_from_rr_event!(SpotPriceOutEvent, OutEvent, PROTOCOL);
|
|||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Seller currently does not accept incoming swap requests, please try again later")]
|
#[error("Seller currently does not accept incoming swap requests, please try again later")]
|
||||||
NoSwapsAccepted,
|
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}")]
|
#[error("Seller refused to buy {buy} because the maximum configured buy limit is {max}")]
|
||||||
MaxBuyAmountExceeded {
|
AmountAboveMaximum {
|
||||||
max: bitcoin::Amount,
|
max: bitcoin::Amount,
|
||||||
buy: bitcoin::Amount,
|
buy: bitcoin::Amount,
|
||||||
},
|
},
|
||||||
@ -59,8 +64,11 @@ impl From<spot_price::Error> for Error {
|
|||||||
fn from(error: spot_price::Error) -> Self {
|
fn from(error: spot_price::Error) -> Self {
|
||||||
match error {
|
match error {
|
||||||
spot_price::Error::NoSwapsAccepted => Error::NoSwapsAccepted,
|
spot_price::Error::NoSwapsAccepted => Error::NoSwapsAccepted,
|
||||||
spot_price::Error::MaxBuyAmountExceeded { max, buy } => {
|
spot_price::Error::AmountBelowMinimum { min, buy } => {
|
||||||
Error::MaxBuyAmountExceeded { max, 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::BalanceTooLow { buy } => Error::BalanceTooLow { buy },
|
||||||
spot_price::Error::Other => Error::Other,
|
spot_price::Error::Other => Error::Other,
|
||||||
|
@ -226,6 +226,7 @@ async fn start_alice(
|
|||||||
|
|
||||||
let current_balance = monero_wallet.get_balance().await.unwrap();
|
let current_balance = monero_wallet.get_balance().await.unwrap();
|
||||||
let lock_fee = monero_wallet.static_tx_fee_estimate();
|
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 max_buy = bitcoin::Amount::from_sat(u64::MAX);
|
||||||
let latest_rate = FixedRate::default();
|
let latest_rate = FixedRate::default();
|
||||||
let resume_only = false;
|
let resume_only = false;
|
||||||
@ -234,6 +235,7 @@ async fn start_alice(
|
|||||||
&seed,
|
&seed,
|
||||||
current_balance,
|
current_balance,
|
||||||
lock_fee,
|
lock_fee,
|
||||||
|
min_buy,
|
||||||
max_buy,
|
max_buy,
|
||||||
latest_rate,
|
latest_rate,
|
||||||
resume_only,
|
resume_only,
|
||||||
@ -248,7 +250,8 @@ async fn start_alice(
|
|||||||
monero_wallet,
|
monero_wallet,
|
||||||
db,
|
db,
|
||||||
FixedRate::default(),
|
FixedRate::default(),
|
||||||
bitcoin::Amount::ONE_BTC,
|
min_buy,
|
||||||
|
max_buy,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user