feat(asb, cli): Listen on onion address, dial onion addresses (#203)

This pull requests
- Adds rust native support for the `asb` to listen on an onion service. Previously we were depedent on a seperately running `torc` client. Instead we now use [arti](https://tpo.pages.torproject.net/core/arti/), a rust implementation of the tor protocol.
- Removes the `tor.control_port` and `tor.socks5_port` property from the config of the `asb`
- Adds a new `tor.register_hidden_service` boolean property to the config of the `asb` which when enabled automatically runs a hidden service at startup
- Adds a new `tor.hidden_service_num_intro_points` config property to specify how many introduction points to register the onion service at
- Adds support for the `cli` to dial onion addresses

This is dependent on https://github.com/umgefahren/libp2p-tor/pull/24

Closes https://github.com/UnstoppableSwap/core/issues/16
This commit is contained in:
binarybaron 2024-12-03 21:24:33 +01:00 committed by GitHub
parent 45a4cf4fb7
commit d53c12d64e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 427 additions and 387 deletions

View file

@ -42,7 +42,7 @@ hex = "0.4"
jsonrpsee = { version = "0.16.2", features = [ "server" ] }
jsonrpsee-core = "0.16.2"
libp2p = { version = "0.53.2", features = [ "tcp", "yamux", "dns", "noise", "request-response", "ping", "rendezvous", "identify", "macros", "cbor", "json", "tokio", "serde", "rsa" ] }
libp2p-community-tor = { git = "https://github.com/UnstoppableSwap/libp2p-tor", branch = "main" }
libp2p-community-tor = { git = "https://github.com/UnstoppableSwap/libp2p-tor", branch = "fix/announce-address-immed", features = [ "listen-onion-service" ] }
monero = { version = "0.12", features = [ "serde_support" ] }
monero-rpc = { path = "../monero-rpc" }
once_cell = "1.19"
@ -96,10 +96,6 @@ tokio-tungstenite = { version = "0.15", features = [ "rustls-tls" ] }
tokio-util = { version = "0.7", features = [ "io", "codec" ] }
toml = "0.8"
tor-rtcompat = "0.24.0"
torut = { version = "0.2", default-features = false, features = [
"v3",
"control",
] }
tower = { version = "0.4.13", features = [ "full" ] }
tower-http = { version = "0.3.4", features = [ "full" ] }
tracing = { version = "0.1", features = [ "attributes" ] }

View file

@ -1,10 +1,9 @@
use crate::env::{Mainnet, Testnet};
use crate::fs::{ensure_directory_exists, system_config_dir, system_data_dir};
use crate::tor::{DEFAULT_CONTROL_PORT, DEFAULT_SOCKS5_PORT};
use anyhow::{bail, Context, Result};
use config::ConfigError;
use dialoguer::theme::ColorfulTheme;
use dialoguer::Input;
use dialoguer::{Input, Select};
use libp2p::core::Multiaddr;
use rust_decimal::prelude::FromPrimitive;
use rust_decimal::Decimal;
@ -200,8 +199,8 @@ pub struct Monero {
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TorConf {
pub control_port: u16,
pub socks5_port: u16,
pub register_hidden_service: bool,
pub hidden_service_num_intro_points: u8,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
@ -219,8 +218,8 @@ pub struct Maker {
impl Default for TorConf {
fn default() -> Self {
Self {
control_port: DEFAULT_CONTROL_PORT,
socks5_port: DEFAULT_SOCKS5_PORT,
register_hidden_service: true,
hidden_service_num_intro_points: 5,
}
}
}
@ -313,15 +312,12 @@ pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
.default(defaults.monero_wallet_rpc_url)
.interact_text()?;
let tor_control_port = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter Tor control port or hit enter to use default. If Tor is not running on your machine, no hidden service will be created.")
.default(DEFAULT_CONTROL_PORT.to_owned())
.interact_text()?;
let tor_socks5_port = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter Tor socks5 port or hit enter to use default")
.default(DEFAULT_SOCKS5_PORT.to_owned())
.interact_text()?;
let register_hidden_service = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Do you want a Tor hidden service to be created? This will allow you to run from behind a firewall without opening ports, and hide your IP address. You do not have to run a Tor daemon yourself. We recommend this for most users. (y/n)")
.items(&["yes", "no"])
.default(0)
.interact()?
== 0;
let min_buy = Input::with_theme(&ColorfulTheme::default())
.with_prompt("Enter minimum Bitcoin amount you are willing to accept per swap or hit enter to use default.")
@ -387,8 +383,8 @@ pub fn query_user_for_initial_config(testnet: bool) -> Result<Config> {
network: monero_network,
},
tor: TorConf {
control_port: tor_control_port,
socks5_port: tor_socks5_port,
register_hidden_service,
..Default::default()
},
maker: Maker {
min_buy_btc: min_buy,

View file

@ -490,7 +490,7 @@ where
)
})?;
tracing::debug!(%ask_price, %xmr_balance, %max_bitcoin_for_monero, "Computed quote");
tracing::trace!(%ask_price, %xmr_balance, %max_bitcoin_for_monero, "Computed quote");
if min_buy > max_bitcoin_for_monero {
tracing::trace!(

View file

@ -22,16 +22,81 @@ use std::time::Duration;
use uuid::Uuid;
pub mod transport {
use libp2p::{dns, identity, tcp, Transport};
use std::sync::Arc;
use arti_client::{config::onion_service::OnionServiceConfigBuilder, TorClient};
use libp2p::{core::transport::OptionalTransport, dns, identity, tcp, Transport};
use libp2p_community_tor::AddressConversion;
use tor_rtcompat::tokio::TokioRustlsRuntime;
use super::*;
static ASB_ONION_SERVICE_NICKNAME: &str = "asb";
static ASB_ONION_SERVICE_PORT: u16 = 9939;
type OnionTransportWithAddresses = (Boxed<(PeerId, StreamMuxerBox)>, Vec<Multiaddr>);
/// Creates the libp2p transport for the ASB.
pub fn new(identity: &identity::Keypair) -> Result<Boxed<(PeerId, StreamMuxerBox)>> {
let tcp = tcp::tokio::Transport::new(tcp::Config::new().nodelay(true));
///
/// If you pass in a `None` for `maybe_tor_client`, the ASB will not use Tor at all.
///
/// If you pass in a `Some(tor_client)`, the ASB will listen on an onion service and return
/// the onion address. If it fails to listen on the onion address, it will only use tor for
/// dialing and not listening.
pub fn new(
identity: &identity::Keypair,
maybe_tor_client: Option<Arc<TorClient<TokioRustlsRuntime>>>,
num_intro_points: u8,
register_hidden_service: bool,
) -> Result<OnionTransportWithAddresses> {
let (maybe_tor_transport, onion_addresses) = if let Some(tor_client) = maybe_tor_client {
let mut tor_transport = libp2p_community_tor::TorTransport::from_client(
tor_client,
AddressConversion::DnsOnly,
);
let addresses = if register_hidden_service {
let onion_service_config = OnionServiceConfigBuilder::default()
.nickname(
ASB_ONION_SERVICE_NICKNAME
.parse()
.expect("Static nickname to be valid"),
)
.num_intro_points(num_intro_points)
.build()
.expect("We specified a valid nickname");
match tor_transport.add_onion_service(onion_service_config, ASB_ONION_SERVICE_PORT)
{
Ok(addr) => {
tracing::debug!(
%addr,
"Setting up onion service for libp2p to listen on"
);
vec![addr]
}
Err(err) => {
tracing::warn!(error=%err, "Failed to listen on onion address");
vec![]
}
}
} else {
vec![]
};
(OptionalTransport::some(tor_transport), addresses)
} else {
(OptionalTransport::none(), vec![])
};
let tcp = maybe_tor_transport
.or_transport(tcp::tokio::Transport::new(tcp::Config::new().nodelay(true)));
let tcp_with_dns = dns::tokio::Transport::system(tcp)?;
authenticate_and_multiplex(tcp_with_dns.boxed(), identity)
Ok((
authenticate_and_multiplex(tcp_with_dns.boxed(), identity)?,
onion_addresses,
))
}
}

View file

@ -14,12 +14,9 @@
use anyhow::{bail, Context, Result};
use comfy_table::Table;
use libp2p::core::multiaddr::Protocol;
use libp2p::core::Multiaddr;
use libp2p::Swarm;
use std::convert::TryInto;
use std::env;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use structopt::clap;
use structopt::clap::ErrorKind;
@ -28,6 +25,7 @@ use swap::asb::config::{
initial_setup, query_user_for_initial_config, read_config, Config, ConfigNotInitialized,
};
use swap::asb::{cancel, punish, redeem, refund, safely_abort, EventLoop, Finality, KrakenRate};
use swap::common::tor::init_tor_client;
use swap::common::tracing_util::Format;
use swap::common::{self, get_logs, warn_if_outdated};
use swap::database::{open_db, AccessMode};
@ -35,8 +33,7 @@ use swap::network::rendezvous::XmrBtcNamespace;
use swap::network::swarm;
use swap::protocol::alice::{run, AliceState};
use swap::seed::Seed;
use swap::tor::AuthenticatedClient;
use swap::{bitcoin, kraken, monero, tor};
use swap::{bitcoin, kraken, monero};
use tracing_subscriber::filter::LevelFilter;
const DEFAULT_WALLET_NAME: &str = "asb-wallet";
@ -149,29 +146,16 @@ pub async fn main() -> Result<()> {
let bitcoin_balance = bitcoin_wallet.balance().await?;
tracing::info!(%bitcoin_balance, "Bitcoin wallet balance");
// Connect to Kraken
let kraken_price_updates = kraken::connect(config.maker.price_ticker_ws_url.clone())?;
// Setup Tor hidden services
let tor_client =
tor::Client::new(config.tor.socks5_port).with_control_port(config.tor.control_port);
let _ac = match tor_client.assert_tor_running().await {
Ok(_) => {
tracing::info!("Setting up Tor hidden service");
let ac =
register_tor_services(config.network.clone().listen, tor_client, &seed)
.await?;
Some(ac)
}
Err(_) => {
tracing::warn!("Tor not found. Running on clear net");
None
}
};
let kraken_rate = KrakenRate::new(config.maker.ask_spread, kraken_price_updates);
let namespace = XmrBtcNamespace::from_is_testnet(testnet);
let mut swarm = swarm::asb(
// Initialize Tor client
let tor_client = init_tor_client(&config.data.dir).await?.into();
let (mut swarm, onion_addresses) = swarm::asb(
&seed,
config.maker.min_buy_btc,
config.maker.max_buy_btc,
@ -180,6 +164,8 @@ pub async fn main() -> Result<()> {
env_config,
namespace,
&rendezvous_addrs,
tor_client,
config.tor,
)?;
for listen in config.network.listen.clone() {
@ -188,10 +174,25 @@ pub async fn main() -> Result<()> {
}
}
for onion_address in onion_addresses {
match swarm.listen_on(onion_address.clone()) {
Err(e) => {
tracing::warn!(
"Failed to listen on onion address {}: {}",
onion_address,
e
);
}
_ => {
swarm.add_external_address(onion_address);
}
}
}
tracing::info!(peer_id = %swarm.local_peer_id(), "Network layer initialized");
for external_address in config.network.external_addresses {
Swarm::add_external_address(&mut swarm, external_address);
swarm.add_external_address(external_address);
}
let (event_loop, mut swap_receiver) = EventLoop::new(
@ -391,46 +392,3 @@ async fn init_monero_wallet(
Ok(wallet)
}
/// Registers a hidden service for each network.
/// Note: Once ac goes out of scope, the services will be de-registered.
async fn register_tor_services(
networks: Vec<Multiaddr>,
tor_client: tor::Client,
seed: &Seed,
) -> Result<AuthenticatedClient> {
let mut ac = tor_client.into_authenticated_client().await?;
let hidden_services_details = networks
.iter()
.flat_map(|network| {
network.iter().map(|protocol| match protocol {
Protocol::Tcp(port) => Some((
port,
SocketAddr::new(IpAddr::from(Ipv4Addr::new(127, 0, 0, 1)), port),
)),
_ => {
// We only care for Tcp for now.
None
}
})
})
.flatten()
.collect::<Vec<_>>();
let key = seed.derive_torv3_key();
ac.add_services(&hidden_services_details, &key).await?;
let onion_address = key
.public()
.get_onion_address()
.get_address_without_dot_onion();
hidden_services_details.iter().for_each(|(port, _)| {
let onion_address = format!("/onion3/{}:{}", onion_address, port);
tracing::info!(%onion_address, "Successfully created hidden service");
});
Ok(ac)
}

View file

@ -4,7 +4,6 @@ pub mod cancel_and_refund;
pub mod command;
mod event_loop;
mod list_sellers;
mod tor;
pub mod transport;
pub mod watcher;

View file

@ -2,6 +2,7 @@ pub mod request;
pub mod tauri_bindings;
use crate::cli::command::{Bitcoin, Monero};
use crate::common::tor::init_tor_client;
use crate::common::tracing_util::Format;
use crate::database::{open_db, AccessMode};
use crate::env::{Config as EnvConfig, GetConfig, Mainnet, Testnet};
@ -29,7 +30,6 @@ use tracing::Level;
use url::Url;
use uuid::Uuid;
use super::tor::init_tor_client;
use super::watcher::Watcher;
static START: Once = Once::new();

View file

@ -1,3 +1,4 @@
pub mod tor;
pub mod tracing_util;
use anyhow::anyhow;

View file

@ -21,6 +21,8 @@ pub async fn init_tor_client(data_dir: &Path) -> Result<Arc<TorClient<TokioRustl
// It uses cached information when possible.)
let runtime = TokioRustlsRuntime::current().expect("We are always running with tokio");
tracing::debug!("Bootstrapping Tor client");
let tor_client = TorClient::with_runtime(runtime)
.config(config)
.create_bootstrapped()

View file

@ -31,9 +31,19 @@ pub fn init(
dir: impl AsRef<Path>,
tauri_handle: Option<TauriHandle>,
) -> Result<()> {
// file logger will always write in JSON format and with timestamps
let ALL_CRATES: Vec<&str> = vec![
"swap",
"asb",
"libp2p_community_tor",
"unstoppableswap-gui-rs",
"arti",
];
let OUR_CRATES: Vec<&str> = vec!["swap", "asb"];
// General log file for non-verbose logs
let file_appender: RollingFileAppender = tracing_appender::rolling::never(&dir, "swap-all.log");
// Verbose log file, rotated hourly, with a maximum of 24 files
let tracing_file_appender: RollingFileAppender = RollingFileAppender::builder()
.rotation(Rotation::HOURLY)
.filename_prefix("tracing")
@ -42,24 +52,31 @@ pub fn init(
.build(&dir)
.expect("initializing rolling file appender failed");
// Log to file
// Layer for writing to the general log file
// Crates: swap, asb
// Level: Passed in
let file_layer = fmt::layer()
.with_writer(file_appender)
.with_ansi(false)
.with_timer(UtcTime::rfc_3339())
.with_target(false)
.json()
.with_filter(env_filter(level_filter)?);
.with_filter(env_filter(level_filter, OUR_CRATES.clone())?);
// Layer for writing to the verbose log file
// Crates: swap, asb, libp2p_community_tor, unstoppableswap-gui-rs, arti (all relevant crates)
// Level: TRACE
let tracing_file_layer = fmt::layer()
.with_writer(tracing_file_appender)
.with_ansi(false)
.with_timer(UtcTime::rfc_3339())
.with_target(false)
.json()
.with_filter(env_filter(LevelFilter::TRACE)?);
.with_filter(env_filter(LevelFilter::TRACE, ALL_CRATES.clone())?);
// Log to stdout
// Layer for writing to the terminal
// Crates: swap, asb
// Level: Passed in
let is_terminal = atty::is(atty::Stream::Stderr);
let terminal_layer = fmt::layer()
.with_writer(std::io::stdout)
@ -67,17 +84,23 @@ pub fn init(
.with_timer(UtcTime::rfc_3339())
.with_target(false);
// Forwards logs to the tauri guest
// Layer for writing to the Tauri guest. This will be displayed in the GUI.
// Crates: swap, asb, libp2p_community_tor, unstoppableswap-gui-rs, arti
// Level: Passed in
let tauri_layer = fmt::layer()
.with_writer(TauriWriter::new(tauri_handle))
.with_ansi(false)
.with_timer(UtcTime::rfc_3339())
.with_target(true)
.json()
.with_filter(env_filter(level_filter)?);
.with_filter(env_filter(level_filter, ALL_CRATES.clone())?);
let env_filtered = env_filter(level_filter)?;
// We only log the bare minimum to the terminal
// Crates: swap, asb
// Level: Passed in
let env_filtered = env_filter(level_filter, OUR_CRATES.clone())?;
// Apply the environment filter and box the layer for the terminal
let final_terminal_layer = match format {
Format::Json => terminal_layer.json().with_filter(env_filtered).boxed(),
Format::Raw => terminal_layer.with_filter(env_filtered).boxed(),
@ -97,19 +120,18 @@ pub fn init(
}
/// This function controls which crate's logs actually get logged and from which level.
fn env_filter(level_filter: LevelFilter) -> Result<EnvFilter> {
Ok(EnvFilter::from_default_env()
.add_directive(Directive::from_str(&format!("asb={}", &level_filter))?)
.add_directive(Directive::from_str(&format!("swap={}", &level_filter))?)
.add_directive(Directive::from_str(&format!("arti={}", &level_filter))?)
.add_directive(Directive::from_str(&format!(
"libp2p_community_tor={}",
&level_filter
))?)
.add_directive(Directive::from_str(&format!(
"unstoppableswap-gui-rs={}",
&level_filter
))?))
fn env_filter(level_filter: LevelFilter, crates: Vec<&str>) -> Result<EnvFilter> {
let mut filter = EnvFilter::from_default_env();
// Add directives for each crate in the provided list
for crate_name in crates {
filter = filter.add_directive(Directive::from_str(&format!(
"{}={}",
crate_name, &level_filter
))?);
}
Ok(filter)
}
/// A writer that forwards tracing log messages to the tauri guest.

View file

@ -31,7 +31,6 @@ pub mod network;
pub mod protocol;
pub mod rpc;
pub mod seed;
pub mod tor;
pub mod tracing_ext;
#[cfg(test)]

View file

@ -1,3 +1,4 @@
use crate::asb::config::TorConf;
use crate::asb::{LatestRate, RendezvousNode};
use crate::libp2p_ext::MultiAddrExt;
use crate::network::rendezvous::XmrBtcNamespace;
@ -23,7 +24,9 @@ pub fn asb<LR>(
env_config: env::Config,
namespace: XmrBtcNamespace,
rendezvous_addrs: &[Multiaddr],
) -> Result<Swarm<asb::Behaviour<LR>>>
maybe_tor_client: Option<Arc<TorClient<TokioRustlsRuntime>>>,
tor_conf: TorConf,
) -> Result<(Swarm<asb::Behaviour<LR>>, Vec<Multiaddr>)>
where
LR: LatestRate + Send + 'static + Debug + Clone,
{
@ -50,7 +53,12 @@ where
rendezvous_nodes,
);
let transport = asb::transport::new(&identity)?;
let (transport, onion_addresses) = asb::transport::new(
&identity,
maybe_tor_client,
tor_conf.hidden_service_num_intro_points,
tor_conf.register_hidden_service,
)?;
let swarm = SwarmBuilder::with_existing_identity(identity)
.with_tokio()
@ -59,7 +67,7 @@ where
.with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::MAX))
.build();
Ok(swarm)
Ok((swarm, onion_addresses))
}
pub async fn cli<T>(

View file

@ -12,7 +12,6 @@ use std::fmt;
use std::fs::{self, File};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use torut::onion::TorSecretKeyV3;
pub const SEED_LENGTH: usize = 32;
@ -47,14 +46,6 @@ impl Seed {
identity::Keypair::ed25519_from_bytes(bytes).expect("we always pass 32 bytes")
}
pub fn derive_torv3_key(&self) -> TorSecretKeyV3 {
let bytes = self.derive(b"TOR").bytes();
let sk = ed25519_dalek::SecretKey::from_bytes(&bytes)
.expect("Failed to create a new extended secret key for Tor.");
let esk = ed25519_dalek::ExpandedSecretKey::from(&sk);
esk.to_bytes().into()
}
pub fn from_file_or_generate(data_dir: &Path) -> Result<Self, Error> {
let file_path_buf = data_dir.join("seed.pem");
let file_path = Path::new(&file_path_buf);

View file

@ -1,128 +0,0 @@
use anyhow::{bail, Context, Result};
use std::future::Future;
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use tokio::net::TcpStream;
use torut::control::{AsyncEvent, AuthenticatedConn, ConnError, UnauthenticatedConn};
use torut::onion::TorSecretKeyV3;
pub const DEFAULT_SOCKS5_PORT: u16 = 9050;
pub const DEFAULT_CONTROL_PORT: u16 = 9051;
#[derive(Debug, Clone, Copy)]
pub struct Client {
socks5_address: SocketAddrV4,
control_port_address: SocketAddr,
}
impl Default for Client {
fn default() -> Self {
Self {
socks5_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, DEFAULT_SOCKS5_PORT),
control_port_address: SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::LOCALHOST,
DEFAULT_CONTROL_PORT,
)),
}
}
}
impl Client {
pub fn new(socks5_port: u16) -> Self {
Self {
socks5_address: SocketAddrV4::new(Ipv4Addr::LOCALHOST, socks5_port),
control_port_address: SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::LOCALHOST,
DEFAULT_CONTROL_PORT,
)),
}
}
pub fn with_control_port(self, control_port: u16) -> Self {
Self {
control_port_address: SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::LOCALHOST,
control_port,
)),
..self
}
}
/// checks if tor is running
pub async fn assert_tor_running(&self) -> Result<()> {
// Make sure you are running tor and this is your socks port
let proxy = reqwest::Proxy::all(format!("socks5h://{}", self.socks5_address).as_str())
.context("Failed to construct Tor proxy URL")?;
let client = reqwest::Client::builder().proxy(proxy).build()?;
let res = client.get("https://check.torproject.org").send().await?;
let text = res.text().await?;
if !text.contains("Congratulations. This browser is configured to use Tor.") {
bail!("Tor is currently not running")
}
Ok(())
}
async fn init_unauthenticated_connection(&self) -> Result<UnauthenticatedConn<TcpStream>> {
// Connect to local tor service via control port
let sock = TcpStream::connect(self.control_port_address).await?;
let uc = UnauthenticatedConn::new(sock);
Ok(uc)
}
/// Create a new authenticated connection to your local Tor service
pub async fn into_authenticated_client(self) -> Result<AuthenticatedClient> {
self.assert_tor_running().await?;
let mut uc = self
.init_unauthenticated_connection()
.await
.context("Failed to connect to Tor")?;
let tor_info = uc
.load_protocol_info()
.await
.context("Failed to load protocol info from Tor")?;
let tor_auth_data = tor_info
.make_auth_data()?
.context("Failed to make Tor auth data")?;
// Get an authenticated connection to the Tor via the Tor Controller protocol.
uc.authenticate(&tor_auth_data)
.await
.context("Failed to authenticate with Tor")?;
Ok(AuthenticatedClient {
inner: uc.into_authenticated().await,
})
}
pub fn tor_proxy_port(&self) -> u16 {
self.socks5_address.port()
}
}
type Handler = fn(AsyncEvent<'_>) -> Box<dyn Future<Output = Result<(), ConnError>> + Unpin>;
#[allow(missing_debug_implementations)]
pub struct AuthenticatedClient {
inner: AuthenticatedConn<TcpStream, Handler>,
}
impl AuthenticatedClient {
/// Add an ephemeral tor service on localhost with the provided key
/// `service_port` and `onion_port` can be different but don't have to as
/// they are on different networks.
pub async fn add_services(
&mut self,
services: &[(u16, SocketAddr)],
tor_key: &TorSecretKeyV3,
) -> Result<()> {
let mut listeners = services.iter();
self.inner
.add_onion_v3(tor_key, false, false, false, None, &mut listeners)
.await
.context("Failed to add onion service")
}
}

View file

@ -243,7 +243,7 @@ async fn start_alice(
let latest_rate = FixedRate::default();
let resume_only = false;
let mut swarm = swarm::asb(
let (mut swarm, _) = swarm::asb(
seed,
min_buy,
max_buy,
@ -252,6 +252,7 @@ async fn start_alice(
env_config,
XmrBtcNamespace::Testnet,
&[],
None,
)
.unwrap();
swarm.listen_on(listen_address).unwrap();