daemon: backbone and configuration parsing

Taken from revaultd at 7cd856d5a345319cebc815aa61f3b66cebb48b86. Credits
to the revaultd contributors.
This commit is contained in:
Antoine Poinsot 2022-07-20 18:36:41 +02:00
parent da9149ccde
commit 1b35196448
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304
6 changed files with 878 additions and 0 deletions

300
Cargo.lock generated Normal file
View File

@ -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"

34
Cargo.toml Normal file
View File

@ -0,0 +1,34 @@
[package]
name = "minisafed"
version = "0.0.1"
authors = ["Antoine Poinsot <darosior@protonmail.com>"]
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"

174
src/bin/cli.rs Normal file
View File

@ -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] <command> [<param 1> <param 2> ...]");
process::exit(1);
}
// Returns (Maybe(special conf file), Raw, Method name, Maybe(List of parameters))
fn parse_args(mut args: Vec<String>) -> (Option<PathBuf>, bool, String, Vec<String>) {
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<String>) -> Json {
let method = Json::String(method);
let params = Json::Array(params.into_iter().map(from_str_hack).collect::<Vec<Json>>());
let mut object = serde_json::Map::<String, Json>::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>) -> 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<u8>, bytes_read: usize) -> Vec<u8> {
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::<Json>(&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,
}
}
}

64
src/bin/daemon.rs Normal file
View File

@ -0,0 +1,64 @@
use std::{
env,
io::{self, Write},
path::PathBuf,
process, time,
};
use minisafed::config::Config;
fn parse_args(args: Vec<String>) -> Option<PathBuf> {
if args.len() == 1 {
return None;
}
if args.len() != 3 {
eprintln!("Unknown arguments '{:?}'.", args);
eprintln!("Only '--conf <configuration file path>' 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");
}

305
src/config.rs Normal file
View File

@ -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<T, D::Error>
where
D: Deserializer<'de>,
T: FromStr,
<T as 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<T: std::fmt::Display, S: Serializer>(
field: T,
s: S,
) -> Result<S::Ok, S::Error> {
s.serialize_str(&field.to_string())
}
fn deserialize_duration<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
let secs = u64::deserialize(deserializer)?;
Ok(Duration::from_secs(secs))
}
pub fn serialize_duration<S: Serializer>(duration: &Duration, s: S) -> Result<S::Ok, S::Error> {
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<PathBuf>,
/// Whether to daemonize the process
pub daemon: Option<bool>,
/// 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<DescriptorPublicKey>,
/// 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<DescriptorPublicKey>),
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<std::io::Error> 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/<network>/" directory in the XDG standard configuration directory for
/// all OSes but Linux-based ones, for which it's `~/.minisafe/<network>/`.
/// 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<PathBuf> {
#[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<PathBuf> {
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<PathBuf>) -> Result<Config, ConfigError> {
let config_file =
custom_path.unwrap_or(config_file_path().ok_or_else(|| ConfigError::DatadirNotFound)?);
let config = toml::from_slice::<Config>(&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::<Config>(&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::<Config>(&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<Config, toml::de::Error> = 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<Config, toml::de::Error> = 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"#));
}
}

1
src/lib.rs Normal file
View File

@ -0,0 +1 @@
pub mod config;