diff --git a/swap/src/bin/cli.rs b/swap/src/bin/cli.rs index 03176ae3..4d8950cb 100644 --- a/swap/src/bin/cli.rs +++ b/swap/src/bin/cli.rs @@ -19,10 +19,11 @@ use std::{path::PathBuf, sync::Arc}; use structopt::StructOpt; use swap::{ bitcoin, - cli::{Cancel, Command, Options, Refund, Resume}, - config, - config::{ - initial_setup, query_user_for_initial_testnet_config, read_config, ConfigNotInitialized, + cli::{ + command::{Arguments, Cancel, Command, Refund, Resume}, + config::{ + initial_setup, query_user_for_initial_testnet_config, read_config, ConfigNotInitialized, + }, }, database::Database, execution_params, @@ -35,6 +36,7 @@ use swap::{ bob::{cancel::CancelError, Builder}, SwapAmounts, }, + seed::Seed, trace::init_tracing, }; use tracing::{error, info, warn}; @@ -49,7 +51,7 @@ const MONERO_BLOCKCHAIN_MONITORING_WALLET_NAME: &str = "swap-tool-blockchain-mon async fn main() -> Result<()> { init_tracing(LevelFilter::Debug).expect("initialize tracing"); - let opt = Options::from_args(); + let opt = Arguments::from_args(); let data_dir = if let Some(data_dir) = opt.data_dir { data_dir @@ -63,9 +65,7 @@ async fn main() -> Result<()> { ); let db_path = data_dir.join("database"); - let seed = config::Seed::from_file_or_generate(&data_dir) - .expect("Could not retrieve/initialize seed") - .into(); + let seed = Seed::from_file_or_generate(&data_dir).expect("Could not retrieve/initialize seed"); // hardcode to testnet/stagenet let bitcoin_network = bitcoin::Network::Testnet; diff --git a/swap/src/cli.rs b/swap/src/cli.rs index 9a1d91a5..6d982acc 100644 --- a/swap/src/cli.rs +++ b/swap/src/cli.rs @@ -1,121 +1,2 @@ -use crate::{bitcoin, monero}; -use libp2p::{core::Multiaddr, PeerId}; -use std::path::PathBuf; -use uuid::Uuid; - -#[derive(structopt::StructOpt, Debug)] -pub struct Options { - #[structopt( - long = "data-dir", - help = "Provide a custom path to the data directory.", - parse(from_os_str) - )] - pub data_dir: Option, - - #[structopt(subcommand)] - pub cmd: Command, -} - -#[derive(structopt::StructOpt, Debug)] -#[structopt(name = "xmr_btc-swap", about = "XMR BTC atomic swap")] -pub enum Command { - BuyXmr { - #[structopt(long = "connect-peer-id")] - alice_peer_id: PeerId, - - #[structopt(long = "connect-addr")] - alice_addr: Multiaddr, - - #[structopt(long = "send-btc", help = "Bitcoin amount as floating point nr without denomination (e.g. 1.25)", parse(try_from_str = parse_btc))] - send_bitcoin: bitcoin::Amount, - - #[structopt(long = "receive-xmr", help = "Monero amount as floating point nr without denomination (e.g. 125.1)", parse(try_from_str = parse_xmr))] - receive_monero: monero::Amount, - - #[structopt(flatten)] - config: Config, - }, - History, - Resume(Resume), - Cancel(Cancel), - Refund(Refund), -} - -#[derive(structopt::StructOpt, Debug)] -pub enum Resume { - BuyXmr { - #[structopt(long = "swap-id")] - swap_id: Uuid, - - #[structopt(long = "counterpart-peer-id")] - alice_peer_id: PeerId, - - #[structopt(long = "counterpart-addr")] - alice_addr: Multiaddr, - - #[structopt(flatten)] - config: Config, - }, -} - -#[derive(structopt::StructOpt, Debug)] -pub enum Cancel { - BuyXmr { - #[structopt(long = "swap-id")] - swap_id: Uuid, - - // TODO: Remove Alice peer-id/address, it should be saved in the database when running swap - // and loaded from the database when running resume/cancel/refund - #[structopt(long = "counterpart-peer-id")] - alice_peer_id: PeerId, - #[structopt(long = "counterpart-addr")] - alice_addr: Multiaddr, - - #[structopt(flatten)] - config: Config, - - #[structopt(short, long)] - force: bool, - }, -} - -#[derive(structopt::StructOpt, Debug)] -pub enum Refund { - BuyXmr { - #[structopt(long = "swap-id")] - swap_id: Uuid, - - // TODO: Remove Alice peer-id/address, it should be saved in the database when running swap - // and loaded from the database when running resume/cancel/refund - #[structopt(long = "counterpart-peer-id")] - alice_peer_id: PeerId, - #[structopt(long = "counterpart-addr")] - alice_addr: Multiaddr, - - #[structopt(flatten)] - config: Config, - - #[structopt(short, long)] - force: bool, - }, -} - -#[derive(structopt::StructOpt, Debug)] -pub struct Config { - #[structopt( - long = "config", - help = "Provide a custom path to the configuration file. The configuration file must be a toml file.", - parse(from_os_str) - )] - pub path: Option, -} - -fn parse_btc(str: &str) -> anyhow::Result { - let amount = bitcoin::Amount::from_str_in(str, ::bitcoin::Denomination::Bitcoin)?; - Ok(amount) -} - -fn parse_xmr(str: &str) -> anyhow::Result { - let amount = monero::Amount::parse_monero(str)?; - Ok(amount) -} +pub mod command; +pub mod config; diff --git a/swap/src/cli/command.rs b/swap/src/cli/command.rs new file mode 100644 index 00000000..67cdcb80 --- /dev/null +++ b/swap/src/cli/command.rs @@ -0,0 +1,121 @@ +use crate::{bitcoin, monero}; +use libp2p::{core::Multiaddr, PeerId}; +use std::path::PathBuf; +use uuid::Uuid; + +#[derive(structopt::StructOpt, Debug)] +pub struct Arguments { + #[structopt( + long = "data-dir", + help = "Provide a custom path to the data directory.", + parse(from_os_str) + )] + pub data_dir: Option, + + #[structopt(subcommand)] + pub cmd: Command, +} + +#[derive(structopt::StructOpt, Debug)] +#[structopt(name = "xmr_btc-swap", about = "XMR BTC atomic swap")] +pub enum Command { + BuyXmr { + #[structopt(long = "connect-peer-id")] + alice_peer_id: PeerId, + + #[structopt(long = "connect-addr")] + alice_addr: Multiaddr, + + #[structopt(long = "send-btc", help = "Bitcoin amount as floating point nr without denomination (e.g. 1.25)", parse(try_from_str = parse_btc))] + send_bitcoin: bitcoin::Amount, + + #[structopt(long = "receive-xmr", help = "Monero amount as floating point nr without denomination (e.g. 125.1)", parse(try_from_str = parse_xmr))] + receive_monero: monero::Amount, + + #[structopt(flatten)] + config: Config, + }, + History, + Resume(Resume), + Cancel(Cancel), + Refund(Refund), +} + +#[derive(structopt::StructOpt, Debug)] +pub enum Resume { + BuyXmr { + #[structopt(long = "swap-id")] + swap_id: Uuid, + + #[structopt(long = "counterpart-peer-id")] + alice_peer_id: PeerId, + + #[structopt(long = "counterpart-addr")] + alice_addr: Multiaddr, + + #[structopt(flatten)] + config: Config, + }, +} + +#[derive(structopt::StructOpt, Debug)] +pub enum Cancel { + BuyXmr { + #[structopt(long = "swap-id")] + swap_id: Uuid, + + // TODO: Remove Alice peer-id/address, it should be saved in the database when running swap + // and loaded from the database when running resume/cancel/refund + #[structopt(long = "counterpart-peer-id")] + alice_peer_id: PeerId, + #[structopt(long = "counterpart-addr")] + alice_addr: Multiaddr, + + #[structopt(flatten)] + config: Config, + + #[structopt(short, long)] + force: bool, + }, +} + +#[derive(structopt::StructOpt, Debug)] +pub enum Refund { + BuyXmr { + #[structopt(long = "swap-id")] + swap_id: Uuid, + + // TODO: Remove Alice peer-id/address, it should be saved in the database when running swap + // and loaded from the database when running resume/cancel/refund + #[structopt(long = "counterpart-peer-id")] + alice_peer_id: PeerId, + #[structopt(long = "counterpart-addr")] + alice_addr: Multiaddr, + + #[structopt(flatten)] + config: Config, + + #[structopt(short, long)] + force: bool, + }, +} + +#[derive(structopt::StructOpt, Debug)] +pub struct Config { + #[structopt( + long = "config", + help = "Provide a custom path to the configuration file. The configuration file must be a toml file.", + parse(from_os_str) + )] + pub path: Option, +} + +fn parse_btc(str: &str) -> anyhow::Result { + let amount = bitcoin::Amount::from_str_in(str, ::bitcoin::Denomination::Bitcoin)?; + Ok(amount) +} + +fn parse_xmr(str: &str) -> anyhow::Result { + let amount = monero::Amount::parse_monero(str)?; + Ok(amount) +} diff --git a/swap/src/config.rs b/swap/src/cli/config.rs similarity index 81% rename from swap/src/config.rs rename to swap/src/cli/config.rs index f9f56431..1f7f95fb 100644 --- a/swap/src/config.rs +++ b/swap/src/cli/config.rs @@ -1,6 +1,6 @@ use crate::fs::ensure_directory_exists; use anyhow::{Context, Result}; -use config::{Config, ConfigError}; +use config::ConfigError; use dialoguer::{theme::ColorfulTheme, Input}; use serde::{Deserialize, Serialize}; use std::{ @@ -11,27 +11,23 @@ use std::{ use tracing::info; use url::Url; -pub mod seed; - -pub use seed::Seed; - const DEFAULT_BITCOIND_TESTNET_URL: &str = "http://127.0.0.1:18332"; const DEFAULT_MONERO_WALLET_RPC_TESTNET_URL: &str = "http://127.0.0.1:38083/json_rpc"; #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] -pub struct File { +pub struct Config { pub bitcoin: Bitcoin, pub monero: Monero, } -impl File { +impl Config { pub fn read(config_file: D) -> Result where D: AsRef, { let config_file = Path::new(&config_file); - let mut config = Config::new(); + let mut config = config::Config::new(); config.merge(config::File::from(config_file))?; config.try_into() } @@ -54,7 +50,7 @@ pub struct Monero { #[error("config not initialized")] pub struct ConfigNotInitialized {} -pub fn read_config(config_path: PathBuf) -> Result> { +pub fn read_config(config_path: PathBuf) -> Result> { if config_path.exists() { info!( "Using config file at default path: {}", @@ -64,7 +60,7 @@ pub fn read_config(config_path: PathBuf) -> Result Result(config_path: PathBuf, config_file: F) -> Result<()> where - F: Fn() -> Result, + F: Fn() -> Result, { info!("Config file not found, running initial setup..."); ensure_directory_exists(config_path.as_path())?; @@ -88,26 +84,26 @@ where Ok(()) } -pub fn query_user_for_initial_testnet_config() -> Result { +pub fn query_user_for_initial_testnet_config() -> Result { println!(); - let bitcoind_url: String = Input::with_theme(&ColorfulTheme::default()) + let bitcoind_url = Input::with_theme(&ColorfulTheme::default()) .with_prompt("Enter Bitcoind URL (including username and password if applicable) or hit return to use default") .default(DEFAULT_BITCOIND_TESTNET_URL.to_owned()) .interact_text()?; - let bitcoind_url = Url::parse(bitcoind_url.as_str())?; + let bitcoind_url = bitcoind_url.as_str().parse()?; - let bitcoin_wallet_name: String = Input::with_theme(&ColorfulTheme::default()) + let bitcoin_wallet_name = Input::with_theme(&ColorfulTheme::default()) .with_prompt("Enter Bitcoind wallet name") .interact_text()?; - let monero_wallet_rpc_url: String = Input::with_theme(&ColorfulTheme::default()) + 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()) .interact_text()?; - let monero_wallet_rpc_url = Url::parse(monero_wallet_rpc_url.as_str())?; + let monero_wallet_rpc_url = monero_wallet_rpc_url.as_str().parse()?; println!(); - Ok(File { + Ok(Config { bitcoin: Bitcoin { bitcoind_url, wallet_name: bitcoin_wallet_name, @@ -129,7 +125,7 @@ mod tests { let temp_dir = tempdir().unwrap().path().to_path_buf(); let config_path = Path::join(&temp_dir, "config.toml"); - let expected = File { + let expected = Config { bitcoin: Bitcoin { bitcoind_url: Url::from_str("http://127.0.0.1:18332").unwrap(), wallet_name: "alice".to_string(), diff --git a/swap/src/config/seed.rs b/swap/src/config/seed.rs deleted file mode 100644 index ef6382f7..00000000 --- a/swap/src/config/seed.rs +++ /dev/null @@ -1,196 +0,0 @@ -use crate::{fs::ensure_directory_exists, seed}; -use pem::{encode, Pem}; -use seed::SEED_LENGTH; -use std::{ - ffi::OsStr, - fmt, - fs::{self, File}, - io::{self, Write}, - path::{Path, PathBuf}, -}; - -#[derive(Clone, Copy, PartialEq)] -pub struct Seed(seed::Seed); - -impl Seed { - pub fn random() -> Result { - Ok(Seed(seed::Seed::random()?)) - } - - pub fn from_file_or_generate(data_dir: &Path) -> Result { - let file_path_buf = data_dir.join("seed.pem"); - let file_path = Path::new(&file_path_buf); - - if file_path.exists() { - return Self::from_file(&file_path); - } - - tracing::info!("No seed file found, creating at: {}", file_path.display()); - - let random_seed = Seed::random()?; - random_seed.write_to(file_path.to_path_buf())?; - - Ok(random_seed) - } - - fn from_file(seed_file: D) -> Result - where - D: AsRef, - { - let file = Path::new(&seed_file); - let contents = fs::read_to_string(file)?; - let pem = pem::parse(contents)?; - - tracing::info!("Read in seed from file: {}", file.display()); - - Self::from_pem(pem) - } - - fn from_pem(pem: pem::Pem) -> Result { - if pem.contents.len() != SEED_LENGTH { - Err(Error::IncorrectLength(pem.contents.len())) - } else { - let mut array = [0; SEED_LENGTH]; - for (i, b) in pem.contents.iter().enumerate() { - array[i] = *b; - } - - Ok(Self::from(array)) - } - } - - fn write_to(&self, seed_file: PathBuf) -> Result<(), Error> { - ensure_directory_exists(&seed_file)?; - - let data = (self.0).bytes(); - let pem = Pem { - tag: String::from("SEED"), - contents: data.to_vec(), - }; - - let pem_string = encode(&pem); - - let mut file = File::create(seed_file)?; - file.write_all(pem_string.as_bytes())?; - - Ok(()) - } -} - -impl fmt::Debug for Seed { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Seed([*****])") - } -} - -impl fmt::Display for Seed { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -impl From<[u8; SEED_LENGTH]> for Seed { - fn from(bytes: [u8; 32]) -> Self { - Seed(seed::Seed::from(bytes)) - } -} - -impl From for seed::Seed { - fn from(seed: Seed) -> Self { - seed.0 - } -} - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("Seed generation: ")] - SeedGeneration(#[from] crate::seed::Error), - #[error("io: ")] - Io(#[from] io::Error), - #[error("PEM parse: ")] - PemParse(#[from] pem::PemError), - #[error("expected 32 bytes of base64 encode, got {0} bytes")] - IncorrectLength(usize), - #[error("RNG: ")] - Rand(#[from] rand::Error), - #[error("no default path")] - NoDefaultPath, -} - -#[cfg(test)] -mod tests { - use super::*; - use std::env::temp_dir; - - #[test] - fn seed_byte_string_must_be_32_bytes_long() { - let _seed = Seed::from(*b"this string is exactly 32 bytes!"); - } - - #[test] - fn seed_from_pem_works() { - let payload: &str = "syl9wSYaruvgxg9P5Q1qkZaq5YkM6GvXkxe+VYrL/XM="; - - // 32 bytes base64 encoded. - let pem_string: &str = "-----BEGIN SEED----- -syl9wSYaruvgxg9P5Q1qkZaq5YkM6GvXkxe+VYrL/XM= ------END SEED----- -"; - - let want = base64::decode(payload).unwrap(); - let pem = pem::parse(pem_string).unwrap(); - let got = Seed::from_pem(pem).unwrap(); - - assert_eq!((got.0).bytes(), *want); - } - - #[test] - fn seed_from_pem_fails_for_short_seed() { - let short = "-----BEGIN SEED----- -VnZUNFZ4dlY= ------END SEED----- -"; - let pem = pem::parse(short).unwrap(); - match Seed::from_pem(pem) { - Ok(_) => panic!("should fail for short payload"), - Err(e) => { - match e { - Error::IncorrectLength(_) => {} // pass - _ => panic!("should fail with IncorrectLength error"), - } - } - } - } - - #[test] - #[should_panic] - fn seed_from_pem_fails_for_long_seed() { - let long = "-----BEGIN SEED----- -mbKANv2qKGmNVg1qtquj6Hx1pFPelpqOfE2JaJJAMEg1FlFhNRNlFlE= -mbKANv2qKGmNVg1qtquj6Hx1pFPelpqOfE2JaJJAMEg1FlFhNRNlFlE= ------END SEED----- -"; - let pem = pem::parse(long).unwrap(); - match Seed::from_pem(pem) { - Ok(_) => panic!("should fail for long payload"), - Err(e) => { - match e { - Error::IncorrectLength(_) => {} // pass - _ => panic!("should fail with IncorrectLength error"), - } - } - } - } - - #[test] - fn round_trip_through_file_write_read() { - let tmpfile = temp_dir().join("seed.pem"); - - let seed = Seed::random().unwrap(); - seed.write_to(tmpfile.clone()) - .expect("Write seed to temp file"); - - let rinsed = Seed::from_file(tmpfile).expect("Read from temp file"); - assert_eq!(seed.0, rinsed.0); - } -} diff --git a/swap/src/lib.rs b/swap/src/lib.rs index f042cddc..b23fe07c 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -18,7 +18,6 @@ pub mod bitcoin; pub mod cli; -pub mod config; pub mod database; pub mod execution_params; pub mod fs; diff --git a/swap/src/seed.rs b/swap/src/seed.rs index 382f64da..2a0b3836 100644 --- a/swap/src/seed.rs +++ b/swap/src/seed.rs @@ -1,6 +1,14 @@ +use crate::fs::ensure_directory_exists; use ::bitcoin::secp256k1::{self, constants::SECRET_KEY_SIZE, SecretKey}; +use pem::{encode, Pem}; use rand::prelude::*; -use std::fmt; +use std::{ + ffi::OsStr, + fmt, + fs::{self, File}, + io::{self, Write}, + path::{Path, PathBuf}, +}; pub const SEED_LENGTH: usize = 32; @@ -21,6 +29,65 @@ impl Seed { pub fn bytes(&self) -> [u8; SEED_LENGTH] { self.0 } + + pub fn from_file_or_generate(data_dir: &Path) -> Result { + let file_path_buf = data_dir.join("seed.pem"); + let file_path = Path::new(&file_path_buf); + + if file_path.exists() { + return Self::from_file(&file_path); + } + + tracing::info!("No seed file found, creating at: {}", file_path.display()); + + let random_seed = Seed::random()?; + random_seed.write_to(file_path.to_path_buf())?; + + Ok(random_seed) + } + + fn from_file(seed_file: D) -> Result + where + D: AsRef, + { + let file = Path::new(&seed_file); + let contents = fs::read_to_string(file)?; + let pem = pem::parse(contents)?; + + tracing::info!("Read in seed from file: {}", file.display()); + + Self::from_pem(pem) + } + + fn from_pem(pem: pem::Pem) -> Result { + if pem.contents.len() != SEED_LENGTH { + Err(Error::IncorrectLength(pem.contents.len())) + } else { + let mut array = [0; SEED_LENGTH]; + for (i, b) in pem.contents.iter().enumerate() { + array[i] = *b; + } + + Ok(Self::from(array)) + } + } + + fn write_to(&self, seed_file: PathBuf) -> Result<(), Error> { + ensure_directory_exists(&seed_file)?; + + let data = self.bytes(); + let pem = Pem { + tag: String::from("SEED"), + contents: data.to_vec(), + }; + + let pem_string = encode(&pem); + + let mut file = File::create(seed_file)?; + file.write_all(pem_string.as_bytes())?; + + Ok(()) + } } impl fmt::Debug for Seed { @@ -41,18 +108,101 @@ impl From<[u8; SEED_LENGTH]> for Seed { } } -#[derive(Debug, Copy, Clone, thiserror::Error)] +#[derive(Debug, thiserror::Error)] pub enum Error { #[error("Secp256k1: ")] Secp256k1(#[from] secp256k1::Error), + #[error("io: ")] + Io(#[from] io::Error), + #[error("PEM parse: ")] + PemParse(#[from] pem::PemError), + #[error("expected 32 bytes of base64 encode, got {0} bytes")] + IncorrectLength(usize), + #[error("RNG: ")] + Rand(#[from] rand::Error), + #[error("no default path")] + NoDefaultPath, } #[cfg(test)] mod tests { use super::*; + use std::env::temp_dir; #[test] fn generate_random_seed() { let _ = Seed::random().unwrap(); } + + #[test] + fn seed_byte_string_must_be_32_bytes_long() { + let _seed = Seed::from(*b"this string is exactly 32 bytes!"); + } + + #[test] + fn seed_from_pem_works() { + let payload: &str = "syl9wSYaruvgxg9P5Q1qkZaq5YkM6GvXkxe+VYrL/XM="; + + // 32 bytes base64 encoded. + let pem_string: &str = "-----BEGIN SEED----- +syl9wSYaruvgxg9P5Q1qkZaq5YkM6GvXkxe+VYrL/XM= +-----END SEED----- +"; + + let want = base64::decode(payload).unwrap(); + let pem = pem::parse(pem_string).unwrap(); + let got = Seed::from_pem(pem).unwrap(); + + assert_eq!(got.bytes(), *want); + } + + #[test] + fn seed_from_pem_fails_for_short_seed() { + let short = "-----BEGIN SEED----- +VnZUNFZ4dlY= +-----END SEED----- +"; + let pem = pem::parse(short).unwrap(); + match Seed::from_pem(pem) { + Ok(_) => panic!("should fail for short payload"), + Err(e) => { + match e { + Error::IncorrectLength(_) => {} // pass + _ => panic!("should fail with IncorrectLength error"), + } + } + } + } + + #[test] + #[should_panic] + fn seed_from_pem_fails_for_long_seed() { + let long = "-----BEGIN SEED----- +mbKANv2qKGmNVg1qtquj6Hx1pFPelpqOfE2JaJJAMEg1FlFhNRNlFlE= +mbKANv2qKGmNVg1qtquj6Hx1pFPelpqOfE2JaJJAMEg1FlFhNRNlFlE= +-----END SEED----- +"; + let pem = pem::parse(long).unwrap(); + match Seed::from_pem(pem) { + Ok(_) => panic!("should fail for long payload"), + Err(e) => { + match e { + Error::IncorrectLength(_) => {} // pass + _ => panic!("should fail with IncorrectLength error"), + } + } + } + } + + #[test] + fn round_trip_through_file_write_read() { + let tmpfile = temp_dir().join("seed.pem"); + + let seed = Seed::random().unwrap(); + seed.write_to(tmpfile.clone()) + .expect("Write seed to temp file"); + + let rinsed = Seed::from_file(tmpfile).expect("Read from temp file"); + assert_eq!(seed.0, rinsed.0); + } }