From bc46d95985c168a20df9a245a34551213f68df9c Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Thu, 1 Apr 2021 12:03:17 +1100 Subject: [PATCH 1/5] Remove unnecessary `Serialize` implementations --- swap/src/kraken.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/swap/src/kraken.rs b/swap/src/kraken.rs index 4ebce36f..4bea5059 100644 --- a/swap/src/kraken.rs +++ b/swap/src/kraken.rs @@ -1,7 +1,7 @@ use crate::asb::Rate; use anyhow::{anyhow, Context, Result}; use futures::{SinkExt, StreamExt, TryStreamExt}; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use std::convert::{Infallible, TryFrom}; use std::sync::Arc; use std::time::Duration; @@ -224,7 +224,7 @@ mod wire { use bitcoin::util::amount::ParseAmountError; use serde_json::Value; - #[derive(Debug, Serialize, Deserialize, PartialEq)] + #[derive(Debug, Deserialize, PartialEq)] #[serde(tag = "event")] pub enum Event { #[serde(rename = "systemStatus")] @@ -247,18 +247,18 @@ mod wire { BitcoinParseAmount(#[from] ParseAmountError), } - #[derive(Debug, Serialize, Deserialize)] + #[derive(Debug, Deserialize)] #[serde(transparent)] pub struct TickerUpdate(Vec); - #[derive(Debug, Serialize, Deserialize)] + #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum TickerField { Data(TickerData), Metadata(Value), } - #[derive(Debug, Serialize, Deserialize)] + #[derive(Debug, Deserialize)] pub struct TickerData { #[serde(rename = "a")] ask: Vec, @@ -266,7 +266,7 @@ mod wire { bid: Vec, } - #[derive(Debug, Serialize, Deserialize)] + #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum RateElement { Text(String), From cfc530e8abfccf225774334a45d69339f66742e6 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Thu, 1 Apr 2021 12:04:12 +1100 Subject: [PATCH 2/5] Make `ask` field of `Rate` private --- swap/src/asb/fixed_rate.rs | 6 +++--- swap/src/asb/rate.rs | 10 +++++++++- swap/src/kraken.rs | 23 ++++++++++++++--------- swap/src/protocol/alice/event_loop.rs | 2 +- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/swap/src/asb/fixed_rate.rs b/swap/src/asb/fixed_rate.rs index bf42be53..4d0b2045 100644 --- a/swap/src/asb/fixed_rate.rs +++ b/swap/src/asb/fixed_rate.rs @@ -13,8 +13,8 @@ impl FixedRate { impl Default for FixedRate { fn default() -> Self { - Self(Rate { - 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"); + + Self(Rate::new(ask)) } } diff --git a/swap/src/asb/rate.rs b/swap/src/asb/rate.rs index 08eb443d..9058f90d 100644 --- a/swap/src/asb/rate.rs +++ b/swap/src/asb/rate.rs @@ -9,7 +9,7 @@ use std::fmt::{Debug, Display, Formatter}; /// sell 1 XMR. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Rate { - pub ask: bitcoin::Amount, + ask: bitcoin::Amount, } impl Rate { @@ -17,6 +17,14 @@ impl Rate { ask: bitcoin::Amount::ZERO, }; + pub fn new(ask: bitcoin::Amount) -> Self { + Self { ask } + } + + pub fn ask(&self) -> bitcoin::Amount { + self.ask + } + // This function takes the quote amount as it is what Bob sends to Alice in the // swap request pub fn sell_quote(&self, quote: bitcoin::Amount) -> Result { diff --git a/swap/src/kraken.rs b/swap/src/kraken.rs index 4bea5059..7aee6c5c 100644 --- a/swap/src/kraken.rs +++ b/swap/src/kraken.rs @@ -31,7 +31,7 @@ pub fn connect() -> Result { let mut stream = connection::new().await?; while let Some(update) = stream.try_next().await.map_err(to_backoff)? { - let send_result = rate_update.send(Ok(update)); + let send_result = rate_update.send(Ok(Rate::new(update.ask))); if send_result.is_err() { return Err(backoff::Error::Permanent(anyhow!( @@ -120,7 +120,7 @@ mod connection { use futures::stream::BoxStream; use tokio_tungstenite::tungstenite; - pub async fn new() -> Result>> { + pub async fn new() -> Result>> { let (mut rate_stream, _) = tokio_tungstenite::connect_async("wss://ws.kraken.com") .await .context("Failed to connect to Kraken websocket API")?; @@ -134,12 +134,12 @@ mod connection { Ok(stream) } - /// Parse a websocket message into a [`Rate`]. + /// Parse a websocket message into a [`wire::PriceUpdate`]. /// /// Messages which are not actually ticker updates are ignored and result in /// `None` being returned. In the context of a [`TryStream`], these will /// simply be filtered out. - async fn parse_message(msg: tungstenite::Message) -> Result, Error> { + async fn parse_message(msg: tungstenite::Message) -> Result, Error> { let msg = match msg { tungstenite::Message::Text(msg) => msg, tungstenite::Message::Close(close_frame) => { @@ -182,7 +182,7 @@ mod connection { return Ok(None); } // if the message is not an event, it is a ticker update or an unknown event - Err(_) => match serde_json::from_str::(&msg) { + Err(_) => match serde_json::from_str::(&msg) { Ok(ticker) => ticker, Err(e) => { tracing::warn!(%e, "Failed to deserialize message '{}' as ticker update", msg); @@ -192,8 +192,6 @@ mod connection { }, }; - let update = Rate::try_from(update)?; - Ok(Some(update)) } @@ -247,6 +245,13 @@ mod wire { BitcoinParseAmount(#[from] ParseAmountError), } + /// Represents an update within the price ticker. + #[derive(Debug, Deserialize)] + #[serde(try_from = "TickerUpdate")] + pub struct PriceUpdate { + pub ask: bitcoin::Amount, + } + #[derive(Debug, Deserialize)] #[serde(transparent)] pub struct TickerUpdate(Vec); @@ -273,7 +278,7 @@ mod wire { Number(u64), } - impl TryFrom for Rate { + impl TryFrom for PriceUpdate { type Error = Error; fn try_from(value: TickerUpdate) -> Result { @@ -293,7 +298,7 @@ mod wire { _ => return Err(Error::UnexpectedAskRateElementType), }; - Ok(Self { ask }) + Ok(PriceUpdate { ask }) } } diff --git a/swap/src/protocol/alice/event_loop.rs b/swap/src/protocol/alice/event_loop.rs index 2ed686fe..5179fb6f 100644 --- a/swap/src/protocol/alice/event_loop.rs +++ b/swap/src/protocol/alice/event_loop.rs @@ -278,7 +278,7 @@ where .context("Failed to get latest rate")?; Ok(BidQuote { - price: rate.ask, + price: rate.ask(), max_quantity: max_buy, }) } From 654cfff2a87248cc14ed8d820f984c1cf849fb64 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Thu, 1 Apr 2021 12:09:51 +1100 Subject: [PATCH 3/5] Make `kraken` module emit `PriceUpdate`s instead of `Rate`s --- swap/src/bin/asb.rs | 4 ++-- swap/src/bin/kraken_ticker.rs | 4 ++-- swap/src/kraken.rs | 31 +++++++++++++-------------- swap/src/protocol/alice/event_loop.rs | 7 ++++-- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/swap/src/bin/asb.rs b/swap/src/bin/asb.rs index 637e659f..6694e35d 100644 --- a/swap/src/bin/asb.rs +++ b/swap/src/bin/asb.rs @@ -92,7 +92,7 @@ async fn main() -> Result<()> { info!("Monero balance: {}", monero_balance); } - let kraken_rate_updates = kraken::connect()?; + let kraken_price_updates = kraken::connect()?; let mut swarm = swarm::new::(&seed)?; Swarm::listen_on(&mut swarm, config.network.listen) @@ -104,7 +104,7 @@ async fn main() -> Result<()> { Arc::new(bitcoin_wallet), Arc::new(monero_wallet), Arc::new(db), - kraken_rate_updates, + kraken_price_updates, max_buy, ) .unwrap(); diff --git a/swap/src/bin/kraken_ticker.rs b/swap/src/bin/kraken_ticker.rs index 9c0d3fdd..a28bac50 100644 --- a/swap/src/bin/kraken_ticker.rs +++ b/swap/src/bin/kraken_ticker.rs @@ -9,8 +9,8 @@ async fn main() -> Result<()> { let mut ticker = swap::kraken::connect().context("Failed to connect to kraken")?; loop { - match ticker.wait_for_update().await? { - Ok(rate) => println!("Rate update: {}", rate), + match ticker.wait_for_next_update().await? { + Ok(update) => println!("Price update: {}", update.ask), Err(e) => println!("Error: {:#}", e), } } diff --git a/swap/src/kraken.rs b/swap/src/kraken.rs index 7aee6c5c..57e2861c 100644 --- a/swap/src/kraken.rs +++ b/swap/src/kraken.rs @@ -1,4 +1,3 @@ -use crate::asb::Rate; use anyhow::{anyhow, Context, Result}; use futures::{SinkExt, StreamExt, TryStreamExt}; use serde::Deserialize; @@ -10,9 +9,9 @@ use tokio::sync::watch; /// Connect to Kraken websocket API for a constant stream of rate updates. /// /// If the connection fails, it will automatically be re-established. -pub fn connect() -> Result { - let (rate_update, rate_update_receiver) = watch::channel(Err(Error::NotYetAvailable)); - let rate_update = Arc::new(rate_update); +pub fn connect() -> Result { + let (price_update, price_update_receiver) = watch::channel(Err(Error::NotYetAvailable)); + let price_update = Arc::new(price_update); tokio::spawn(async move { // The default backoff config is fine for us apart from one thing: @@ -26,12 +25,12 @@ pub fn connect() -> Result { let result = backoff::future::retry_notify::( backoff, || { - let rate_update = rate_update.clone(); + let price_update = price_update.clone(); async move { let mut stream = connection::new().await?; while let Some(update) = stream.try_next().await.map_err(to_backoff)? { - let send_result = rate_update.send(Ok(Rate::new(update.ask))); + let send_result = price_update.send(Ok(update)); if send_result.is_err() { return Err(backoff::Error::Permanent(anyhow!( @@ -54,30 +53,30 @@ pub fn connect() -> Result { tracing::warn!("Rate updates incurred an unrecoverable error: {:#}", e); // in case the retries fail permanently, let the subscribers know - rate_update.send(Err(Error::PermanentFailure)) + price_update.send(Err(Error::PermanentFailure)) } Ok(never) => match never {}, } }); - Ok(RateUpdateStream { - inner: rate_update_receiver, + Ok(PriceUpdates { + inner: price_update_receiver, }) } #[derive(Clone, Debug)] -pub struct RateUpdateStream { - inner: watch::Receiver, +pub struct PriceUpdates { + inner: watch::Receiver, } -impl RateUpdateStream { - pub async fn wait_for_update(&mut self) -> Result { +impl PriceUpdates { + pub async fn wait_for_next_update(&mut self) -> Result { self.inner.changed().await?; Ok(self.inner.borrow().clone()) } - pub fn latest_update(&mut self) -> RateUpdate { + pub fn latest_update(&mut self) -> PriceUpdate { self.inner.borrow().clone() } } @@ -90,7 +89,7 @@ pub enum Error { PermanentFailure, } -type RateUpdate = Result; +type PriceUpdate = Result; /// Maps a [`connection::Error`] to a backoff error, effectively defining our /// retry strategy. @@ -246,7 +245,7 @@ mod wire { } /// Represents an update within the price ticker. - #[derive(Debug, Deserialize)] + #[derive(Clone, Debug, Deserialize)] #[serde(try_from = "TickerUpdate")] pub struct PriceUpdate { pub ask: bitcoin::Amount, diff --git a/swap/src/protocol/alice/event_loop.rs b/swap/src/protocol/alice/event_loop.rs index 5179fb6f..f48fd080 100644 --- a/swap/src/protocol/alice/event_loop.rs +++ b/swap/src/protocol/alice/event_loop.rs @@ -354,11 +354,14 @@ impl LatestRate for FixedRate { } } -impl LatestRate for kraken::RateUpdateStream { +impl LatestRate for kraken::PriceUpdates { type Error = kraken::Error; fn latest_rate(&mut self) -> Result { - self.latest_update() + let update = self.latest_update()?; + let rate = Rate::new(update.ask); + + Ok(rate) } } From 3e0301a9d43fbdf788c3c0110272bdc7250841a3 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Thu, 1 Apr 2021 16:48:37 +1100 Subject: [PATCH 4/5] Move `FixedRate` into event_loop module This is where these types are used, they can be defined in there. --- swap/src/asb.rs | 4 +--- swap/src/asb/fixed_rate.rs | 20 -------------------- swap/src/protocol/alice/event_loop.rs | 24 +++++++++++++++++++++--- swap/tests/harness/mod.rs | 2 +- 4 files changed, 23 insertions(+), 27 deletions(-) delete mode 100644 swap/src/asb/fixed_rate.rs diff --git a/swap/src/asb.rs b/swap/src/asb.rs index f755822e..262f8a6a 100644 --- a/swap/src/asb.rs +++ b/swap/src/asb.rs @@ -1,7 +1,5 @@ pub mod command; pub mod config; -mod fixed_rate; mod rate; -pub use self::fixed_rate::FixedRate; -pub use self::rate::Rate; +pub use rate::Rate; diff --git a/swap/src/asb/fixed_rate.rs b/swap/src/asb/fixed_rate.rs deleted file mode 100644 index 4d0b2045..00000000 --- a/swap/src/asb/fixed_rate.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::asb::Rate; - -#[derive(Clone, Copy, Debug)] -pub struct FixedRate(Rate); - -impl FixedRate { - pub const RATE: f64 = 0.01; - - pub fn value(&self) -> Rate { - self.0 - } -} - -impl Default for FixedRate { - fn default() -> Self { - let ask = bitcoin::Amount::from_btc(Self::RATE).expect("Static value should never fail"); - - Self(Rate::new(ask)) - } -} diff --git a/swap/src/protocol/alice/event_loop.rs b/swap/src/protocol/alice/event_loop.rs index f48fd080..7837c96c 100644 --- a/swap/src/protocol/alice/event_loop.rs +++ b/swap/src/protocol/alice/event_loop.rs @@ -1,4 +1,4 @@ -use crate::asb::{FixedRate, Rate}; +use crate::asb::Rate; use crate::database::Database; use crate::env::Config; use crate::monero::BalanceTooLow; @@ -346,6 +346,25 @@ pub trait LatestRate { fn latest_rate(&mut self) -> Result; } +#[derive(Clone, Debug)] +pub struct FixedRate(Rate); + +impl FixedRate { + pub const RATE: f64 = 0.01; + + pub fn value(&self) -> Rate { + self.0 + } +} + +impl Default for FixedRate { + fn default() -> Self { + let ask = bitcoin::Amount::from_btc(Self::RATE).expect("Static value should never fail"); + + Self(Rate::new(ask)) + } +} + impl LatestRate for FixedRate { type Error = Infallible; @@ -359,9 +378,8 @@ impl LatestRate for kraken::PriceUpdates { fn latest_rate(&mut self) -> Result { let update = self.latest_update()?; - let rate = Rate::new(update.ask); - Ok(rate) + Ok(Rate::new(update.ask)) } } diff --git a/swap/tests/harness/mod.rs b/swap/tests/harness/mod.rs index ad0eb1e8..9549d785 100644 --- a/swap/tests/harness/mod.rs +++ b/swap/tests/harness/mod.rs @@ -15,11 +15,11 @@ use std::fmt; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; -use swap::asb::FixedRate; use swap::bitcoin::{CancelTimelock, PunishTimelock}; use swap::database::Database; use swap::env::{Config, GetConfig}; use swap::network::swarm; +use swap::protocol::alice::event_loop::FixedRate; use swap::protocol::alice::{AliceState, Swap}; use swap::protocol::bob::BobState; use swap::protocol::{alice, bob}; From a99d12b9dfdbdfa7f2a68a6ea1481efd4337dc02 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Thu, 1 Apr 2021 13:00:15 +1100 Subject: [PATCH 5/5] Add a configurable spread to the ASB Fixes #381. --- CHANGELOG.md | 3 ++ swap/src/asb/command.rs | 7 +++ swap/src/asb/rate.rs | 77 ++++++++++++++++++++++----- swap/src/bin/asb.rs | 8 ++- swap/src/monero.rs | 4 ++ swap/src/protocol/alice/event_loop.rs | 30 +++++++++-- 6 files changed, 109 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f96cedf4..ef1ba088 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A changelog file. - Automatic resume of unfinished swaps for the `asb` upon startup. 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 diff --git a/swap/src/asb/command.rs b/swap/src/asb/command.rs index eb0ea13b..298f5570 100644 --- a/swap/src/asb/command.rs +++ b/swap/src/asb/command.rs @@ -1,6 +1,7 @@ use crate::bitcoin::Amount; use bitcoin::util::amount::ParseAmountError; use bitcoin::{Address, Denomination}; +use rust_decimal::Decimal; use std::path::PathBuf; #[derive(structopt::StructOpt, Debug)] @@ -27,6 +28,12 @@ pub enum Command { 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, }, History, WithdrawBtc { diff --git a/swap/src/asb/rate.rs b/swap/src/asb/rate.rs index 9058f90d..ffcdcf7c 100644 --- a/swap/src/asb/rate.rs +++ b/swap/src/asb/rate.rs @@ -4,31 +4,47 @@ use rust_decimal::prelude::ToPrimitive; use rust_decimal::Decimal; use std::fmt::{Debug, Display, Formatter}; -/// Prices at which 1 XMR will be traded, in BTC (XMR/BTC pair) -/// The `ask` represents the minimum price in BTC for which we are willing to -/// sell 1 XMR. +/// Represents the rate at which we are willing to trade 1 XMR. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Rate { + /// Represents the asking price from the market. 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 { pub const ZERO: Rate = Rate { ask: bitcoin::Amount::ZERO, + ask_spread: ZERO_SPREAD, }; - pub fn new(ask: bitcoin::Amount) -> Self { - Self { ask } + pub fn new(ask: bitcoin::Amount, ask_spread: Decimal) -> Self { + Self { ask, ask_spread } } - pub fn ask(&self) -> bitcoin::Amount { - self.ask + /// Computes the asking price at which we are willing to sell 1 XMR. + /// + /// This applies the spread to the market asking price. + pub fn ask(&self) -> Result { + 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 - // swap request + /// Calculate a sell quote for a given BTC amount. pub fn sell_quote(&self, quote: bitcoin::Amount) -> Result { - Self::quote(self.ask, quote) + Self::quote(self.ask()?, quote) } fn quote(rate: bitcoin::Amount, quote: bitcoin::Amount) -> Result { @@ -67,11 +83,13 @@ impl Display for Rate { mod tests { 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] fn sell_quote() { - let rate = Rate { - ask: bitcoin::Amount::from_btc(0.002_500).unwrap(), - }; + let asking_price = 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(); @@ -79,4 +97,37 @@ mod tests { 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 + } } diff --git a/swap/src/bin/asb.rs b/swap/src/bin/asb.rs index 6694e35d..3140d946 100644 --- a/swap/src/bin/asb.rs +++ b/swap/src/bin/asb.rs @@ -26,6 +26,7 @@ use swap::env::GetConfig; use swap::fs::default_config_path; use swap::monero::Amount; use swap::network::swarm; +use swap::protocol::alice::event_loop::KrakenRate; use swap::protocol::alice::{run, Behaviour, EventLoop}; use swap::seed::Seed; use swap::trace::init_tracing; @@ -74,7 +75,10 @@ async fn main() -> Result<()> { let env_config = env::Testnet::get_config(); 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 monero_wallet = init_monero_wallet(&config, env_config).await?; @@ -104,7 +108,7 @@ async fn main() -> Result<()> { Arc::new(bitcoin_wallet), Arc::new(monero_wallet), Arc::new(db), - kraken_price_updates, + KrakenRate::new(ask_spread, kraken_price_updates), max_buy, ) .unwrap(); diff --git a/swap/src/monero.rs b/swap/src/monero.rs index 407f7aa2..c52bd0b5 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -96,6 +96,10 @@ impl Amount { Self::from_decimal(decimal) } + pub fn as_piconero_decimal(&self) -> Decimal { + Decimal::from(self.as_piconero()) + } + fn from_decimal(amount: Decimal) -> Result { let piconeros_dec = amount.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64")); diff --git a/swap/src/protocol/alice/event_loop.rs b/swap/src/protocol/alice/event_loop.rs index 7837c96c..601c18ff 100644 --- a/swap/src/protocol/alice/event_loop.rs +++ b/swap/src/protocol/alice/event_loop.rs @@ -13,6 +13,7 @@ use futures::stream::{FuturesUnordered, StreamExt}; use libp2p::swarm::SwarmEvent; use libp2p::{PeerId, Swarm}; use rand::rngs::OsRng; +use rust_decimal::Decimal; use std::collections::HashMap; use std::convert::Infallible; use std::sync::Arc; @@ -278,7 +279,7 @@ where .context("Failed to get latest rate")?; Ok(BidQuote { - price: rate.ask(), + price: rate.ask().context("Failed to compute asking price")?, max_quantity: max_buy, }) } @@ -360,8 +361,9 @@ impl FixedRate { impl Default for FixedRate { fn default() -> Self { 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; fn latest_rate(&mut self) -> Result { - 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) } }