diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ee491b2..f601fd4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- 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. + ### Fixed - An issue where both the ASB and the CLI point to the same default directory `xmr-btc-swap` for storing data. diff --git a/swap/src/asb/command.rs b/swap/src/asb/command.rs index 298f5570..85c82d1f 100644 --- a/swap/src/asb/command.rs +++ b/swap/src/asb/command.rs @@ -34,6 +34,12 @@ pub enum Command { default_value = "0.02" )] ask_spread: Decimal, + + #[structopt( + long = "resume-only", + help = "For maintenance only. When set, no new swap requests will be accepted, but existing unfinished swaps will be resumed." + )] + resume_only: bool, }, History, WithdrawBtc { diff --git a/swap/src/bin/asb.rs b/swap/src/bin/asb.rs index 4d99aadb..80416503 100644 --- a/swap/src/bin/asb.rs +++ b/swap/src/bin/asb.rs @@ -81,6 +81,7 @@ async fn main() -> Result<()> { Command::Start { max_buy, ask_spread, + resume_only, } => { let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?; let monero_wallet = init_monero_wallet(&config, env_config).await?; @@ -133,6 +134,7 @@ async fn main() -> Result<()> { Arc::new(db), KrakenRate::new(ask_spread, kraken_price_updates), max_buy, + resume_only, ) .unwrap(); diff --git a/swap/src/network/spot_price.rs b/swap/src/network/spot_price.rs index 560f8b17..bd2ca0a7 100644 --- a/swap/src/network/spot_price.rs +++ b/swap/src/network/spot_price.rs @@ -41,7 +41,16 @@ pub struct Request { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Response { - pub xmr: monero::Amount, + pub xmr: Option, + pub error: Option, +} + +#[derive(Clone, Debug, thiserror::Error, Serialize, Deserialize)] +pub enum Error { + #[error( + "This seller currently does not accept incoming swap requests, please try again later" + )] + MaintenanceMode, } /// Constructs a new instance of the `spot-price` behaviour to be used by Alice. diff --git a/swap/src/protocol/alice/event_loop.rs b/swap/src/protocol/alice/event_loop.rs index 45258ef8..b81fecfe 100644 --- a/swap/src/protocol/alice/event_loop.rs +++ b/swap/src/protocol/alice/event_loop.rs @@ -42,6 +42,7 @@ pub struct EventLoop { max_buy: bitcoin::Amount, swap_sender: mpsc::Sender, + resume_only: bool, /// Stores incoming [`EncryptedSignature`]s per swap. recv_encrypted_signature: HashMap>, @@ -62,6 +63,7 @@ impl EventLoop where LR: LatestRate, { + #[allow(clippy::too_many_arguments)] pub fn new( swarm: Swarm, env_config: Config, @@ -70,6 +72,7 @@ where db: Arc, latest_rate: LR, max_buy: bitcoin::Amount, + resume_only: bool, ) -> Result<(Self, mpsc::Receiver)> { let swap_channel = MpscChannels::default(); @@ -81,6 +84,7 @@ where db, latest_rate, swap_sender: swap_channel.sender, + resume_only, max_buy, recv_encrypted_signature: Default::default(), inflight_encrypted_signatures: Default::default(), @@ -144,6 +148,20 @@ where swarm_event = self.swarm.next_event() => { match swarm_event { SwarmEvent::Behaviour(OutEvent::SpotPriceRequested { request, channel, peer }) => { + if self.resume_only { + tracing::warn!(%peer, "Ignoring spot price request from {} because ASB started in resume-only mode", peer); + + match self.swarm.behaviour_mut().spot_price.send_response(channel, spot_price::Response { xmr: None, error: Some(spot_price::Error::MaintenanceMode) }) { + Ok(_) => {}, + Err(_) => { + tracing::debug!(%peer, "Failed to respond with error to spot price request"); + continue; + } + } + + continue; + } + let btc = request.btc; let xmr = match self.handle_spot_price_request(btc, self.monero_wallet.clone()).await { Ok(xmr) => xmr, @@ -153,7 +171,7 @@ where } }; - match self.swarm.behaviour_mut().spot_price.send_response(channel, spot_price::Response { xmr }) { + match self.swarm.behaviour_mut().spot_price.send_response(channel, spot_price::Response { xmr: Some(xmr), error: None }) { Ok(_) => {}, Err(_) => { // if we can't respond, the peer probably just disconnected so it is not a huge deal, only log this on debug diff --git a/swap/src/protocol/bob/event_loop.rs b/swap/src/protocol/bob/event_loop.rs index c4bfc0a8..1b978efe 100644 --- a/swap/src/protocol/bob/event_loop.rs +++ b/swap/src/protocol/bob/event_loop.rs @@ -3,7 +3,7 @@ use crate::network::quote::BidQuote; use crate::network::{encrypted_signature, spot_price}; use crate::protocol::bob::{Behaviour, OutEvent, State0, State2}; use crate::{bitcoin, monero}; -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use futures::future::{BoxFuture, OptionFuture}; use futures::{FutureExt, StreamExt}; use libp2p::request_response::{RequestId, ResponseChannel}; @@ -261,11 +261,22 @@ impl EventLoopHandle { } pub async fn request_spot_price(&mut self, btc: bitcoin::Amount) -> Result { - Ok(self + let response = self .spot_price .send_receive(spot_price::Request { btc }) - .await? - .xmr) + .await?; + + match (response.xmr, response.error) { + (Some(xmr), None) => Ok(xmr), + (_, Some(error)) => { + bail!(error); + } + (None, None) => { + bail!( + "Unexpected response for spot-price request, neither price nor error received" + ); + } + } } pub async fn request_quote(&mut self) -> Result { diff --git a/swap/tests/harness/mod.rs b/swap/tests/harness/mod.rs index cadce4c4..de8bd925 100644 --- a/swap/tests/harness/mod.rs +++ b/swap/tests/harness/mod.rs @@ -234,6 +234,7 @@ fn start_alice( db, FixedRate::default(), bitcoin::Amount::ONE_BTC, + false, ) .unwrap();