From 601bf072557c3b63216dcf25a91cf703c4b78821 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Wed, 3 Mar 2021 16:54:47 +1100 Subject: [PATCH] Introduce `quote` protocol and display it to the user before they fund Previously, the user neither knew the price nor the maximum quantity they could trade. We now request a quote from the user and display it to them. Fixes #255. --- swap/src/bin/swap_cli.rs | 33 +++++++++------ swap/src/network.rs | 1 + swap/src/network/quote.rs | 53 ++++++++++++++++++++++++ swap/src/protocol/alice/behaviour.rs | 58 ++++++++++++++++++++++++--- swap/src/protocol/alice/event_loop.rs | 36 +++++++++++++++-- swap/src/protocol/bob.rs | 48 ++++++++++++++++++++-- swap/src/protocol/bob/event_loop.rs | 40 ++++++++++++++++-- 7 files changed, 242 insertions(+), 27 deletions(-) create mode 100644 swap/src/network/quote.rs diff --git a/swap/src/bin/swap_cli.rs b/swap/src/bin/swap_cli.rs index b5e6be94..4d01d907 100644 --- a/swap/src/bin/swap_cli.rs +++ b/swap/src/bin/swap_cli.rs @@ -15,6 +15,7 @@ use anyhow::{bail, Context, Result}; use prettytable::{row, Table}; use reqwest::Url; +use std::cmp::min; use std::path::Path; use std::sync::Arc; use std::time::Duration; @@ -115,14 +116,27 @@ async fn main() -> Result<()> { let monero_wallet = init_monero_wallet(monero_network, monero_wallet_rpc_process.endpoint()).await?; let bitcoin_wallet = Arc::new(bitcoin_wallet); + let (event_loop, mut event_loop_handle) = EventLoop::new( + &seed.derive_libp2p_identity(), + alice_peer_id, + alice_addr, + bitcoin_wallet.clone(), + )?; + let handle = tokio::spawn(event_loop.run()); - let swap_id = Uuid::new_v4(); + let bid_quote = event_loop_handle + .request_quote() + .await + .context("failed to request quote")?; + + info!("Received quote: 1 XMR ~ {}", bid_quote.price); // TODO: Also wait for more funds if balance < dust if bitcoin_wallet.balance().await? == Amount::ZERO { info!( - "Please deposit BTC to {}", - bitcoin_wallet.new_address().await? + "Please deposit the BTC you want to swap to {} (max {})", + bitcoin_wallet.new_address().await?, + bid_quote.max_quantity ); while bitcoin_wallet.balance().await? == Amount::ZERO { @@ -139,19 +153,14 @@ async fn main() -> Result<()> { ); } - let send_bitcoin = bitcoin_wallet.max_giveable(TxLock::script_size()).await?; + let max_giveable = bitcoin_wallet.max_giveable(TxLock::script_size()).await?; + let max_accepted = bid_quote.max_quantity; - let (event_loop, event_loop_handle) = EventLoop::new( - &seed.derive_libp2p_identity(), - alice_peer_id, - alice_addr, - bitcoin_wallet.clone(), - )?; - let handle = tokio::spawn(event_loop.run()); + let send_bitcoin = min(max_giveable, max_accepted); let swap = Builder::new( db, - swap_id, + Uuid::new_v4(), bitcoin_wallet.clone(), Arc::new(monero_wallet), execution_params, diff --git a/swap/src/network.rs b/swap/src/network.rs index 67f39f0e..7a82065b 100644 --- a/swap/src/network.rs +++ b/swap/src/network.rs @@ -1,4 +1,5 @@ pub mod peer_tracker; +pub mod quote; pub mod request_response; pub mod spot_price; pub mod transport; diff --git a/swap/src/network/quote.rs b/swap/src/network/quote.rs new file mode 100644 index 00000000..4e536a0b --- /dev/null +++ b/swap/src/network/quote.rs @@ -0,0 +1,53 @@ +use crate::bitcoin; +use crate::network::request_response::CborCodec; +use libp2p::core::ProtocolName; +use libp2p::request_response::{ + ProtocolSupport, RequestResponse, RequestResponseConfig, RequestResponseEvent, +}; +use serde::{Deserialize, Serialize}; + +pub type OutEvent = RequestResponseEvent<(), BidQuote>; + +#[derive(Debug, Clone, Copy, Default)] +pub struct BidQuoteProtocol; + +impl ProtocolName for BidQuoteProtocol { + fn protocol_name(&self) -> &[u8] { + b"/comit/xmr/btc/bid-quote/1.0.0" + } +} + +/// Represents a quote for buying XMR. +#[derive(Serialize, Deserialize, Debug, Clone)] +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 maximum quantity the maker is willing to buy. + #[serde(with = "::bitcoin::util::amount::serde::as_sat")] + pub max_quantity: bitcoin::Amount, +} + +pub type Behaviour = RequestResponse>; + +/// Constructs a new instance of the `quote` behaviour to be used by Alice. +/// +/// Alice only supports inbound connections, i.e. handing out quotes. +pub fn alice() -> Behaviour { + Behaviour::new( + CborCodec::default(), + vec![(BidQuoteProtocol, ProtocolSupport::Inbound)], + RequestResponseConfig::default(), + ) +} + +/// Constructs a new instance of the `quote` behaviour to be used by Bob. +/// +/// Bob only supports outbound connections, i.e. requesting quotes. +pub fn bob() -> Behaviour { + Behaviour::new( + CborCodec::default(), + vec![(BidQuoteProtocol, ProtocolSupport::Outbound)], + RequestResponseConfig::default(), + ) +} diff --git a/swap/src/protocol/alice/behaviour.rs b/swap/src/protocol/alice/behaviour.rs index 87b65981..bb6adf87 100644 --- a/swap/src/protocol/alice/behaviour.rs +++ b/swap/src/protocol/alice/behaviour.rs @@ -1,6 +1,6 @@ use crate::execution_params::ExecutionParams; -use crate::network::spot_price::{Request, Response}; -use crate::network::{peer_tracker, spot_price}; +use crate::network::quote::BidQuote; +use crate::network::{peer_tracker, quote, spot_price}; use crate::protocol::alice::{ encrypted_signature, execution_setup, transfer_proof, State0, State3, TransferProof, }; @@ -16,8 +16,12 @@ use tracing::debug; pub enum OutEvent { ConnectionEstablished(PeerId), SpotPriceRequested { - msg: Request, - channel: ResponseChannel, + msg: spot_price::Request, + channel: ResponseChannel, + peer: PeerId, + }, + QuoteRequested { + channel: ResponseChannel, peer: PeerId, }, ExecutionSetupDone { @@ -78,6 +82,34 @@ impl From for OutEvent { } } +impl From for OutEvent { + fn from(event: quote::OutEvent) -> Self { + match event { + quote::OutEvent::Message { + peer, + message: RequestResponseMessage::Request { channel, .. }, + } => OutEvent::QuoteRequested { channel, peer }, + quote::OutEvent::Message { + message: RequestResponseMessage::Response { .. }, + .. + } => OutEvent::Failure(anyhow!( + "Alice is only meant to hand out quotes, not receive them" + )), + quote::OutEvent::ResponseSent { .. } => OutEvent::ResponseSent, + quote::OutEvent::InboundFailure { peer, error, .. } => OutEvent::Failure(anyhow!( + "quote protocol with peer {} failed due to {:?}", + peer, + error + )), + quote::OutEvent::OutboundFailure { peer, error, .. } => OutEvent::Failure(anyhow!( + "quote protocol with peer {} failed due to {:?}", + peer, + error + )), + } + } +} + impl From for OutEvent { fn from(event: execution_setup::OutEvent) -> Self { use crate::protocol::alice::execution_setup::OutEvent::*; @@ -124,6 +156,7 @@ impl From for OutEvent { #[allow(missing_debug_implementations)] pub struct Behaviour { pt: peer_tracker::Behaviour, + quote: quote::Behaviour, spot_price: spot_price::Behaviour, execution_setup: execution_setup::Behaviour, transfer_proof: transfer_proof::Behaviour, @@ -134,6 +167,7 @@ impl Default for Behaviour { fn default() -> Self { Self { pt: Default::default(), + quote: quote::alice(), spot_price: spot_price::alice(), execution_setup: Default::default(), transfer_proof: Default::default(), @@ -143,10 +177,22 @@ impl Default for Behaviour { } impl Behaviour { + pub fn send_quote( + &mut self, + channel: ResponseChannel, + response: BidQuote, + ) -> Result<()> { + self.quote + .send_response(channel, response) + .map_err(|_| anyhow!("failed to respond with quote"))?; + + Ok(()) + } + pub fn send_spot_price( &mut self, - channel: ResponseChannel, - response: Response, + channel: ResponseChannel, + response: spot_price::Response, ) -> Result<()> { self.spot_price .send_response(channel, response) diff --git a/swap/src/protocol/alice/event_loop.rs b/swap/src/protocol/alice/event_loop.rs index 9f8cd9bc..7df924ff 100644 --- a/swap/src/protocol/alice/event_loop.rs +++ b/swap/src/protocol/alice/event_loop.rs @@ -2,8 +2,8 @@ use crate::asb::LatestRate; use crate::database::Database; use crate::execution_params::ExecutionParams; use crate::monero::BalanceTooLow; -use crate::network::spot_price::Response; -use crate::network::{transport, TokioExecutor}; +use crate::network::quote::BidQuote; +use crate::network::{spot_price, transport, TokioExecutor}; use crate::protocol::alice; use crate::protocol::alice::{AliceState, Behaviour, OutEvent, State3, Swap, TransferProof}; use crate::protocol::bob::EncryptedSignature; @@ -171,7 +171,7 @@ where } }; - match self.swarm.send_spot_price(channel, Response { xmr }) { + match self.swarm.send_spot_price(channel, spot_price::Response { xmr }) { Ok(_) => {}, Err(e) => { // if we can't respond, the peer probably just disconnected so it is not a huge deal, only log this on debug @@ -187,6 +187,24 @@ where } } } + OutEvent::QuoteRequested { channel, peer } => { + let quote = match self.make_quote(self.max_buy).await { + Ok(quote) => quote, + Err(e) => { + tracing::warn!(%peer, "failed to make quote: {:#}", e); + continue; + } + }; + + match self.swarm.send_quote(channel, quote) { + Ok(_) => {}, + Err(e) => { + // if we can't respond, the peer probably just disconnected so it is not a huge deal, only log this on debug + debug!(%peer, "failed to respond with quote: {:#}", e); + continue; + } + } + } OutEvent::ExecutionSetupDone{bob_peer_id, state3} => { let _ = self.handle_execution_setup_done(bob_peer_id, *state3).await; } @@ -245,6 +263,18 @@ where Ok(xmr) } + async fn make_quote(&mut self, max_buy: bitcoin::Amount) -> Result { + let rate = self + .rate_service + .latest_rate() + .context("Failed to get latest rate")?; + + Ok(BidQuote { + price: rate.ask, + max_quantity: max_buy, + }) + } + async fn handle_execution_setup_done( &mut self, bob_peer_id: PeerId, diff --git a/swap/src/protocol/bob.rs b/swap/src/protocol/bob.rs index afc952fc..f70eaf63 100644 --- a/swap/src/protocol/bob.rs +++ b/swap/src/protocol/bob.rs @@ -2,8 +2,10 @@ use crate::database::Database; use crate::execution_params::ExecutionParams; use crate::network::{peer_tracker, spot_price}; use crate::protocol::alice::TransferProof; +use crate::protocol::bob; use crate::{bitcoin, monero}; use anyhow::{anyhow, Error, Result}; +pub use execution_setup::{Message0, Message2, Message4}; use libp2p::core::Multiaddr; use libp2p::request_response::{RequestResponseMessage, ResponseChannel}; use libp2p::{NetworkBehaviour, PeerId}; @@ -14,10 +16,11 @@ use uuid::Uuid; pub use self::cancel::cancel; pub use self::encrypted_signature::EncryptedSignature; pub use self::event_loop::{EventLoop, EventLoopHandle}; -pub use self::execution_setup::{Message0, Message2, Message4}; pub use self::refund::refund; pub use self::state::*; pub use self::swap::{run, run_until}; +use crate::network::quote; +use crate::network::quote::BidQuote; pub mod cancel; mod encrypted_signature; @@ -30,7 +33,7 @@ mod transfer_proof; pub struct Swap { pub state: BobState, - pub event_loop_handle: EventLoopHandle, + pub event_loop_handle: bob::EventLoopHandle, pub db: Database, pub bitcoin_wallet: Arc, pub monero_wallet: Arc, @@ -89,7 +92,7 @@ impl Builder { } } - pub fn build(self) -> Result { + pub fn build(self) -> Result { let state = match self.init_params { InitParams::New { btc_amount } => BobState::Started { btc_amount }, InitParams::None => self.db.get_state(self.swap_id)?.try_into_bob()?.into(), @@ -111,6 +114,7 @@ impl Builder { #[derive(Debug)] pub enum OutEvent { ConnectionEstablished(PeerId), + QuoteReceived(BidQuote), SpotPriceReceived(spot_price::Response), ExecutionSetupDone(Result>), TransferProof { @@ -164,6 +168,38 @@ impl From for OutEvent { } } +impl From for OutEvent { + fn from(event: quote::OutEvent) -> Self { + match event { + quote::OutEvent::Message { + message: RequestResponseMessage::Response { response, .. }, + .. + } => OutEvent::QuoteReceived(response), + quote::OutEvent::Message { + message: RequestResponseMessage::Request { .. }, + .. + } => OutEvent::CommunicationError(anyhow!( + "Bob is only meant to receive quotes, not hand them out" + )), + quote::OutEvent::ResponseSent { .. } => OutEvent::ResponseSent, + quote::OutEvent::InboundFailure { peer, error, .. } => { + OutEvent::CommunicationError(anyhow!( + "quote protocol with peer {} failed due to {:?}", + peer, + error + )) + } + quote::OutEvent::OutboundFailure { peer, error, .. } => { + OutEvent::CommunicationError(anyhow!( + "quote protocol with peer {} failed due to {:?}", + peer, + error + )) + } + } + } +} + impl From for OutEvent { fn from(event: execution_setup::OutEvent) -> Self { match event { @@ -206,6 +242,7 @@ impl From for OutEvent { #[allow(missing_debug_implementations)] pub struct Behaviour { pt: peer_tracker::Behaviour, + quote: quote::Behaviour, spot_price: spot_price::Behaviour, execution_setup: execution_setup::Behaviour, transfer_proof: transfer_proof::Behaviour, @@ -216,6 +253,7 @@ impl Default for Behaviour { fn default() -> Self { Self { pt: Default::default(), + quote: quote::bob(), spot_price: spot_price::bob(), execution_setup: Default::default(), transfer_proof: Default::default(), @@ -225,6 +263,10 @@ impl Default for Behaviour { } impl Behaviour { + pub fn request_quote(&mut self, alice: PeerId) { + let _ = self.quote.send_request(&alice, ()); + } + pub fn request_spot_price(&mut self, alice: PeerId, request: spot_price::Request) { let _ = self.spot_price.send_request(&alice, request); } diff --git a/swap/src/protocol/bob/event_loop.rs b/swap/src/protocol/bob/event_loop.rs index 6bad38fd..a8609936 100644 --- a/swap/src/protocol/bob/event_loop.rs +++ b/swap/src/protocol/bob/event_loop.rs @@ -1,4 +1,5 @@ use crate::bitcoin::EncryptedSignature; +use crate::network::quote::BidQuote; use crate::network::{spot_price, transport, TokioExecutor}; use crate::protocol::alice::TransferProof; use crate::protocol::bob::{Behaviour, OutEvent, State0, State2}; @@ -41,6 +42,8 @@ pub struct EventLoopHandle { send_encrypted_signature: Sender, request_spot_price: Sender, recv_spot_price: Receiver, + request_quote: Sender<()>, + recv_quote: Receiver, } impl EventLoopHandle { @@ -74,7 +77,10 @@ impl EventLoopHandle { } pub async fn request_spot_price(&mut self, btc: bitcoin::Amount) -> Result { - let _ = self.request_spot_price.send(Request { btc }).await?; + let _ = self + .request_spot_price + .send(spot_price::Request { btc }) + .await?; let response = self .recv_spot_price @@ -85,6 +91,18 @@ impl EventLoopHandle { Ok(response.xmr) } + pub async fn request_quote(&mut self) -> Result { + let _ = self.request_quote.send(()).await?; + + let quote = self + .recv_quote + .recv() + .await + .ok_or_else(|| anyhow!("Failed to receive quote from Alice"))?; + + Ok(quote) + } + pub async fn send_encrypted_signature( &mut self, tx_redeem_encsig: EncryptedSignature, @@ -100,14 +118,16 @@ pub struct EventLoop { swarm: libp2p::Swarm, bitcoin_wallet: Arc, alice_peer_id: PeerId, - request_spot_price: Receiver, - recv_spot_price: Sender, + request_spot_price: Receiver, + recv_spot_price: Sender, start_execution_setup: Receiver, done_execution_setup: Sender>, recv_transfer_proof: Sender, dial_alice: Receiver<()>, conn_established: Sender, send_encrypted_signature: Receiver, + request_quote: Receiver<()>, + recv_quote: Sender, } impl EventLoop { @@ -140,6 +160,8 @@ impl EventLoop { let send_encrypted_signature = Channels::new(); let request_spot_price = Channels::new(); let recv_spot_price = Channels::new(); + let request_quote = Channels::new(); + let recv_quote = Channels::new(); let event_loop = EventLoop { swarm, @@ -153,6 +175,8 @@ impl EventLoop { send_encrypted_signature: send_encrypted_signature.receiver, request_spot_price: request_spot_price.receiver, recv_spot_price: recv_spot_price.sender, + request_quote: request_quote.receiver, + recv_quote: recv_quote.sender, }; let handle = EventLoopHandle { @@ -164,6 +188,8 @@ impl EventLoop { send_encrypted_signature: send_encrypted_signature.sender, request_spot_price: request_spot_price.sender, recv_spot_price: recv_spot_price.receiver, + request_quote: request_quote.sender, + recv_quote: recv_quote.receiver, }; Ok((event_loop, handle)) @@ -180,6 +206,9 @@ impl EventLoop { OutEvent::SpotPriceReceived(msg) => { let _ = self.recv_spot_price.send(msg).await; }, + OutEvent::QuoteReceived(msg) => { + let _ = self.recv_quote.send(msg).await; + }, OutEvent::ExecutionSetupDone(res) => { let _ = self.done_execution_setup.send(res.map(|state|*state)).await; } @@ -216,6 +245,11 @@ impl EventLoop { self.swarm.request_spot_price(self.alice_peer_id, request); } }, + quote_request = self.request_quote.recv().fuse() => { + if quote_request.is_some() { + self.swarm.request_quote(self.alice_peer_id); + } + }, option = self.start_execution_setup.recv().fuse() => { if let Some(state0) = option { let _ = self