use std::{
collections::BTreeMap,
fs::File,
io::{Read, Write},
path::PathBuf,
str::FromStr,
sync::mpsc::{self},
};
use miniscript::bitcoin::{self, bip32};
use crate::{
descriptor::wpkh,
signer::{HotSigner, JsonSigner, Signer, SignerNotif},
};
#[derive(Debug, Clone)]
pub enum Error {
ParsePsbt,
}
#[derive(Debug)]
pub struct SigningManager {
data_dir: PathBuf,
dir_name: &'static str,
receiver: mpsc::Receiver<SignerNotif>,
sender: mpsc::Sender<SignerNotif>,
hot_signers: BTreeMap<bip32::Fingerprint, HotSigner>,
#[allow(unused)]
signers: BTreeMap<bip32::Fingerprint, ()>,
persist: bool,
}
impl SigningManager {
pub fn new(data_dir: PathBuf, dir_name: &'static str) -> Self {
let (sender, receiver) = mpsc::channel();
Self {
data_dir,
dir_name,
receiver,
sender,
hot_signers: Default::default(),
signers: Default::default(),
persist: true,
}
}
pub fn path(data_dir: PathBuf, dir_name: &'static str) -> PathBuf {
let mut path = data_dir;
path.push(dir_name);
path.push(".signers");
path
}
pub fn from_file(data_dir: PathBuf, dir_name: &'static str) -> Self {
if let Ok(mut file) = File::open(Self::path(data_dir.clone(), dir_name)) {
let mut content = String::new();
let _ = file.read_to_string(&mut content);
let json_signers: Result<Vec<JsonSigner>, _> = serde_json::from_str(&content);
if let Ok(signers) = json_signers {
let hot_signers = signers
.into_iter()
.map(|s| {
let signer = HotSigner::from_json(s);
(signer.fingerprint(), signer)
})
.collect();
let mut manager = SigningManager::new(data_dir, dir_name);
manager.hot_signers = hot_signers;
let sender = manager.sender.clone();
for signer in manager.hot_signers.values_mut() {
signer.init(sender.clone());
}
manager
} else {
SigningManager::new(data_dir, dir_name)
}
} else {
SigningManager::new(data_dir, dir_name)
}
}
pub fn enable_persist(mut self, persist: bool) -> Self {
self.persist = persist;
self
}
pub fn persist(&self) {
if !self.persist {
return;
}
match File::create(Self::path(self.data_dir.clone(), self.dir_name)) {
Ok(mut file) => {
let content: Vec<_> = self
.hot_signers
.clone()
.into_values()
.map(|s| s.to_json())
.collect();
let str_content = serde_json::to_string_pretty(&content).expect("cannot_fail");
let _ = file.write(str_content.as_bytes());
}
Err(e) => {
log::error!("SigningManager::persist() fail to open file: {e}");
}
}
}
pub fn poll(&self) -> Option<SignerNotif> {
self.receiver.try_recv().ok()
}
pub fn new_hot_signer(&mut self, network: bitcoin::Network) {
let mnemomic = bip39::Mnemonic::generate(12).unwrap();
self.new_hot_signer_from_mnemonic(network, mnemomic.to_string());
}
pub fn new_hot_signer_from_mnemonic(&mut self, network: bitcoin::Network, mnemonic: String) {
let mut signer = HotSigner::new_from_mnemonics(network, &mnemonic).unwrap();
signer.init(self.sender.clone());
self.hot_signers.insert(signer.fingerprint(), signer);
}
pub fn sign(&self, network: bitcoin::Network, psbt: String) {
let psbt = match bitcoin::Psbt::from_str(&psbt) {
Ok(p) => p,
Err(_) => {
if self
.sender
.send(SignerNotif::Manager(Error::ParsePsbt))
.is_err()
{
log::error!("SigningManager::sign() fails to send notif")
}
return;
}
};
let signer = self
.hot_signers
.iter()
.next()
.expect("at least one signer")
.1;
let n_path = match network {
bitcoin::Network::Bitcoin => 0,
_ => 1,
};
let deriv_path = bip32::DerivationPath::from_str(&format!("m/84'/{}'/0'", n_path)).unwrap();
let xpub = signer.xpub(&deriv_path);
let descriptor = wpkh(xpub);
signer.sign(psbt, descriptor);
}
}
#[cfg(test)]
mod tests {
use bip32::Fingerprint;
use super::*;
use std::str::FromStr;
#[test]
fn test_manager_hot_signer() {
let mut manager = SigningManager::new(PathBuf::new(), ".bwk");
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string();
manager.new_hot_signer_from_mnemonic(bitcoin::Network::Regtest, mnemonic);
if let SignerNotif::Info(fg, _info) = manager.poll().unwrap() {
assert_eq!(fg, Fingerprint::from_str("73c5da0a").unwrap());
} else {
panic!("expect info");
}
}
}