Application arg tests

Includes validation for given Bitcoin address against network.
This commit is contained in:
Daniel Karzel 2021-05-27 14:17:18 +10:00
parent 202f6d1fa0
commit 766ac706de
No known key found for this signature in database
GPG Key ID: 30C3FC2E438ADB6E
2 changed files with 562 additions and 33 deletions

View File

@ -1,15 +1,210 @@
use crate::asb::config::GetDefaults;
use crate::bitcoin::Amount;
use crate::env;
use crate::env::GetConfig;
use anyhow::{bail, Result};
use bitcoin::Address;
use serde::Serialize;
use std::ffi::OsString;
use std::path::PathBuf;
use structopt::StructOpt;
use uuid::Uuid;
pub fn parse_args<I, T>(raw_args: I) -> Result<Arguments>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let matches = RawArguments::clap().get_matches_from_safe(raw_args)?;
let args = RawArguments::from_clap(&matches);
let is_json = args.json;
let is_testnet = args.testnet;
let config = args.config;
let command: RawCommand = args.cmd;
let arguments = match command {
RawCommand::Start { resume_only } => Arguments {
testnet: is_testnet,
json: is_json,
config_path: config_path(config, is_testnet)?,
env_config: env_config(is_testnet),
cmd: Command::Start { resume_only },
},
RawCommand::History => Arguments {
testnet: is_testnet,
json: is_json,
config_path: config_path(config, is_testnet)?,
env_config: env_config(is_testnet),
cmd: Command::History,
},
RawCommand::WithdrawBtc { amount, address } => Arguments {
testnet: is_testnet,
json: is_json,
config_path: config_path(config, is_testnet)?,
env_config: env_config(is_testnet),
cmd: Command::WithdrawBtc {
amount,
address: bitcoin_address(address, is_testnet)?,
},
},
RawCommand::Balance => Arguments {
testnet: is_testnet,
json: is_json,
config_path: config_path(config, is_testnet)?,
env_config: env_config(is_testnet),
cmd: Command::Balance,
},
RawCommand::ManualRecovery(manual_recovery) => match manual_recovery {
ManualRecovery::Redeem {
redeem_params: RecoverCommandParams { swap_id, force },
do_not_await_finality,
} => Arguments {
testnet: is_testnet,
json: is_json,
config_path: config_path(config, is_testnet)?,
env_config: env_config(is_testnet),
cmd: Command::Redeem {
swap_id,
force,
do_not_await_finality,
},
},
ManualRecovery::Cancel {
cancel_params: RecoverCommandParams { swap_id, force },
} => Arguments {
testnet: is_testnet,
json: is_json,
config_path: config_path(config, is_testnet)?,
env_config: env_config(is_testnet),
cmd: Command::Cancel { swap_id, force },
},
ManualRecovery::Refund {
refund_params: RecoverCommandParams { swap_id, force },
} => Arguments {
testnet: is_testnet,
json: is_json,
config_path: config_path(config, is_testnet)?,
env_config: env_config(is_testnet),
cmd: Command::Refund { swap_id, force },
},
ManualRecovery::Punish {
punish_params: RecoverCommandParams { swap_id, force },
} => Arguments {
testnet: is_testnet,
json: is_json,
config_path: config_path(config, is_testnet)?,
env_config: env_config(is_testnet),
cmd: Command::Punish { swap_id, force },
},
ManualRecovery::SafelyAbort { swap_id } => Arguments {
testnet: is_testnet,
json: is_json,
config_path: config_path(config, is_testnet)?,
env_config: env_config(is_testnet),
cmd: Command::SafelyAbort { swap_id },
},
},
};
Ok(arguments)
}
fn bitcoin_address(address: Address, is_testnet: bool) -> Result<Address> {
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 config_path(config: Option<PathBuf>, is_testnet: bool) -> Result<PathBuf> {
let config_path = if let Some(config_path) = config {
config_path
} else if is_testnet {
env::Testnet::getConfigFileDefaults()?.config_path
} else {
env::Mainnet::getConfigFileDefaults()?.config_path
};
Ok(config_path)
}
fn env_config(is_testnet: bool) -> env::Config {
if is_testnet {
env::Testnet::get_config()
} else {
env::Mainnet::get_config()
}
}
#[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,
}
#[derive(Debug, PartialEq)]
pub struct Arguments {
pub testnet: bool,
pub json: bool,
pub config_path: PathBuf,
pub env_config: env::Config,
pub cmd: Command,
}
#[derive(Debug, PartialEq)]
pub enum Command {
Start {
resume_only: bool,
},
History,
WithdrawBtc {
amount: Option<Amount>,
address: Address,
},
Balance,
Redeem {
swap_id: Uuid,
force: bool,
do_not_await_finality: bool,
},
Cancel {
swap_id: Uuid,
force: bool,
},
Refund {
swap_id: Uuid,
force: bool,
},
Punish {
swap_id: Uuid,
force: bool,
},
SafelyAbort {
swap_id: Uuid,
},
}
#[derive(structopt::StructOpt, Debug)]
#[structopt(
name = "asb",
about = "Automated Swap Backend for swapping XMR for BTC",
author
)]
pub struct Arguments {
pub struct RawArguments {
#[structopt(long, help = "Swap on testnet")]
pub testnet: bool,
@ -28,12 +223,12 @@ pub struct Arguments {
pub config: Option<PathBuf>,
#[structopt(subcommand)]
pub cmd: Command,
pub cmd: RawCommand,
}
#[derive(structopt::StructOpt, Debug)]
#[structopt(name = "xmr_btc-swap", about = "XMR BTC atomic swap")]
pub enum Command {
pub enum RawCommand {
#[structopt(about = "Main command to run the ASB.")]
Start {
#[structopt(
@ -123,3 +318,333 @@ pub struct RecoverCommandParams {
)]
pub force: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
const BINARY_NAME: &str = "asb";
const BITCOIN_MAINNET_ADDRESS: &str = "1KFHE7w8BhaENAswwryaoccDb6qcT6DbYY";
const BITCOIN_TESTNET_ADDRESS: &str = "tb1qyccwk4yun26708qg5h6g6we8kxln232wclxf5a";
const SWAP_ID: &str = "ea030832-3be9-454f-bb98-5ea9a788406b";
#[test]
fn ensure_command_mapping_for_mainnet() {
let default_mainnet_conf_path = env::Mainnet::getConfigFileDefaults().unwrap().config_path;
let mainnet_env_config = env::Mainnet::get_config();
let raw_ars = vec![BINARY_NAME, "start"];
let expected_args = Arguments {
testnet: false,
json: false,
config_path: default_mainnet_conf_path.clone(),
env_config: mainnet_env_config,
cmd: Command::Start { resume_only: false },
};
let args = parse_args(raw_ars).unwrap();
assert_eq!(expected_args, args);
let raw_ars = vec![BINARY_NAME, "history"];
let expected_args = Arguments {
testnet: false,
json: false,
config_path: default_mainnet_conf_path.clone(),
env_config: mainnet_env_config,
cmd: Command::History,
};
let args = parse_args(raw_ars).unwrap();
assert_eq!(expected_args, args);
let raw_ars = vec![BINARY_NAME, "balance"];
let expected_args = Arguments {
testnet: false,
json: false,
config_path: default_mainnet_conf_path.clone(),
env_config: mainnet_env_config,
cmd: Command::Balance,
};
let args = parse_args(raw_ars).unwrap();
assert_eq!(expected_args, args);
let raw_ars = vec![
BINARY_NAME,
"withdraw-btc",
"--address",
BITCOIN_MAINNET_ADDRESS,
];
let expected_args = Arguments {
testnet: false,
json: false,
config_path: default_mainnet_conf_path.clone(),
env_config: mainnet_env_config,
cmd: Command::WithdrawBtc {
amount: None,
address: Address::from_str(BITCOIN_MAINNET_ADDRESS).unwrap(),
},
};
let args = parse_args(raw_ars).unwrap();
assert_eq!(expected_args, args);
let raw_ars = vec![
BINARY_NAME,
"manual-recovery",
"cancel",
"--swap-id",
SWAP_ID,
];
let expected_args = Arguments {
testnet: false,
json: false,
config_path: default_mainnet_conf_path.clone(),
env_config: mainnet_env_config,
cmd: Command::Cancel {
swap_id: Uuid::parse_str(SWAP_ID).unwrap(),
force: false,
},
};
let args = parse_args(raw_ars).unwrap();
assert_eq!(expected_args, args);
let raw_ars = vec![
BINARY_NAME,
"manual-recovery",
"refund",
"--swap-id",
SWAP_ID,
];
let expected_args = Arguments {
testnet: false,
json: false,
config_path: default_mainnet_conf_path.clone(),
env_config: mainnet_env_config,
cmd: Command::Refund {
swap_id: Uuid::parse_str(SWAP_ID).unwrap(),
force: false,
},
};
let args = parse_args(raw_ars).unwrap();
assert_eq!(expected_args, args);
let raw_ars = vec![
BINARY_NAME,
"manual-recovery",
"punish",
"--swap-id",
SWAP_ID,
];
let expected_args = Arguments {
testnet: false,
json: false,
config_path: default_mainnet_conf_path.clone(),
env_config: mainnet_env_config,
cmd: Command::Punish {
swap_id: Uuid::parse_str(SWAP_ID).unwrap(),
force: false,
},
};
let args = parse_args(raw_ars).unwrap();
assert_eq!(expected_args, args);
let raw_ars = vec![
BINARY_NAME,
"manual-recovery",
"safely-abort",
"--swap-id",
SWAP_ID,
];
let expected_args = Arguments {
testnet: false,
json: false,
config_path: default_mainnet_conf_path,
env_config: mainnet_env_config,
cmd: Command::SafelyAbort {
swap_id: Uuid::parse_str(SWAP_ID).unwrap(),
},
};
let args = parse_args(raw_ars).unwrap();
assert_eq!(expected_args, args);
}
#[test]
fn ensure_command_mapping_for_testnet() {
let default_testnet_conf_path = env::Testnet::getConfigFileDefaults().unwrap().config_path;
let testnet_env_config = env::Testnet::get_config();
let raw_ars = vec![BINARY_NAME, "--testnet", "start"];
let expected_args = Arguments {
testnet: true,
json: false,
config_path: default_testnet_conf_path.clone(),
env_config: testnet_env_config,
cmd: Command::Start { resume_only: false },
};
let args = parse_args(raw_ars).unwrap();
assert_eq!(expected_args, args);
let raw_ars = vec![BINARY_NAME, "--testnet", "history"];
let expected_args = Arguments {
testnet: true,
json: false,
config_path: default_testnet_conf_path.clone(),
env_config: testnet_env_config,
cmd: Command::History,
};
let args = parse_args(raw_ars).unwrap();
assert_eq!(expected_args, args);
let raw_ars = vec![BINARY_NAME, "--testnet", "balance"];
let expected_args = Arguments {
testnet: true,
json: false,
config_path: default_testnet_conf_path.clone(),
env_config: testnet_env_config,
cmd: Command::Balance,
};
let args = parse_args(raw_ars).unwrap();
assert_eq!(expected_args, args);
let raw_ars = vec![
BINARY_NAME,
"--testnet",
"withdraw-btc",
"--address",
BITCOIN_TESTNET_ADDRESS,
];
let expected_args = Arguments {
testnet: true,
json: false,
config_path: default_testnet_conf_path.clone(),
env_config: testnet_env_config,
cmd: Command::WithdrawBtc {
amount: None,
address: Address::from_str(BITCOIN_TESTNET_ADDRESS).unwrap(),
},
};
let args = parse_args(raw_ars).unwrap();
assert_eq!(expected_args, args);
let raw_ars = vec![
BINARY_NAME,
"--testnet",
"manual-recovery",
"cancel",
"--swap-id",
SWAP_ID,
];
let expected_args = Arguments {
testnet: true,
json: false,
config_path: default_testnet_conf_path.clone(),
env_config: testnet_env_config,
cmd: Command::Cancel {
swap_id: Uuid::parse_str(SWAP_ID).unwrap(),
force: false,
},
};
let args = parse_args(raw_ars).unwrap();
assert_eq!(expected_args, args);
let raw_ars = vec![
BINARY_NAME,
"--testnet",
"manual-recovery",
"refund",
"--swap-id",
SWAP_ID,
];
let expected_args = Arguments {
testnet: true,
json: false,
config_path: default_testnet_conf_path.clone(),
env_config: testnet_env_config,
cmd: Command::Refund {
swap_id: Uuid::parse_str(SWAP_ID).unwrap(),
force: false,
},
};
let args = parse_args(raw_ars).unwrap();
assert_eq!(expected_args, args);
let raw_ars = vec![
BINARY_NAME,
"--testnet",
"manual-recovery",
"punish",
"--swap-id",
SWAP_ID,
];
let expected_args = Arguments {
testnet: true,
json: false,
config_path: default_testnet_conf_path.clone(),
env_config: testnet_env_config,
cmd: Command::Punish {
swap_id: Uuid::parse_str(SWAP_ID).unwrap(),
force: false,
},
};
let args = parse_args(raw_ars).unwrap();
assert_eq!(expected_args, args);
let raw_ars = vec![
BINARY_NAME,
"--testnet",
"manual-recovery",
"safely-abort",
"--swap-id",
SWAP_ID,
];
let expected_args = Arguments {
testnet: true,
json: false,
config_path: default_testnet_conf_path,
env_config: testnet_env_config,
cmd: Command::SafelyAbort {
swap_id: Uuid::parse_str(SWAP_ID).unwrap(),
},
};
let args = parse_args(raw_ars).unwrap();
assert_eq!(expected_args, args);
}
#[test]
fn given_user_provides_config_path_then_no_default_config_path_returned() {
let cp = PathBuf::from_str("/some/config/path").unwrap();
let expected = config_path(Some(cp.clone()), true).unwrap();
assert_eq!(expected, cp);
let expected = config_path(Some(cp.clone()), false).unwrap();
assert_eq!(expected, cp)
}
#[test]
fn given_bitcoin_address_network_mismatch_then_error() {
let error =
bitcoin_address(Address::from_str(BITCOIN_MAINNET_ADDRESS).unwrap(), true).unwrap_err();
assert_eq!(
error
.downcast_ref::<BitcoinAddressNetworkMismatch>()
.unwrap(),
&BitcoinAddressNetworkMismatch {
expected: bitcoin::Network::Testnet,
actual: bitcoin::Network::Bitcoin
}
);
let error = bitcoin_address(Address::from_str(BITCOIN_TESTNET_ADDRESS).unwrap(), false)
.unwrap_err();
assert_eq!(
error
.downcast_ref::<BitcoinAddressNetworkMismatch>()
.unwrap(),
&BitcoinAddressNetworkMismatch {
expected: bitcoin::Network::Bitcoin,
actual: bitcoin::Network::Testnet
}
);
}
}

View File

@ -17,13 +17,14 @@ use libp2p::core::multiaddr::Protocol;
use libp2p::core::Multiaddr;
use libp2p::Swarm;
use prettytable::{row, Table};
use std::env;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use structopt::StructOpt;
use swap::asb::command::{Arguments, Command, ManualRecovery, RecoverCommandParams};
use structopt::clap;
use structopt::clap::ErrorKind;
use swap::asb::command::{parse_args, Arguments, Command};
use swap::asb::config::{
initial_setup, query_user_for_initial_config, read_config, Config, ConfigNotInitialized,
GetDefaults,
};
use swap::database::Database;
use swap::monero::Amount;
@ -33,7 +34,7 @@ use swap::protocol::alice::event_loop::KrakenRate;
use swap::protocol::alice::{redeem, run, EventLoop};
use swap::seed::Seed;
use swap::tor::AuthenticatedClient;
use swap::{asb, bitcoin, env, kraken, monero, tor};
use swap::{asb, bitcoin, kraken, monero, tor};
use tracing::{debug, info, warn};
use tracing_subscriber::filter::LevelFilter;
@ -47,19 +48,29 @@ async fn main() -> Result<()> {
let Arguments {
testnet,
json,
config,
config_path,
env_config,
cmd,
} = Arguments::from_args();
asb::tracing::init(LevelFilter::DEBUG, json).expect("initialize tracing");
let config_path = if let Some(config_path) = config {
config_path
} else if testnet {
env::Testnet::getConfigFileDefaults()?.config_path
} else {
env::Mainnet::getConfigFileDefaults()?.config_path
} = match parse_args(env::args_os()) {
Ok(args) => args,
Err(e) => {
if let Some(clap_err) = e.downcast_ref::<clap::Error>() {
match clap_err.kind {
ErrorKind::HelpDisplayed | ErrorKind::VersionDisplayed => {
println!("{}", clap_err.message);
std::process::exit(0);
}
_ => {
bail!(e);
}
}
}
bail!(e);
}
};
asb::tracing::init(LevelFilter::DEBUG, json).expect("initialize tracing");
let config = match read_config(config_path.clone())? {
Ok(config) => config,
Err(ConfigNotInitialized {}) => {
@ -68,8 +79,6 @@ async fn main() -> Result<()> {
}
};
let env_config = env::new(testnet, &config);
if config.monero.network != env_config.monero_network {
bail!(format!(
"Expected monero network in config file to be {:?} but was {:?}",
@ -228,9 +237,7 @@ async fn main() -> Result<()> {
%monero_balance,
"Current balance");
}
Command::ManualRecovery(ManualRecovery::Cancel {
cancel_params: RecoverCommandParams { swap_id, force },
}) => {
Command::Cancel { swap_id, force } => {
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
let (txid, _) =
@ -238,9 +245,7 @@ async fn main() -> Result<()> {
tracing::info!("Cancel transaction successfully published with id {}", txid);
}
Command::ManualRecovery(ManualRecovery::Refund {
refund_params: RecoverCommandParams { swap_id, force },
}) => {
Command::Refund { swap_id, force } => {
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
let monero_wallet = init_monero_wallet(&config, env_config).await?;
@ -255,9 +260,7 @@ async fn main() -> Result<()> {
tracing::info!("Monero successfully refunded");
}
Command::ManualRecovery(ManualRecovery::Punish {
punish_params: RecoverCommandParams { swap_id, force },
}) => {
Command::Punish { swap_id, force } => {
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
let (txid, _) =
@ -265,15 +268,16 @@ async fn main() -> Result<()> {
tracing::info!("Punish transaction successfully published with id {}", txid);
}
Command::ManualRecovery(ManualRecovery::SafelyAbort { swap_id }) => {
Command::SafelyAbort { swap_id } => {
alice::safely_abort(swap_id, Arc::new(db)).await?;
tracing::info!("Swap safely aborted");
}
Command::ManualRecovery(ManualRecovery::Redeem {
redeem_params: RecoverCommandParams { swap_id, force },
Command::Redeem {
swap_id,
force,
do_not_await_finality,
}) => {
} => {
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
let (txid, _) = alice::redeem(
@ -287,7 +291,7 @@ async fn main() -> Result<()> {
tracing::info!("Redeem transaction successfully published with id {}", txid);
}
};
}
Ok(())
}