daemon: backbone and configuration parsing
Taken from revaultd at 7cd856d5a345319cebc815aa61f3b66cebb48b86. Credits to the revaultd contributors.
This commit is contained in:
parent
da9149ccde
commit
1b35196448
300
Cargo.lock
generated
Normal file
300
Cargo.lock
generated
Normal 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
34
Cargo.toml
Normal 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
174
src/bin/cli.rs
Normal 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
64
src/bin/daemon.rs
Normal 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
305
src/config.rs
Normal 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
1
src/lib.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod config;
|
||||
Loading…
x
Reference in New Issue
Block a user