use crate::bitcoin::Amount; use crate::env::GetConfig; use crate::fs::system_data_dir; use crate::network::rendezvous::XmrBtcNamespace; use crate::{env, monero}; use anyhow::{bail, Context, Result}; use bitcoin::{Address, AddressType}; use libp2p::core::Multiaddr; use serde::Serialize; use std::ffi::OsString; use std::path::PathBuf; use std::str::FromStr; use structopt::{clap, StructOpt}; use url::Url; use uuid::Uuid; // See: https://moneroworld.com/ pub const DEFAULT_MONERO_DAEMON_ADDRESS: &str = "node.melo.tools:18081"; pub const DEFAULT_MONERO_DAEMON_ADDRESS_STAGENET: &str = "stagenet.melo.tools:38081"; // See: https://1209k.com/bitcoin-eye/ele.php?chain=btc const DEFAULT_ELECTRUM_RPC_URL: &str = "ssl://blockstream.info:700"; // 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 json: bool, pub data_dir: PathBuf, pub cmd: Command, } /// Represents the result of parsing the command-line parameters. #[derive(Debug, PartialEq)] pub enum ParseResult { /// The arguments we were invoked in. Arguments(Box), /// A flag or command was given that does not need further processing other /// than printing the provided message. /// /// The caller should exit the program with exit code 0. PrintAndExitZero { message: String }, } pub fn parse_args_and_apply_defaults(raw_args: I) -> Result where I: IntoIterator, T: Into + Clone, { let args = match RawArguments::clap().get_matches_from_safe(raw_args) { Ok(matches) => RawArguments::from_clap(&matches), Err(clap::Error { message, kind: clap::ErrorKind::HelpDisplayed | clap::ErrorKind::VersionDisplayed, .. }) => return Ok(ParseResult::PrintAndExitZero { message }), Err(e) => anyhow::bail!(e), }; let debug = args.debug; let json = args.json; let is_testnet = args.testnet; let data = args.data; let arguments = match args.cmd { RawCommand::BuyXmr { seller: Seller { seller }, bitcoin, bitcoin_change_address, monero, monero_receive_address, tor: Tor { tor_socks5_port }, } => { let (bitcoin_electrum_rpc_url, bitcoin_target_block) = bitcoin.apply_defaults(is_testnet)?; let monero_daemon_address = monero.apply_defaults(is_testnet); let monero_receive_address = validate_monero_address(monero_receive_address, is_testnet)?; let bitcoin_change_address = validate_bitcoin_address(bitcoin_change_address, is_testnet)?; Arguments { env_config: env_config_from(is_testnet), debug, json, data_dir: data::data_dir_from(data, is_testnet)?, cmd: Command::BuyXmr { seller, bitcoin_electrum_rpc_url, bitcoin_target_block, bitcoin_change_address, monero_receive_address, monero_daemon_address, tor_socks5_port, }, } } RawCommand::History => Arguments { env_config: env_config_from(is_testnet), debug, json, data_dir: data::data_dir_from(data, is_testnet)?, cmd: Command::History, }, RawCommand::Config => Arguments { env_config: env_config_from(is_testnet), debug, json, data_dir: data::data_dir_from(data, is_testnet)?, cmd: Command::Config, }, RawCommand::Balance { bitcoin_electrum_rpc_url, } => { let bitcoin = Bitcoin { bitcoin_electrum_rpc_url, bitcoin_target_block: None, }; let (bitcoin_electrum_rpc_url, bitcoin_target_block) = bitcoin.apply_defaults(is_testnet)?; Arguments { env_config: env_config_from(is_testnet), debug, json, data_dir: data::data_dir_from(data, is_testnet)?, cmd: Command::Balance { bitcoin_electrum_rpc_url, bitcoin_target_block, }, } } RawCommand::WithdrawBtc { bitcoin, amount, address, } => { let (bitcoin_electrum_rpc_url, bitcoin_target_block) = bitcoin.apply_defaults(is_testnet)?; Arguments { env_config: env_config_from(is_testnet), debug, json, data_dir: data::data_dir_from(data, is_testnet)?, cmd: Command::WithdrawBtc { bitcoin_electrum_rpc_url, bitcoin_target_block, amount, address: bitcoin_address(address, is_testnet)?, }, } } RawCommand::Resume { swap_id: SwapId { swap_id }, bitcoin, monero, tor: Tor { tor_socks5_port }, } => { let (bitcoin_electrum_rpc_url, bitcoin_target_block) = bitcoin.apply_defaults(is_testnet)?; let monero_daemon_address = monero.apply_defaults(is_testnet); Arguments { env_config: env_config_from(is_testnet), debug, json, data_dir: data::data_dir_from(data, is_testnet)?, cmd: Command::Resume { swap_id, bitcoin_electrum_rpc_url, bitcoin_target_block, monero_daemon_address, tor_socks5_port, }, } } RawCommand::Cancel { swap_id: SwapId { swap_id }, bitcoin, } => { let (bitcoin_electrum_rpc_url, bitcoin_target_block) = bitcoin.apply_defaults(is_testnet)?; Arguments { env_config: env_config_from(is_testnet), debug, json, data_dir: data::data_dir_from(data, is_testnet)?, cmd: Command::Cancel { swap_id, bitcoin_electrum_rpc_url, bitcoin_target_block, }, } } RawCommand::Refund { swap_id: SwapId { swap_id }, bitcoin, } => { let (bitcoin_electrum_rpc_url, bitcoin_target_block) = bitcoin.apply_defaults(is_testnet)?; Arguments { env_config: env_config_from(is_testnet), debug, json, data_dir: data::data_dir_from(data, is_testnet)?, cmd: Command::Refund { swap_id, bitcoin_electrum_rpc_url, bitcoin_target_block, }, } } RawCommand::ListSellers { rendezvous_point, tor: Tor { tor_socks5_port }, } => Arguments { env_config: env_config_from(is_testnet), debug, json, data_dir: data::data_dir_from(data, is_testnet)?, cmd: Command::ListSellers { rendezvous_point, namespace: rendezvous_namespace_from(is_testnet), tor_socks5_port, }, }, RawCommand::ExportBitcoinWallet { bitcoin } => { let (bitcoin_electrum_rpc_url, bitcoin_target_block) = bitcoin.apply_defaults(is_testnet)?; Arguments { env_config: env_config_from(is_testnet), debug, json, data_dir: data::data_dir_from(data, is_testnet)?, cmd: Command::ExportBitcoinWallet { bitcoin_electrum_rpc_url, bitcoin_target_block, }, } } RawCommand::MoneroRecovery { swap_id } => Arguments { env_config: env_config_from(is_testnet), debug, json, data_dir: data::data_dir_from(data, is_testnet)?, cmd: Command::MoneroRecovery { swap_id: swap_id.swap_id, }, }, }; Ok(ParseResult::Arguments(Box::new(arguments))) } #[derive(Debug, PartialEq)] pub enum Command { BuyXmr { seller: Multiaddr, bitcoin_electrum_rpc_url: Url, bitcoin_target_block: usize, bitcoin_change_address: bitcoin::Address, monero_receive_address: monero::Address, monero_daemon_address: String, tor_socks5_port: u16, }, History, Config, WithdrawBtc { bitcoin_electrum_rpc_url: Url, bitcoin_target_block: usize, amount: Option, address: Address, }, Balance { bitcoin_electrum_rpc_url: Url, bitcoin_target_block: usize, }, Resume { swap_id: Uuid, bitcoin_electrum_rpc_url: Url, bitcoin_target_block: usize, monero_daemon_address: String, tor_socks5_port: u16, }, Cancel { swap_id: Uuid, bitcoin_electrum_rpc_url: Url, bitcoin_target_block: usize, }, Refund { swap_id: Uuid, bitcoin_electrum_rpc_url: Url, bitcoin_target_block: usize, }, ListSellers { rendezvous_point: Multiaddr, namespace: XmrBtcNamespace, tor_socks5_port: u16, }, ExportBitcoinWallet { bitcoin_electrum_rpc_url: Url, bitcoin_target_block: usize, }, MoneroRecovery { swap_id: Uuid, }, } #[derive(structopt::StructOpt, Debug)] #[structopt( name = "swap", about = "CLI for swapping BTC for XMR", author, version = env!("VERGEN_GIT_SEMVER_LIGHTWEIGHT") )] 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 )] testnet: bool, #[structopt( long = "--data-base-dir", help = "The base data directory to be used for mainnet / testnet specific data like database, wallets etc" )] data: Option, #[structopt(long, help = "Activate debug logging")] debug: bool, #[structopt( short, long = "json", help = "Outputs all logs in JSON format instead of plain text" )] json: bool, #[structopt(subcommand)] cmd: RawCommand, } #[derive(structopt::StructOpt, Debug)] enum RawCommand { /// Start a BTC for XMR swap BuyXmr { #[structopt(flatten)] seller: Seller, #[structopt(flatten)] bitcoin: Bitcoin, #[structopt( long = "change-address", help = "The bitcoin address where any form of change or excess funds should be sent to" )] bitcoin_change_address: bitcoin::Address, #[structopt(flatten)] monero: Monero, #[structopt(long = "receive-address", help = "The monero address where you would like to receive monero", parse(try_from_str = parse_monero_address) )] monero_receive_address: monero::Address, #[structopt(flatten)] tor: Tor, }, /// Show a list of past, ongoing and completed swaps History, #[structopt(about = "Prints the current config")] Config, #[structopt(about = "Allows withdrawing BTC from the internal Bitcoin wallet.")] WithdrawBtc { #[structopt(flatten)] bitcoin: Bitcoin, #[structopt( long = "amount", help = "Optionally specify the amount of Bitcoin to be withdrawn. If not specified the wallet will be drained." )] amount: Option, #[structopt(long = "address", help = "The address to receive the Bitcoin.")] address: Address, }, #[structopt(about = "Prints the Bitcoin balance.")] Balance { #[structopt(long = "electrum-rpc", help = "Provide the Bitcoin Electrum RPC URL")] bitcoin_electrum_rpc_url: Option, }, /// Resume a swap Resume { #[structopt(flatten)] swap_id: SwapId, #[structopt(flatten)] bitcoin: Bitcoin, #[structopt(flatten)] monero: Monero, #[structopt(flatten)] tor: Tor, }, /// Force submission of the cancel transaction overriding the protocol state /// machine and blockheight checks (expert users only) Cancel { #[structopt(flatten)] swap_id: SwapId, #[structopt(flatten)] bitcoin: Bitcoin, }, /// Force submission of the refund transaction overriding the protocol state /// machine and blockheight checks (expert users only) Refund { #[structopt(flatten)] swap_id: SwapId, #[structopt(flatten)] bitcoin: Bitcoin, }, /// Discover and list sellers (i.e. ASB providers) ListSellers { #[structopt( long, help = "Address of the rendezvous point you want to use to discover ASBs" )] rendezvous_point: Multiaddr, #[structopt(flatten)] tor: Tor, }, /// Print the internal bitcoin wallet descriptor ExportBitcoinWallet { #[structopt(flatten)] bitcoin: Bitcoin, }, /// Prints Monero information related to the swap in case the generated /// wallet fails to detect the funds. This can only be used for swaps /// that are in a `btc is redeemed` state. MoneroRecovery { #[structopt(flatten)] swap_id: SwapId, }, } #[derive(structopt::StructOpt, Debug)] struct Monero { #[structopt( long = "monero-daemon-address", help = "Specify to connect to a monero daemon of your choice: :" )] monero_daemon_address: Option, } impl Monero { fn apply_defaults(self, testnet: bool) -> String { if let Some(address) = self.monero_daemon_address { address } else if testnet { DEFAULT_MONERO_DAEMON_ADDRESS_STAGENET.to_string() } else { DEFAULT_MONERO_DAEMON_ADDRESS.to_string() } } } #[derive(structopt::StructOpt, Debug)] struct Bitcoin { #[structopt(long = "electrum-rpc", help = "Provide the Bitcoin Electrum RPC URL")] bitcoin_electrum_rpc_url: Option, #[structopt( long = "bitcoin-target-block", help = "Estimate Bitcoin fees such that transactions are confirmed within the specified number of blocks" )] bitcoin_target_block: Option, } impl Bitcoin { fn apply_defaults(self, testnet: bool) -> Result<(Url, usize)> { let bitcoin_electrum_rpc_url = if let Some(url) = self.bitcoin_electrum_rpc_url { url } else if testnet { Url::from_str(DEFAULT_ELECTRUM_RPC_URL_TESTNET)? } else { Url::from_str(DEFAULT_ELECTRUM_RPC_URL)? }; let bitcoin_target_block = if let Some(target_block) = self.bitcoin_target_block { target_block } else if testnet { DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET } else { DEFAULT_BITCOIN_CONFIRMATION_TARGET }; Ok((bitcoin_electrum_rpc_url, bitcoin_target_block)) } } #[derive(structopt::StructOpt, Debug)] struct Tor { #[structopt( long = "tor-socks5-port", help = "Your local Tor socks5 proxy port", default_value = DEFAULT_TOR_SOCKS5_PORT )] tor_socks5_port: u16, } #[derive(structopt::StructOpt, Debug)] struct SwapId { #[structopt( long = "swap-id", help = "The swap id can be retrieved using the history subcommand" )] swap_id: Uuid, } #[derive(structopt::StructOpt, Debug)] struct Seller { #[structopt( long, help = "The seller's address. Must include a peer ID part, i.e. `/p2p/`" )] seller: Multiaddr, } mod data { use super::*; pub fn data_dir_from(arg_dir: Option, testnet: bool) -> Result { let base_dir = match arg_dir { Some(custom_base_dir) => custom_base_dir, None => os_default()?, }; let sub_directory = if testnet { "testnet" } else { "mainnet" }; Ok(base_dir.join(sub_directory)) } fn os_default() -> Result { Ok(system_data_dir()?.join("cli")) } } fn rendezvous_namespace_from(is_testnet: bool) -> XmrBtcNamespace { if is_testnet { XmrBtcNamespace::Testnet } else { XmrBtcNamespace::Mainnet } } fn env_config_from(testnet: bool) -> env::Config { if testnet { env::Testnet::get_config() } else { env::Mainnet::get_config() } } fn bitcoin_address(address: Address, is_testnet: bool) -> Result
{ let network = if is_testnet { bitcoin::Network::Testnet } else { bitcoin::Network::Bitcoin }; if address.network != network { bail!(BitcoinAddressNetworkMismatch { expected: network, actual: address.network }); } Ok(address) } 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 validate_bitcoin_address(address: bitcoin::Address, testnet: bool) -> Result { let expected_network = if testnet { bitcoin::Network::Testnet } else { bitcoin::Network::Bitcoin }; if address.network != expected_network { anyhow::bail!( "Invalid Bitcoin address provided; expected network {} but provided address is for {}", expected_network, address.network ); } if address.address_type() != Some(AddressType::P2wpkh) { anyhow::bail!("Invalid Bitcoin address provided, only bech32 format is supported!") } Ok(address) } fn parse_monero_address(s: &str) -> Result { monero::Address::from_str(s).with_context(|| { format!( "Failed to parse {} as a monero address, please make sure it is a valid address", s ) }) } #[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, } #[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Serialize)] #[error("Invalid Bitcoin address provided, expected address on network {expected:?} but address provided is on {actual:?}")] pub struct BitcoinAddressNetworkMismatch { #[serde(with = "crate::bitcoin::network")] expected: bitcoin::Network, #[serde(with = "crate::bitcoin::network")] actual: bitcoin::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 BITCOIN_TESTNET_ADDRESS: &str = "tb1qr3em6k3gfnyl8r7q0v7t4tlnyxzgxma3lressv"; const MONERO_MAINNET_ADDRESS: &str = "44Ato7HveWidJYUAVw5QffEcEtSH1DwzSP3FPPkHxNAS4LX9CqgucphTisH978FLHE34YNEx7FcbBfQLQUU8m3NUC4VqsRa"; const BITCOIN_MAINNET_ADDRESS: &str = "bc1qe4epnfklcaa0mun26yz5g8k24em5u9f92hy325"; const MULTI_ADDRESS: &str = "/ip4/127.0.0.1/tcp/9939/p2p/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, "--change-address", BITCOIN_MAINNET_ADDRESS, "--seller", MULTI_ADDRESS, ]; let expected_args = ParseResult::Arguments(Arguments::buy_xmr_mainnet_defaults().into_boxed()); 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, "--change-address", BITCOIN_TESTNET_ADDRESS, "--seller", MULTI_ADDRESS, ]; let args = parse_args_and_apply_defaults(raw_ars).unwrap(); assert_eq!( args, ParseResult::Arguments(Arguments::buy_xmr_testnet_defaults().into_boxed()) ); } #[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, "--change-address", BITCOIN_TESTNET_ADDRESS, "--seller", MULTI_ADDRESS, ]; 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, "--change-address", BITCOIN_MAINNET_ADDRESS, "--seller", MULTI_ADDRESS, ]; 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]; let args = parse_args_and_apply_defaults(raw_ars).unwrap(); assert_eq!( args, ParseResult::Arguments(Arguments::resume_mainnet_defaults().into_boxed()) ); } #[test] fn given_resume_on_testnet_then_defaults_to_testnet() { let raw_ars = vec![BINARY_NAME, "--testnet", "resume", "--swap-id", SWAP_ID]; let args = parse_args_and_apply_defaults(raw_ars).unwrap(); assert_eq!( args, ParseResult::Arguments(Arguments::resume_testnet_defaults().into_boxed()) ); } #[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, ParseResult::Arguments(Arguments::cancel_mainnet_defaults().into_boxed()) ); } #[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, ParseResult::Arguments(Arguments::cancel_testnet_defaults().into_boxed()) ); } #[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, ParseResult::Arguments(Arguments::refund_mainnet_defaults().into_boxed()) ); } #[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, ParseResult::Arguments(Arguments::refund_testnet_defaults().into_boxed()) ); } #[test] fn given_with_data_dir_then_data_dir_set() { let data_dir = "/some/path/to/dir"; let raw_ars = vec![ BINARY_NAME, "--data-base-dir", data_dir, "buy-xmr", "--change-address", BITCOIN_MAINNET_ADDRESS, "--receive-address", MONERO_MAINNET_ADDRESS, "--seller", MULTI_ADDRESS, ]; let args = parse_args_and_apply_defaults(raw_ars).unwrap(); assert_eq!( args, ParseResult::Arguments( Arguments::buy_xmr_mainnet_defaults() .with_data_dir(PathBuf::from_str(data_dir).unwrap().join("mainnet")) .into_boxed() ) ); let raw_ars = vec![ BINARY_NAME, "--testnet", "--data-base-dir", data_dir, "buy-xmr", "--change-address", BITCOIN_TESTNET_ADDRESS, "--receive-address", MONERO_STAGENET_ADDRESS, "--seller", MULTI_ADDRESS, ]; let args = parse_args_and_apply_defaults(raw_ars).unwrap(); assert_eq!( args, ParseResult::Arguments( Arguments::buy_xmr_testnet_defaults() .with_data_dir(PathBuf::from_str(data_dir).unwrap().join("testnet")) .into_boxed() ) ); let raw_ars = vec![ BINARY_NAME, "--data-base-dir", data_dir, "resume", "--swap-id", SWAP_ID, ]; let args = parse_args_and_apply_defaults(raw_ars).unwrap(); assert_eq!( args, ParseResult::Arguments( Arguments::resume_mainnet_defaults() .with_data_dir(PathBuf::from_str(data_dir).unwrap().join("mainnet")) .into_boxed() ) ); let raw_ars = vec![ BINARY_NAME, "--testnet", "--data-base-dir", data_dir, "resume", "--swap-id", SWAP_ID, ]; let args = parse_args_and_apply_defaults(raw_ars).unwrap(); assert_eq!( args, ParseResult::Arguments( Arguments::resume_testnet_defaults() .with_data_dir(PathBuf::from_str(data_dir).unwrap().join("testnet")) .into_boxed() ) ); } #[test] fn given_with_debug_then_debug_set() { let raw_ars = vec![ BINARY_NAME, "--debug", "buy-xmr", "--change-address", BITCOIN_MAINNET_ADDRESS, "--receive-address", MONERO_MAINNET_ADDRESS, "--seller", MULTI_ADDRESS, ]; let args = parse_args_and_apply_defaults(raw_ars).unwrap(); assert_eq!( args, ParseResult::Arguments( Arguments::buy_xmr_mainnet_defaults() .with_debug() .into_boxed() ) ); let raw_ars = vec![ BINARY_NAME, "--testnet", "--debug", "buy-xmr", "--change-address", BITCOIN_TESTNET_ADDRESS, "--receive-address", MONERO_STAGENET_ADDRESS, "--seller", MULTI_ADDRESS, ]; let args = parse_args_and_apply_defaults(raw_ars).unwrap(); assert_eq!( args, ParseResult::Arguments( Arguments::buy_xmr_testnet_defaults() .with_debug() .into_boxed() ) ); let raw_ars = vec![BINARY_NAME, "--debug", "resume", "--swap-id", SWAP_ID]; let args = parse_args_and_apply_defaults(raw_ars).unwrap(); assert_eq!( args, ParseResult::Arguments( Arguments::resume_mainnet_defaults() .with_debug() .into_boxed() ) ); let raw_ars = vec![ BINARY_NAME, "--testnet", "--debug", "resume", "--swap-id", SWAP_ID, ]; let args = parse_args_and_apply_defaults(raw_ars).unwrap(); assert_eq!( args, ParseResult::Arguments( Arguments::resume_testnet_defaults() .with_debug() .into_boxed() ) ); } #[test] fn given_with_json_then_json_set() { let raw_ars = vec![ BINARY_NAME, "--json", "buy-xmr", "--change-address", BITCOIN_MAINNET_ADDRESS, "--receive-address", MONERO_MAINNET_ADDRESS, "--seller", MULTI_ADDRESS, ]; let args = parse_args_and_apply_defaults(raw_ars).unwrap(); assert_eq!( args, ParseResult::Arguments( Arguments::buy_xmr_mainnet_defaults() .with_json() .into_boxed() ) ); let raw_ars = vec![ BINARY_NAME, "--testnet", "--json", "buy-xmr", "--change-address", BITCOIN_TESTNET_ADDRESS, "--receive-address", MONERO_STAGENET_ADDRESS, "--seller", MULTI_ADDRESS, ]; let args = parse_args_and_apply_defaults(raw_ars).unwrap(); assert_eq!( args, ParseResult::Arguments( Arguments::buy_xmr_testnet_defaults() .with_json() .into_boxed() ) ); let raw_ars = vec![BINARY_NAME, "--json", "resume", "--swap-id", SWAP_ID]; let args = parse_args_and_apply_defaults(raw_ars).unwrap(); assert_eq!( args, ParseResult::Arguments( Arguments::resume_mainnet_defaults() .with_json() .into_boxed() ) ); let raw_ars = vec![ BINARY_NAME, "--testnet", "--json", "resume", "--swap-id", SWAP_ID, ]; let args = parse_args_and_apply_defaults(raw_ars).unwrap(); assert_eq!( args, ParseResult::Arguments( Arguments::resume_testnet_defaults() .with_json() .into_boxed() ) ); } #[test] fn only_bech32_addresses_mainnet_are_allowed() { let raw_ars = vec![ BINARY_NAME, "buy-xmr", "--change-address", "1A5btpLKZjgYm8R22rJAhdbTFVXgSRA2Mp", "--receive-address", MONERO_MAINNET_ADDRESS, "--seller", MULTI_ADDRESS, ]; let result = parse_args_and_apply_defaults(raw_ars); assert_eq!( result.unwrap_err().to_string(), "Invalid Bitcoin address provided, only bech32 format is supported!" ); let raw_ars = vec![ BINARY_NAME, "buy-xmr", "--change-address", "36vn4mFhmTXn7YcNwELFPxTXhjorw2ppu2", "--receive-address", MONERO_MAINNET_ADDRESS, "--seller", MULTI_ADDRESS, ]; let result = parse_args_and_apply_defaults(raw_ars); assert_eq!( result.unwrap_err().to_string(), "Invalid Bitcoin address provided, only bech32 format is supported!" ); let raw_ars = vec![ BINARY_NAME, "buy-xmr", "--change-address", "bc1qh4zjxrqe3trzg7s6m7y67q2jzrw3ru5mx3z7j3", "--receive-address", MONERO_MAINNET_ADDRESS, "--seller", MULTI_ADDRESS, ]; let result = parse_args_and_apply_defaults(raw_ars).unwrap(); assert!(matches!(result, ParseResult::Arguments(_))); } #[test] fn only_bech32_addresses_testnet_are_allowed() { let raw_ars = vec![ BINARY_NAME, "--testnet", "buy-xmr", "--change-address", "n2czxyeFCQp9e8WRyGpy4oL4YfQAeKkkUH", "--receive-address", MONERO_STAGENET_ADDRESS, "--seller", MULTI_ADDRESS, ]; let result = parse_args_and_apply_defaults(raw_ars); assert_eq!( result.unwrap_err().to_string(), "Invalid Bitcoin address provided, only bech32 format is supported!" ); let raw_ars = vec![ BINARY_NAME, "--testnet", "buy-xmr", "--change-address", "2ND9a4xmQG89qEWG3ETRuytjKpLmGrW7Jvf", "--receive-address", MONERO_STAGENET_ADDRESS, "--seller", MULTI_ADDRESS, ]; let result = parse_args_and_apply_defaults(raw_ars); assert_eq!( result.unwrap_err().to_string(), "Invalid Bitcoin address provided, only bech32 format is supported!" ); let raw_ars = vec![ BINARY_NAME, "--testnet", "buy-xmr", "--change-address", "tb1q958vfh3wkdp232pktq8zzvmttyxeqnj80zkz3v", "--receive-address", MONERO_STAGENET_ADDRESS, "--seller", MULTI_ADDRESS, ]; let result = parse_args_and_apply_defaults(raw_ars).unwrap(); assert!(matches!(result, ParseResult::Arguments(_))); } impl Arguments { pub fn buy_xmr_testnet_defaults() -> Self { Self { env_config: env::Testnet::get_config(), debug: false, json: false, data_dir: data_dir_path_cli().join(TESTNET), cmd: Command::BuyXmr { seller: Multiaddr::from_str(MULTI_ADDRESS).unwrap(), bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL_TESTNET) .unwrap(), bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET, bitcoin_change_address: BITCOIN_TESTNET_ADDRESS.parse().unwrap(), 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, json: false, data_dir: data_dir_path_cli().join(MAINNET), cmd: Command::BuyXmr { seller: Multiaddr::from_str(MULTI_ADDRESS).unwrap(), bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(), bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET, bitcoin_change_address: BITCOIN_MAINNET_ADDRESS.parse().unwrap(), 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, json: false, data_dir: data_dir_path_cli().join(TESTNET), cmd: Command::Resume { swap_id: Uuid::from_str(SWAP_ID).unwrap(), bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL_TESTNET) .unwrap(), bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET_TESTNET, 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, json: false, data_dir: data_dir_path_cli().join(MAINNET), cmd: Command::Resume { swap_id: Uuid::from_str(SWAP_ID).unwrap(), bitcoin_electrum_rpc_url: Url::from_str(DEFAULT_ELECTRUM_RPC_URL).unwrap(), bitcoin_target_block: DEFAULT_BITCOIN_CONFIRMATION_TARGET, 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, json: false, data_dir: data_dir_path_cli().join(TESTNET), cmd: Command::Cancel { swap_id: Uuid::from_str(SWAP_ID).unwrap(), 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, json: false, data_dir: data_dir_path_cli().join(MAINNET), cmd: Command::Cancel { swap_id: Uuid::from_str(SWAP_ID).unwrap(), 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, json: false, data_dir: data_dir_path_cli().join(TESTNET), cmd: Command::Refund { swap_id: Uuid::from_str(SWAP_ID).unwrap(), 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, json: false, data_dir: data_dir_path_cli().join(MAINNET), cmd: Command::Refund { swap_id: Uuid::from_str(SWAP_ID).unwrap(), 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 } pub fn with_json(mut self) -> Self { self.json = true; self } pub fn into_boxed(self) -> Box { Box::new(self) } } fn data_dir_path_cli() -> PathBuf { system_data_dir().unwrap().join("cli") } }