diff --git a/CHANGELOG.md b/CHANGELOG.md index 5914aabe..5ac46f25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,11 +43,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 1. Balance of ASB too low 2. Buy amount sent by CLI exceeds maximum buy amount accepted by ASB 3. ASB is running in resume-only mode and does not accept incoming swap requests +- An issue where the monero daemon port used by the `monero-wallet-rpc` could not be specified. + The CLI parameter `--monero-daemon-host` was changed to `--monero-daemon-address` where host and port have to be specified. ### Changed - The ASB's `--max-buy` and `ask-spread` parameter were removed in favour of entries in the config file. The initial setup includes setting these two values now. +- From this version on the CLI and ASB run on **mainnet** by default! + When running either application with `--testnet` Monero network defaults to `stagenet` and Bitcoin network to `testnet3`. + This is a breaking change. + It is recommended to run the applications with `--testnet` first and not just run the application on `mainnet` without experience. ## [0.5.0] - 2021-04-17 diff --git a/swap/src/asb/command.rs b/swap/src/asb/command.rs index b669e5fb..e19b893f 100644 --- a/swap/src/asb/command.rs +++ b/swap/src/asb/command.rs @@ -10,6 +10,9 @@ use uuid::Uuid; author )] pub struct Arguments { + #[structopt(long, help = "Swap on testnet")] + pub testnet: bool, + #[structopt( short, long = "json", diff --git a/swap/src/asb/config.rs b/swap/src/asb/config.rs index f1709964..984bc3ee 100644 --- a/swap/src/asb/config.rs +++ b/swap/src/asb/config.rs @@ -1,3 +1,4 @@ +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}; @@ -11,14 +12,71 @@ use serde::{Deserialize, Serialize}; use std::ffi::OsStr; use std::fs; use std::path::{Path, PathBuf}; +use std::str::FromStr; use tracing::info; use url::Url; -const DEFAULT_LISTEN_ADDRESS_TCP: &str = "/ip4/0.0.0.0/tcp/9939"; -const DEFAULT_LISTEN_ADDRESS_WS: &str = "/ip4/0.0.0.0/tcp/9940/ws"; -const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://electrum.blockstream.info:60002"; -const DEFAULT_MONERO_WALLET_RPC_TESTNET_URL: &str = "http://127.0.0.1:38083/json_rpc"; -const DEFAULT_BITCOIN_CONFIRMATION_TARGET: usize = 3; +pub trait GetDefaults { + fn getConfigFileDefaults() -> Result; +} + +pub struct Defaults { + pub config_path: PathBuf, + data_dir: PathBuf, + listen_address_tcp: Multiaddr, + listen_address_ws: Multiaddr, + electrum_rpc_url: Url, + monero_wallet_rpc_url: Url, + bitcoin_confirmation_target: usize, +} + +impl GetDefaults for Testnet { + fn getConfigFileDefaults() -> Result { + let defaults = Defaults { + config_path: default_asb_config_dir()? + .join("testnet") + .join("config.toml"), + data_dir: default_asb_data_dir()?.join("testnet"), + listen_address_tcp: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9939")?, + listen_address_ws: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9940/ws")?, + electrum_rpc_url: Url::parse("ssl://electrum.blockstream.info:60002")?, + monero_wallet_rpc_url: Url::parse("http://127.0.0.1:38083/json_rpc")?, + bitcoin_confirmation_target: 1, + }; + + Ok(defaults) + } +} + +impl GetDefaults for Mainnet { + fn getConfigFileDefaults() -> Result { + let defaults = Defaults { + config_path: default_asb_config_dir()? + .join("mainnet") + .join("config.toml"), + data_dir: default_asb_data_dir()?.join("mainnet"), + listen_address_tcp: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9939")?, + listen_address_ws: Multiaddr::from_str("/ip4/0.0.0.0/tcp/9940/ws")?, + electrum_rpc_url: Url::parse("ssl://electrum.blockstream.info:50002")?, + monero_wallet_rpc_url: Url::parse("http://127.0.0.1:18083/json_rpc")?, + bitcoin_confirmation_target: 3, + }; + + Ok(defaults) + } +} + +fn default_asb_config_dir() -> Result { + system_config_dir() + .map(|dir| Path::join(&dir, "asb")) + .context("Could not generate default config file path") +} + +fn default_asb_data_dir() -> Result { + system_data_dir() + .map(|dir| Path::join(&dir, "asb")) + .context("Could not generate default config file path") +} const DEFAULT_MIN_BUY_AMOUNT: f64 = 0.002f64; const DEFAULT_MAX_BUY_AMOUNT: f64 = 0.02f64; @@ -64,12 +122,18 @@ pub struct Network { pub struct Bitcoin { pub electrum_rpc_url: Url, pub target_block: usize, + pub finality_confirmations: Option, + #[serde(with = "crate::bitcoin::network")] + pub network: bitcoin::Network, } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct Monero { pub wallet_rpc_url: Url, + pub finality_confirmations: Option, + #[serde(with = "crate::monero::network")] + pub network: monero::Network, } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] @@ -118,31 +182,8 @@ pub fn read_config(config_path: PathBuf) -> Result Result { - system_config_dir() - .map(|dir| Path::join(&dir, "asb")) - .map(|dir| Path::join(&dir, "config.toml")) - .context("Could not generate default config file path") -} - -/// Default location for storing data for the CLI -// Takes the default system data-dir and adds a `/asb` -fn default_data_dir() -> Result { - system_data_dir() - .map(|proj_dir| Path::join(&proj_dir, "asb")) - .context("Could not generate default data dir") -} - -pub fn initial_setup(config_path: PathBuf, config_file: F) -> Result<()> -where - F: Fn() -> Result, -{ - info!("Config file not found, running initial setup..."); - let initial_config = config_file()?; - - let toml = toml::to_string(&initial_config)?; +pub fn initial_setup(config_path: PathBuf, config: Config) -> Result<()> { + let toml = toml::to_string(&config)?; ensure_directory_exists(config_path.as_path())?; fs::write(&config_path, toml)?; @@ -154,13 +195,30 @@ where Ok(()) } -pub fn query_user_for_initial_testnet_config() -> Result { +pub fn query_user_for_initial_config(testnet: bool) -> Result { + let (bitcoin_network, monero_network, defaults) = if testnet { + tracing::info!("Running initial setup for testnet"); + + let bitcoin_network = bitcoin::Network::Testnet; + let monero_network = monero::Network::Stagenet; + let defaults = Testnet::getConfigFileDefaults()?; + + (bitcoin_network, monero_network, defaults) + } else { + tracing::info!("Running initial setup for mainnet"); + let bitcoin_network = bitcoin::Network::Bitcoin; + let monero_network = monero::Network::Mainnet; + let defaults = Mainnet::getConfigFileDefaults()?; + + (bitcoin_network, monero_network, defaults) + }; + println!(); let data_dir = Input::with_theme(&ColorfulTheme::default()) .with_prompt("Enter data directory for asb or hit return to use default") .default( - default_data_dir() - .context("No default data dir value for this system")? + defaults + .data_dir .to_str() .context("Unsupported characters in default path")? .to_string(), @@ -170,28 +228,27 @@ pub fn query_user_for_initial_testnet_config() -> Result { let target_block = Input::with_theme(&ColorfulTheme::default()) .with_prompt("How fast should your Bitcoin transactions be confirmed? Your transaction fee will be calculated based on this target. Hit return to use default") - .default(DEFAULT_BITCOIN_CONFIRMATION_TARGET) + .default(defaults.bitcoin_confirmation_target) .interact_text()?; + let listen_addresses = Input::with_theme(&ColorfulTheme::default()) .with_prompt("Enter multiaddresses (comma separated) on which asb should list for peer-to-peer communications or hit return to use default") - .default( format!("{},{}", DEFAULT_LISTEN_ADDRESS_TCP, DEFAULT_LISTEN_ADDRESS_WS)) + .default( format!("{},{}", defaults.listen_address_tcp, defaults.listen_address_ws)) .interact_text()?; let listen_addresses = listen_addresses .split(',') .map(|str| str.parse()) .collect::, _>>()?; - let electrum_rpc_url: String = Input::with_theme(&ColorfulTheme::default()) + let electrum_rpc_url: Url = Input::with_theme(&ColorfulTheme::default()) .with_prompt("Enter Electrum RPC URL or hit return to use default") - .default(DEFAULT_ELECTRUM_RPC_URL.to_owned()) + .default(defaults.electrum_rpc_url) .interact_text()?; - let electrum_rpc_url = Url::parse(electrum_rpc_url.as_str())?; let monero_wallet_rpc_url = Input::with_theme(&ColorfulTheme::default()) .with_prompt("Enter Monero Wallet RPC URL or hit enter to use default") - .default(DEFAULT_MONERO_WALLET_RPC_TESTNET_URL.to_owned()) + .default(defaults.monero_wallet_rpc_url) .interact_text()?; - let monero_wallet_rpc_url = monero_wallet_rpc_url.as_str().parse()?; 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.") @@ -234,9 +291,13 @@ pub fn query_user_for_initial_testnet_config() -> Result { bitcoin: Bitcoin { electrum_rpc_url, target_block, + finality_confirmations: None, + network: bitcoin_network, }, monero: Monero { wallet_rpc_url: monero_wallet_rpc_url, + finality_confirmations: None, + network: monero_network, }, tor: TorConf { control_port: tor_control_port, @@ -253,31 +314,33 @@ pub fn query_user_for_initial_testnet_config() -> Result { #[cfg(test)] mod tests { use super::*; - use std::str::FromStr; use tempfile::tempdir; #[test] - fn config_roundtrip() { + fn config_roundtrip_testnet() { let temp_dir = tempdir().unwrap().path().to_path_buf(); let config_path = Path::join(&temp_dir, "config.toml"); + let defaults = Testnet::getConfigFileDefaults().unwrap(); + let expected = Config { data: Data { dir: Default::default(), }, bitcoin: Bitcoin { - electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(), - target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET, + electrum_rpc_url: defaults.electrum_rpc_url, + target_block: defaults.bitcoin_confirmation_target, + finality_confirmations: None, + network: bitcoin::Network::Testnet, }, network: Network { - listen: vec![ - DEFAULT_LISTEN_ADDRESS_TCP.parse().unwrap(), - DEFAULT_LISTEN_ADDRESS_WS.parse().unwrap(), - ], + listen: vec![defaults.listen_address_tcp, defaults.listen_address_ws], }, monero: Monero { - wallet_rpc_url: Url::from_str(DEFAULT_MONERO_WALLET_RPC_TESTNET_URL).unwrap(), + wallet_rpc_url: defaults.monero_wallet_rpc_url, + finality_confirmations: None, + network: monero::Network::Stagenet, }, tor: Default::default(), maker: Maker { @@ -287,7 +350,47 @@ mod tests { }, }; - initial_setup(config_path.clone(), || Ok(expected.clone())).unwrap(); + initial_setup(config_path.clone(), expected.clone()).unwrap(); + let actual = read_config(config_path).unwrap().unwrap(); + + assert_eq!(expected, actual); + } + + #[test] + fn config_roundtrip_mainnet() { + let temp_dir = tempdir().unwrap().path().to_path_buf(); + let config_path = Path::join(&temp_dir, "config.toml"); + + let defaults = Mainnet::getConfigFileDefaults().unwrap(); + + let expected = Config { + data: Data { + dir: Default::default(), + }, + bitcoin: Bitcoin { + electrum_rpc_url: defaults.electrum_rpc_url, + target_block: defaults.bitcoin_confirmation_target, + finality_confirmations: None, + network: bitcoin::Network::Bitcoin, + }, + network: Network { + listen: vec![defaults.listen_address_tcp, defaults.listen_address_ws], + }, + + monero: Monero { + wallet_rpc_url: defaults.monero_wallet_rpc_url, + finality_confirmations: None, + network: monero::Network::Mainnet, + }, + tor: Default::default(), + maker: Maker { + min_buy_btc: bitcoin::Amount::from_btc(DEFAULT_MIN_BUY_AMOUNT).unwrap(), + max_buy_btc: bitcoin::Amount::from_btc(DEFAULT_MAX_BUY_AMOUNT).unwrap(), + ask_spread: Decimal::from_f64(DEFAULT_SPREAD).unwrap(), + }, + }; + + initial_setup(config_path.clone(), expected.clone()).unwrap(); let actual = read_config(config_path).unwrap().unwrap(); assert_eq!(expected, actual); diff --git a/swap/src/bin/asb.rs b/swap/src/bin/asb.rs index 9ea75f13..afef29f3 100644 --- a/swap/src/bin/asb.rs +++ b/swap/src/bin/asb.rs @@ -12,7 +12,7 @@ #![forbid(unsafe_code)] #![allow(non_snake_case)] -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use libp2p::core::multiaddr::Protocol; use libp2p::core::Multiaddr; use libp2p::Swarm; @@ -22,11 +22,10 @@ use std::sync::Arc; use structopt::StructOpt; use swap::asb::command::{Arguments, Command, ManualRecovery, RecoverCommandParams}; use swap::asb::config::{ - default_config_path, initial_setup, query_user_for_initial_testnet_config, read_config, Config, - ConfigNotInitialized, + initial_setup, query_user_for_initial_config, read_config, Config, ConfigNotInitialized, + GetDefaults, }; use swap::database::Database; -use swap::env::GetConfig; use swap::monero::Amount; use swap::network::swarm; use swap::protocol::alice; @@ -45,23 +44,45 @@ const DEFAULT_WALLET_NAME: &str = "asb-wallet"; #[tokio::main] async fn main() -> Result<()> { - let opt = Arguments::from_args(); - asb::tracing::init(LevelFilter::DEBUG, opt.json).expect("initialize tracing"); + let Arguments { + testnet, + json, + config, + cmd, + } = Arguments::from_args(); + asb::tracing::init(LevelFilter::DEBUG, json).expect("initialize tracing"); - let config_path = if let Some(config_path) = opt.config { + let config_path = if let Some(config_path) = config { config_path + } else if testnet { + env::Testnet::getConfigFileDefaults()?.config_path } else { - default_config_path()? + env::Mainnet::getConfigFileDefaults()?.config_path }; let config = match read_config(config_path.clone())? { Ok(config) => config, Err(ConfigNotInitialized {}) => { - initial_setup(config_path.clone(), query_user_for_initial_testnet_config)?; + initial_setup(config_path.clone(), query_user_for_initial_config(testnet)?)?; read_config(config_path)?.expect("after initial setup config can be read") } }; + let env_config = env::new(testnet, &config); + + if config.monero.network != env_config.monero_network { + bail!(format!( + "Expected monero network in config file to be {:?} but was {:?}", + env_config.monero_network, config.monero.network + )); + } + if config.bitcoin.network != env_config.bitcoin_network { + bail!(format!( + "Expected bitcoin network in config file to be {:?} but was {:?}", + env_config.bitcoin_network, config.bitcoin.network + )); + } + info!( db_folder = %config.data.dir.display(), "Database and Seed will be stored in", @@ -75,9 +96,7 @@ async fn main() -> Result<()> { let seed = Seed::from_file_or_generate(&config.data.dir).expect("Could not retrieve/initialize seed"); - let env_config = env::Testnet::get_config(); - - match opt.cmd { + match cmd { Command::Start { resume_only } => { let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?; @@ -127,6 +146,7 @@ async fn main() -> Result<()> { config.maker.max_buy_btc, kraken_rate.clone(), resume_only, + env_config, )?; for listen in config.network.listen { diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index 0daa0289..09ec0f88 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -15,21 +15,21 @@ use anyhow::{bail, Context, Result}; use prettytable::{row, Table}; use std::cmp::min; +use std::env; use std::future::Future; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; -use structopt::StructOpt; use swap::bitcoin::TxLock; -use swap::cli::command::{Arguments, Command, MoneroParams}; +use swap::cli::command::{parse_args_and_apply_defaults, Arguments, Command}; use swap::database::Database; -use swap::env::{Config, GetConfig}; +use swap::env::Config; use swap::network::quote::BidQuote; use swap::network::swarm; use swap::protocol::bob; use swap::protocol::bob::{EventLoop, Swap}; use swap::seed::Seed; -use swap::{bitcoin, cli, env, monero}; +use swap::{bitcoin, cli, monero}; use tracing::{debug, error, info, warn}; use url::Url; use uuid::Uuid; @@ -39,41 +39,33 @@ extern crate prettytable; #[tokio::main] async fn main() -> Result<()> { - let Arguments { data, debug, cmd } = Arguments::from_args(); + let Arguments { + env_config, + data_dir, + debug, + cmd, + } = parse_args_and_apply_defaults(env::args_os())?; match cmd { Command::BuyXmr { - alice_peer_id, - alice_multiaddr, - monero_params: - MoneroParams { - receive_monero_address, - monero_daemon_host, - }, - electrum_rpc_url, - tor_socks5_port, + seller_peer_id, + seller_addr, + bitcoin_electrum_rpc_url, bitcoin_target_block, + monero_receive_address, + monero_daemon_address, + tor_socks5_port, } => { let swap_id = Uuid::new_v4(); - let data_dir = data.0; cli::tracing::init(debug, data_dir.join("logs"), swap_id)?; let db = Database::open(data_dir.join("database").as_path()) .context("Failed to open database")?; let seed = Seed::from_file_or_generate(data_dir.as_path()) .context("Failed to read in seed file")?; - let env_config = env::Testnet::get_config(); - - if receive_monero_address.network != env_config.monero_network { - bail!( - "Given monero address is on network {:?}, expected address on network {:?}", - receive_monero_address.network, - env_config.monero_network - ) - } let bitcoin_wallet = init_bitcoin_wallet( - electrum_rpc_url, + bitcoin_electrum_rpc_url, &seed, data_dir.clone(), env_config, @@ -81,17 +73,22 @@ async fn main() -> Result<()> { ) .await?; let (monero_wallet, _process) = - init_monero_wallet(data_dir, monero_daemon_host, env_config).await?; + init_monero_wallet(data_dir, monero_daemon_address, env_config).await?; let bitcoin_wallet = Arc::new(bitcoin_wallet); - let mut swarm = swarm::bob(&seed, alice_peer_id, tor_socks5_port).await?; + let mut swarm = swarm::bob(&seed, seller_peer_id, tor_socks5_port).await?; swarm .behaviour_mut() - .add_address(alice_peer_id, alice_multiaddr); + .add_address(seller_peer_id, seller_addr); let swap_id = Uuid::new_v4(); - let (event_loop, mut event_loop_handle) = - EventLoop::new(swap_id, swarm, alice_peer_id, bitcoin_wallet.clone())?; + let (event_loop, mut event_loop_handle) = EventLoop::new( + swap_id, + swarm, + seller_peer_id, + bitcoin_wallet.clone(), + env_config, + )?; let event_loop = tokio::spawn(event_loop.run()); let max_givable = || bitcoin_wallet.max_giveable(TxLock::script_size()); @@ -106,7 +103,7 @@ async fn main() -> Result<()> { info!("Swapping {} with {} fees", send_bitcoin, fees); - db.insert_peer_id(swap_id, alice_peer_id).await?; + db.insert_peer_id(swap_id, seller_peer_id).await?; let swap = Swap::new( db, @@ -115,7 +112,7 @@ async fn main() -> Result<()> { Arc::new(monero_wallet), env_config, event_loop_handle, - receive_monero_address, + monero_receive_address, send_bitcoin, ); @@ -130,8 +127,6 @@ async fn main() -> Result<()> { } } Command::History => { - let data_dir = data.0; - let db = Database::open(data_dir.join("database").as_path()) .context("Failed to open database")?; @@ -148,30 +143,25 @@ async fn main() -> Result<()> { } Command::Resume { swap_id, - alice_multiaddr, - monero_params: - MoneroParams { - receive_monero_address, - monero_daemon_host, - }, - electrum_rpc_url, - tor_socks5_port, + seller_addr, + bitcoin_electrum_rpc_url, bitcoin_target_block, + monero_receive_address, + monero_daemon_address, + tor_socks5_port, } => { - let data_dir = data.0; cli::tracing::init(debug, data_dir.join("logs"), swap_id)?; let db = Database::open(data_dir.join("database").as_path()) .context("Failed to open database")?; let seed = Seed::from_file_or_generate(data_dir.as_path()) .context("Failed to read in seed file")?; - let env_config = env::Testnet::get_config(); - if receive_monero_address.network != env_config.monero_network { - bail!("The given monero address is on network {:?}, expected address of network {:?}.", receive_monero_address.network, env_config.monero_network) + if monero_receive_address.network != env_config.monero_network { + bail!("The given monero address is on network {:?}, expected address of network {:?}.", monero_receive_address.network, env_config.monero_network) } let bitcoin_wallet = init_bitcoin_wallet( - electrum_rpc_url, + bitcoin_electrum_rpc_url, &seed, data_dir.clone(), env_config, @@ -179,20 +169,25 @@ async fn main() -> Result<()> { ) .await?; let (monero_wallet, _process) = - init_monero_wallet(data_dir, monero_daemon_host, env_config).await?; + init_monero_wallet(data_dir, monero_daemon_address, env_config).await?; let bitcoin_wallet = Arc::new(bitcoin_wallet); - let alice_peer_id = db.get_peer_id(swap_id)?; + let seller_peer_id = db.get_peer_id(swap_id)?; - let mut swarm = swarm::bob(&seed, alice_peer_id, tor_socks5_port).await?; + let mut swarm = swarm::bob(&seed, seller_peer_id, tor_socks5_port).await?; let bob_peer_id = swarm.local_peer_id(); tracing::debug!("Our peer-id: {}", bob_peer_id); swarm .behaviour_mut() - .add_address(alice_peer_id, alice_multiaddr); + .add_address(seller_peer_id, seller_addr); - let (event_loop, event_loop_handle) = - EventLoop::new(swap_id, swarm, alice_peer_id, bitcoin_wallet.clone())?; + let (event_loop, event_loop_handle) = EventLoop::new( + swap_id, + swarm, + seller_peer_id, + bitcoin_wallet.clone(), + env_config, + )?; let handle = tokio::spawn(event_loop.run()); let swap = Swap::from_db( @@ -202,7 +197,7 @@ async fn main() -> Result<()> { Arc::new(monero_wallet), env_config, event_loop_handle, - receive_monero_address, + monero_receive_address, )?; tokio::select! { @@ -217,19 +212,17 @@ async fn main() -> Result<()> { Command::Cancel { swap_id, force, - electrum_rpc_url, + bitcoin_electrum_rpc_url, bitcoin_target_block, } => { - let data_dir = data.0; cli::tracing::init(debug, data_dir.join("logs"), swap_id)?; let db = Database::open(data_dir.join("database").as_path()) .context("Failed to open database")?; let seed = Seed::from_file_or_generate(data_dir.as_path()) .context("Failed to read in seed file")?; - let env_config = env::Testnet::get_config(); let bitcoin_wallet = init_bitcoin_wallet( - electrum_rpc_url, + bitcoin_electrum_rpc_url, &seed, data_dir, env_config, @@ -251,19 +244,17 @@ async fn main() -> Result<()> { Command::Refund { swap_id, force, - electrum_rpc_url, + bitcoin_electrum_rpc_url, bitcoin_target_block, } => { - let data_dir = data.0; cli::tracing::init(debug, data_dir.join("logs"), swap_id)?; let db = Database::open(data_dir.join("database").as_path()) .context("Failed to open database")?; let seed = Seed::from_file_or_generate(data_dir.as_path()) .context("Failed to read in seed file")?; - let env_config = env::Testnet::get_config(); let bitcoin_wallet = init_bitcoin_wallet( - electrum_rpc_url, + bitcoin_electrum_rpc_url, &seed, data_dir, env_config, @@ -303,7 +294,7 @@ async fn init_bitcoin_wallet( async fn init_monero_wallet( data_dir: PathBuf, - monero_daemon_host: String, + monero_daemon_address: String, env_config: Config, ) -> Result<(monero::Wallet, monero::WalletRpcProcess)> { let network = env_config.monero_network; @@ -313,7 +304,7 @@ async fn init_monero_wallet( let monero_wallet_rpc = monero::WalletRpc::new(data_dir.join("monero")).await?; let monero_wallet_rpc_process = monero_wallet_rpc - .run(network, monero_daemon_host.as_str()) + .run(network, monero_daemon_address.as_str()) .await?; let monero_wallet = monero::Wallet::open_or_create( diff --git a/swap/src/bitcoin.rs b/swap/src/bitcoin.rs index 9e7f323c..9534bd04 100644 --- a/swap/src/bitcoin.rs +++ b/swap/src/bitcoin.rs @@ -37,6 +37,17 @@ use serde::{Deserialize, Serialize}; use sha2::Sha256; use std::str::FromStr; +#[derive(Serialize, Deserialize)] +#[serde(remote = "Network")] +#[allow(non_camel_case_types)] +pub enum network { + #[serde(rename = "Mainnet")] + Bitcoin, + Testnet, + Signet, + Regtest, +} + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub struct SecretKey { inner: Scalar, diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index 3de1107a..6268d64e 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -16,6 +16,7 @@ use reqwest::Url; use rust_decimal::prelude::*; use rust_decimal::Decimal; use rust_decimal_macros::dec; +use std::cmp::Ordering; use std::collections::{BTreeMap, HashMap}; use std::convert::TryFrom; use std::fmt; @@ -108,18 +109,6 @@ impl Wallet { Ok((txid, subscription)) } - pub async fn sign_and_finalize(&self, psbt: PartiallySignedTransaction) -> Result { - let (signed_psbt, finalized) = self.wallet.lock().await.sign(psbt, None)?; - - if !finalized { - bail!("PSBT is not finalized") - } - - let tx = signed_psbt.extract_tx(); - - Ok(tx) - } - pub async fn get_raw_transaction(&self, txid: Txid) -> Result { self.get_tx(txid) .await? @@ -257,6 +246,18 @@ where C: EstimateFeeRate, D: BatchDatabase, { + pub async fn sign_and_finalize(&self, psbt: PartiallySignedTransaction) -> Result { + let (signed_psbt, finalized) = self.wallet.lock().await.sign(psbt, None)?; + + if !finalized { + bail!("PSBT is not finalized") + } + + let tx = signed_psbt.extract_tx(); + + Ok(tx) + } + pub async fn balance(&self) -> Result { let balance = self .wallet @@ -293,6 +294,10 @@ where Ok(Amount::from_sat(fees)) } + /// Builds a partially signed transaction + /// + /// Ensures that the address script is at output index `0` + /// for the partially signed transaction. pub async fn send_to_address( &self, address: Address, @@ -301,11 +306,30 @@ where let wallet = self.wallet.lock().await; let client = self.client.lock().await; let fee_rate = client.estimate_feerate(self.target_block)?; + let script = address.script_pubkey(); let mut tx_builder = wallet.build_tx(); - tx_builder.add_recipient(address.script_pubkey(), amount.as_sat()); + tx_builder.add_recipient(script.clone(), amount.as_sat()); tx_builder.fee_rate(fee_rate); let (psbt, _details) = tx_builder.finish()?; + let mut psbt: PartiallySignedTransaction = psbt; + + // When subscribing to transactions we depend on the relevant script being at + // output index 0, thus we ensure the relevant output to be at index `0`. + psbt.outputs.sort_by(|a, _| { + if a.witness_script.as_ref() == Some(&script) { + Ordering::Less + } else { + Ordering::Greater + } + }); + psbt.global.unsigned_tx.output.sort_by(|a, _| { + if a.script_pubkey == script { + Ordering::Less + } else { + Ordering::Greater + } + }); Ok(psbt) } @@ -480,7 +504,7 @@ where use bitcoin::OutPoint; use testutils::testutils; - let descriptors = testutils!(@descriptors ("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)")); + let descriptors = testutils!(@descriptors ("wpkh(tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m/*)")); let mut database = MemoryDatabase::new(); bdk::populate_test_db!( @@ -527,59 +551,39 @@ impl Watchable for (Txid, Script) { pub struct Client { electrum: bdk::electrum_client::Client, - latest_block: BlockHeight, - last_ping: Instant, - interval: Duration, + latest_block_height: BlockHeight, + last_sync: Instant, + sync_interval: Duration, script_history: BTreeMap>, subscriptions: HashMap<(Txid, Script), Subscription>, } impl Client { fn new(electrum: bdk::electrum_client::Client, interval: Duration) -> Result { + // Initially fetch the latest block for storing the height. + // We do not act on this subscription after this call. let latest_block = electrum .block_headers_subscribe() .context("Failed to subscribe to header notifications")?; Ok(Self { electrum, - latest_block: BlockHeight::try_from(latest_block)?, - last_ping: Instant::now(), - interval, + latest_block_height: BlockHeight::try_from(latest_block)?, + last_sync: Instant::now(), + sync_interval: interval, script_history: Default::default(), subscriptions: Default::default(), }) } - /// Ping the electrum server unless we already did within the set interval. - /// - /// Returns a boolean indicating whether we actually pinged the server. - fn ping(&mut self) -> bool { - if self.last_ping.elapsed() <= self.interval { - return false; - } - - match self.electrum.ping() { - Ok(()) => { - self.last_ping = Instant::now(); - - true - } - Err(error) => { - tracing::debug!(?error, "Failed to ping electrum server"); - - false - } - } - } - - fn drain_notifications(&mut self) -> Result<()> { - let pinged = self.ping(); - - if !pinged { + fn update_state(&mut self) -> Result<()> { + let now = Instant::now(); + if now < self.last_sync + self.sync_interval { return Ok(()); } - self.drain_blockheight_notifications()?; + self.last_sync = now; + self.update_latest_block()?; self.update_script_histories()?; Ok(()) @@ -596,7 +600,7 @@ impl Client { self.script_history.insert(script.clone(), vec![]); } - self.drain_notifications()?; + self.update_state()?; let history = self.script_history.entry(script).or_default(); @@ -618,7 +622,7 @@ impl Client { Ok(ScriptStatus::Confirmed( Confirmed::from_inclusion_and_latest_block( u32::try_from(last.height)?, - u32::from(self.latest_block), + u32::from(self.latest_block_height), ), )) } @@ -626,18 +630,24 @@ impl Client { } } - fn drain_blockheight_notifications(&mut self) -> Result<()> { - let latest_block = std::iter::from_fn(|| self.electrum.block_headers_pop().transpose()) - .last() - .transpose() - .context("Failed to pop header notification")?; + fn update_latest_block(&mut self) -> Result<()> { + // Fetch the latest block for storing the height. + // We do not act on this subscription after this call, as we cannot rely on + // subscription push notifications because eventually the Electrum server will + // close the connection and subscriptions are not automatically renewed + // upon renewing the connection. + let latest_block = self + .electrum + .block_headers_subscribe() + .context("Failed to subscribe to header notifications")?; + let latest_block_height = BlockHeight::try_from(latest_block)?; - if let Some(new_block) = latest_block { + if latest_block_height > self.latest_block_height { tracing::debug!( - block_height = new_block.height, + block_height = u32::from(latest_block_height), "Got notification for new block" ); - self.latest_block = BlockHeight::try_from(new_block)?; + self.latest_block_height = latest_block_height; } Ok(()) @@ -774,7 +784,7 @@ impl fmt::Display for ScriptStatus { #[cfg(test)] mod tests { use super::*; - use crate::bitcoin::TxLock; + use crate::bitcoin::{PublicKey, TxLock}; use proptest::prelude::*; #[test] @@ -1004,4 +1014,51 @@ mod tests { assert!(amount.as_sat() > 0); } + + /// This test ensures that the relevant script output of the transaction + /// created out of the PSBT is at index 0. This is important because + /// subscriptions to the transaction are on index `0` when broadcasting the + /// transaction. + #[tokio::test] + async fn given_amounts_with_change_outputs_when_signing_tx_then_output_index_0_is_ensured_for_script( + ) { + // We don't care about fees in this test, thus use a zero fee rate + struct NoFeeRate(); + impl EstimateFeeRate for NoFeeRate { + fn estimate_feerate(&self, _target_block: usize) -> Result { + Ok(FeeRate::from_sat_per_vb(0.0)) + } + + fn min_relay_fee(&self) -> Result { + Ok(bitcoin::Amount::from_sat(0)) + } + } + + // This value is somewhat arbitrary but the indexation problem usually occurred + // on the first or second value (i.e. 547, 548) We keep the test + // iterations relatively low because these tests are expensive. + let above_dust = 547; + let balance = 2000; + + let wallet = Wallet::new_funded(balance, NoFeeRate()); + + // sorting is only relevant for amounts that have a change output + // if the change output is below dust it will be dropped by the BDK + for amount in above_dust..(balance - (above_dust - 1)) { + let (A, B) = (PublicKey::random(), PublicKey::random()); + let txlock = TxLock::new(&wallet, bitcoin::Amount::from_sat(amount), A, B) + .await + .unwrap(); + let txlock_output = txlock.script_pubkey(); + + let tx = wallet.sign_and_finalize(txlock.into()).await.unwrap(); + let tx_output = tx.output[0].script_pubkey.clone(); + + assert_eq!( + tx_output, txlock_output, + "Output {:?} index mismatch for amount {} and balance {}", + tx.output, amount, balance + ); + } + } } diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index deb19ce5..3f8a8850 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -1,184 +1,434 @@ +use crate::env::GetConfig; use crate::fs::system_data_dir; +use crate::{env, monero}; use anyhow::{Context, Result}; use libp2p::core::Multiaddr; use libp2p::PeerId; -use std::path::{Path, PathBuf}; +use std::ffi::OsString; +use std::path::PathBuf; use std::str::FromStr; +use structopt::StructOpt; use url::Url; use uuid::Uuid; -// Port is assumed to be stagenet standard port 38081 -pub const DEFAULT_STAGENET_MONERO_DAEMON_HOST: &str = "monero-stagenet.exan.tech"; +// See: https://moneroworld.com/ +pub const DEFAULT_MONERO_DAEMON_ADDRESS: &str = "node.moneroworld.com:18089"; +pub const DEFAULT_MONERO_DAEMON_ADDRESS_STAGENET: &str = "monero-stagenet.exan.tech:38081"; -pub const DEFAULT_ELECTRUM_HTTP_URL: &str = "https://blockstream.info/testnet/api/"; -const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://electrum.blockstream.info:60002"; +// See: https://1209k.com/bitcoin-eye/ele.php?chain=btc +const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://electrum.blockstream.info:50002"; +// See: https://1209k.com/bitcoin-eye/ele.php?chain=tbtc +pub const DEFAULT_ELECTRUM_RPC_URL_TESTNET: &str = "ssl://electrum.blockstream.info:60002"; + +const DEFAULT_BITCOIN_CONFIRMATION_TARGET: usize = 3; +const DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET: usize = 1; const DEFAULT_TOR_SOCKS5_PORT: &str = "9050"; -// Bitcoin transactions should be confirmed within X blocks -const DEFAULT_BITCOIN_CONFIRMATION_TARGET: &str = "3"; +#[derive(Debug, PartialEq)] +pub struct Arguments { + pub env_config: env::Config, + pub debug: bool, + pub data_dir: PathBuf, + pub cmd: Command, +} + +pub fn parse_args_and_apply_defaults(raw_args: I) -> Result +where + I: IntoIterator, + T: Into + Clone, +{ + let matches = RawArguments::clap().get_matches_from_safe(raw_args)?; + let args = RawArguments::from_clap(&matches); + + let debug = args.debug; + let is_testnet = args.testnet; + let data = args.data; + + match args.cmd { + RawCommand::BuyXmr { + seller_peer_id, + seller_addr: SellerAddr { seller_addr }, + bitcoin: + Bitcoin { + bitcoin_electrum_rpc_url, + bitcoin_target_block, + }, + monero: + Monero { + monero_receive_address, + monero_daemon_address, + }, + tor: Tor { tor_socks5_port }, + } => Ok(Arguments { + env_config: env_config_from(is_testnet), + debug, + data_dir: data::data_dir_from(data, is_testnet)?, + cmd: Command::BuyXmr { + seller_peer_id, + seller_addr, + bitcoin_electrum_rpc_url: bitcoin_electrum_rpc_url_from( + bitcoin_electrum_rpc_url, + is_testnet, + )?, + bitcoin_target_block: bitcoin_target_block_from(bitcoin_target_block, is_testnet), + monero_receive_address: validate_monero_address( + monero_receive_address, + is_testnet, + )?, + monero_daemon_address: monero_daemon_address_from( + monero_daemon_address, + is_testnet, + ), + tor_socks5_port, + }, + }), + RawCommand::History => Ok(Arguments { + env_config: env_config_from(is_testnet), + debug, + data_dir: data::data_dir_from(data, is_testnet)?, + cmd: Command::History, + }), + RawCommand::Resume { + swap_id: SwapId { swap_id }, + seller_addr: SellerAddr { seller_addr }, + bitcoin: + Bitcoin { + bitcoin_electrum_rpc_url, + bitcoin_target_block, + }, + monero: + Monero { + monero_receive_address, + monero_daemon_address, + }, + tor: Tor { tor_socks5_port }, + } => Ok(Arguments { + env_config: env_config_from(is_testnet), + debug, + data_dir: data::data_dir_from(data, is_testnet)?, + cmd: Command::Resume { + swap_id, + seller_addr, + bitcoin_electrum_rpc_url: bitcoin_electrum_rpc_url_from( + bitcoin_electrum_rpc_url, + is_testnet, + )?, + bitcoin_target_block: bitcoin_target_block_from(bitcoin_target_block, is_testnet), + monero_receive_address, + monero_daemon_address: monero_daemon_address_from( + monero_daemon_address, + is_testnet, + ), + tor_socks5_port, + }, + }), + RawCommand::Cancel { + swap_id: SwapId { swap_id }, + force, + bitcoin: + Bitcoin { + bitcoin_electrum_rpc_url, + bitcoin_target_block, + }, + } => Ok(Arguments { + env_config: env_config_from(is_testnet), + debug, + data_dir: data::data_dir_from(data, is_testnet)?, + cmd: Command::Cancel { + swap_id, + force, + bitcoin_electrum_rpc_url: bitcoin_electrum_rpc_url_from( + bitcoin_electrum_rpc_url, + is_testnet, + )?, + bitcoin_target_block: bitcoin_target_block_from(bitcoin_target_block, is_testnet), + }, + }), + RawCommand::Refund { + swap_id: SwapId { swap_id }, + force, + bitcoin: + Bitcoin { + bitcoin_electrum_rpc_url, + bitcoin_target_block, + }, + } => Ok(Arguments { + env_config: env_config_from(is_testnet), + debug, + data_dir: data::data_dir_from(data, is_testnet)?, + cmd: Command::Refund { + swap_id, + force, + bitcoin_electrum_rpc_url: bitcoin_electrum_rpc_url_from( + bitcoin_electrum_rpc_url, + is_testnet, + )?, + bitcoin_target_block: bitcoin_target_block_from(bitcoin_target_block, is_testnet), + }, + }), + } +} + +#[derive(Debug, PartialEq)] +pub enum Command { + BuyXmr { + seller_peer_id: PeerId, + seller_addr: Multiaddr, + bitcoin_electrum_rpc_url: Url, + bitcoin_target_block: usize, + monero_receive_address: monero::Address, + monero_daemon_address: String, + tor_socks5_port: u16, + }, + History, + Resume { + swap_id: Uuid, + seller_addr: Multiaddr, + bitcoin_electrum_rpc_url: Url, + bitcoin_target_block: usize, + monero_receive_address: monero::Address, + monero_daemon_address: String, + tor_socks5_port: u16, + }, + Cancel { + swap_id: Uuid, + force: bool, + bitcoin_electrum_rpc_url: Url, + bitcoin_target_block: usize, + }, + Refund { + swap_id: Uuid, + force: bool, + bitcoin_electrum_rpc_url: Url, + bitcoin_target_block: usize, + }, +} #[derive(structopt::StructOpt, Debug)] #[structopt(name = "swap", about = "CLI for swapping BTC for XMR", author)] -pub struct Arguments { +pub struct RawArguments { + // global is necessary to ensure that clap can match against testnet in subcommands + #[structopt( + long, + help = "Swap on testnet and assume testnet defaults for data-dir and the blockchain related parameters", + global = true + )] + pub testnet: bool, + #[structopt( long = "--data-dir", - help = "Provide the data directory path to be used to store application data", - default_value + help = "Provide the data directory path to be used to store application data using testnet and mainnet as subfolder" )] - pub data: Data, + pub data: Option, #[structopt(long, help = "Activate debug logging.")] pub debug: bool, #[structopt(subcommand)] - pub cmd: Command, + pub cmd: RawCommand, } #[derive(structopt::StructOpt, Debug)] -pub enum Command { +pub enum RawCommand { /// Start a XMR for BTC swap BuyXmr { #[structopt(long = "seller-peer-id", help = "The seller's peer id")] - alice_peer_id: PeerId, - - #[structopt(long = "seller-addr", help = "The seller's multiaddress")] - alice_multiaddr: Multiaddr, - - #[structopt(long = "electrum-rpc", - help = "Provide the Bitcoin Electrum RPC URL", - default_value = DEFAULT_ELECTRUM_RPC_URL - )] - electrum_rpc_url: Url, + seller_peer_id: PeerId, #[structopt(flatten)] - monero_params: MoneroParams, + seller_addr: SellerAddr, - #[structopt(long = "tor-socks5-port", help = "Your local Tor socks5 proxy port", default_value = DEFAULT_TOR_SOCKS5_PORT)] - tor_socks5_port: u16, + #[structopt(flatten)] + bitcoin: Bitcoin, - #[structopt(long = "bitcoin-target-block", help = "Within how many blocks should the Bitcoin transactions be confirmed.", default_value = DEFAULT_BITCOIN_CONFIRMATION_TARGET)] - bitcoin_target_block: usize, + #[structopt(flatten)] + monero: Monero, + + #[structopt(flatten)] + tor: Tor, }, /// Show a list of past ongoing and completed swaps History, /// Resume a swap Resume { - #[structopt( - long = "swap-id", - help = "The swap id can be retrieved using the history subcommand" - )] - swap_id: Uuid, - - #[structopt(long = "seller-addr", help = "The seller's multiaddress")] - alice_multiaddr: Multiaddr, - - #[structopt(long = "electrum-rpc", - help = "Provide the Bitcoin Electrum RPC URL", - default_value = DEFAULT_ELECTRUM_RPC_URL - )] - electrum_rpc_url: Url, + #[structopt(flatten)] + swap_id: SwapId, #[structopt(flatten)] - monero_params: MoneroParams, + seller_addr: SellerAddr, - #[structopt(long = "tor-socks5-port", help = "Your local Tor socks5 proxy port", default_value = DEFAULT_TOR_SOCKS5_PORT)] - tor_socks5_port: u16, + #[structopt(flatten)] + bitcoin: Bitcoin, - #[structopt(long = "bitcoin-target-block", help = "Within how many blocks should the Bitcoin transactions be confirmed.", default_value = DEFAULT_BITCOIN_CONFIRMATION_TARGET)] - bitcoin_target_block: usize, + #[structopt(flatten)] + monero: Monero, + + #[structopt(flatten)] + tor: Tor, }, /// Try to cancel an ongoing swap (expert users only) Cancel { - #[structopt( - long = "swap-id", - help = "The swap id can be retrieved using the history subcommand" - )] - swap_id: Uuid, + #[structopt(flatten)] + swap_id: SwapId, #[structopt(short, long)] force: bool, - #[structopt(long = "electrum-rpc", - help = "Provide the Bitcoin Electrum RPC URL", - default_value = DEFAULT_ELECTRUM_RPC_URL - )] - electrum_rpc_url: Url, - - #[structopt(long = "bitcoin-target-block", help = "Within how many blocks should the Bitcoin transactions be confirmed.", default_value = DEFAULT_BITCOIN_CONFIRMATION_TARGET)] - bitcoin_target_block: usize, + #[structopt(flatten)] + bitcoin: Bitcoin, }, /// Try to cancel a swap and refund my BTC (expert users only) Refund { - #[structopt( - long = "swap-id", - help = "The swap id can be retrieved using the history subcommand" - )] - swap_id: Uuid, + #[structopt(flatten)] + swap_id: SwapId, #[structopt(short, long)] force: bool, - #[structopt(long = "electrum-rpc", - help = "Provide the Bitcoin Electrum RPC URL", - default_value = DEFAULT_ELECTRUM_RPC_URL - )] - electrum_rpc_url: Url, - - #[structopt(long = "bitcoin-target-block", help = "Within how many blocks should the Bitcoin transactions be confirmed.", default_value = DEFAULT_BITCOIN_CONFIRMATION_TARGET)] - bitcoin_target_block: usize, + #[structopt(flatten)] + bitcoin: Bitcoin, }, } #[derive(structopt::StructOpt, Debug)] -pub struct MoneroParams { +pub struct Monero { #[structopt(long = "receive-address", help = "Provide the monero address where you would like to receive monero", parse(try_from_str = parse_monero_address) )] - pub receive_monero_address: monero::Address, + pub monero_receive_address: monero::Address, #[structopt( - long = "monero-daemon-host", - help = "Specify to connect to a monero daemon of your choice", - default_value = DEFAULT_STAGENET_MONERO_DAEMON_HOST + long = "monero-daemon-address", + help = "Specify to connect to a monero daemon of your choice: :" )] - pub monero_daemon_host: String, + pub monero_daemon_address: Option, } -#[derive(Clone, Debug)] -pub struct Data(pub PathBuf); +#[derive(structopt::StructOpt, Debug)] +pub struct Bitcoin { + #[structopt(long = "electrum-rpc", help = "Provide the Bitcoin Electrum RPC URL")] + pub bitcoin_electrum_rpc_url: Option, -/// Default location for storing data for the CLI -// Takes the default system data-dir and adds a `/cli` -impl Default for Data { - fn default() -> Self { - Data( - system_data_dir() - .map(|proj_dir| Path::join(&proj_dir, "cli")) - .expect("computed valid path for data dir"), - ) + #[structopt( + long = "bitcoin-target-block", + help = "Use for fee estimation, decides within how many blocks the Bitcoin transactions should be confirmed." + )] + pub bitcoin_target_block: Option, +} + +#[derive(structopt::StructOpt, Debug)] +pub struct Tor { + #[structopt( + long = "tor-socks5-port", + help = "Your local Tor socks5 proxy port", + default_value = DEFAULT_TOR_SOCKS5_PORT + )] + pub tor_socks5_port: u16, +} + +#[derive(structopt::StructOpt, Debug)] +pub struct SwapId { + #[structopt( + long = "swap-id", + help = "The swap id can be retrieved using the history subcommand" + )] + pub swap_id: Uuid, +} + +#[derive(structopt::StructOpt, Debug)] +pub struct SellerAddr { + #[structopt(long = "seller-addr", help = "The seller's multiaddress")] + pub seller_addr: Multiaddr, +} + +mod data { + use super::*; + + pub fn data_dir_from(arg_dir: Option, testnet: bool) -> Result { + let dir = if let Some(dir) = arg_dir { + dir + } else if testnet { + testnet_default()? + } else { + mainnet_default()? + }; + + Ok(dir) + } + + fn testnet_default() -> Result { + Ok(os_default()?.join("testnet")) + } + + fn mainnet_default() -> Result { + Ok(os_default()?.join("mainnet")) + } + + fn os_default() -> Result { + Ok(system_data_dir()?.join("cli")) } } -impl FromStr for Data { - type Err = core::convert::Infallible; - - fn from_str(s: &str) -> Result { - Ok(Data(PathBuf::from_str(s)?)) +fn bitcoin_electrum_rpc_url_from(url: Option, testnet: bool) -> Result { + if let Some(url) = url { + Ok(url) + } else if testnet { + Ok(Url::from_str(DEFAULT_ELECTRUM_RPC_URL_TESTNET)?) + } else { + Ok(Url::from_str(DEFAULT_ELECTRUM_RPC_URL)?) } } -impl ToString for Data { - fn to_string(&self) -> String { - self.0 - .clone() - .into_os_string() - .into_string() - .expect("default datadir to be convertible to string") +fn bitcoin_target_block_from(target_block: Option, testnet: bool) -> usize { + if let Some(target_block) = target_block { + target_block + } else if testnet { + DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET + } else { + DEFAULT_BITCOIN_CONFIRMATION_TARGET } } +fn monero_daemon_address_from(address: Option, testnet: bool) -> String { + if let Some(address) = address { + address + } else if testnet { + DEFAULT_MONERO_DAEMON_ADDRESS_STAGENET.to_string() + } else { + DEFAULT_MONERO_DAEMON_ADDRESS.to_string() + } +} + +fn env_config_from(testnet: bool) -> env::Config { + if testnet { + env::Testnet::get_config() + } else { + env::Mainnet::get_config() + } +} + +fn validate_monero_address( + address: monero::Address, + testnet: bool, +) -> Result { + let expected_network = if testnet { + monero::Network::Stagenet + } else { + monero::Network::Mainnet + }; + + if address.network != expected_network { + return Err(MoneroAddressNetworkMismatch { + expected: expected_network, + actual: address.network, + }); + } + + Ok(address) +} + fn parse_monero_address(s: &str) -> Result { monero::Address::from_str(s).with_context(|| { format!( @@ -187,3 +437,479 @@ fn parse_monero_address(s: &str) -> Result { ) }) } + +#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq)] +#[error("Invalid monero address provided, expected address on network {expected:?} but address provided is on {actual:?}")] +pub struct MoneroAddressNetworkMismatch { + expected: monero::Network, + actual: monero::Network, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tor::DEFAULT_SOCKS5_PORT; + + const BINARY_NAME: &str = "swap"; + + const TESTNET: &str = "testnet"; + const MAINNET: &str = "mainnet"; + + const MONERO_STAGENET_ADDRESS: &str = "53gEuGZUhP9JMEBZoGaFNzhwEgiG7hwQdMCqFxiyiTeFPmkbt1mAoNybEUvYBKHcnrSgxnVWgZsTvRBaHBNXPa8tHiCU51a"; + const MONERO_MAINNET_ADDRESS: &str = "44Ato7HveWidJYUAVw5QffEcEtSH1DwzSP3FPPkHxNAS4LX9CqgucphTisH978FLHE34YNEx7FcbBfQLQUU8m3NUC4VqsRa"; + const MUTLI_ADDRESS: &str = "/ip4/127.0.0.1/tcp/9939"; + const PEER_ID: &str = "12D3KooWCdMKjesXMJz1SiZ7HgotrxuqhQJbP5sgBm2BwP1cqThi"; + const SWAP_ID: &str = "ea030832-3be9-454f-bb98-5ea9a788406b"; + + #[test] + fn given_buy_xmr_on_mainnet_then_defaults_to_mainnet() { + let raw_ars = vec![ + BINARY_NAME, + "buy-xmr", + "--receive-address", + MONERO_MAINNET_ADDRESS, + "--seller-addr", + MUTLI_ADDRESS, + "--seller-peer-id", + PEER_ID, + ]; + + let expected_args = Arguments::buy_xmr_mainnet_defaults(); + let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + assert_eq!(expected_args, args); + } + + #[test] + fn given_buy_xmr_on_testnet_then_defaults_to_testnet() { + let raw_ars = vec![ + BINARY_NAME, + "--testnet", + "buy-xmr", + "--receive-address", + MONERO_STAGENET_ADDRESS, + "--seller-addr", + MUTLI_ADDRESS, + "--seller-peer-id", + PEER_ID, + ]; + + let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + assert_eq!(args, Arguments::buy_xmr_testnet_defaults()); + } + + #[test] + fn given_buy_xmr_on_mainnet_with_testnet_address_then_fails() { + let raw_ars = vec![ + BINARY_NAME, + "buy-xmr", + "--receive-address", + MONERO_STAGENET_ADDRESS, + "--seller-addr", + MUTLI_ADDRESS, + "--seller-peer-id", + PEER_ID, + ]; + + let err = parse_args_and_apply_defaults(raw_ars).unwrap_err(); + + assert_eq!( + err.downcast_ref::().unwrap(), + &MoneroAddressNetworkMismatch { + expected: monero::Network::Mainnet, + actual: monero::Network::Stagenet + } + ); + } + + #[test] + fn given_buy_xmr_on_testnet_with_mainnet_address_then_fails() { + let raw_ars = vec![ + BINARY_NAME, + "--testnet", + "buy-xmr", + "--receive-address", + MONERO_MAINNET_ADDRESS, + "--seller-addr", + MUTLI_ADDRESS, + "--seller-peer-id", + PEER_ID, + ]; + + let err = parse_args_and_apply_defaults(raw_ars).unwrap_err(); + + assert_eq!( + err.downcast_ref::().unwrap(), + &MoneroAddressNetworkMismatch { + expected: monero::Network::Stagenet, + actual: monero::Network::Mainnet + } + ); + } + + #[test] + fn given_resume_on_mainnet_then_defaults_to_mainnet() { + let raw_ars = vec![ + BINARY_NAME, + "resume", + "--swap-id", + SWAP_ID, + "--receive-address", + MONERO_MAINNET_ADDRESS, + "--seller-addr", + MUTLI_ADDRESS, + ]; + + let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + assert_eq!(args, Arguments::resume_mainnet_defaults()); + } + + #[test] + fn given_resume_on_testnet_then_defaults_to_testnet() { + let raw_ars = vec![ + BINARY_NAME, + "--testnet", + "resume", + "--swap-id", + SWAP_ID, + "--receive-address", + MONERO_STAGENET_ADDRESS, + "--seller-addr", + MUTLI_ADDRESS, + ]; + + let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + assert_eq!(args, Arguments::resume_testnet_defaults()); + } + + #[test] + fn given_cancel_on_mainnet_then_defaults_to_mainnet() { + let raw_ars = vec![BINARY_NAME, "cancel", "--swap-id", SWAP_ID]; + + let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + assert_eq!(args, Arguments::cancel_mainnet_defaults()); + } + + #[test] + fn given_cancel_on_testnet_then_defaults_to_testnet() { + let raw_ars = vec![BINARY_NAME, "--testnet", "cancel", "--swap-id", SWAP_ID]; + + let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + assert_eq!(args, Arguments::cancel_testnet_defaults()); + } + + #[test] + fn given_refund_on_mainnet_then_defaults_to_mainnet() { + let raw_ars = vec![BINARY_NAME, "refund", "--swap-id", SWAP_ID]; + + let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + assert_eq!(args, Arguments::refund_mainnet_defaults()); + } + + #[test] + fn given_refund_on_testnet_then_defaults_to_testnet() { + let raw_ars = vec![BINARY_NAME, "--testnet", "refund", "--swap-id", SWAP_ID]; + + let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + assert_eq!(args, Arguments::refund_testnet_defaults()); + } + + #[test] + fn given_with_data_dir_then_data_dir_set() { + let data_dir = "/some/path/to/dir"; + + let raw_ars = vec![ + BINARY_NAME, + "--data-dir", + data_dir, + "buy-xmr", + "--receive-address", + MONERO_MAINNET_ADDRESS, + "--seller-addr", + MUTLI_ADDRESS, + "--seller-peer-id", + PEER_ID, + ]; + + let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + assert_eq!( + args, + Arguments::buy_xmr_mainnet_defaults() + .with_data_dir(PathBuf::from_str(data_dir).unwrap()) + ); + + let raw_ars = vec![ + BINARY_NAME, + "--testnet", + "--data-dir", + data_dir, + "buy-xmr", + "--receive-address", + MONERO_STAGENET_ADDRESS, + "--seller-addr", + MUTLI_ADDRESS, + "--seller-peer-id", + PEER_ID, + ]; + + let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + assert_eq!( + args, + Arguments::buy_xmr_testnet_defaults() + .with_data_dir(PathBuf::from_str(data_dir).unwrap()) + ); + + let raw_ars = vec![ + BINARY_NAME, + "--data-dir", + data_dir, + "resume", + "--swap-id", + SWAP_ID, + "--receive-address", + MONERO_MAINNET_ADDRESS, + "--seller-addr", + MUTLI_ADDRESS, + ]; + + let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + assert_eq!( + args, + Arguments::resume_mainnet_defaults() + .with_data_dir(PathBuf::from_str(data_dir).unwrap()) + ); + + let raw_ars = vec![ + BINARY_NAME, + "--testnet", + "--data-dir", + data_dir, + "resume", + "--swap-id", + SWAP_ID, + "--receive-address", + MONERO_STAGENET_ADDRESS, + "--seller-addr", + MUTLI_ADDRESS, + ]; + + let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + assert_eq!( + args, + Arguments::resume_testnet_defaults() + .with_data_dir(PathBuf::from_str(data_dir).unwrap()) + ); + } + + #[test] + fn given_with_debug_then_debug_set() { + let raw_ars = vec![ + BINARY_NAME, + "--debug", + "buy-xmr", + "--receive-address", + MONERO_MAINNET_ADDRESS, + "--seller-addr", + MUTLI_ADDRESS, + "--seller-peer-id", + PEER_ID, + ]; + + let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + assert_eq!(args, Arguments::buy_xmr_mainnet_defaults().with_debug()); + + let raw_ars = vec![ + BINARY_NAME, + "--testnet", + "--debug", + "buy-xmr", + "--receive-address", + MONERO_STAGENET_ADDRESS, + "--seller-addr", + MUTLI_ADDRESS, + "--seller-peer-id", + PEER_ID, + ]; + + let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + assert_eq!(args, Arguments::buy_xmr_testnet_defaults().with_debug()); + + let raw_ars = vec![ + BINARY_NAME, + "--debug", + "resume", + "--swap-id", + SWAP_ID, + "--receive-address", + MONERO_MAINNET_ADDRESS, + "--seller-addr", + MUTLI_ADDRESS, + ]; + + let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + assert_eq!(args, Arguments::resume_mainnet_defaults().with_debug()); + + let raw_ars = vec![ + BINARY_NAME, + "--testnet", + "--debug", + "resume", + "--swap-id", + SWAP_ID, + "--receive-address", + MONERO_STAGENET_ADDRESS, + "--seller-addr", + MUTLI_ADDRESS, + ]; + + let args = parse_args_and_apply_defaults(raw_ars).unwrap(); + assert_eq!(args, Arguments::resume_testnet_defaults().with_debug()); + } + + impl Arguments { + pub fn buy_xmr_testnet_defaults() -> Self { + Self { + env_config: env::Testnet::get_config(), + debug: false, + data_dir: data_dir_path_cli().join(TESTNET), + cmd: Command::BuyXmr { + seller_peer_id: PeerId::from_str(PEER_ID).unwrap(), + seller_addr: Multiaddr::from_str(MUTLI_ADDRESS).unwrap(), + bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL_TESTNET) + .unwrap(), + bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET, + monero_receive_address: monero::Address::from_str(MONERO_STAGENET_ADDRESS) + .unwrap(), + monero_daemon_address: DEFAULT_MONERO_DAEMON_ADDRESS_STAGENET.to_string(), + tor_socks5_port: DEFAULT_SOCKS5_PORT, + }, + } + } + + pub fn buy_xmr_mainnet_defaults() -> Self { + Self { + env_config: env::Mainnet::get_config(), + debug: false, + data_dir: data_dir_path_cli().join(MAINNET), + cmd: Command::BuyXmr { + seller_peer_id: PeerId::from_str(PEER_ID).unwrap(), + seller_addr: Multiaddr::from_str(MUTLI_ADDRESS).unwrap(), + bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(), + bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET, + monero_receive_address: monero::Address::from_str(MONERO_MAINNET_ADDRESS) + .unwrap(), + monero_daemon_address: DEFAULT_MONERO_DAEMON_ADDRESS.to_string(), + tor_socks5_port: DEFAULT_SOCKS5_PORT, + }, + } + } + + pub fn resume_testnet_defaults() -> Self { + Self { + env_config: env::Testnet::get_config(), + debug: false, + data_dir: data_dir_path_cli().join(TESTNET), + cmd: Command::Resume { + swap_id: Uuid::from_str(SWAP_ID).unwrap(), + seller_addr: Multiaddr::from_str(MUTLI_ADDRESS).unwrap(), + bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL_TESTNET) + .unwrap(), + bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET, + monero_receive_address: monero::Address::from_str(MONERO_STAGENET_ADDRESS) + .unwrap(), + monero_daemon_address: DEFAULT_MONERO_DAEMON_ADDRESS_STAGENET.to_string(), + tor_socks5_port: DEFAULT_SOCKS5_PORT, + }, + } + } + + pub fn resume_mainnet_defaults() -> Self { + Self { + env_config: env::Mainnet::get_config(), + debug: false, + data_dir: data_dir_path_cli().join(MAINNET), + cmd: Command::Resume { + swap_id: Uuid::from_str(SWAP_ID).unwrap(), + seller_addr: Multiaddr::from_str(MUTLI_ADDRESS).unwrap(), + bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(), + bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET, + monero_receive_address: monero::Address::from_str(MONERO_MAINNET_ADDRESS) + .unwrap(), + monero_daemon_address: DEFAULT_MONERO_DAEMON_ADDRESS.to_string(), + tor_socks5_port: DEFAULT_SOCKS5_PORT, + }, + } + } + + pub fn cancel_testnet_defaults() -> Self { + Self { + env_config: env::Testnet::get_config(), + debug: false, + data_dir: data_dir_path_cli().join(TESTNET), + cmd: Command::Cancel { + swap_id: Uuid::from_str(SWAP_ID).unwrap(), + force: false, + bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL_TESTNET) + .unwrap(), + bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET, + }, + } + } + + pub fn cancel_mainnet_defaults() -> Self { + Self { + env_config: env::Mainnet::get_config(), + debug: false, + data_dir: data_dir_path_cli().join(MAINNET), + cmd: Command::Cancel { + swap_id: Uuid::from_str(SWAP_ID).unwrap(), + force: false, + bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(), + bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET, + }, + } + } + + pub fn refund_testnet_defaults() -> Self { + Self { + env_config: env::Testnet::get_config(), + debug: false, + data_dir: data_dir_path_cli().join(TESTNET), + cmd: Command::Refund { + swap_id: Uuid::from_str(SWAP_ID).unwrap(), + force: false, + bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL_TESTNET) + .unwrap(), + bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET, + }, + } + } + + pub fn refund_mainnet_defaults() -> Self { + Self { + env_config: env::Mainnet::get_config(), + debug: false, + data_dir: data_dir_path_cli().join(MAINNET), + cmd: Command::Refund { + swap_id: Uuid::from_str(SWAP_ID).unwrap(), + force: false, + bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(), + bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET, + }, + } + } + + pub fn with_data_dir(mut self, data_dir: PathBuf) -> Self { + self.data_dir = data_dir; + self + } + + pub fn with_debug(mut self) -> Self { + self.debug = true; + self + } + } + + fn data_dir_path_cli() -> PathBuf { + system_data_dir().unwrap().join("cli") + } +} diff --git a/swap/src/env.rs b/swap/src/env.rs index 04deb791..95d4488a 100644 --- a/swap/src/env.rs +++ b/swap/src/env.rs @@ -1,9 +1,10 @@ +use crate::asb; use crate::bitcoin::{CancelTimelock, PunishTimelock}; use std::cmp::max; use std::time::Duration; use time::NumericalStdDurationShort; -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq)] pub struct Config { pub bitcoin_lock_confirmed_timeout: Duration, pub bitcoin_finality_confirmations: u32, @@ -43,13 +44,13 @@ impl GetConfig for Mainnet { fn get_config() -> Config { Config { bitcoin_lock_confirmed_timeout: 24.hours(), - bitcoin_finality_confirmations: 3, + bitcoin_finality_confirmations: 2, bitcoin_avg_block_time: 10.minutes(), bitcoin_cancel_timelock: CancelTimelock::new(72), bitcoin_punish_timelock: PunishTimelock::new(72), bitcoin_network: bitcoin::Network::Bitcoin, monero_avg_block_time: 2.minutes(), - monero_finality_confirmations: 15, + monero_finality_confirmations: 10, monero_network: monero::Network::Mainnet, } } @@ -59,8 +60,8 @@ impl GetConfig for Testnet { fn get_config() -> Config { Config { bitcoin_lock_confirmed_timeout: 12.hours(), - bitcoin_finality_confirmations: 1, - bitcoin_avg_block_time: 5.minutes(), + bitcoin_finality_confirmations: 2, + bitcoin_avg_block_time: 10.minutes(), bitcoin_cancel_timelock: CancelTimelock::new(12), bitcoin_punish_timelock: PunishTimelock::new(6), bitcoin_network: bitcoin::Network::Testnet, @@ -91,6 +92,33 @@ fn sync_interval(avg_block_time: Duration) -> Duration { max(avg_block_time / 10, Duration::from_secs(1)) } +pub fn new(is_testnet: bool, asb_config: &asb::config::Config) -> Config { + let env_config = if is_testnet { + Testnet::get_config() + } else { + Mainnet::get_config() + }; + + let env_config = + if let Some(bitcoin_finality_confirmations) = asb_config.bitcoin.finality_confirmations { + Config { + bitcoin_finality_confirmations, + ..env_config + } + } else { + env_config + }; + + if let Some(monero_finality_confirmations) = asb_config.monero.finality_confirmations { + Config { + monero_finality_confirmations, + ..env_config + } + } else { + env_config + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/swap/src/monero.rs b/swap/src/monero.rs index 6886cbf1..c4649410 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -1,7 +1,8 @@ pub mod wallet; mod wallet_rpc; -pub use ::monero::{Address, Network, PrivateKey, PublicKey}; +pub use ::monero::network::Network; +pub use ::monero::{Address, PrivateKey, PublicKey}; pub use curve25519_dalek::scalar::Scalar; pub use wallet::Wallet; pub use wallet_rpc::{WalletRpc, WalletRpcProcess}; @@ -19,6 +20,15 @@ use std::str::FromStr; pub const PICONERO_OFFSET: u64 = 1_000_000_000_000; +#[derive(Serialize, Deserialize)] +#[serde(remote = "Network")] +#[allow(non_camel_case_types)] +pub enum network { + Mainnet, + Stagenet, + Testnet, +} + pub fn private_key_from_secp256k1_scalar(scalar: bitcoin::Scalar) -> PrivateKey { let mut bytes = scalar.to_bytes(); diff --git a/swap/src/monero/wallet_rpc.rs b/swap/src/monero/wallet_rpc.rs index f84a943f..9a6619ea 100644 --- a/swap/src/monero/wallet_rpc.rs +++ b/swap/src/monero/wallet_rpc.rs @@ -115,7 +115,7 @@ impl WalletRpc { Ok(monero_wallet_rpc) } - pub async fn run(&self, network: Network, daemon_host: &str) -> Result { + pub async fn run(&self, network: Network, daemon_address: &str) -> Result { let port = tokio::net::TcpListener::bind("127.0.0.1:0") .await? .local_addr()? @@ -126,17 +126,25 @@ impl WalletRpc { "Starting monero-wallet-rpc on" ); + let network_flag = match network { + Network::Mainnet => { + vec![] + } + Network::Stagenet => { + vec!["--stagenet"] + } + Network::Testnet => { + vec!["--testnet"] + } + }; + let mut child = Command::new(self.exec_path()) .env("LANG", "en_AU.UTF-8") .stdout(Stdio::piped()) .kill_on_drop(true) - .arg(match network { - Network::Mainnet => "--mainnet", - Network::Stagenet => "--stagenet", - Network::Testnet => "--testnet", - }) - .arg("--daemon-host") - .arg(daemon_host) + .args(network_flag) + .arg("--daemon-address") + .arg(daemon_address) .arg("--rpc-bind-port") .arg(format!("{}", port)) .arg("--disable-rpc-login") diff --git a/swap/src/network/spot_price.rs b/swap/src/network/spot_price.rs index 84838c27..8268d146 100644 --- a/swap/src/network/spot_price.rs +++ b/swap/src/network/spot_price.rs @@ -32,6 +32,7 @@ impl ProtocolName for SpotPriceProtocol { pub struct Request { #[serde(with = "::bitcoin::util::amount::serde::as_sat")] pub btc: bitcoin::Amount, + pub blockchain_network: BlockchainNetwork, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -59,11 +60,23 @@ pub enum Error { #[serde(with = "::bitcoin::util::amount::serde::as_sat")] buy: bitcoin::Amount, }, + BlockchainNetworkMismatch { + cli: BlockchainNetwork, + asb: BlockchainNetwork, + }, /// To be used for errors that cannot be explained on the CLI side (e.g. /// rate update problems on the seller side) Other, } +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] +pub struct BlockchainNetwork { + #[serde(with = "crate::bitcoin::network")] + pub bitcoin: bitcoin::Network, + #[serde(with = "crate::monero::network")] + pub monero: monero::Network, +} + #[cfg(test)] mod tests { use super::*; @@ -103,6 +116,21 @@ mod tests { .unwrap(); assert_eq!(error, serialized); + let error = r#"{"Error":{"BlockchainNetworkMismatch":{"cli":{"bitcoin":"Mainnet","monero":"Mainnet"},"asb":{"bitcoin":"Testnet","monero":"Stagenet"}}}}"#.to_string(); + let serialized = + serde_json::to_string(&Response::Error(Error::BlockchainNetworkMismatch { + cli: BlockchainNetwork { + bitcoin: bitcoin::Network::Bitcoin, + monero: monero::Network::Mainnet, + }, + asb: BlockchainNetwork { + bitcoin: bitcoin::Network::Testnet, + monero: monero::Network::Stagenet, + }, + })) + .unwrap(); + assert_eq!(error, serialized); + let error = r#"{"Error":"Other"}"#.to_string(); let serialized = serde_json::to_string(&Response::Error(Error::Other)).unwrap(); assert_eq!(error, serialized); diff --git a/swap/src/network/swarm.rs b/swap/src/network/swarm.rs index 27171392..5a27a8c0 100644 --- a/swap/src/network/swarm.rs +++ b/swap/src/network/swarm.rs @@ -2,12 +2,13 @@ use crate::network::transport; use crate::protocol::alice::event_loop::LatestRate; use crate::protocol::{alice, bob}; use crate::seed::Seed; -use crate::{monero, tor}; +use crate::{env, monero, tor}; use anyhow::Result; use libp2p::swarm::{NetworkBehaviour, SwarmBuilder}; use libp2p::{PeerId, Swarm}; use std::fmt::Debug; +#[allow(clippy::too_many_arguments)] pub fn alice( seed: &Seed, balance: monero::Amount, @@ -16,6 +17,7 @@ pub fn alice( max_buy: bitcoin::Amount, latest_rate: LR, resume_only: bool, + env_config: env::Config, ) -> Result>> where LR: LatestRate + Send + 'static + Debug, @@ -29,6 +31,7 @@ where max_buy, latest_rate, resume_only, + env_config, ), ) } diff --git a/swap/src/protocol/alice/behaviour.rs b/swap/src/protocol/alice/behaviour.rs index 7e036040..6d2ad548 100644 --- a/swap/src/protocol/alice/behaviour.rs +++ b/swap/src/protocol/alice/behaviour.rs @@ -1,8 +1,8 @@ -use crate::monero; use crate::network::quote::BidQuote; use crate::network::{encrypted_signature, quote, transfer_proof}; use crate::protocol::alice::event_loop::LatestRate; use crate::protocol::alice::{execution_setup, spot_price, State3}; +use crate::{env, monero}; use anyhow::{anyhow, Error}; use libp2p::request_response::{RequestId, ResponseChannel}; use libp2p::{NetworkBehaviour, PeerId}; @@ -88,6 +88,7 @@ where max_buy: bitcoin::Amount, latest_rate: LR, resume_only: bool, + env_config: env::Config, ) -> Self { Self { quote: quote::alice(), @@ -96,6 +97,7 @@ where lock_fee, min_buy, max_buy, + env_config, latest_rate, resume_only, ), diff --git a/swap/src/protocol/alice/event_loop.rs b/swap/src/protocol/alice/event_loop.rs index ae26d1f1..832f658c 100644 --- a/swap/src/protocol/alice/event_loop.rs +++ b/swap/src/protocol/alice/event_loop.rs @@ -211,7 +211,8 @@ where match error { Error::ResumeOnlyMode | Error::AmountBelowMinimum { .. } - | Error::AmountAboveMaximum { .. } => { + | Error::AmountAboveMaximum { .. } + | Error::BlockchainNetworkMismatch { .. } => { tracing::warn!(%peer, "Ignoring spot price request because: {}", error); } Error::BalanceTooLow { .. } diff --git a/swap/src/protocol/alice/spot_price.rs b/swap/src/protocol/alice/spot_price.rs index 81b0bdb8..43fcf600 100644 --- a/swap/src/protocol/alice/spot_price.rs +++ b/swap/src/protocol/alice/spot_price.rs @@ -1,9 +1,9 @@ -use crate::monero; use crate::network::cbor_request_response::CborCodec; use crate::network::spot_price; -use crate::network::spot_price::SpotPriceProtocol; +use crate::network::spot_price::{BlockchainNetwork, SpotPriceProtocol}; use crate::protocol::alice; use crate::protocol::alice::event_loop::LatestRate; +use crate::{env, monero}; use libp2p::request_response::{ ProtocolSupport, RequestResponseConfig, RequestResponseEvent, RequestResponseMessage, ResponseChannel, @@ -48,6 +48,8 @@ where #[behaviour(ignore)] max_buy: bitcoin::Amount, #[behaviour(ignore)] + env_config: env::Config, + #[behaviour(ignore)] latest_rate: LR, #[behaviour(ignore)] resume_only: bool, @@ -66,6 +68,7 @@ where lock_fee: monero::Amount, min_buy: bitcoin::Amount, max_buy: bitcoin::Amount, + env_config: env::Config, latest_rate: LR, resume_only: bool, ) -> Self { @@ -80,6 +83,7 @@ where lock_fee, min_buy, max_buy, + env_config, latest_rate, resume_only, } @@ -154,6 +158,19 @@ where } }; + let blockchain_network = BlockchainNetwork { + bitcoin: self.env_config.bitcoin_network, + monero: self.env_config.monero_network, + }; + + if request.blockchain_network != blockchain_network { + self.decline(peer, channel, Error::BlockchainNetworkMismatch { + cli: request.blockchain_network, + asb: blockchain_network, + }); + return; + } + if self.resume_only { self.decline(peer, channel, Error::ResumeOnlyMode); return; @@ -246,12 +263,15 @@ pub enum Error { balance: monero::Amount, buy: bitcoin::Amount, }, - #[error("Failed to fetch latest rate")] LatestRateFetchFailed(#[source] Box), - #[error("Failed to calculate quote: {0}")] SellQuoteCalculationFailed(#[source] anyhow::Error), + #[error("Blockchain networks did not match, we are on {asb:?}, but request from {cli:?}")] + BlockchainNetworkMismatch { + cli: spot_price::BlockchainNetwork, + asb: spot_price::BlockchainNetwork, + }, } impl Error { @@ -267,6 +287,12 @@ impl Error { buy: *buy, }, Error::BalanceTooLow { buy, .. } => spot_price::Error::BalanceTooLow { buy: *buy }, + Error::BlockchainNetworkMismatch { cli, asb } => { + spot_price::Error::BlockchainNetworkMismatch { + cli: *cli, + asb: *asb, + } + } Error::LatestRateFetchFailed(_) | Error::SellQuoteCalculationFailed(_) => { spot_price::Error::Other } @@ -278,6 +304,7 @@ impl Error { mod tests { use super::*; use crate::asb::Rate; + use crate::env::GetConfig; use crate::monero; use crate::network::test::{await_events_or_timeout, connect, new_swarm}; use crate::protocol::{alice, bob}; @@ -294,6 +321,7 @@ mod tests { max_buy: bitcoin::Amount::from_btc(0.01).unwrap(), rate: TestRate::default(), // 0.01 resume_only: false, + env_config: env::Testnet::get_config(), } } } @@ -305,9 +333,7 @@ mod tests { let btc_to_swap = bitcoin::Amount::from_btc(0.01).unwrap(); let expected_xmr = monero::Amount::from_monero(1.0).unwrap(); - let request = spot_price::Request { btc: btc_to_swap }; - - test.send_request(request); + test.construct_and_send_request(btc_to_swap); test.assert_price((btc_to_swap, expected_xmr), expected_xmr) .await; } @@ -321,9 +347,7 @@ mod tests { let btc_to_swap = bitcoin::Amount::from_btc(0.01).unwrap(); - let request = spot_price::Request { btc: btc_to_swap }; - - test.send_request(request); + test.construct_and_send_request(btc_to_swap); test.assert_error( alice::spot_price::Error::BalanceTooLow { balance: monero::Amount::ZERO, @@ -341,9 +365,7 @@ mod tests { let btc_to_swap = bitcoin::Amount::from_btc(0.01).unwrap(); let expected_xmr = monero::Amount::from_monero(1.0).unwrap(); - let request = spot_price::Request { btc: btc_to_swap }; - - test.send_request(request); + test.construct_and_send_request(btc_to_swap); test.assert_price((btc_to_swap, expected_xmr), expected_xmr) .await; @@ -351,9 +373,7 @@ mod tests { .behaviour_mut() .update_balance(monero::Amount::ZERO); - let request = spot_price::Request { btc: btc_to_swap }; - - test.send_request(request); + test.construct_and_send_request(btc_to_swap); test.assert_error( alice::spot_price::Error::BalanceTooLow { balance: monero::Amount::ZERO, @@ -376,10 +396,7 @@ mod tests { .await; let btc_to_swap = bitcoin::Amount::from_btc(0.01).unwrap(); - - let request = spot_price::Request { btc: btc_to_swap }; - - test.send_request(request); + test.construct_and_send_request(btc_to_swap); test.assert_error( alice::spot_price::Error::BalanceTooLow { balance, @@ -398,10 +415,7 @@ mod tests { SpotPriceTest::setup(AliceBehaviourValues::default().with_min_buy(min_buy)).await; let btc_to_swap = bitcoin::Amount::from_btc(0.0001).unwrap(); - - let request = spot_price::Request { btc: btc_to_swap }; - - test.send_request(request); + test.construct_and_send_request(btc_to_swap); test.assert_error( alice::spot_price::Error::AmountBelowMinimum { buy: btc_to_swap, @@ -424,9 +438,7 @@ mod tests { let btc_to_swap = bitcoin::Amount::from_btc(0.01).unwrap(); - let request = spot_price::Request { btc: btc_to_swap }; - - test.send_request(request); + test.construct_and_send_request(btc_to_swap); test.assert_error( alice::spot_price::Error::AmountAboveMaximum { buy: btc_to_swap, @@ -446,10 +458,7 @@ mod tests { SpotPriceTest::setup(AliceBehaviourValues::default().with_resume_only(true)).await; let btc_to_swap = bitcoin::Amount::from_btc(0.01).unwrap(); - - let request = spot_price::Request { btc: btc_to_swap }; - - test.send_request(request); + test.construct_and_send_request(btc_to_swap); test.assert_error( alice::spot_price::Error::ResumeOnlyMode, bob::spot_price::Error::NoSwapsAccepted, @@ -464,10 +473,7 @@ mod tests { .await; let btc_to_swap = bitcoin::Amount::from_btc(0.01).unwrap(); - - let request = spot_price::Request { btc: btc_to_swap }; - - test.send_request(request); + test.construct_and_send_request(btc_to_swap); test.assert_error( alice::spot_price::Error::LatestRateFetchFailed(Box::new(TestRateError {})), bob::spot_price::Error::Other, @@ -484,9 +490,7 @@ mod tests { let btc_to_swap = bitcoin::Amount::from_btc(0.01).unwrap(); - let request = spot_price::Request { btc: btc_to_swap }; - - test.send_request(request); + test.construct_and_send_request(btc_to_swap); test.assert_error( alice::spot_price::Error::SellQuoteCalculationFailed(anyhow!( "Error text irrelevant, won't be checked here" @@ -496,6 +500,79 @@ mod tests { .await; } + #[tokio::test] + async fn given_alice_mainnnet_bob_testnet_then_network_mismatch_error() { + let mut test = SpotPriceTest::setup( + AliceBehaviourValues::default().with_env_config(env::Mainnet::get_config()), + ) + .await; + + let btc_to_swap = bitcoin::Amount::from_btc(0.01).unwrap(); + test.construct_and_send_request(btc_to_swap); + test.assert_error( + alice::spot_price::Error::BlockchainNetworkMismatch { + cli: BlockchainNetwork { + bitcoin: bitcoin::Network::Testnet, + monero: monero::Network::Stagenet, + }, + asb: BlockchainNetwork { + bitcoin: bitcoin::Network::Bitcoin, + monero: monero::Network::Mainnet, + }, + }, + bob::spot_price::Error::BlockchainNetworkMismatch { + cli: BlockchainNetwork { + bitcoin: bitcoin::Network::Testnet, + monero: monero::Network::Stagenet, + }, + asb: BlockchainNetwork { + bitcoin: bitcoin::Network::Bitcoin, + monero: monero::Network::Mainnet, + }, + }, + ) + .await; + } + + #[tokio::test] + async fn given_alice_testnet_bob_mainnet_then_network_mismatch_error() { + let mut test = SpotPriceTest::setup(AliceBehaviourValues::default()).await; + + let btc_to_swap = bitcoin::Amount::from_btc(0.01).unwrap(); + let request = spot_price::Request { + btc: btc_to_swap, + blockchain_network: BlockchainNetwork { + bitcoin: bitcoin::Network::Bitcoin, + monero: monero::Network::Mainnet, + }, + }; + + test.send_request(request); + test.assert_error( + alice::spot_price::Error::BlockchainNetworkMismatch { + cli: BlockchainNetwork { + bitcoin: bitcoin::Network::Bitcoin, + monero: monero::Network::Mainnet, + }, + asb: BlockchainNetwork { + bitcoin: bitcoin::Network::Testnet, + monero: monero::Network::Stagenet, + }, + }, + bob::spot_price::Error::BlockchainNetworkMismatch { + cli: BlockchainNetwork { + bitcoin: bitcoin::Network::Bitcoin, + monero: monero::Network::Mainnet, + }, + asb: BlockchainNetwork { + bitcoin: bitcoin::Network::Testnet, + monero: monero::Network::Stagenet, + }, + }, + ) + .await; + } + struct SpotPriceTest { alice_swarm: Swarm>, bob_swarm: Swarm, @@ -511,6 +588,7 @@ mod tests { values.lock_fee, values.min_buy, values.max_buy, + values.env_config, values.rate.clone(), values.resume_only, ) @@ -526,6 +604,17 @@ mod tests { } } + pub fn construct_and_send_request(&mut self, btc_to_swap: bitcoin::Amount) { + let request = spot_price::Request { + btc: btc_to_swap, + blockchain_network: BlockchainNetwork { + bitcoin: bitcoin::Network::Testnet, + monero: monero::Network::Stagenet, + }, + }; + self.send_request(request); + } + pub fn send_request(&mut self, spot_price_request: spot_price::Request) { self.bob_swarm .behaviour_mut() @@ -588,6 +677,19 @@ mod tests { assert_eq!(balance1, balance2); assert_eq!(buy1, buy2); } + ( + alice::spot_price::Error::BlockchainNetworkMismatch { + cli: cli1, + asb: asb1, + }, + alice::spot_price::Error::BlockchainNetworkMismatch { + cli: cli2, + asb: asb2, + }, + ) => { + assert_eq!(cli1, cli2); + assert_eq!(asb1, asb2); + } ( alice::spot_price::Error::AmountBelowMinimum { .. }, alice::spot_price::Error::AmountBelowMinimum { .. }, @@ -640,6 +742,7 @@ mod tests { pub max_buy: bitcoin::Amount, pub rate: TestRate, // 0.01 pub resume_only: bool, + pub env_config: env::Config, } impl AliceBehaviourValues { @@ -672,6 +775,11 @@ mod tests { self.rate = rate; self } + + pub fn with_env_config(mut self, env_config: env::Config) -> AliceBehaviourValues { + self.env_config = env_config; + self + } } #[derive(Clone, Debug)] diff --git a/swap/src/protocol/bob/event_loop.rs b/swap/src/protocol/bob/event_loop.rs index d3aa2a29..4fe347bf 100644 --- a/swap/src/protocol/bob/event_loop.rs +++ b/swap/src/protocol/bob/event_loop.rs @@ -1,10 +1,10 @@ use crate::bitcoin::EncryptedSignature; use crate::network::quote::BidQuote; -use crate::network::spot_price::Response; +use crate::network::spot_price::{BlockchainNetwork, Response}; use crate::network::{encrypted_signature, spot_price}; use crate::protocol::bob; use crate::protocol::bob::{Behaviour, OutEvent, State0, State2}; -use crate::{bitcoin, monero}; +use crate::{bitcoin, env, monero}; use anyhow::{bail, Context, Result}; use futures::future::{BoxFuture, OptionFuture}; use futures::{FutureExt, StreamExt}; @@ -55,6 +55,7 @@ impl EventLoop { swarm: Swarm, alice_peer_id: PeerId, bitcoin_wallet: Arc, + env_config: env::Config, ) -> Result<(Self, EventLoopHandle)> { let execution_setup = bmrng::channel_with_timeout(1, Duration::from_secs(30)); let transfer_proof = bmrng::channel_with_timeout(1, Duration::from_secs(30)); @@ -85,6 +86,7 @@ impl EventLoop { encrypted_signature: encrypted_signature.0, spot_price: spot_price.0, quote: quote.0, + env_config, }; Ok((event_loop, handle)) @@ -242,6 +244,7 @@ pub struct EventLoopHandle { encrypted_signature: bmrng::RequestSender, spot_price: bmrng::RequestSender, quote: bmrng::RequestSender<(), BidQuote>, + env_config: env::Config, } impl EventLoopHandle { @@ -265,7 +268,13 @@ impl EventLoopHandle { pub async fn request_spot_price(&mut self, btc: bitcoin::Amount) -> Result { let response = self .spot_price - .send_receive(spot_price::Request { btc }) + .send_receive(spot_price::Request { + btc, + blockchain_network: BlockchainNetwork { + bitcoin: self.env_config.bitcoin_network, + monero: self.env_config.monero_network, + }, + }) .await?; match response { diff --git a/swap/src/protocol/bob/spot_price.rs b/swap/src/protocol/bob/spot_price.rs index 58d03643..072fdb2d 100644 --- a/swap/src/protocol/bob/spot_price.rs +++ b/swap/src/protocol/bob/spot_price.rs @@ -54,6 +54,12 @@ pub enum Error { #[error("Seller's XMR balance is currently too low to fulfill the swap request to buy {buy}, please try again later")] BalanceTooLow { buy: bitcoin::Amount }, + #[error("Seller blockchain network {asb:?} setup did not match your blockchain network setup {cli:?}")] + BlockchainNetworkMismatch { + cli: spot_price::BlockchainNetwork, + asb: spot_price::BlockchainNetwork, + }, + /// To be used for errors that cannot be explained on the CLI side (e.g. /// rate update problems on the seller side) #[error("Seller encountered a problem, please try again later.")] @@ -71,6 +77,9 @@ impl From for Error { Error::AmountAboveMaximum { max, buy } } spot_price::Error::BalanceTooLow { buy } => Error::BalanceTooLow { buy }, + spot_price::Error::BlockchainNetworkMismatch { cli, asb } => { + Error::BlockchainNetworkMismatch { cli, asb } + } spot_price::Error::Other => Error::Other, } } diff --git a/swap/tests/harness/mod.rs b/swap/tests/harness/mod.rs index 52e1e6f5..3807cb94 100644 --- a/swap/tests/harness/mod.rs +++ b/swap/tests/harness/mod.rs @@ -239,6 +239,7 @@ async fn start_alice( max_buy, latest_rate, resume_only, + env_config, ) .unwrap(); swarm.listen_on(listen_address).unwrap(); @@ -458,6 +459,7 @@ impl BobParams { swarm, self.alice_peer_id, self.bitcoin_wallet.clone(), + self.env_config, ) } }