diff --git a/swap/src/asb.rs b/swap/src/asb.rs index 2bd9ae4c..fccd2f8b 100644 --- a/swap/src/asb.rs +++ b/swap/src/asb.rs @@ -16,3 +16,6 @@ pub use recovery::redeem::{redeem, Finality}; pub use recovery::refund::refund; pub use recovery::safely_abort::safely_abort; pub use recovery::{cancel, refund}; + +#[cfg(test)] +pub use network::rendezous; diff --git a/swap/src/asb/network.rs b/swap/src/asb/network.rs index b6e621c0..1ae68d90 100644 --- a/swap/src/asb/network.rs +++ b/swap/src/asb/network.rs @@ -170,7 +170,7 @@ pub mod behaviour { } } -mod rendezous { +pub mod rendezous { use super::*; use std::pin::Pin; diff --git a/swap/src/cli.rs b/swap/src/cli.rs index 5c88f133..c15ac410 100644 --- a/swap/src/cli.rs +++ b/swap/src/cli.rs @@ -12,3 +12,168 @@ pub use cancel::cancel; pub use event_loop::{EventLoop, EventLoopHandle}; pub use list_sellers::list_sellers; pub use refund::refund; + +#[cfg(test)] +mod tests { + use super::*; + use crate::asb; + use crate::cli::list_sellers::Seller; + use crate::network::quote; + use crate::network::quote::BidQuote; + use crate::network::rendezvous::XmrBtcNamespace; + use crate::network::test::{new_swarm, SwarmExt}; + use futures::StreamExt; + use libp2p::multiaddr::Protocol; + use libp2p::request_response::RequestResponseEvent; + use libp2p::swarm::{AddressScore, NetworkBehaviourEventProcess}; + use libp2p::{identity, Multiaddr, PeerId}; + use std::collections::HashSet; + use std::iter::FromIterator; + use std::time::Duration; + + #[tokio::test] + async fn list_sellers_should_report_all_registered_asbs_with_a_quote() { + let namespace = XmrBtcNamespace::Mainnet; + let (rendezvous_address, rendezvous_peer_id) = setup_rendezvous_point().await; + let expected_seller_1 = + setup_asb(rendezvous_peer_id, rendezvous_address.clone(), namespace).await; + let expected_seller_2 = + setup_asb(rendezvous_peer_id, rendezvous_address.clone(), namespace).await; + + let list_sellers = list_sellers( + rendezvous_peer_id, + rendezvous_address, + namespace, + 0, + identity::Keypair::generate_ed25519(), + ); + let sellers = tokio::time::timeout(Duration::from_secs(15), list_sellers) + .await + .unwrap() + .unwrap(); + + assert_eq!( + HashSet::::from_iter(sellers), + HashSet::::from_iter([expected_seller_1, expected_seller_2]) + ) + } + + async fn setup_rendezvous_point() -> (Multiaddr, PeerId) { + let mut rendezvous_node = new_swarm(|_, identity| RendezvousPointBehaviour { + rendezvous: libp2p::rendezvous::Rendezvous::new( + identity, + libp2p::rendezvous::Config::default(), + ), + ping: Default::default(), + }); + let rendezvous_address = rendezvous_node.listen_on_tcp_localhost().await; + let rendezvous_peer_id = *rendezvous_node.local_peer_id(); + + tokio::spawn(async move { + loop { + rendezvous_node.next().await; + } + }); + + (rendezvous_address, rendezvous_peer_id) + } + + async fn setup_asb( + rendezvous_peer_id: PeerId, + rendezvous_address: Multiaddr, + namespace: XmrBtcNamespace, + ) -> Seller { + let static_quote = BidQuote { + price: bitcoin::Amount::from_sat(1337), + min_quantity: bitcoin::Amount::from_sat(42), + max_quantity: bitcoin::Amount::from_sat(9001), + }; + + let mut asb = new_swarm(|_, identity| StaticQuoteAsbBehaviour { + rendezvous: asb::rendezous::Behaviour::new( + identity, + rendezvous_peer_id, + rendezvous_address, + namespace, + None, + ), + ping: Default::default(), + quote: quote::asb(), + static_quote, + registered: false, + }); + + let asb_address = asb.listen_on_tcp_localhost().await; + asb.add_external_address(asb_address.clone(), AddressScore::Infinite); + + let asb_peer_id = *asb.local_peer_id(); + + // avoid race condition where `list_sellers` tries to discover before we are + // registered block this function until we are registered + while !asb.behaviour().registered { + asb.next().await; + } + + tokio::spawn(async move { + loop { + asb.next().await; + } + }); + + Seller { + multiaddr: asb_address.with(Protocol::P2p(asb_peer_id.into())), + quote: static_quote, + } + } + + #[derive(libp2p::NetworkBehaviour)] + struct StaticQuoteAsbBehaviour { + rendezvous: asb::rendezous::Behaviour, + // Support `Ping` as a workaround until https://github.com/libp2p/rust-libp2p/issues/2109 is fixed. + ping: libp2p::ping::Ping, + quote: quote::Behaviour, + + #[behaviour(ignore)] + static_quote: BidQuote, + #[behaviour(ignore)] + registered: bool, + } + + impl NetworkBehaviourEventProcess for StaticQuoteAsbBehaviour { + fn inject_event(&mut self, event: libp2p::rendezvous::Event) { + if let libp2p::rendezvous::Event::Registered { .. } = event { + self.registered = true; + } + } + } + impl NetworkBehaviourEventProcess for StaticQuoteAsbBehaviour { + fn inject_event(&mut self, _: libp2p::ping::PingEvent) {} + } + impl NetworkBehaviourEventProcess for StaticQuoteAsbBehaviour { + fn inject_event(&mut self, event: quote::OutEvent) { + if let RequestResponseEvent::Message { + message: quote::Message::Request { channel, .. }, + .. + } = event + { + self.quote + .send_response(channel, self.static_quote) + .unwrap(); + } + } + } + + #[derive(libp2p::NetworkBehaviour)] + struct RendezvousPointBehaviour { + rendezvous: libp2p::rendezvous::Rendezvous, + // Support `Ping` as a workaround until https://github.com/libp2p/rust-libp2p/issues/2109 is fixed. + ping: libp2p::ping::Ping, + } + + impl NetworkBehaviourEventProcess for RendezvousPointBehaviour { + fn inject_event(&mut self, _: libp2p::rendezvous::Event) {} + } + impl NetworkBehaviourEventProcess for RendezvousPointBehaviour { + fn inject_event(&mut self, _: libp2p::ping::PingEvent) {} + } +} diff --git a/swap/src/cli/list_sellers.rs b/swap/src/cli/list_sellers.rs index b0df7932..1710c340 100644 --- a/swap/src/cli/list_sellers.rs +++ b/swap/src/cli/list_sellers.rs @@ -1,7 +1,7 @@ use crate::network::quote::BidQuote; use crate::network::rendezvous::XmrBtcNamespace; use crate::network::{quote, swarm}; -use anyhow::Result; +use anyhow::{Context, Result}; use futures::StreamExt; use libp2p::multiaddr::Protocol; use libp2p::ping::{Ping, PingConfig, PingEvent}; @@ -32,7 +32,13 @@ pub async fn list_sellers( }; let mut swarm = swarm::cli(identity, tor_socks5_port, behaviour).await?; - let _ = swarm.dial_addr(rendezvous_node_addr.clone()); + swarm + .behaviour_mut() + .quote + .add_address(&rendezvous_node_peer_id, rendezvous_node_addr.clone()); + swarm + .dial(&rendezvous_node_peer_id) + .context("Failed to dial rendezvous node")?; let event_loop = EventLoop::new( swarm, @@ -46,7 +52,7 @@ pub async fn list_sellers( } #[serde_as] -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, PartialEq, Eq, Hash)] pub struct Seller { #[serde_as(as = "DisplayFromStr")] pub multiaddr: Multiaddr, @@ -87,6 +93,7 @@ enum QuoteStatus { Received(BidQuote), } +#[derive(Debug)] enum State { WaitForDiscovery, WaitForQuoteCompletion, @@ -264,6 +271,7 @@ impl EventLoop { } } +#[derive(Debug)] struct StillPending {} impl From for OutEvent { diff --git a/swap/src/network/quote.rs b/swap/src/network/quote.rs index fc2d93f4..0ddf1af0 100644 --- a/swap/src/network/quote.rs +++ b/swap/src/network/quote.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; const PROTOCOL: &str = "/comit/xmr/btc/bid-quote/1.0.0"; pub type OutEvent = RequestResponseEvent<(), BidQuote>; -type Message = RequestResponseMessage<(), BidQuote>; +pub type Message = RequestResponseMessage<(), BidQuote>; pub type Behaviour = RequestResponse>; @@ -24,7 +24,7 @@ impl ProtocolName for BidQuoteProtocol { } /// Represents a quote for buying XMR. -#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct BidQuote { /// The price at which the maker is willing to buy at. #[serde(with = "::bitcoin::util::amount::serde::as_sat")]