From a4895648575b89fcf177a1f7a988686b6f28a875 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Tue, 11 May 2021 11:30:48 +1000 Subject: [PATCH 01/19] Remove unused Electrum HTTP default URL --- swap/src/cli/command.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index deb19ce5..d2164070 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -10,14 +10,11 @@ use uuid::Uuid; // Port is assumed to be stagenet standard port 38081 pub const DEFAULT_STAGENET_MONERO_DAEMON_HOST: &str = "monero-stagenet.exan.tech"; -pub const DEFAULT_ELECTRUM_HTTP_URL: &str = "https://blockstream.info/testnet/api/"; const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://electrum.blockstream.info:60002"; +const DEFAULT_BITCOIN_CONFIRMATION_TARGET: &str = "3"; const DEFAULT_TOR_SOCKS5_PORT: &str = "9050"; -// Bitcoin transactions should be confirmed within X blocks -const DEFAULT_BITCOIN_CONFIRMATION_TARGET: &str = "3"; - #[derive(structopt::StructOpt, Debug)] #[structopt(name = "swap", about = "CLI for swapping BTC for XMR", author)] pub struct Arguments { From 657ac1e2e43c6268f5006dd4c8f3027799b08b39 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Tue, 11 May 2021 11:36:44 +1000 Subject: [PATCH 02/19] Bitcoin parameters that can be reused Get rid of parameter duplication. --- swap/src/bin/swap.rs | 30 +++++++++++++++++------- swap/src/cli/command.rs | 52 ++++++++++++++++------------------------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index 0daa0289..2b62fbc1 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -21,7 +21,7 @@ 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::{Arguments, BitcoinParams, Command, MoneroParams}; use swap::database::Database; use swap::env::{Config, GetConfig}; use swap::network::quote::BidQuote; @@ -45,14 +45,17 @@ async fn main() -> Result<()> { Command::BuyXmr { alice_peer_id, alice_multiaddr, + bitcoin_params: + BitcoinParams { + electrum_rpc_url, + bitcoin_target_block, + }, monero_params: MoneroParams { receive_monero_address, monero_daemon_host, }, - electrum_rpc_url, tor_socks5_port, - bitcoin_target_block, } => { let swap_id = Uuid::new_v4(); @@ -149,14 +152,17 @@ async fn main() -> Result<()> { Command::Resume { swap_id, alice_multiaddr, + bitcoin_params: + BitcoinParams { + electrum_rpc_url, + bitcoin_target_block, + }, monero_params: MoneroParams { receive_monero_address, monero_daemon_host, }, - electrum_rpc_url, tor_socks5_port, - bitcoin_target_block, } => { let data_dir = data.0; cli::tracing::init(debug, data_dir.join("logs"), swap_id)?; @@ -217,8 +223,11 @@ async fn main() -> Result<()> { Command::Cancel { swap_id, force, - electrum_rpc_url, - bitcoin_target_block, + bitcoin_params: + BitcoinParams { + electrum_rpc_url, + bitcoin_target_block, + }, } => { let data_dir = data.0; cli::tracing::init(debug, data_dir.join("logs"), swap_id)?; @@ -251,8 +260,11 @@ async fn main() -> Result<()> { Command::Refund { swap_id, force, - electrum_rpc_url, - bitcoin_target_block, + bitcoin_params: + BitcoinParams { + electrum_rpc_url, + bitcoin_target_block, + }, } => { let data_dir = data.0; cli::tracing::init(debug, data_dir.join("logs"), swap_id)?; diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index d2164070..34d370b9 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -42,20 +42,14 @@ pub enum Command { #[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)] + bitcoin_params: BitcoinParams, #[structopt(flatten)] monero_params: MoneroParams, #[structopt(long = "tor-socks5-port", help = "Your local Tor socks5 proxy port", default_value = DEFAULT_TOR_SOCKS5_PORT)] tor_socks5_port: u16, - - #[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, }, /// Show a list of past ongoing and completed swaps History, @@ -70,20 +64,14 @@ pub enum Command { #[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)] + bitcoin_params: BitcoinParams, #[structopt(flatten)] monero_params: MoneroParams, #[structopt(long = "tor-socks5-port", help = "Your local Tor socks5 proxy port", default_value = DEFAULT_TOR_SOCKS5_PORT)] tor_socks5_port: u16, - - #[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, }, /// Try to cancel an ongoing swap (expert users only) Cancel { @@ -96,14 +84,8 @@ pub enum Command { #[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_params: BitcoinParams, }, /// Try to cancel a swap and refund my BTC (expert users only) Refund { @@ -116,14 +98,8 @@ pub enum Command { #[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_params: BitcoinParams, }, } @@ -143,6 +119,18 @@ pub struct MoneroParams { pub monero_daemon_host: String, } +#[derive(structopt::StructOpt, Debug)] +pub struct BitcoinParams { + #[structopt(long = "electrum-rpc", + help = "Provide the Bitcoin Electrum RPC URL", + default_value = DEFAULT_ELECTRUM_RPC_URL + )] + pub 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)] + pub bitcoin_target_block: usize, +} + #[derive(Clone, Debug)] pub struct Data(pub PathBuf); From 4e1f3f82bdad006c4603e4d0cfe9d92a6668b120 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Tue, 11 May 2021 11:44:21 +1000 Subject: [PATCH 03/19] Remove `param` from names Did not add any value, more readable with just bitcoin and monero. --- swap/src/bin/swap.rs | 26 +++++++++++++------------- swap/src/cli/command.rs | 16 ++++++++-------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index 2b62fbc1..98ce7db1 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -21,7 +21,7 @@ use std::sync::Arc; use std::time::Duration; use structopt::StructOpt; use swap::bitcoin::TxLock; -use swap::cli::command::{Arguments, BitcoinParams, Command, MoneroParams}; +use swap::cli::command::{Arguments, Bitcoin, Command, Monero}; use swap::database::Database; use swap::env::{Config, GetConfig}; use swap::network::quote::BidQuote; @@ -45,13 +45,13 @@ async fn main() -> Result<()> { Command::BuyXmr { alice_peer_id, alice_multiaddr, - bitcoin_params: - BitcoinParams { + bitcoin: + Bitcoin { electrum_rpc_url, bitcoin_target_block, }, - monero_params: - MoneroParams { + monero: + Monero { receive_monero_address, monero_daemon_host, }, @@ -152,13 +152,13 @@ async fn main() -> Result<()> { Command::Resume { swap_id, alice_multiaddr, - bitcoin_params: - BitcoinParams { + bitcoin: + Bitcoin { electrum_rpc_url, bitcoin_target_block, }, - monero_params: - MoneroParams { + monero: + Monero { receive_monero_address, monero_daemon_host, }, @@ -223,8 +223,8 @@ async fn main() -> Result<()> { Command::Cancel { swap_id, force, - bitcoin_params: - BitcoinParams { + bitcoin: + Bitcoin { electrum_rpc_url, bitcoin_target_block, }, @@ -260,8 +260,8 @@ async fn main() -> Result<()> { Command::Refund { swap_id, force, - bitcoin_params: - BitcoinParams { + bitcoin: + Bitcoin { electrum_rpc_url, bitcoin_target_block, }, diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index 34d370b9..872eb70b 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -43,10 +43,10 @@ pub enum Command { alice_multiaddr: Multiaddr, #[structopt(flatten)] - bitcoin_params: BitcoinParams, + bitcoin: Bitcoin, #[structopt(flatten)] - monero_params: MoneroParams, + monero: Monero, #[structopt(long = "tor-socks5-port", help = "Your local Tor socks5 proxy port", default_value = DEFAULT_TOR_SOCKS5_PORT)] tor_socks5_port: u16, @@ -65,10 +65,10 @@ pub enum Command { alice_multiaddr: Multiaddr, #[structopt(flatten)] - bitcoin_params: BitcoinParams, + bitcoin: Bitcoin, #[structopt(flatten)] - monero_params: MoneroParams, + monero: Monero, #[structopt(long = "tor-socks5-port", help = "Your local Tor socks5 proxy port", default_value = DEFAULT_TOR_SOCKS5_PORT)] tor_socks5_port: u16, @@ -85,7 +85,7 @@ pub enum Command { force: bool, #[structopt(flatten)] - bitcoin_params: BitcoinParams, + bitcoin: Bitcoin, }, /// Try to cancel a swap and refund my BTC (expert users only) Refund { @@ -99,12 +99,12 @@ pub enum Command { force: bool, #[structopt(flatten)] - bitcoin_params: BitcoinParams, + 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) @@ -120,7 +120,7 @@ pub struct MoneroParams { } #[derive(structopt::StructOpt, Debug)] -pub struct BitcoinParams { +pub struct Bitcoin { #[structopt(long = "electrum-rpc", help = "Provide the Bitcoin Electrum RPC URL", default_value = DEFAULT_ELECTRUM_RPC_URL From 343badbb4bd6894e71969a5732f61d6126ea31b2 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Tue, 11 May 2021 12:00:11 +1000 Subject: [PATCH 04/19] Remove duplication and cleanup In the past we had problems with flags/parameter changes several times, where on instance was changed, buy another one was missed. This should mitigate this problem. This patch introduces structs for all duplicated parameters and uses flatten to only have one point for changes. Additionally removes all mentions of `alice` from the commands / variables. This code is on an application level and should not be concerned with swap protocol roles. --- swap/src/bin/swap.rs | 34 +++++++++++------------ swap/src/cli/command.rs | 60 ++++++++++++++++++++++++----------------- 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index 98ce7db1..7b5596fe 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -21,7 +21,7 @@ use std::sync::Arc; use std::time::Duration; use structopt::StructOpt; use swap::bitcoin::TxLock; -use swap::cli::command::{Arguments, Bitcoin, Command, Monero}; +use swap::cli::command::{Arguments, Bitcoin, Command, Monero, SellerAddr, SwapId, Tor}; use swap::database::Database; use swap::env::{Config, GetConfig}; use swap::network::quote::BidQuote; @@ -43,8 +43,8 @@ async fn main() -> Result<()> { match cmd { Command::BuyXmr { - alice_peer_id, - alice_multiaddr, + seller_peer_id, + seller_addr: SellerAddr { seller_addr }, bitcoin: Bitcoin { electrum_rpc_url, @@ -55,7 +55,7 @@ async fn main() -> Result<()> { receive_monero_address, monero_daemon_host, }, - tor_socks5_port, + tor: Tor { tor_socks5_port }, } => { let swap_id = Uuid::new_v4(); @@ -87,14 +87,14 @@ async fn main() -> Result<()> { init_monero_wallet(data_dir, monero_daemon_host, 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())?; + EventLoop::new(swap_id, swarm, seller_peer_id, bitcoin_wallet.clone())?; let event_loop = tokio::spawn(event_loop.run()); let max_givable = || bitcoin_wallet.max_giveable(TxLock::script_size()); @@ -109,7 +109,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, @@ -150,8 +150,8 @@ async fn main() -> Result<()> { table.printstd(); } Command::Resume { - swap_id, - alice_multiaddr, + swap_id: SwapId { swap_id }, + seller_addr: SellerAddr { seller_addr }, bitcoin: Bitcoin { electrum_rpc_url, @@ -162,7 +162,7 @@ async fn main() -> Result<()> { receive_monero_address, monero_daemon_host, }, - tor_socks5_port, + tor: Tor { tor_socks5_port }, } => { let data_dir = data.0; cli::tracing::init(debug, data_dir.join("logs"), swap_id)?; @@ -188,17 +188,17 @@ async fn main() -> Result<()> { init_monero_wallet(data_dir, monero_daemon_host, 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())?; + EventLoop::new(swap_id, swarm, seller_peer_id, bitcoin_wallet.clone())?; let handle = tokio::spawn(event_loop.run()); let swap = Swap::from_db( @@ -221,7 +221,7 @@ async fn main() -> Result<()> { } } Command::Cancel { - swap_id, + swap_id: SwapId { swap_id }, force, bitcoin: Bitcoin { @@ -258,7 +258,7 @@ async fn main() -> Result<()> { } } Command::Refund { - swap_id, + swap_id: SwapId { swap_id }, force, bitcoin: Bitcoin { diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index 872eb70b..a2454cbc 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -37,10 +37,10 @@ pub enum Command { /// Start a XMR for BTC swap BuyXmr { #[structopt(long = "seller-peer-id", help = "The seller's peer id")] - alice_peer_id: PeerId, + seller_peer_id: PeerId, - #[structopt(long = "seller-addr", help = "The seller's multiaddress")] - alice_multiaddr: Multiaddr, + #[structopt(flatten)] + seller_addr: SellerAddr, #[structopt(flatten)] bitcoin: Bitcoin, @@ -48,21 +48,18 @@ pub enum Command { #[structopt(flatten)] monero: Monero, - #[structopt(long = "tor-socks5-port", help = "Your local Tor socks5 proxy port", default_value = DEFAULT_TOR_SOCKS5_PORT)] - tor_socks5_port: u16, + #[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(flatten)] + swap_id: SwapId, - #[structopt(long = "seller-addr", help = "The seller's multiaddress")] - alice_multiaddr: Multiaddr, + #[structopt(flatten)] + seller_addr: SellerAddr, #[structopt(flatten)] bitcoin: Bitcoin, @@ -70,16 +67,13 @@ pub enum Command { #[structopt(flatten)] monero: Monero, - #[structopt(long = "tor-socks5-port", help = "Your local Tor socks5 proxy port", default_value = DEFAULT_TOR_SOCKS5_PORT)] - tor_socks5_port: u16, + #[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, @@ -89,11 +83,8 @@ pub enum Command { }, /// 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, @@ -131,6 +122,27 @@ pub struct Bitcoin { pub bitcoin_target_block: usize, } +#[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, +} + #[derive(Clone, Debug)] pub struct Data(pub PathBuf); From 1cdc23de3234e315d568918ba1feea871a6fefdb Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Tue, 11 May 2021 12:37:13 +1000 Subject: [PATCH 05/19] Explicitly specify `monero-wallet-rpc` deamon port In order to allow people to plug into public nodes / be more flexible with their own setup we now enforce specifying the monero daemon port to be used by the `monero-wallet-rpc`. --- CHANGELOG.md | 2 ++ swap/src/bin/swap.rs | 12 ++++++------ swap/src/cli/command.rs | 11 +++++------ swap/src/monero/wallet_rpc.rs | 6 +++--- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5914aabe..44f23bb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ 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 diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index 7b5596fe..1db7bac0 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -53,7 +53,7 @@ async fn main() -> Result<()> { monero: Monero { receive_monero_address, - monero_daemon_host, + monero_daemon_address, }, tor: Tor { tor_socks5_port }, } => { @@ -84,7 +84,7 @@ 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, seller_peer_id, tor_socks5_port).await?; @@ -160,7 +160,7 @@ async fn main() -> Result<()> { monero: Monero { receive_monero_address, - monero_daemon_host, + monero_daemon_address, }, tor: Tor { tor_socks5_port }, } => { @@ -185,7 +185,7 @@ 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 seller_peer_id = db.get_peer_id(swap_id)?; @@ -315,7 +315,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; @@ -325,7 +325,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/cli/command.rs b/swap/src/cli/command.rs index a2454cbc..6ceddcf4 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -7,8 +7,7 @@ use std::str::FromStr; 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"; +pub const DEFAULT_STAGENET_MONERO_DAEMON_ADDRESS: &str = "monero-stagenet.exan.tech:38081"; const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://electrum.blockstream.info:60002"; const DEFAULT_BITCOIN_CONFIRMATION_TARGET: &str = "3"; @@ -103,11 +102,11 @@ pub struct Monero { pub receive_monero_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: :", + default_value = DEFAULT_STAGENET_MONERO_DAEMON_ADDRESS )] - pub monero_daemon_host: String, + pub monero_daemon_address: String, } #[derive(structopt::StructOpt, Debug)] diff --git a/swap/src/monero/wallet_rpc.rs b/swap/src/monero/wallet_rpc.rs index f84a943f..24495530 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()? @@ -135,8 +135,8 @@ impl WalletRpc { Network::Stagenet => "--stagenet", Network::Testnet => "--testnet", }) - .arg("--daemon-host") - .arg(daemon_host) + .arg("--daemon-address") + .arg(daemon_address) .arg("--rpc-bind-port") .arg(format!("{}", port)) .arg("--disable-rpc-login") From 69cf12620dd02f59049417931f5d0755798c708a Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Tue, 11 May 2021 16:52:40 +1000 Subject: [PATCH 06/19] Activate mainnet for the CLI This includes testing CLI commandline args Clap's `default_value_with` actually did not work on `Subcommand`s because the parent's flags were not picked up. This was fixed by changing parameters dependent on testnet/mainnet to options. This problem should have been detected by tests, that's why the command line parameter tests were finally (re-)added. Thanks to @rishflab for some pre-work for this. --- swap/src/bin/swap.rs | 105 ++--- swap/src/cli/command.rs | 820 +++++++++++++++++++++++++++++++++++++--- swap/src/env.rs | 2 +- 3 files changed, 813 insertions(+), 114 deletions(-) diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index 1db7bac0..630b374f 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, Bitcoin, Command, Monero, SellerAddr, SwapId, Tor}; +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,44 +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 { seller_peer_id, - seller_addr: SellerAddr { seller_addr }, - bitcoin: - Bitcoin { - electrum_rpc_url, - bitcoin_target_block, - }, - monero: - Monero { - receive_monero_address, - monero_daemon_address, - }, - tor: Tor { tor_socks5_port }, + 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, @@ -118,7 +107,7 @@ async fn main() -> Result<()> { Arc::new(monero_wallet), env_config, event_loop_handle, - receive_monero_address, + monero_receive_address, send_bitcoin, ); @@ -133,8 +122,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")?; @@ -150,34 +137,26 @@ async fn main() -> Result<()> { table.printstd(); } Command::Resume { - swap_id: SwapId { swap_id }, - seller_addr: SellerAddr { seller_addr }, - bitcoin: - Bitcoin { - electrum_rpc_url, - bitcoin_target_block, - }, - monero: - Monero { - receive_monero_address, - monero_daemon_address, - }, - tor: Tor { tor_socks5_port }, + swap_id, + 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, @@ -208,7 +187,7 @@ async fn main() -> Result<()> { Arc::new(monero_wallet), env_config, event_loop_handle, - receive_monero_address, + monero_receive_address, )?; tokio::select! { @@ -221,24 +200,19 @@ async fn main() -> Result<()> { } } Command::Cancel { - swap_id: SwapId { swap_id }, + swap_id, force, - bitcoin: - Bitcoin { - electrum_rpc_url, - bitcoin_target_block, - }, + 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, @@ -258,24 +232,19 @@ async fn main() -> Result<()> { } } Command::Refund { - swap_id: SwapId { swap_id }, + swap_id, force, - bitcoin: - Bitcoin { - electrum_rpc_url, - bitcoin_target_block, - }, + 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, diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs index 6ceddcf4..3f8a8850 100644 --- a/swap/src/cli/command.rs +++ b/swap/src/cli/command.rs @@ -1,38 +1,236 @@ +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; -pub const DEFAULT_STAGENET_MONERO_DAEMON_ADDRESS: &str = "monero-stagenet.exan.tech:38081"; +// 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"; -const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://electrum.blockstream.info:60002"; -const DEFAULT_BITCOIN_CONFIRMATION_TARGET: &str = "3"; +// 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"; +#[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")] @@ -99,31 +297,34 @@ pub struct Monero { 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-address", - help = "Specify to connect to a monero daemon of your choice: :", - default_value = DEFAULT_STAGENET_MONERO_DAEMON_ADDRESS + help = "Specify to connect to a monero daemon of your choice: :" )] - pub monero_daemon_address: String, + pub monero_daemon_address: Option, } #[derive(structopt::StructOpt, Debug)] pub struct Bitcoin { - #[structopt(long = "electrum-rpc", - help = "Provide the Bitcoin Electrum RPC URL", - default_value = DEFAULT_ELECTRUM_RPC_URL - )] - pub electrum_rpc_url: Url, + #[structopt(long = "electrum-rpc", help = "Provide the Bitcoin Electrum RPC URL")] + pub bitcoin_electrum_rpc_url: Option, - #[structopt(long = "bitcoin-target-block", help = "Within how many blocks should the Bitcoin transactions be confirmed.", default_value = DEFAULT_BITCOIN_CONFIRMATION_TARGET)] - pub bitcoin_target_block: usize, + #[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)] + #[structopt( + long = "tor-socks5-port", + help = "Your local Tor socks5 proxy port", + default_value = DEFAULT_TOR_SOCKS5_PORT + )] pub tor_socks5_port: u16, } @@ -142,39 +343,92 @@ pub struct SellerAddr { pub seller_addr: Multiaddr, } -#[derive(Clone, Debug)] -pub struct Data(pub PathBuf); +mod data { + use super::*; -/// 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"), - ) + 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!( @@ -183,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..3164c3bd 100644 --- a/swap/src/env.rs +++ b/swap/src/env.rs @@ -3,7 +3,7 @@ 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, From 9ac5b635d72c6bb2bccdadd70bb8261c7fec5983 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Tue, 11 May 2021 18:03:33 +1000 Subject: [PATCH 07/19] Introduce own de-/serializable `monero::Network` --- swap/src/bin/swap.rs | 4 ++-- swap/src/env.rs | 1 + swap/src/monero.rs | 29 ++++++++++++++++++++++++++++- swap/src/monero/wallet.rs | 2 +- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/swap/src/bin/swap.rs b/swap/src/bin/swap.rs index 630b374f..5e643dd1 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -151,7 +151,7 @@ async fn main() -> Result<()> { let seed = Seed::from_file_or_generate(data_dir.as_path()) .context("Failed to read in seed file")?; - if monero_receive_address.network != env_config.monero_network { + if monero_receive_address.network != env_config.monero_network.into() { bail!("The given monero address is on network {:?}, expected address of network {:?}.", monero_receive_address.network, env_config.monero_network) } @@ -294,7 +294,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_address.as_str()) + .run(network.into(), monero_daemon_address.as_str()) .await?; let monero_wallet = monero::Wallet::open_or_create( diff --git a/swap/src/env.rs b/swap/src/env.rs index 3164c3bd..debe18c7 100644 --- a/swap/src/env.rs +++ b/swap/src/env.rs @@ -1,4 +1,5 @@ use crate::bitcoin::{CancelTimelock, PunishTimelock}; +use crate::monero; use std::cmp::max; use std::time::Duration; use time::NumericalStdDurationShort; diff --git a/swap/src/monero.rs b/swap/src/monero.rs index 6886cbf1..c56a4622 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -1,7 +1,7 @@ pub mod wallet; mod wallet_rpc; -pub use ::monero::{Address, Network, PrivateKey, PublicKey}; +pub use ::monero::{Address, PrivateKey, PublicKey}; pub use curve25519_dalek::scalar::Scalar; pub use wallet::Wallet; pub use wallet_rpc::{WalletRpc, WalletRpcProcess}; @@ -19,6 +19,33 @@ use std::str::FromStr; pub const PICONERO_OFFSET: u64 = 1_000_000_000_000; +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)] +pub enum Network { + Mainnet, + Stagenet, + Testnet, +} + +impl From for Network { + fn from(network: monero::Network) -> Self { + match network { + monero::Network::Mainnet => Self::Mainnet, + monero::Network::Stagenet => Self::Stagenet, + monero::Network::Testnet => Self::Testnet, + } + } +} + +impl From for monero::Network { + fn from(network: Network) -> Self { + match network { + Network::Mainnet => monero::Network::Mainnet, + Network::Stagenet => monero::Network::Stagenet, + Network::Testnet => monero::Network::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.rs b/swap/src/monero/wallet.rs index 4732a632..686c783c 100644 --- a/swap/src/monero/wallet.rs +++ b/swap/src/monero/wallet.rs @@ -47,7 +47,7 @@ impl Wallet { monero::Address::from_str(client.get_address(0).await?.address.as_str())?; Ok(Self { inner: Mutex::new(client), - network: env_config.monero_network, + network: env_config.monero_network.into(), name, main_address, sync_interval: env_config.monero_sync_interval(), From 02974811adda29fa9ccc668b51f157502062f7c0 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Mon, 10 May 2021 18:06:10 +1000 Subject: [PATCH 08/19] Activate mainnet for the ASB To run the ASB on testnet, one actively has to provide the `--testnet` flag. Mainnet and testnet data and config are separated into sub-folders, i.e. `{data/config-dir}/asb/testnet` and `{data-dir}/asb/mainnet`. The initial setup is also per network. If (default) config for the network cannot be found the initial setup is triggered. Startup includes network check to ensure the bitcoin/monero network in config file is the same as the one in the `env::Config`. Note: Wallet initialization is done with the network set in the `env::Config`, the network saved in the config file is just to indicate what network the config file is for. --- swap/src/asb/command.rs | 3 + swap/src/asb/config.rs | 190 ++++++++++++++++++++++++++++++---------- swap/src/bin/asb.rs | 46 +++++++--- 3 files changed, 181 insertions(+), 58 deletions(-) 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..2c34ec73 100644 --- a/swap/src/asb/config.rs +++ b/swap/src/asb/config.rs @@ -1,4 +1,6 @@ +use crate::env::{Mainnet, Testnet}; use crate::fs::{ensure_directory_exists, system_config_dir, system_data_dir}; +use crate::monero; use crate::tor::{DEFAULT_CONTROL_PORT, DEFAULT_SOCKS5_PORT}; use anyhow::{bail, Context, Result}; use config::ConfigError; @@ -11,14 +13,69 @@ 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: system_config_dir() + .map(|dir| Path::join(&dir, "asb")) + .map(|dir| Path::join(&dir, "testnet")) + .map(|dir| Path::join(&dir, "config.toml")) + .context("Could not generate default config file path")?, + data_dir: system_data_dir() + .map(|proj_dir| Path::join(&proj_dir, "asb")) + .map(|proj_dir| Path::join(&proj_dir, "testnet")) + .context("Could not generate default data dir")?, + 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: system_config_dir() + .map(|dir| Path::join(&dir, "asb")) + .map(|dir| Path::join(&dir, "mainnet")) + .map(|dir| Path::join(&dir, "config.toml")) + .context("Could not generate default config file path")?, + data_dir: system_data_dir() + .map(|proj_dir| Path::join(&proj_dir, "asb")) + .map(|proj_dir| Path::join(&proj_dir, "mainnet")) + .context("Could not generate default data dir")?, + 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) + } +} const DEFAULT_MIN_BUY_AMOUNT: f64 = 0.002f64; const DEFAULT_MAX_BUY_AMOUNT: f64 = 0.02f64; @@ -64,12 +121,14 @@ pub struct Network { pub struct Bitcoin { pub electrum_rpc_url: Url, pub target_block: usize, + pub network: bitcoin::Network, } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(deny_unknown_fields)] pub struct Monero { pub wallet_rpc_url: Url, + pub network: monero::Network, } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] @@ -118,30 +177,11 @@ 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<()> +pub fn initial_setup(config_path: PathBuf, config_file: F, testnet: bool) -> Result<()> where - F: Fn() -> Result, + F: Fn(bool) -> Result, { - info!("Config file not found, running initial setup..."); - let initial_config = config_file()?; - + let initial_config = config_file(testnet)?; let toml = toml::to_string(&initial_config)?; ensure_directory_exists(config_path.as_path())?; @@ -154,13 +194,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 +227,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 +290,11 @@ pub fn query_user_for_initial_testnet_config() -> Result { bitcoin: Bitcoin { electrum_rpc_url, target_block, + network: bitcoin_network, }, monero: Monero { wallet_rpc_url: monero_wallet_rpc_url, + network: monero_network, }, tor: TorConf { control_port: tor_control_port, @@ -253,31 +311,31 @@ 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, + 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, + network: monero::Network::Testnet, }, tor: Default::default(), maker: Maker { @@ -287,7 +345,45 @@ mod tests { }, }; - initial_setup(config_path.clone(), || Ok(expected.clone())).unwrap(); + initial_setup(config_path.clone(), |_| Ok(expected.clone()), false).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, + 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, + 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(), |_| Ok(expected.clone()), true).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..6149fd8c 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,8 +22,8 @@ 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; @@ -45,23 +45,49 @@ 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 = if testnet { + env::Testnet::get_config() + } else { + env::Mainnet::get_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 +101,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?; From af60d3bb54fe173343652630de9a29229baf3409 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Tue, 11 May 2021 22:22:59 +1000 Subject: [PATCH 09/19] Network check upon spot price request --- swap/src/asb/config.rs | 7 +- swap/src/bin/asb.rs | 1 + swap/src/bin/swap.rs | 22 ++- swap/src/bitcoin.rs | 11 ++ swap/src/env.rs | 1 - swap/src/monero.rs | 27 +--- swap/src/monero/wallet.rs | 2 +- swap/src/network/spot_price.rs | 28 ++++ swap/src/network/swarm.rs | 5 +- swap/src/protocol/alice/behaviour.rs | 4 +- swap/src/protocol/alice/event_loop.rs | 3 +- swap/src/protocol/alice/spot_price.rs | 184 ++++++++++++++++++++------ swap/src/protocol/bob/event_loop.rs | 15 ++- swap/src/protocol/bob/spot_price.rs | 9 ++ swap/tests/harness/mod.rs | 2 + 15 files changed, 245 insertions(+), 76 deletions(-) diff --git a/swap/src/asb/config.rs b/swap/src/asb/config.rs index 2c34ec73..08ba65c5 100644 --- a/swap/src/asb/config.rs +++ b/swap/src/asb/config.rs @@ -1,6 +1,5 @@ use crate::env::{Mainnet, Testnet}; use crate::fs::{ensure_directory_exists, system_config_dir, system_data_dir}; -use crate::monero; use crate::tor::{DEFAULT_CONTROL_PORT, DEFAULT_SOCKS5_PORT}; use anyhow::{bail, Context, Result}; use config::ConfigError; @@ -121,6 +120,8 @@ 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, } @@ -128,6 +129,8 @@ pub struct Bitcoin { #[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, } @@ -335,7 +338,7 @@ mod tests { monero: Monero { wallet_rpc_url: defaults.monero_wallet_rpc_url, - network: monero::Network::Testnet, + network: monero::Network::Stagenet, }, tor: Default::default(), maker: Maker { diff --git a/swap/src/bin/asb.rs b/swap/src/bin/asb.rs index 6149fd8c..511955ac 100644 --- a/swap/src/bin/asb.rs +++ b/swap/src/bin/asb.rs @@ -151,6 +151,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 5e643dd1..09ec0f88 100644 --- a/swap/src/bin/swap.rs +++ b/swap/src/bin/swap.rs @@ -82,8 +82,13 @@ async fn main() -> Result<()> { .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, seller_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()); @@ -151,7 +156,7 @@ async fn main() -> Result<()> { let seed = Seed::from_file_or_generate(data_dir.as_path()) .context("Failed to read in seed file")?; - if monero_receive_address.network != env_config.monero_network.into() { + 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) } @@ -176,8 +181,13 @@ async fn main() -> Result<()> { .behaviour_mut() .add_address(seller_peer_id, seller_addr); - let (event_loop, event_loop_handle) = - EventLoop::new(swap_id, swarm, seller_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( @@ -294,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.into(), monero_daemon_address.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/env.rs b/swap/src/env.rs index debe18c7..3164c3bd 100644 --- a/swap/src/env.rs +++ b/swap/src/env.rs @@ -1,5 +1,4 @@ use crate::bitcoin::{CancelTimelock, PunishTimelock}; -use crate::monero; use std::cmp::max; use std::time::Duration; use time::NumericalStdDurationShort; diff --git a/swap/src/monero.rs b/swap/src/monero.rs index c56a4622..c4649410 100644 --- a/swap/src/monero.rs +++ b/swap/src/monero.rs @@ -1,6 +1,7 @@ pub mod wallet; mod wallet_rpc; +pub use ::monero::network::Network; pub use ::monero::{Address, PrivateKey, PublicKey}; pub use curve25519_dalek::scalar::Scalar; pub use wallet::Wallet; @@ -19,33 +20,15 @@ use std::str::FromStr; pub const PICONERO_OFFSET: u64 = 1_000_000_000_000; -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize)] -pub enum Network { +#[derive(Serialize, Deserialize)] +#[serde(remote = "Network")] +#[allow(non_camel_case_types)] +pub enum network { Mainnet, Stagenet, Testnet, } -impl From for Network { - fn from(network: monero::Network) -> Self { - match network { - monero::Network::Mainnet => Self::Mainnet, - monero::Network::Stagenet => Self::Stagenet, - monero::Network::Testnet => Self::Testnet, - } - } -} - -impl From for monero::Network { - fn from(network: Network) -> Self { - match network { - Network::Mainnet => monero::Network::Mainnet, - Network::Stagenet => monero::Network::Stagenet, - Network::Testnet => monero::Network::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.rs b/swap/src/monero/wallet.rs index 686c783c..4732a632 100644 --- a/swap/src/monero/wallet.rs +++ b/swap/src/monero/wallet.rs @@ -47,7 +47,7 @@ impl Wallet { monero::Address::from_str(client.get_address(0).await?.address.as_str())?; Ok(Self { inner: Mutex::new(client), - network: env_config.monero_network.into(), + network: env_config.monero_network, name, main_address, sync_interval: env_config.monero_sync_interval(), 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, ) } } From 7ec323ea1f967aad68950bbebfed8b2c04091863 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Tue, 11 May 2021 22:30:48 +1000 Subject: [PATCH 10/19] Mainnet changelog entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44f23bb0..5ac46f25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 From 7f8af7926d9a77924cb2a1c7a9a4f322319f1470 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Wed, 12 May 2021 11:25:45 +1000 Subject: [PATCH 11/19] ASB config may specify finality confirmations By default the finality confirmations of the network's `env::Config` will be applied and no finality confirmations will be persisted on disk in the config file. It is however possible to set finality confirmations in the config file for bitcoin and monero for power users at their own risk. If set the defaults will be overwritten with the parameter from the config file upon startup. --- swap/src/asb/config.rs | 6 ++++++ swap/src/bin/asb.rs | 7 +------ swap/src/env.rs | 28 ++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/swap/src/asb/config.rs b/swap/src/asb/config.rs index 08ba65c5..c27a04da 100644 --- a/swap/src/asb/config.rs +++ b/swap/src/asb/config.rs @@ -293,10 +293,12 @@ pub fn query_user_for_initial_config(testnet: bool) -> 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 { @@ -330,6 +332,7 @@ mod tests { bitcoin: Bitcoin { electrum_rpc_url: defaults.electrum_rpc_url, target_block: defaults.bitcoin_confirmation_target, + finality_confirmations: None, network: bitcoin::Network::Testnet, }, network: Network { @@ -338,6 +341,7 @@ mod tests { monero: Monero { wallet_rpc_url: defaults.monero_wallet_rpc_url, + finality_confirmations: None, network: monero::Network::Stagenet, }, tor: Default::default(), @@ -368,6 +372,7 @@ mod tests { bitcoin: Bitcoin { electrum_rpc_url: defaults.electrum_rpc_url, target_block: defaults.bitcoin_confirmation_target, + finality_confirmations: None, network: bitcoin::Network::Bitcoin, }, network: Network { @@ -376,6 +381,7 @@ mod tests { monero: Monero { wallet_rpc_url: defaults.monero_wallet_rpc_url, + finality_confirmations: None, network: monero::Network::Mainnet, }, tor: Default::default(), diff --git a/swap/src/bin/asb.rs b/swap/src/bin/asb.rs index 511955ac..d32ba3af 100644 --- a/swap/src/bin/asb.rs +++ b/swap/src/bin/asb.rs @@ -26,7 +26,6 @@ use swap::asb::config::{ GetDefaults, }; use swap::database::Database; -use swap::env::GetConfig; use swap::monero::Amount; use swap::network::swarm; use swap::protocol::alice; @@ -69,11 +68,7 @@ async fn main() -> Result<()> { } }; - let env_config = if testnet { - env::Testnet::get_config() - } else { - env::Mainnet::get_config() - }; + let env_config = env::new(testnet, &config); if config.monero.network != env_config.monero_network { bail!(format!( diff --git a/swap/src/env.rs b/swap/src/env.rs index 3164c3bd..e7834e6a 100644 --- a/swap/src/env.rs +++ b/swap/src/env.rs @@ -1,3 +1,4 @@ +use crate::asb; use crate::bitcoin::{CancelTimelock, PunishTimelock}; use std::cmp::max; use std::time::Duration; @@ -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::*; From cfa85e0badc05fc7310df332c6c9f43d58d47fcd Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Thu, 13 May 2021 13:56:40 +1000 Subject: [PATCH 12/19] Simplify ASB initial setup signature --- swap/src/asb/config.rs | 12 ++++-------- swap/src/bin/asb.rs | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/swap/src/asb/config.rs b/swap/src/asb/config.rs index c27a04da..93926c3b 100644 --- a/swap/src/asb/config.rs +++ b/swap/src/asb/config.rs @@ -180,12 +180,8 @@ pub fn read_config(config_path: PathBuf) -> Result(config_path: PathBuf, config_file: F, testnet: bool) -> Result<()> -where - F: Fn(bool) -> Result, -{ - let initial_config = config_file(testnet)?; - 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)?; @@ -352,7 +348,7 @@ mod tests { }, }; - initial_setup(config_path.clone(), |_| Ok(expected.clone()), false).unwrap(); + initial_setup(config_path.clone(), expected.clone()).unwrap(); let actual = read_config(config_path).unwrap().unwrap(); assert_eq!(expected, actual); @@ -392,7 +388,7 @@ mod tests { }, }; - initial_setup(config_path.clone(), |_| Ok(expected.clone()), true).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 d32ba3af..afef29f3 100644 --- a/swap/src/bin/asb.rs +++ b/swap/src/bin/asb.rs @@ -63,7 +63,7 @@ async fn main() -> Result<()> { let config = match read_config(config_path.clone())? { Ok(config) => config, Err(ConfigNotInitialized {}) => { - initial_setup(config_path.clone(), query_user_for_initial_config, testnet)?; + initial_setup(config_path.clone(), query_user_for_initial_config(testnet)?)?; read_config(config_path)?.expect("after initial setup config can be read") } }; From 1aaffb09f9b2153329074eade2b1e97e33f51b7b Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Thu, 13 May 2021 15:28:53 +1000 Subject: [PATCH 13/19] Refactor ASB test-/mainnet default dir init --- swap/src/asb/config.rs | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/swap/src/asb/config.rs b/swap/src/asb/config.rs index 93926c3b..984bc3ee 100644 --- a/swap/src/asb/config.rs +++ b/swap/src/asb/config.rs @@ -33,15 +33,10 @@ pub struct Defaults { impl GetDefaults for Testnet { fn getConfigFileDefaults() -> Result { let defaults = Defaults { - config_path: system_config_dir() - .map(|dir| Path::join(&dir, "asb")) - .map(|dir| Path::join(&dir, "testnet")) - .map(|dir| Path::join(&dir, "config.toml")) - .context("Could not generate default config file path")?, - data_dir: system_data_dir() - .map(|proj_dir| Path::join(&proj_dir, "asb")) - .map(|proj_dir| Path::join(&proj_dir, "testnet")) - .context("Could not generate default data dir")?, + 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")?, @@ -56,15 +51,10 @@ impl GetDefaults for Testnet { impl GetDefaults for Mainnet { fn getConfigFileDefaults() -> Result { let defaults = Defaults { - config_path: system_config_dir() - .map(|dir| Path::join(&dir, "asb")) - .map(|dir| Path::join(&dir, "mainnet")) - .map(|dir| Path::join(&dir, "config.toml")) - .context("Could not generate default config file path")?, - data_dir: system_data_dir() - .map(|proj_dir| Path::join(&proj_dir, "asb")) - .map(|proj_dir| Path::join(&proj_dir, "mainnet")) - .context("Could not generate default data dir")?, + 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")?, @@ -76,6 +66,18 @@ impl GetDefaults for Mainnet { } } +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; const DEFAULT_SPREAD: f64 = 0.02f64; From 4dd696ebe15391e06886f10957f834b969b1121e Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Thu, 13 May 2021 16:19:18 +1000 Subject: [PATCH 14/19] Fix `monero-wallet-rpc` startup for mainnet CLI There is no `--mainnet` flag. Since we cannot just pass an empty string to `.arg()` we use the `.args()` method to pass nothing for mainnet and the respective flags for stagenet and testnet. --- swap/src/monero/wallet_rpc.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/swap/src/monero/wallet_rpc.rs b/swap/src/monero/wallet_rpc.rs index 24495530..9a6619ea 100644 --- a/swap/src/monero/wallet_rpc.rs +++ b/swap/src/monero/wallet_rpc.rs @@ -126,15 +126,23 @@ 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", - }) + .args(network_flag) .arg("--daemon-address") .arg(daemon_address) .arg("--rpc-bind-port") From f2e43ea56525a6dc69864f8acfdf1fe7fac59185 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Mon, 17 May 2021 18:39:33 +1000 Subject: [PATCH 15/19] Let testnet setup reflect mainnet Our test values should reflect what we test on mainnet more closely to avoid bugs that are only observed when using mainnet settings. --- swap/src/env.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/swap/src/env.rs b/swap/src/env.rs index e7834e6a..23b1d6e3 100644 --- a/swap/src/env.rs +++ b/swap/src/env.rs @@ -60,13 +60,13 @@ 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: 3, + bitcoin_avg_block_time: 10.minutes(), bitcoin_cancel_timelock: CancelTimelock::new(12), bitcoin_punish_timelock: PunishTimelock::new(6), bitcoin_network: bitcoin::Network::Testnet, monero_avg_block_time: 2.minutes(), - monero_finality_confirmations: 10, + monero_finality_confirmations: 15, monero_network: monero::Network::Stagenet, } } From efb51820b1c6b164703077f06da6e7e9f0c9b8cc Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Mon, 17 May 2021 18:40:10 +1000 Subject: [PATCH 16/19] Poll block headers for latest block on each iteration The Electrum block-header subscription did not provide us with block headers, because upon the connection being closed by a node the subscription would end. Re-newing the the subscription upon re-connect is not easily achievable, that's why we opted for a polling mode for now, where we start a block header subscription on every update iteration, that is only used once (when the subscription is made). --- swap/src/bitcoin/wallet.rs | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index 3de1107a..64d1aad4 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -527,7 +527,7 @@ impl Watchable for (Txid, Script) { pub struct Client { electrum: bdk::electrum_client::Client, - latest_block: BlockHeight, + latest_block_height: BlockHeight, last_ping: Instant, interval: Duration, script_history: BTreeMap>, @@ -536,13 +536,15 @@ pub struct Client { 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)?, + latest_block_height: BlockHeight::try_from(latest_block)?, last_ping: Instant::now(), interval, script_history: Default::default(), @@ -572,14 +574,14 @@ impl Client { } } - fn drain_notifications(&mut self) -> Result<()> { + fn update_state(&mut self) -> Result<()> { let pinged = self.ping(); if !pinged { return Ok(()); } - self.drain_blockheight_notifications()?; + self.update_latest_block()?; self.update_script_histories()?; Ok(()) @@ -596,7 +598,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 +620,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 +628,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(()) From bae38a712fe6144da7bde2876e9dbe4c030d88ac Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Mon, 17 May 2021 19:10:11 +1000 Subject: [PATCH 17/19] Sync on interval instead of ping Since we don't rely on long running subscriptions anymore we can remove the ping that was used to ensure a connection refresh. --- swap/src/bitcoin/wallet.rs | 36 +++++++----------------------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index 64d1aad4..59fac072 100644 --- a/swap/src/bitcoin/wallet.rs +++ b/swap/src/bitcoin/wallet.rs @@ -528,8 +528,8 @@ impl Watchable for (Txid, Script) { pub struct Client { electrum: bdk::electrum_client::Client, latest_block_height: BlockHeight, - last_ping: Instant, - interval: Duration, + last_sync: Instant, + sync_interval: Duration, script_history: BTreeMap>, subscriptions: HashMap<(Txid, Script), Subscription>, } @@ -545,42 +545,20 @@ impl Client { Ok(Self { electrum, latest_block_height: BlockHeight::try_from(latest_block)?, - last_ping: Instant::now(), - interval, + 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 update_state(&mut self) -> Result<()> { - let pinged = self.ping(); - - if !pinged { + let now = Instant::now(); + if now < self.last_sync + self.sync_interval { return Ok(()); } + self.last_sync = now; self.update_latest_block()?; self.update_script_histories()?; From 6694e4f4e06b6cc4aa9e195287475fc25a6a42d5 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Wed, 19 May 2021 16:21:22 +1000 Subject: [PATCH 18/19] Ensure that output of lock script is at tx-output index `0` We subscribe to transactions upon broadcast, where we use output index `0` for the subscription. In order to ensure that this subscription is guaranteed to be for the locking script (and not a change output) we now ensure that the locking script output is always at index `0` of the outputs of the transaction. We chose this solution because otherwise we would have to add more information to broadcasting a transaction. This solution is less intrusive, because the order of transaction outputs should not have any side effects and ensuring index `0` makes the whole behaviour more deterministic. --- swap/src/bitcoin/wallet.rs | 101 +++++++++++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 15 deletions(-) diff --git a/swap/src/bitcoin/wallet.rs b/swap/src/bitcoin/wallet.rs index 59fac072..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!( @@ -760,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] @@ -990,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 + ); + } + } } From 2db470f0994d4262a30a43b2876eeac7b72ac540 Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Wed, 19 May 2021 16:36:41 +1000 Subject: [PATCH 19/19] Bitcoin=2 Monero=10 default finality confirmations It is currently not expected that ASB and CLI are used for swaps > 10_000$ equivalent to XMR/BTC, thus the finality confirmations were reduced to an equivalent of 20 mins of work (2 blocks for Bitcoin, 10 for Monero). Monero enforces 10 unlocking blocks until the balance is spendable, so the finality confirmations cannot be set lower than 10. --- swap/src/env.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/swap/src/env.rs b/swap/src/env.rs index 23b1d6e3..95d4488a 100644 --- a/swap/src/env.rs +++ b/swap/src/env.rs @@ -44,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, } } @@ -60,13 +60,13 @@ impl GetConfig for Testnet { fn get_config() -> Config { Config { bitcoin_lock_confirmed_timeout: 12.hours(), - bitcoin_finality_confirmations: 3, + 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, monero_avg_block_time: 2.minutes(), - monero_finality_confirmations: 15, + monero_finality_confirmations: 10, monero_network: monero::Network::Stagenet, } }