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.
This commit is contained in:
Thomas Eizinger 2021-03-03 16:54:47 +11:00
parent 3da01ea44a
commit 601bf07255
No known key found for this signature in database
GPG Key ID: 651AC83A6C6C8B96
7 changed files with 242 additions and 27 deletions

View File

@ -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,

View File

@ -1,4 +1,5 @@
pub mod peer_tracker;
pub mod quote;
pub mod request_response;
pub mod spot_price;
pub mod transport;

53
swap/src/network/quote.rs Normal file
View File

@ -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<CborCodec<BidQuoteProtocol, (), BidQuote>>;
/// 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(),
)
}

View File

@ -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<Response>,
msg: spot_price::Request,
channel: ResponseChannel<spot_price::Response>,
peer: PeerId,
},
QuoteRequested {
channel: ResponseChannel<BidQuote>,
peer: PeerId,
},
ExecutionSetupDone {
@ -78,6 +82,34 @@ impl From<spot_price::OutEvent> for OutEvent {
}
}
impl From<quote::OutEvent> 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<execution_setup::OutEvent> for OutEvent {
fn from(event: execution_setup::OutEvent) -> Self {
use crate::protocol::alice::execution_setup::OutEvent::*;
@ -124,6 +156,7 @@ impl From<encrypted_signature::OutEvent> 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<BidQuote>,
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: Response,
channel: ResponseChannel<spot_price::Response>,
response: spot_price::Response,
) -> Result<()> {
self.spot_price
.send_response(channel, response)

View File

@ -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<BidQuote> {
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,

View File

@ -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<bitcoin::Wallet>,
pub monero_wallet: Arc<monero::Wallet>,
@ -89,7 +92,7 @@ impl Builder {
}
}
pub fn build(self) -> Result<Swap> {
pub fn build(self) -> Result<bob::Swap> {
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<Box<State2>>),
TransferProof {
@ -164,6 +168,38 @@ impl From<spot_price::OutEvent> for OutEvent {
}
}
impl From<quote::OutEvent> 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<execution_setup::OutEvent> for OutEvent {
fn from(event: execution_setup::OutEvent) -> Self {
match event {
@ -206,6 +242,7 @@ impl From<encrypted_signature::OutEvent> 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);
}

View File

@ -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<EncryptedSignature>,
request_spot_price: Sender<spot_price::Request>,
recv_spot_price: Receiver<spot_price::Response>,
request_quote: Sender<()>,
recv_quote: Receiver<BidQuote>,
}
impl EventLoopHandle {
@ -74,7 +77,10 @@ impl EventLoopHandle {
}
pub async fn request_spot_price(&mut self, btc: bitcoin::Amount) -> Result<monero::Amount> {
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<BidQuote> {
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<Behaviour>,
bitcoin_wallet: Arc<bitcoin::Wallet>,
alice_peer_id: PeerId,
request_spot_price: Receiver<Request>,
recv_spot_price: Sender<Response>,
request_spot_price: Receiver<spot_price::Request>,
recv_spot_price: Sender<spot_price::Response>,
start_execution_setup: Receiver<State0>,
done_execution_setup: Sender<Result<State2>>,
recv_transfer_proof: Sender<TransferProof>,
dial_alice: Receiver<()>,
conn_established: Sender<PeerId>,
send_encrypted_signature: Receiver<EncryptedSignature>,
request_quote: Receiver<()>,
recv_quote: Sender<BidQuote>,
}
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