firma 0.20.0

Firma is a library to create bitcoin multisig wallets with private keys stored on offline devices.
Documentation
use crate::offline::decrypt::{decrypt, EncryptionKey, MaybeEncrypted};
use crate::{expand_tilde, Error, Result, StringEncoding};
use bitcoin::Network;
use core::fmt::Formatter;
use log::debug;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Display};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::{fs, io};

#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
pub enum Kind {
    Wallet,
    WalletIndexes,
    WalletSignature,
    MasterSecret,
    DescriptorPublicKey,
    Psbt,
}

impl Kind {
    pub fn dir(&self) -> &str {
        match self {
            Kind::Wallet | Kind::WalletIndexes | Kind::WalletSignature => "wallets",
            Kind::MasterSecret | Kind::DescriptorPublicKey => "keys",
            Kind::Psbt => "psbts",
        }
    }

    fn name(&self) -> &str {
        match self {
            Kind::Wallet => "wallet.json",
            Kind::WalletIndexes => "wallet_indexes.json",
            Kind::WalletSignature => "wallet_signature.json",
            Kind::MasterSecret => "master_secret.json",
            Kind::DescriptorPublicKey => "descriptor_public_key.json",
            Kind::Psbt => "psbt.json",
        }
    }
}

impl Display for Kind {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "{:?}", self)
    }
}

impl FromStr for Kind {
    type Err = io::Error;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s {
            "Wallet" => Ok(Kind::Wallet),
            "WalletIndexes" => Ok(Kind::WalletIndexes),
            "WalletSignature" => Ok(Kind::WalletSignature),
            "MasterSecret" => Ok(Kind::MasterSecret),
            "DescriptorPublicKey" => Ok(Kind::DescriptorPublicKey),
            "Psbt" => Ok(Kind::Psbt),
            _ => Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                format!(
                    "({}) valid values are: Wallet, WalletSignature, MasterSecret, Psbt",
                    s
                ),
            )),
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Identifier {
    pub kind: Kind,
    pub name: String,
    pub network: Network,
}

pub trait Identifiable {
    fn id(&self) -> &Identifier;
}

pub trait Overwritable {
    fn can_overwrite() -> bool;
}

pub trait WhichKind {
    fn kind() -> Kind;
}

impl Identifier {
    pub fn new(network: Network, kind: Kind, name: &str) -> Self {
        Identifier {
            network,
            kind,
            name: name.to_string(),
        }
    }

    pub fn with_kind(&self, new_kind: Kind) -> Self {
        Identifier {
            network: self.network,
            kind: new_kind,
            name: self.name.clone(),
        }
    }

    pub fn as_path_buf<P: AsRef<Path>>(
        &self,
        datadir: P,
        create_if_missing: bool,
    ) -> Result<PathBuf> {
        let mut path = expand_tilde(datadir)?;
        path.push(self.network.to_string());
        path.push(self.kind.dir());
        path.push(self.name.to_string());
        if create_if_missing && !path.exists() {
            fs::create_dir_all(&path)?;
            debug!("created {:?}", path);
        }
        path.push(self.kind.name());
        Ok(path)
    }

    pub fn read<T, P>(&self, datadir: P, encryption_key: &Option<StringEncoding>) -> Result<T>
    where
        T: Serialize + DeserializeOwned + Debug,
        P: AsRef<Path>,
    {
        let path = self.as_path_buf(datadir, false)?;
        debug!("reading {:?}", path);

        let data = decrypt(&path, encryption_key)?;

        Ok(data)
    }

    pub fn write<T, P>(
        &self,
        datadir: P,
        value: &T,
        can_overwrite: bool,
        encryption_key: &Option<EncryptionKey>,
    ) -> Result<()>
    where
        T: Serialize + DeserializeOwned + Debug + Clone,
        P: AsRef<Path>,
    {
        let path = self.as_path_buf(datadir, true)?;
        debug!(
            "Identifier::write {:?} can_overwrite:{}",
            path, can_overwrite
        );
        if path.exists() && !can_overwrite {
            return Err(Error::CannotOverwrite(path));
        }

        let plain = MaybeEncrypted::plain(value.clone());
        let data = match encryption_key.as_ref() {
            None => plain,
            Some(encryption_key) => plain.encrypt(encryption_key)?,
        };
        let content = serde_json::to_vec_pretty(&data)?;
        std::fs::write(&path, &content)?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use crate::common::tests::rnd_string;
    use crate::{Identifier, Kind};
    use bitcoin::Network;

    impl Identifier {
        pub fn new_test(kind: Kind) -> Self {
            Identifier {
                kind,
                name: rnd_string(),
                network: Network::Testnet,
            }
        }
    }

    #[test]
    fn test_identifier() {
        let id = Identifier {
            network: Network::Bitcoin,
            kind: Kind::MasterSecret,
            name: "a1".to_string(),
        };
        let expected = "\"/bitcoin/keys/a1/master_secret.json\"";
        let result = format!("{:?}", id.as_path_buf("/", false).unwrap());
        assert_eq!(expected, result);

        let expected = r#"{"kind":"MasterSecret","name":"a1","network":"bitcoin"}"#;
        let result = serde_json::to_value(&id).unwrap();
        assert_eq!(expected, result.to_string());
    }
}