From 1b3519644802de3cfef1723f2f144dc08fe1f358 Mon Sep 17 00:00:00 2001 From: Antoine Poinsot Date: Wed, 20 Jul 2022 18:36:41 +0200 Subject: [PATCH] daemon: backbone and configuration parsing Taken from revaultd at 7cd856d5a345319cebc815aa61f3b66cebb48b86. Credits to the revaultd contributors. --- Cargo.lock | 300 +++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 34 ++++++ src/bin/cli.rs | 174 ++++++++++++++++++++++++++ src/bin/daemon.rs | 64 ++++++++++ src/config.rs | 305 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 6 files changed, 878 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/bin/cli.rs create mode 100644 src/bin/daemon.rs create mode 100644 src/config.rs create mode 100644 src/lib.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..5572d159 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,300 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bech32" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" + +[[package]] +name = "bitcoin" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a41df6ad9642c5c15ae312dd3d074de38fd3eb7cc87ad4ce10f90292a83fe4d" +dependencies = [ + "bech32", + "bitcoin_hashes", + "secp256k1", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006cc91e1a1d99819bc5b8214be3555c1f0611b169f527a1fdc54ed1f2b745b0" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "dirs" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "fern" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bdd7b0849075e79ee9a1836df22c717d1eba30451796fdc631b04565dd11e2a" +dependencies = [ + "log", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "itoa" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" + +[[package]] +name = "libc" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "minisafed" +version = "0.0.1" +dependencies = [ + "dirs", + "fern", + "log", + "miniscript", + "serde", + "serde_json", + "toml", +] + +[[package]] +name = "miniscript" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e292b58407dfbf1384e5aca8428d3b0f2eaa09d24cb17088f6db0b7ca31194a" +dependencies = [ + "bitcoin", + "serde", +] + +[[package]] +name = "proc-macro2" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "ryu" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" + +[[package]] +name = "secp256k1" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d03ceae636d0fed5bae6a7f4f664354c5f4fcedf6eef053fef17e49f837d0a" +dependencies = [ + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957da2573cde917463ece3570eab4a0b3f19de6f1646cde62e6fd3868f566036" +dependencies = [ + "cc", +] + +[[package]] +name = "serde" +version = "1.0.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-ident" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..642ed265 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "minisafed" +version = "0.0.1" +authors = ["Antoine Poinsot "] +edition = "2018" +repository = "https://github.com/revault/minisafed" +license-file = "LICENCE" +keywords = ["bitcoin", "wallet", "safe", "script", "miniscript", "inheritance", "recovery"] +description = "Minisafe wallet daemon" +exclude = [".github/", ".cirrus.yml", "tests/", "test_data/", "contrib/", "pyproject.toml"] + +[[bin]] +name = "minisafed" +path = "src/bin/daemon.rs" + +[[bin]] +name = "minisafe-cli" +path = "src/bin/cli.rs" + +[dependencies] +# For managing transactions (it re-exports the bitcoin crate) +miniscript = { version = "6.0.0", features = ["compiler", "use-serde"] } + +# Don't reinvent the wheel +dirs = "3.0" + +# We use TOML for the config, and JSON for RPC +serde = { version = "1.0", features = ["derive"] } +toml = "0.5" +serde_json = { version = "1.0", features = ["raw_value"] } + +# Logging stuff +log = "0.4" +fern = "0.6" diff --git a/src/bin/cli.rs b/src/bin/cli.rs new file mode 100644 index 00000000..229d1438 --- /dev/null +++ b/src/bin/cli.rs @@ -0,0 +1,174 @@ +use minisafed::config::{config_folder_path, Config}; + +use std::{ + env, + io::{Read, Write}, + path::PathBuf, + process, +}; + +use serde_json::Value as Json; + +use std::os::unix::net::UnixStream; + +// Exits with error +fn show_usage() { + eprintln!("Usage:"); + eprintln!(" revault-cli [--conf conf_path] [--raw] [ ...]"); + process::exit(1); +} + +// Returns (Maybe(special conf file), Raw, Method name, Maybe(List of parameters)) +fn parse_args(mut args: Vec) -> (Option, bool, String, Vec) { + if args.len() < 2 { + eprintln!("Not enough arguments."); + show_usage(); + } + + args.remove(0); // Program name + + let mut args = args.into_iter(); + let mut raw = false; + let mut conf_file = None; + + loop { + match args.next().as_deref() { + Some("--conf") => { + if args.len() < 2 { + eprintln!("Not enough arguments."); + show_usage(); + } + + conf_file = Some(PathBuf::from(args.next().expect("Just checked"))); + } + Some("--raw") => { + if args.len() < 1 { + eprintln!("Not enough arguments."); + show_usage(); + } + raw = true; + } + Some(method) => return (conf_file, raw, method.to_owned(), args.collect()), + None => { + // Should never happen... + eprintln!("Not enough arguments."); + show_usage(); + } + } + } +} + +// Defaults to String Value when parsing fails, as it fails to parse outpoints otherwise... +fn from_str_hack(token: String) -> Json { + match serde_json::from_str(&token) { + Ok(json) => json, + Err(_) => Json::String(token), + } +} + +fn rpc_request(method: String, params: Vec) -> Json { + let method = Json::String(method); + let params = Json::Array(params.into_iter().map(from_str_hack).collect::>()); + let mut object = serde_json::Map::::new(); + object.insert("jsonrpc".to_string(), Json::String("2.0".to_string())); + object.insert( + "id".to_string(), + Json::String(format!("revault-cli-{}", process::id())), + ); + object.insert("method".to_string(), method); + object.insert("params".to_string(), params); + + Json::Object(object) +} + +fn socket_file(conf_file: Option) -> PathBuf { + let config = Config::from_file(conf_file).unwrap_or_else(|e| { + eprintln!("Error getting config: {}", e); + process::exit(1); + }); + let data_dir = config + .data_dir + .unwrap_or_else(|| config_folder_path().unwrap()); + let data_dir = data_dir.to_str().expect("Datadir is valid unicode"); + + [ + data_dir, + config.bitcoind_config.network.to_string().as_str(), + "revaultd_rpc", + ] + .iter() + .collect() +} + +fn trimmed(mut vec: Vec, bytes_read: usize) -> Vec { + vec.truncate(bytes_read); + + // Until there is some whatever-newline character, pop. + while let Some(byte) = vec.last() { + // Of course, we assume utf-8 + if !(&0x0a..=&0x0d).contains(&byte) { + break; + } + vec.pop(); + } + + vec +} + +fn main() { + let args = env::args().collect(); + let (conf_file, raw, method, params) = parse_args(args); + let request = rpc_request(method, params); + let socket_file = socket_file(conf_file); + let mut raw_response = vec![0; 256]; + + let mut socket = UnixStream::connect(&socket_file).unwrap_or_else(|e| { + eprintln!("Could not connect to {:?}: '{}'", socket_file, e); + process::exit(1); + }); + socket + .write_all(request.to_string().as_bytes()) + .unwrap_or_else(|e| { + eprintln!("Writing to {:?}: '{}'", &socket_file, e); + process::exit(1); + }); + + let mut total_read = 0; + loop { + let n = socket + .read(&mut raw_response[total_read..]) + .unwrap_or_else(|e| { + eprintln!("Reading from {:?}: '{}'", &socket_file, e); + process::exit(1); + }); + total_read += n; + if total_read == raw_response.len() { + raw_response.resize(2 * total_read, 0); + continue; + } + + // FIXME: do actual incremental parsing instead of this hack!! + raw_response = trimmed(raw_response, total_read); + match serde_json::from_slice::(&raw_response) { + Ok(response) => { + if response.get("id") == request.get("id") { + if raw { + print!("{}", response); + } else if let Some(r) = response.get("result") { + println!("{:#}", serde_json::json!({ "result": r })); + } else if let Some(e) = response.get("error") { + println!("{:#}", serde_json::json!({ "error": e })); + } else { + log::warn!( + "revaultd response doesn't contain result or error: '{}'", + response + ); + println!("{:#}", response); + } + return; + } + } + Err(_) => continue, + } + } +} diff --git a/src/bin/daemon.rs b/src/bin/daemon.rs new file mode 100644 index 00000000..26e34670 --- /dev/null +++ b/src/bin/daemon.rs @@ -0,0 +1,64 @@ +use std::{ + env, + io::{self, Write}, + path::PathBuf, + process, time, +}; + +use minisafed::config::Config; + +fn parse_args(args: Vec) -> Option { + if args.len() == 1 { + return None; + } + + if args.len() != 3 { + eprintln!("Unknown arguments '{:?}'.", args); + eprintln!("Only '--conf ' is supported."); + process::exit(1); + } + + Some(PathBuf::from(args[2].to_owned())) +} + +fn setup_logger(log_level: log::LevelFilter) -> Result<(), fern::InitError> { + let dispatcher = fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "[{}][{}][{}] {}", + time::SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .unwrap_or_else(|e| { + println!("Can't get time since epoch: '{}'. Using a dummy value.", e); + time::Duration::from_secs(0) + }) + .as_secs(), + record.target(), + record.level(), + message + )) + }) + .level(log_level); + + dispatcher.chain(std::io::stdout()).apply()?; + + Ok(()) +} + +fn main() { + let args = env::args().collect(); + let conf_file = parse_args(args); + + let config = Config::from_file(conf_file).unwrap_or_else(|e| { + eprintln!("Error parsing config: {}", e); + process::exit(1); + }); + setup_logger(config.log_level).unwrap_or_else(|e| { + eprintln!("Error setting up logger: {}", e); + process::exit(1); + }); + + // We are always logging to stdout, should it be then piped to the log file (if self) or + // not. So just make sure that all messages were actually written. + io::stdout().flush().expect("Flushing stdout"); +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 00000000..0620582d --- /dev/null +++ b/src/config.rs @@ -0,0 +1,305 @@ +use std::{net::SocketAddr, path::PathBuf, str::FromStr, time::Duration}; + +use miniscript::{ + bitcoin::Network, + descriptor::{Descriptor, DescriptorPublicKey}, + ForEach, ForEachKey, +}; + +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + +fn deserialize_fromstr<'de, D, T>(deserializer: D) -> Result +where + D: Deserializer<'de>, + T: FromStr, + ::Err: std::fmt::Display, +{ + let string = String::deserialize(deserializer)?; + T::from_str(&string) + .map_err(|e| de::Error::custom(format!("Error parsing descriptor '{}': '{}'", string, e))) +} + +pub fn serialize_to_string( + field: T, + s: S, +) -> Result { + s.serialize_str(&field.to_string()) +} + +fn deserialize_duration<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let secs = u64::deserialize(deserializer)?; + Ok(Duration::from_secs(secs)) +} +pub fn serialize_duration(duration: &Duration, s: S) -> Result { + s.serialize_u64(duration.as_secs()) +} + +fn default_loglevel() -> log::LevelFilter { + log::LevelFilter::Info +} + +fn default_poll_interval() -> Duration { + Duration::from_secs(30) +} + +/// Everything we need to know for talking to bitcoind serenely +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct BitcoindConfig { + /// The network we are operating on, one of "bitcoin", "testnet", "regtest" + pub network: Network, + /// Path to bitcoind's cookie file, to authenticate the RPC connection + pub cookie_path: PathBuf, + /// The IP:port bitcoind's RPC is listening on + pub addr: SocketAddr, + /// The poll interval for bitcoind + #[serde( + deserialize_with = "deserialize_duration", + serialize_with = "serialize_duration", + default = "default_poll_interval" + )] + pub poll_interval_secs: Duration, +} + +/// Static informations we require to operate +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Config { + /// An optional custom data directory + pub data_dir: Option, + /// Whether to daemonize the process + pub daemon: Option, + /// What messages to log + #[serde( + deserialize_with = "deserialize_fromstr", + serialize_with = "serialize_to_string", + default = "default_loglevel" + )] + pub log_level: log::LevelFilter, + /// The descriptor to use for sending/receiving coins + #[serde( + deserialize_with = "deserialize_fromstr", + serialize_with = "serialize_to_string" + )] + pub main_descriptor: Descriptor, + /// Everything we need to know to talk to bitcoind + pub bitcoind_config: BitcoindConfig, +} + +#[derive(PartialEq, Eq, Debug)] +pub enum ConfigError { + DatadirNotFound, + FileNotFound, + ReadingFile(String), + UnexpectedDescriptor(Descriptor), + Unexpected(String), +} + +impl std::fmt::Display for ConfigError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match &self { + Self::DatadirNotFound => write!(f, "Could not locate the configuration directory."), + Self::FileNotFound => write!(f, "Could not locate the configuration file."), + Self::ReadingFile(e) => write!(f, "Failed to read configuration file: {}", e), + Self::UnexpectedDescriptor(desc) => write!( + f, + "Unexpected descriptor '{}'. We only support wsh() descriptors for now.", + desc + ), + Self::Unexpected(e) => write!(f, "Configuration error: {}", e), + } + } +} + +impl From for ConfigError { + fn from(e: std::io::Error) -> Self { + match e.kind() { + std::io::ErrorKind::NotFound => Self::FileNotFound, + _ => Self::ReadingFile(e.to_string()), + } + } +} + +impl std::error::Error for ConfigError {} + +/// Get the absolute path to the minisafe configuration folder. +/// +/// It's a "minisafe//" directory in the XDG standard configuration directory for +/// all OSes but Linux-based ones, for which it's `~/.minisafe//`. +/// There is only one config file at `minisafe/config.toml`, which specifies the network. +/// Rationale: we want to have the database, RPC socket, etc.. in the same folder as the +/// configuration file but for Linux the XDG specifoes a data directory (`~/.local/share/`) +/// different from the configuration one (`~/.config/`). +pub fn config_folder_path() -> Option { + #[cfg(target_os = "linux")] + let configs_dir = dirs::home_dir(); + + #[cfg(not(target_os = "linux"))] + let configs_dir = dirs::config_dir(); + + if let Some(mut path) = configs_dir { + #[cfg(target_os = "linux")] + path.push(".minisafe"); + + #[cfg(not(target_os = "linux"))] + path.push("Minisafe"); + + return Some(path); + } + + None +} + +fn config_file_path() -> Option { + config_folder_path().map(|mut path| { + path.push("minisafe.toml"); + path + }) +} + +impl Config { + /// Get our static configuration out of a mandatory configuration file. + /// + /// We require all settings to be set in the configuration file, and only in the configuration + /// file. We don't allow to set them via the command line or environment variables to avoid a + /// futile duplication. + pub fn from_file(custom_path: Option) -> Result { + let config_file = + custom_path.unwrap_or(config_file_path().ok_or_else(|| ConfigError::DatadirNotFound)?); + + let config = toml::from_slice::(&std::fs::read(&config_file)?) + .map_err(|e| ConfigError::ReadingFile(format!("Parsing configuration file: {}", e)))?; + config.check()?; + + Ok(config) + } + + /// Make sure the settings are sane. + pub fn check(&self) -> Result<(), ConfigError> { + // Check the network of the xpubs in the descriptors + let expected_network = match self.bitcoind_config.network { + Network::Bitcoin => Network::Bitcoin, + _ => Network::Testnet, + }; + let unexpected_net = self.main_descriptor.for_each_key(|pkpkh| { + let xpub = match pkpkh { + // For DescriptorPublicKey, Pk::Hash == Self. + ForEach::Key(xpub) => xpub, + ForEach::Hash(xpub) => xpub, + }; + if let DescriptorPublicKey::XPub(xpub) = xpub { + xpub.xkey.network != expected_network + } else { + false + } + }); + if unexpected_net { + return Err(ConfigError::Unexpected(format!( + "Our bitcoin network is {} but one xpub is not for network {}", + self.bitcoind_config.network, expected_network + ))); + } + + // TODO: check the semantics of the main descriptor + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::{config_file_path, Config}; + + // Test the format of the configuration file + #[test] + fn toml_config() { + // A valid config + let toml_str = r#" + data_dir = "/home/wizardsardine/custom/folder/" + daemon = false + log_level = "debug" + main_descriptor = "wsh(andor(thresh(1,pk(xpub6BaZSKgpaVvibu2k78QsqeDWXp92xLHZxiu1WoqLB9hKhsBf3miBUDX7PJLgSPvkj66ThVHTqdnbXpeu8crXFmDUd4HeM4s4miQS2xsv3Qb/*)),and_v(v:multi(2,03b506a1dbe57b4bf48c95e0c7d417b87dd3b4349d290d2e7e9ba72c912652d80a,0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce),older(4)),thresh(2,pkh(xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU/*),a:pkh(xpub6AaffFGfH6WXfm6pwWzmUMuECQnoLeB3agMKaLyEBZ5ZVfwtnS5VJKqXBt8o5ooCWVy2H87GsZshp7DeKE25eWLyd1Ccuh2ZubQUkgpiVux/*))))#532k8uvf" + + [bitcoind_config] + network = "bitcoin" + cookie_path = "/home/user/.bitcoin/.cookie" + addr = "127.0.0.1:8332" + poll_interval_secs = 18 + "#.trim_start().replace(" ", ""); + toml::from_str::(&toml_str).expect("Deserializing toml_str"); + + // A valid, round-tripping, config + let toml_str = r#" + data_dir = '/home/wizardsardine/custom/folder/' + daemon = false + log_level = 'TRACE' + main_descriptor = 'wsh(andor(thresh(1,pk(xpub6BaZSKgpaVvibu2k78QsqeDWXp92xLHZxiu1WoqLB9hKhsBf3miBUDX7PJLgSPvkj66ThVHTqdnbXpeu8crXFmDUd4HeM4s4miQS2xsv3Qb/*)),and_v(v:multi(2,03b506a1dbe57b4bf48c95e0c7d417b87dd3b4349d290d2e7e9ba72c912652d80a,0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce),older(4)),thresh(2,pkh(xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU/*),a:pkh(xpub6AaffFGfH6WXfm6pwWzmUMuECQnoLeB3agMKaLyEBZ5ZVfwtnS5VJKqXBt8o5ooCWVy2H87GsZshp7DeKE25eWLyd1Ccuh2ZubQUkgpiVux/*))))#532k8uvf' + + [bitcoind_config] + network = 'bitcoin' + cookie_path = '/home/user/.bitcoin/.cookie' + addr = '127.0.0.1:8332' + poll_interval_secs = 18 + "#.trim_start().replace(" ", ""); + let parsed = toml::from_str::(&toml_str).expect("Deserializing toml_str"); + let serialized = toml::to_string_pretty(&parsed).expect("Serializing to toml"); + assert_eq!(toml_str, serialized); + + // Invalid desc checksum + let toml_str = r#" + daemon = false + log_level = "trace" + data_dir = "/home/wizardsardine/custom/folder/" + + # The main descriptor semantics aren't checked, yet. + main_descriptor = "wsh(andor(thresh(1,pk(xpub6BaZSKgpaVvibu2k78QsqeDWXp92xLHZxiu1WoqLB9hKhsBf3miBUDX7PJLgSPvkj66ThVHTqdnbXpeu8crXFmDUd4HeM4s4miQS2xsv3Qb/*)),and_v(v:multi(2,03b506a1dbe57b4bf48c95e0c7d417b87dd3b4349d290d2e7e9ba72c912652d80a,0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce),older(4)),thresh(2,pkh(xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU/*),a:pkh(xpub6AaffFGfH6WXfm6pwWzmUMuECQnoLeB3agMKaLyEBZ5ZVfwtnS5VJKqXBt8o5ooCWVy2H87GsZshp7DeKE25eWLyd1Ccuh2ZubQUkgpiVux/*))))#532k88vf" + + [bitcoind_config] + network = "bitcoin" + cookie_path = "/home/user/.bitcoin/.cookie" + addr = "127.0.0.1:8332" + poll_interval_secs = 18 + "#; + let config_res: Result = toml::from_str(toml_str); + config_res.expect_err("Deserializing an invalid toml_str"); + + // Not enough parameters: missing the network + let toml_str = r#" + daemon = false + log_level = "trace" + data_dir = "/home/wizardsardine/custom/folder/" + + # The main descriptor semantics aren't checked, yet. + main_descriptor = "wsh(andor(thresh(1,pk(xpub6BaZSKgpaVvibu2k78QsqeDWXp92xLHZxiu1WoqLB9hKhsBf3miBUDX7PJLgSPvkj66ThVHTqdnbXpeu8crXFmDUd4HeM4s4miQS2xsv3Qb/*)),and_v(v:multi(2,03b506a1dbe57b4bf48c95e0c7d417b87dd3b4349d290d2e7e9ba72c912652d80a,0295e7f5d12a2061f1fd2286cefec592dff656a19f55f4f01305d6aa56630880ce),older(4)),thresh(2,pkh(xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRmF8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU/*),a:pkh(xpub6AaffFGfH6WXfm6pwWzmUMuECQnoLeB3agMKaLyEBZ5ZVfwtnS5VJKqXBt8o5ooCWVy2H87GsZshp7DeKE25eWLyd1Ccuh2ZubQUkgpiVux/*))))#532k8uvf" + + [bitcoind_config] + cookie_path = "/home/user/.bitcoin/.cookie" + addr = "127.0.0.1:8332" + poll_interval_secs = 18 + "#; + let config_res: Result = toml::from_str(toml_str); + config_res.expect_err("Deserializing an invalid toml_str"); + } + + #[test] + fn config_directory() { + let filepath = config_file_path().expect("Getting config file path"); + + #[cfg(target_os = "linux")] + { + assert!(filepath.as_path().starts_with("/home/")); + assert!(filepath.as_path().ends_with(".minisafe/minisafe.toml")); + } + + #[cfg(target_os = "macos")] + assert!(filepath + .as_path() + .ends_with("Library/Application Support/Minisafe/minisafe.toml")); + + #[cfg(target_os = "windows")] + assert!(filepath + .as_path() + .ends_with(r#"AppData\Roaming\Minisafe\minisafe.toml"#)); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..ef68c369 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod config;