mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-06-20 04:44:10 -04:00
parent
3e0301a9d4
commit
a99d12b9df
6 changed files with 109 additions and 20 deletions
|
@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- A changelog file.
|
- A changelog file.
|
||||||
- Automatic resume of unfinished swaps for the `asb` upon startup.
|
- Automatic resume of unfinished swaps for the `asb` upon startup.
|
||||||
Unfinished swaps from earlier versions will be skipped.
|
Unfinished swaps from earlier versions will be skipped.
|
||||||
|
- A configurable spread for the ASB that is applied to the asking price received from the Kraken price ticker.
|
||||||
|
The default value is 2% and can be configured using the `--ask-spread` parameter.
|
||||||
|
See `./asb --help` for details.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::bitcoin::Amount;
|
use crate::bitcoin::Amount;
|
||||||
use bitcoin::util::amount::ParseAmountError;
|
use bitcoin::util::amount::ParseAmountError;
|
||||||
use bitcoin::{Address, Denomination};
|
use bitcoin::{Address, Denomination};
|
||||||
|
use rust_decimal::Decimal;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(structopt::StructOpt, Debug)]
|
#[derive(structopt::StructOpt, Debug)]
|
||||||
|
@ -27,6 +28,12 @@ pub enum Command {
|
||||||
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))]
|
#[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,
|
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,
|
||||||
},
|
},
|
||||||
History,
|
History,
|
||||||
WithdrawBtc {
|
WithdrawBtc {
|
||||||
|
|
|
@ -4,31 +4,47 @@ use rust_decimal::prelude::ToPrimitive;
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
use std::fmt::{Debug, Display, Formatter};
|
use std::fmt::{Debug, Display, Formatter};
|
||||||
|
|
||||||
/// Prices at which 1 XMR will be traded, in BTC (XMR/BTC pair)
|
/// Represents the rate at which we are willing to trade 1 XMR.
|
||||||
/// The `ask` represents the minimum price in BTC for which we are willing to
|
|
||||||
/// sell 1 XMR.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct Rate {
|
pub struct Rate {
|
||||||
|
/// Represents the asking price from the market.
|
||||||
ask: bitcoin::Amount,
|
ask: bitcoin::Amount,
|
||||||
|
/// The spread which should be applied to the market asking price.
|
||||||
|
ask_spread: Decimal,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ZERO_SPREAD: Decimal = Decimal::from_parts(0, 0, 0, false, 0);
|
||||||
|
|
||||||
impl Rate {
|
impl Rate {
|
||||||
pub const ZERO: Rate = Rate {
|
pub const ZERO: Rate = Rate {
|
||||||
ask: bitcoin::Amount::ZERO,
|
ask: bitcoin::Amount::ZERO,
|
||||||
|
ask_spread: ZERO_SPREAD,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn new(ask: bitcoin::Amount) -> Self {
|
pub fn new(ask: bitcoin::Amount, ask_spread: Decimal) -> Self {
|
||||||
Self { ask }
|
Self { ask, ask_spread }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ask(&self) -> bitcoin::Amount {
|
/// Computes the asking price at which we are willing to sell 1 XMR.
|
||||||
self.ask
|
///
|
||||||
|
/// This applies the spread to the market asking price.
|
||||||
|
pub fn ask(&self) -> Result<bitcoin::Amount> {
|
||||||
|
let sats = self.ask.as_sat();
|
||||||
|
let sats = Decimal::from(sats);
|
||||||
|
|
||||||
|
let additional_sats = sats * self.ask_spread;
|
||||||
|
let additional_sats = bitcoin::Amount::from_sat(
|
||||||
|
additional_sats
|
||||||
|
.to_u64()
|
||||||
|
.context("Failed to fit spread into u64")?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(self.ask + additional_sats)
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function takes the quote amount as it is what Bob sends to Alice in the
|
/// Calculate a sell quote for a given BTC amount.
|
||||||
// swap request
|
|
||||||
pub fn sell_quote(&self, quote: bitcoin::Amount) -> Result<monero::Amount> {
|
pub fn sell_quote(&self, quote: bitcoin::Amount) -> Result<monero::Amount> {
|
||||||
Self::quote(self.ask, quote)
|
Self::quote(self.ask()?, quote)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn quote(rate: bitcoin::Amount, quote: bitcoin::Amount) -> Result<monero::Amount> {
|
fn quote(rate: bitcoin::Amount, quote: bitcoin::Amount) -> Result<monero::Amount> {
|
||||||
|
@ -67,11 +83,13 @@ impl Display for Rate {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
const TWO_PERCENT: Decimal = Decimal::from_parts(2, 0, 0, false, 2);
|
||||||
|
const ONE: Decimal = Decimal::from_parts(1, 0, 0, false, 0);
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sell_quote() {
|
fn sell_quote() {
|
||||||
let rate = Rate {
|
let asking_price = bitcoin::Amount::from_btc(0.002_500).unwrap();
|
||||||
ask: bitcoin::Amount::from_btc(0.002_500).unwrap(),
|
let rate = Rate::new(asking_price, ZERO_SPREAD);
|
||||||
};
|
|
||||||
|
|
||||||
let btc_amount = bitcoin::Amount::from_btc(2.5).unwrap();
|
let btc_amount = bitcoin::Amount::from_btc(2.5).unwrap();
|
||||||
|
|
||||||
|
@ -79,4 +97,37 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(xmr_amount, monero::Amount::from_monero(1000.0).unwrap())
|
assert_eq!(xmr_amount, monero::Amount::from_monero(1000.0).unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn applies_spread_to_asking_price() {
|
||||||
|
let asking_price = bitcoin::Amount::from_sat(100);
|
||||||
|
let rate = Rate::new(asking_price, TWO_PERCENT);
|
||||||
|
|
||||||
|
let amount = rate.ask().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(amount.as_sat(), 102);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn given_spread_of_two_percent_when_caluclating_sell_quote_factor_between_should_be_two_percent(
|
||||||
|
) {
|
||||||
|
let asking_price = bitcoin::Amount::from_btc(0.004).unwrap();
|
||||||
|
|
||||||
|
let rate_no_spread = Rate::new(asking_price, ZERO_SPREAD);
|
||||||
|
let rate_with_spread = Rate::new(asking_price, TWO_PERCENT);
|
||||||
|
|
||||||
|
let xmr_no_spread = rate_no_spread.sell_quote(bitcoin::Amount::ONE_BTC).unwrap();
|
||||||
|
let xmr_with_spread = rate_with_spread
|
||||||
|
.sell_quote(bitcoin::Amount::ONE_BTC)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let xmr_factor =
|
||||||
|
xmr_no_spread.as_piconero_decimal() / xmr_with_spread.as_piconero_decimal() - ONE;
|
||||||
|
|
||||||
|
assert!(xmr_with_spread < xmr_no_spread);
|
||||||
|
assert_eq!(xmr_factor.round_dp(8), TWO_PERCENT); // round to 8 decimal
|
||||||
|
// places to show that
|
||||||
|
// it is really close
|
||||||
|
// to two percent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ use swap::env::GetConfig;
|
||||||
use swap::fs::default_config_path;
|
use swap::fs::default_config_path;
|
||||||
use swap::monero::Amount;
|
use swap::monero::Amount;
|
||||||
use swap::network::swarm;
|
use swap::network::swarm;
|
||||||
|
use swap::protocol::alice::event_loop::KrakenRate;
|
||||||
use swap::protocol::alice::{run, Behaviour, EventLoop};
|
use swap::protocol::alice::{run, Behaviour, EventLoop};
|
||||||
use swap::seed::Seed;
|
use swap::seed::Seed;
|
||||||
use swap::trace::init_tracing;
|
use swap::trace::init_tracing;
|
||||||
|
@ -74,7 +75,10 @@ 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 { max_buy } => {
|
Command::Start {
|
||||||
|
max_buy,
|
||||||
|
ask_spread,
|
||||||
|
} => {
|
||||||
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?;
|
||||||
|
|
||||||
|
@ -104,7 +108,7 @@ async fn main() -> Result<()> {
|
||||||
Arc::new(bitcoin_wallet),
|
Arc::new(bitcoin_wallet),
|
||||||
Arc::new(monero_wallet),
|
Arc::new(monero_wallet),
|
||||||
Arc::new(db),
|
Arc::new(db),
|
||||||
kraken_price_updates,
|
KrakenRate::new(ask_spread, kraken_price_updates),
|
||||||
max_buy,
|
max_buy,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -96,6 +96,10 @@ impl Amount {
|
||||||
Self::from_decimal(decimal)
|
Self::from_decimal(decimal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn as_piconero_decimal(&self) -> Decimal {
|
||||||
|
Decimal::from(self.as_piconero())
|
||||||
|
}
|
||||||
|
|
||||||
fn from_decimal(amount: Decimal) -> Result<Self> {
|
fn from_decimal(amount: Decimal) -> Result<Self> {
|
||||||
let piconeros_dec =
|
let piconeros_dec =
|
||||||
amount.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64"));
|
amount.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64"));
|
||||||
|
|
|
@ -13,6 +13,7 @@ use futures::stream::{FuturesUnordered, StreamExt};
|
||||||
use libp2p::swarm::SwarmEvent;
|
use libp2p::swarm::SwarmEvent;
|
||||||
use libp2p::{PeerId, Swarm};
|
use libp2p::{PeerId, Swarm};
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
|
use rust_decimal::Decimal;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -278,7 +279,7 @@ where
|
||||||
.context("Failed to get latest rate")?;
|
.context("Failed to get latest rate")?;
|
||||||
|
|
||||||
Ok(BidQuote {
|
Ok(BidQuote {
|
||||||
price: rate.ask(),
|
price: rate.ask().context("Failed to compute asking price")?,
|
||||||
max_quantity: max_buy,
|
max_quantity: max_buy,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -360,8 +361,9 @@ impl FixedRate {
|
||||||
impl Default for FixedRate {
|
impl Default for FixedRate {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let ask = bitcoin::Amount::from_btc(Self::RATE).expect("Static value should never fail");
|
let ask = bitcoin::Amount::from_btc(Self::RATE).expect("Static value should never fail");
|
||||||
|
let spread = Decimal::from(0u64);
|
||||||
|
|
||||||
Self(Rate::new(ask))
|
Self(Rate::new(ask, spread))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -373,13 +375,31 @@ impl LatestRate for FixedRate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LatestRate for kraken::PriceUpdates {
|
/// Produces [`Rate`]s based on [`PriceUpdate`]s from kraken and a configured
|
||||||
|
/// spread.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct KrakenRate {
|
||||||
|
ask_spread: Decimal,
|
||||||
|
price_updates: kraken::PriceUpdates,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KrakenRate {
|
||||||
|
pub fn new(ask_spread: Decimal, price_updates: kraken::PriceUpdates) -> Self {
|
||||||
|
Self {
|
||||||
|
ask_spread,
|
||||||
|
price_updates,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LatestRate for KrakenRate {
|
||||||
type Error = kraken::Error;
|
type Error = kraken::Error;
|
||||||
|
|
||||||
fn latest_rate(&mut self) -> Result<Rate, Self::Error> {
|
fn latest_rate(&mut self) -> Result<Rate, Self::Error> {
|
||||||
let update = self.latest_update()?;
|
let update = self.price_updates.latest_update()?;
|
||||||
|
let rate = Rate::new(update.ask, self.ask_spread);
|
||||||
|
|
||||||
Ok(Rate::new(update.ask))
|
Ok(rate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue