From 45dccb8be2dda1635048f6d32657f299ece676b4 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Thu, 11 Feb 2021 09:38:29 +1100 Subject: [PATCH 1/5] Rename the config struct `Config` --- swap/src/config.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/swap/src/config.rs b/swap/src/config.rs index f9f56431..f8ad3bb4 100644 --- a/swap/src/config.rs +++ b/swap/src/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::{ @@ -19,19 +19,19 @@ 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 +54,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 +64,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,7 +88,7 @@ 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()) .with_prompt("Enter Bitcoind URL (including username and password if applicable) or hit return to use default") @@ -107,7 +107,7 @@ pub fn query_user_for_initial_testnet_config() -> Result { let monero_wallet_rpc_url = Url::parse(monero_wallet_rpc_url.as_str())?; println!(); - Ok(File { + Ok(Config { bitcoin: Bitcoin { bitcoind_url, wallet_name: bitcoin_wallet_name, @@ -129,7 +129,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(), From 7d392c308652bbf653221dab913ec2306a8c4e41 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Thu, 11 Feb 2021 09:47:42 +1100 Subject: [PATCH 2/5] This files contains `Command` and `Arguments` structs --- swap/src/bin/cli.rs | 4 ++-- swap/src/{cli.rs => command.rs} | 2 +- swap/src/lib.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename swap/src/{cli.rs => command.rs} (99%) diff --git a/swap/src/bin/cli.rs b/swap/src/bin/cli.rs index 03176ae3..82093a9f 100644 --- a/swap/src/bin/cli.rs +++ b/swap/src/bin/cli.rs @@ -19,7 +19,7 @@ use std::{path::PathBuf, sync::Arc}; use structopt::StructOpt; use swap::{ bitcoin, - cli::{Cancel, Command, Options, Refund, Resume}, + command::{Arguments, Cancel, Command, Refund, Resume}, config, config::{ initial_setup, query_user_for_initial_testnet_config, read_config, ConfigNotInitialized, @@ -49,7 +49,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 diff --git a/swap/src/cli.rs b/swap/src/command.rs similarity index 99% rename from swap/src/cli.rs rename to swap/src/command.rs index 9a1d91a5..67cdcb80 100644 --- a/swap/src/cli.rs +++ b/swap/src/command.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use uuid::Uuid; #[derive(structopt::StructOpt, Debug)] -pub struct Options { +pub struct Arguments { #[structopt( long = "data-dir", help = "Provide a custom path to the data directory.", diff --git a/swap/src/lib.rs b/swap/src/lib.rs index f042cddc..3dd85f26 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -17,7 +17,7 @@ )] pub mod bitcoin; -pub mod cli; +pub mod command; pub mod config; pub mod database; pub mod execution_params; From 901c9e89c9f8fd029ea3c8f6408f59c7b0a0a5b8 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Thu, 11 Feb 2021 09:57:17 +1100 Subject: [PATCH 3/5] No evident added value of having separate `Seed` structs Hence, reducing complexity of the codebase. Note that the seed will be used by both nectar and the cli whereas the config mod will be different so this changes helps with the next step of having a dedicated config module for each binary. --- swap/src/bin/cli.rs | 6 +- swap/src/config.rs | 4 - swap/src/config/seed.rs | 196 ---------------------------------------- swap/src/seed.rs | 154 ++++++++++++++++++++++++++++++- 4 files changed, 154 insertions(+), 206 deletions(-) delete mode 100644 swap/src/config/seed.rs diff --git a/swap/src/bin/cli.rs b/swap/src/bin/cli.rs index 82093a9f..5def8d61 100644 --- a/swap/src/bin/cli.rs +++ b/swap/src/bin/cli.rs @@ -20,7 +20,6 @@ use structopt::StructOpt; use swap::{ bitcoin, command::{Arguments, Cancel, Command, Refund, Resume}, - config, config::{ initial_setup, query_user_for_initial_testnet_config, read_config, ConfigNotInitialized, }, @@ -35,6 +34,7 @@ use swap::{ bob::{cancel::CancelError, Builder}, SwapAmounts, }, + seed::Seed, trace::init_tracing, }; use tracing::{error, info, warn}; @@ -63,9 +63,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/config.rs b/swap/src/config.rs index f8ad3bb4..9542880f 100644 --- a/swap/src/config.rs +++ b/swap/src/config.rs @@ -11,10 +11,6 @@ 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"; 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/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); + } } From 7805a6d684cf2112875a3bcecdd3377eb0536b0f Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Thu, 11 Feb 2021 09:59:24 +1100 Subject: [PATCH 4/5] Move cli specific modules under cli folder nectar will also have its own folder with a separate config. --- swap/src/bin/cli.rs | 8 +++++--- swap/src/cli.rs | 2 ++ swap/src/{ => cli}/command.rs | 0 swap/src/{ => cli}/config.rs | 0 swap/src/lib.rs | 3 +-- 5 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 swap/src/cli.rs rename swap/src/{ => cli}/command.rs (100%) rename swap/src/{ => cli}/config.rs (100%) diff --git a/swap/src/bin/cli.rs b/swap/src/bin/cli.rs index 5def8d61..4d8950cb 100644 --- a/swap/src/bin/cli.rs +++ b/swap/src/bin/cli.rs @@ -19,9 +19,11 @@ use std::{path::PathBuf, sync::Arc}; use structopt::StructOpt; use swap::{ bitcoin, - command::{Arguments, Cancel, Command, Refund, Resume}, - 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, diff --git a/swap/src/cli.rs b/swap/src/cli.rs new file mode 100644 index 00000000..6d982acc --- /dev/null +++ b/swap/src/cli.rs @@ -0,0 +1,2 @@ +pub mod command; +pub mod config; diff --git a/swap/src/command.rs b/swap/src/cli/command.rs similarity index 100% rename from swap/src/command.rs rename to swap/src/cli/command.rs diff --git a/swap/src/config.rs b/swap/src/cli/config.rs similarity index 100% rename from swap/src/config.rs rename to swap/src/cli/config.rs diff --git a/swap/src/lib.rs b/swap/src/lib.rs index 3dd85f26..b23fe07c 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -17,8 +17,7 @@ )] pub mod bitcoin; -pub mod command; -pub mod config; +pub mod cli; pub mod database; pub mod execution_params; pub mod fs; From fac5b59d1774333ccc4df97be3ff052da697f9c9 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Thu, 11 Feb 2021 10:19:26 +1100 Subject: [PATCH 5/5] Remove unnecessary specification of the types --- swap/src/cli/config.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/swap/src/cli/config.rs b/swap/src/cli/config.rs index 9542880f..1f7f95fb 100644 --- a/swap/src/cli/config.rs +++ b/swap/src/cli/config.rs @@ -86,21 +86,21 @@ where 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(Config {