firma 0.20.0

Firma is a library to create bitcoin multisig wallets with private keys stored on offline devices.
Documentation
use crate::common::entities::identifier::{Identifiable, Identifier, Overwritable, WhichKind};
use crate::list::ListOptions;
use crate::offline::decrypt::EncryptionKey;
use crate::offline::sign::find_or_create;
use crate::offline::sign::get_psbt_name;
use crate::*;
use bitcoin::blockdata::constants::genesis_block;
use bitcoin::consensus::deserialize;
use bitcoin::Network;
use bitcoincore_rpc::{Auth, Client, RpcApi};
use core::ops::DerefMut;
use log::{debug, info};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use std::io::Read;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use structopt::StructOpt;

#[derive(StructOpt, Debug, Clone, Serialize, Deserialize)]
pub struct Context {
    /// Network (bitcoin, testnet, regtest, signet)
    #[structopt(short, long, default_value = "testnet")]
    pub network: bitcoin::Network,

    /// Directory where wallet info are saved
    #[structopt(short, long, default_value = "~/.firma/")]
    pub datadir: String,

    #[structopt(skip)]
    pub encryption_key: Option<StringEncoding>,
    //TODO add secp context all here
}

#[derive(StructOpt, Debug, Clone)]
pub struct OnlineContext {
    #[structopt(flatten)]
    context: Context,
}

#[derive(StructOpt, Debug, Clone)]
pub struct OfflineContext {
    #[structopt(flatten)]
    context: Context,
}

macro_rules! impl_context {
    ( $for:ty ) => {
        impl Deref for $for {
            type Target = Context;

            fn deref(&self) -> &Self::Target {
                &self.context
            }
        }

        impl DerefMut for $for {
            fn deref_mut(&mut self) -> &mut Self::Target {
                &mut self.context
            }
        }

        impl From<Context> for $for {
            fn from(context: Context) -> Self {
                Self { context }
            }
        }
    };
}

impl_context!(OnlineContext);
impl_context!(OfflineContext);

#[derive(StructOpt, Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct DaemonOpts {
    /// Bitcoin node rpc url
    #[structopt(long)]
    pub url: String,

    /// Bitcoin node cookie file
    #[structopt(long)]
    pub cookie_file: PathBuf,
}

impl DaemonOpts {
    /// creates RPC client to bitcoin node, with the specified wallet name.
    /// It also checks the `network` parameter is the same as the connecting node
    pub fn make_client(&self, wallet_name: Option<String>, network: Network) -> Result<Client> {
        let url = match wallet_name {
            Some(wallet) => format!("{}/wallet/{}", self.url, wallet),
            None => self.url.to_string(),
        };
        debug!("creating client with url {}", url);
        let client = Client::new(&url, Auth::CookieFile(self.cookie_file.clone()))?;
        let node_genesis = client.get_block_hash(0)?;
        let firma_genesis = genesis_block(network).block_hash();
        if node_genesis != firma_genesis {
            return Err(Error::IncompatibleGenesis {
                node: node_genesis,
                firma: firma_genesis,
            });
        }
        Ok(client)
    }
}

impl Context {
    pub fn base(&self) -> Result<PathBuf> {
        let mut path = expand_tilde(&self.datadir)?;
        path.push(self.network.to_string());
        if !path.exists() {
            std::fs::create_dir_all(&path)?;
        }
        Ok(path)
    }

    pub fn encryption_key(&self) -> Option<EncryptionKey> {
        self.encryption_key
            .as_ref()
            .map(|k| k.get_exactly_32().unwrap())
    }

    pub fn read<T>(&self, name: &str) -> Result<T>
    where
        T: Serialize + DeserializeOwned + Debug + WhichKind,
    {
        Identifier::new(self.network, T::kind(), name).read(&self.datadir, &self.encryption_key)
    }

    pub fn write<T>(&self, value: &T) -> Result<()>
    where
        T: Serialize + DeserializeOwned + Debug + Clone + Identifiable + Overwritable,
    {
        value.id().write(
            &self.datadir,
            value,
            T::can_overwrite(),
            &self.encryption_key(),
        )
    }

    pub fn write_keys(&self, master_key: &MasterSecret) -> Result<()> {
        self.write(master_key)?;
        let public: DescriptorPublicKey = master_key.as_desc_pub_key()?;
        self.write(&public)
    }

    fn daemon_opts_path(&self) -> Result<PathBuf> {
        let mut path = self.base()?;
        path.push("daemon_opts.json");
        Ok(path)
    }

    pub fn read_daemon_opts(&self) -> Result<DaemonOpts> {
        let path = self.daemon_opts_path()?;
        debug!("reading daemon_opts from {:?}", path);
        let bytes = std::fs::read(&path)
            .map_err(|e| crate::Error::FileNotFoundOrCorrupt(path.clone(), e.to_string()))?;
        Ok(serde_json::from_slice(&bytes)?)
    }

    pub fn write_daemon_opts(&self, daemon_opts: DaemonOpts) -> Result<DaemonOpts> {
        let path = self.daemon_opts_path()?;
        debug!("writing daemon_opts in {:?}", path);
        let bytes = serde_json::to_vec(&daemon_opts)?;
        std::fs::write(&path, &bytes)
            .map_err(|e| crate::Error::FileNotFoundOrCorrupt(path, e.to_string()))?;
        Ok(daemon_opts)
    }

    pub fn make_client(&self, wallet_name: &str) -> Result<Client> {
        let opts = self.read_daemon_opts()?;
        let default_client = opts.make_client(None, self.network)?;
        let wallet_name_string = wallet_name.to_string();
        if !default_client.list_wallets()?.contains(&wallet_name_string) {
            return Err(Error::WalletNotExistsInNode(wallet_name_string));
        }
        let client = opts.make_client(Some(wallet_name_string), self.network)?;
        load_if_unloaded(&client, wallet_name)?;
        Ok(client)
    }

    pub fn read_desc_pub_keys_from_names(
        &self,
        names: &[String],
    ) -> Result<Vec<miniscript::DescriptorPublicKey>> {
        let mut result = vec![];
        for name in names {
            let id = Identifier::new(self.network, Kind::DescriptorPublicKey, name);
            let json: DescriptorPublicKey = id.read(&self.datadir, &self.encryption_key)?;
            result.push(json.key()?);
        }
        Ok(result)
    }

    pub fn read_encryption_key(&mut self) -> Result<()> {
        // read encryption key from stdin and initialize encryption_key field
        let mut buffer = vec![];
        std::io::stdin().read_to_end(&mut buffer)?;
        let encoded = StringEncoding::new_base64(&buffer);
        self.encryption_key = Some(encoded);
        Ok(())
    }

    pub fn save_psbt_options(&self, opt: &SavePsbtOptions) -> Result<()> {
        info!("save_psbt_options {:?}", opt);
        let bytes = opt
            .psbt
            .as_bytes()
            .map_err(|_| Error::PsbtBadStringEncoding(opt.psbt.kind()))?;
        let mut psbt: BitcoinPsbt = deserialize(&bytes).map_err(Error::PsbtCannotDeserialize)?;

        self.save_psbt(&mut psbt)?;
        Ok(())
    }

    /// psbts_dir is general psbts dir, name is extracted from PSBT
    /// if file exists a PSBT merge will be attempted
    pub fn save_psbt(&self, psbt: &mut BitcoinPsbt) -> Result<String> {
        debug!("save_psbt");

        let name = match get_psbt_name(psbt) {
            Some(name) => name,
            None => {
                let opt = ListOptions { kind: Kind::Psbt };
                let psbts = self.list(&opt)?.psbts;
                find_or_create(psbt, psbts)?
            }
        };

        debug!("psbt_name: {}", name);
        let id = Identifier::new(self.network, Kind::Psbt, &name);
        if let Ok(existing_psbt) = self.read::<Psbt>(&name) {
            info!("old psbt exist, merging together");
            let existing_psbt = existing_psbt.psbt()?;
            psbt.merge(existing_psbt.clone())?;
            if psbt == &existing_psbt {
                return Err(Error::PsbtNotChangedAfterMerge);
            }
        }
        let psbt = psbt_to_base64(psbt).1;
        let psbt_json = Psbt { id, psbt };
        self.write(&psbt_json)?;
        debug!("finish");
        Ok(name)
    }
}

pub fn load_if_unloaded(client: &Client, wallet_name: &str) -> Result<()> {
    match client.load_wallet(wallet_name) {
        Ok(_) => info!("wallet {} loaded", wallet_name),
        Err(e) => {
            debug!("load_if_unloaded error {:?}", e);
            if e.to_string().contains("not found") {
                return Err(format!("{} not found in the bitcoin node", wallet_name).into());
            } else {
                debug!("wallet {} already loaded", wallet_name);
            }
        }
    }
    Ok(())
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct SavePsbtOptions {
    pub psbt: StringEncoding,
}

// from https://stackoverflow.com/questions/54267608/expand-tilde-in-rust-path-idiomatically
pub fn expand_tilde<P: AsRef<Path>>(path_user_input: P) -> Result<PathBuf> {
    let p = path_user_input.as_ref();
    if p.starts_with("~") {
        let mut home_dir = home::home_dir().ok_or(Error::CannotRetrieveHomeDir)?;
        if p == Path::new("~") {
            Ok(home_dir)
        } else if home_dir == Path::new("/").to_path_buf() {
            // Corner case: `home_dir` root directory;
            // don't prepend extra `/`, just drop the tilde.
            Ok(p.strip_prefix("~")?.to_path_buf())
        } else {
            home_dir.push(p.strip_prefix("~/")?);
            Ok(home_dir)
        }
    } else {
        Ok(p.to_path_buf())
    }
}

#[cfg(test)]
pub mod tests {
    use crate::offline::random::RandomOptions;
    use crate::{psbt_from_base64, Context, DescriptorPublicKey, MasterSecret, OfflineContext};
    use bitcoin::Network;
    use std::ops::Deref;
    use tempfile::TempDir;

    #[derive(Debug)]
    pub struct TestContext {
        pub context: OfflineContext,
        #[allow(unused)]
        datadir: TempDir, // must be here so directory isn't removed before dropping the object
    }

    impl TestContext {
        pub fn with_network(network: Network) -> Self {
            let datadir = TempDir::new().unwrap();
            let firma_datadir = format!("{}/", datadir.path().display());
            TestContext {
                context: OfflineContext {
                    context: Context {
                        network,
                        datadir: firma_datadir,
                        encryption_key: None,
                    },
                },
                datadir,
            }
        }
    }

    impl Default for TestContext {
        fn default() -> Self {
            Self::with_network(Network::Testnet)
        }
    }

    impl Deref for TestContext {
        type Target = OfflineContext;

        fn deref(&self) -> &Self::Target {
            &self.context
        }
    }

    #[test]
    fn test_write_keys() {
        let context = TestContext::default();
        let key_name = "a";
        let key = context
            .context
            .create_key(&RandomOptions {
                key_name: key_name.to_string(),
            })
            .unwrap();
        assert!(
            context.write_keys(&key).is_err(),
            "can overwrite key material"
        );
        let key_read: MasterSecret = context.read(&key_name).unwrap();
        assert_eq!(key, key_read);
        let _: DescriptorPublicKey = context.read(&key_name).unwrap();
    }

    #[test]
    fn test_save_psbt() {
        let context = TestContext::default();
        let without_name= "cHNidP8BAH4CAAAAAQQYGYyRDjWA/D08BEjU3Q9P34Sv8q0mW9UV5niEqBZ4AQAAAAD+////AiDLAAAAAAAAF6kUaV+OwCj7iV87pOHOFXNLuZMc7tyHBwIAAAAAAAAiACAGYNwSo/z0dYfDuCUPL2Li/SSY10gjxu8hZ9pREpEaCwAAAAAAAQChAgAAAAEbuYvreUkM84tDJuxdjxZmErxAyO/PkP+ozooG1kBiZAAAAAAjIgAg/KddPamHVwK3NnYT58PR3q+a5k9zwFC8zJXE6Nwr5zX9////AkyLBgAAAAAAF6kUZ3Eos+P2CT0g41zAxb+TPZLthgiHpM4AAAAAAAAiACD1kVciHGvQL+7uoaNv7Llt2eZU+dje0fnze3ZLwfI+qn6FHQABASukzgAAAAAAACIAIPWRVyIca9Av7u6ho2/suW3Z5lT52N7R+fN7dkvB8j6qAQVHUiECkrOcW23z58qUY5yOArPCYSDLw7Z63tq2U190DltvzS4hA310Wde+Bx0Dh+YtZuXAolu7NrO6BLd3Nzo+uUOrZ93gUq4iBgKSs5xbbfPnypRjnI4Cs8JhIMvDtnre2rZTX3QOW2/NLhyi6+BOMAAAgAEAAIAAAACAAgAAgAAAAAAAAAAAIgYDfXRZ174HHQOH5i1m5cCiW7s2s7oEt3c3Oj65Q6tn3eAcH15D2DAAAIABAACAAAAAgAIAAIAAAAAAAAAAAAAAAQFHUiEC44KejAc2m+q4YRPxJQIeqbuVLKapKyW7ZTgHZV1n2EAhA6jiEl6pWjkOeUk/P/ZhSfeh3ItYgcjUYE4RvN2iQlF/Uq4iAgLjgp6MBzab6rhhE/ElAh6pu5UspqkrJbtlOAdlXWfYQByi6+BOMAAAgAEAAIAAAACAAgAAgAAAAAABAAAAIgIDqOISXqlaOQ55ST8/9mFJ96Hci1iByNRgThG83aJCUX8cH15D2DAAAIABAACAAAAAgAIAAIAAAAAAAQAAAAA=";
        let (_, mut psbt) = psbt_from_base64(without_name).unwrap();
        let name = context.save_psbt(&mut psbt).unwrap();
        assert_eq!(name, "psbt-0");

        let context = TestContext::default(); // new context, so I don't get PSBTNotChangedAfterMerge
        let with_name = "cHNidP8BAH4CAAAAAQQYGYyRDjWA/D08BEjU3Q9P34Sv8q0mW9UV5niEqBZ4AQAAAAD+////AiDLAAAAAAAAF6kUaV+OwCj7iV87pOHOFXNLuZMc7tyHBwIAAAAAAAAiACAGYNwSo/z0dYfDuCUPL2Li/SSY10gjxu8hZ9pREpEaCwAAAAAM/AVmaXJtYQBuYW1lCHRvLWNhcm9sAAEAoQIAAAABG7mL63lJDPOLQybsXY8WZhK8QMjvz5D/qM6KBtZAYmQAAAAAIyIAIPynXT2ph1cCtzZ2E+fD0d6vmuZPc8BQvMyVxOjcK+c1/f///wJMiwYAAAAAABepFGdxKLPj9gk9IONcwMW/kz2S7YYIh6TOAAAAAAAAIgAg9ZFXIhxr0C/u7qGjb+y5bdnmVPnY3tH583t2S8HyPqp+hR0AAQErpM4AAAAAAAAiACD1kVciHGvQL+7uoaNv7Llt2eZU+dje0fnze3ZLwfI+qgEFR1IhApKznFtt8+fKlGOcjgKzwmEgy8O2et7atlNfdA5bb80uIQN9dFnXvgcdA4fmLWblwKJbuzazugS3dzc6PrlDq2fd4FKuIgYCkrOcW23z58qUY5yOArPCYSDLw7Z63tq2U190DltvzS4couvgTjAAAIABAACAAAAAgAIAAIAAAAAAAAAAACIGA310Wde+Bx0Dh+YtZuXAolu7NrO6BLd3Nzo+uUOrZ93gHB9eQ9gwAACAAQAAgAAAAIACAACAAAAAAAAAAAAAAAEBR1IhAuOCnowHNpvquGET8SUCHqm7lSymqSslu2U4B2VdZ9hAIQOo4hJeqVo5DnlJPz/2YUn3odyLWIHI1GBOEbzdokJRf1KuIgIC44KejAc2m+q4YRPxJQIeqbuVLKapKyW7ZTgHZV1n2EAcouvgTjAAAIABAACAAAAAgAIAAIAAAAAAAQAAACICA6jiEl6pWjkOeUk/P/ZhSfeh3ItYgcjUYE4RvN2iQlF/HB9eQ9gwAACAAQAAgAAAAIACAACAAAAAAAEAAAAA";
        let (_, mut psbt) = psbt_from_base64(with_name).unwrap();
        let name = context.save_psbt(&mut psbt).unwrap();
        assert_eq!(name, "to-carol");

        let context = TestContext::default(); // new context, so I don't get PSBTNotChangedAfterMerge
        let with_name = "cHNidP8BAH4CAAAAAQQYGYyRDjWA/D08BEjU3Q9P34Sv8q0mW9UV5niEqBZ4AQAAAAD+////AiDLAAAAAAAAF6kUaV+OwCj7iV87pOHOFXNLuZMc7tyHBwIAAAAAAAAiACAGYNwSo/z0dYfDuCUPL2Li/SSY10gjxu8hZ9pREpEaCwAAAAAM/AVmaXJtYQBuYW1lCHRvLWNhcm9sAAEAoQIAAAABG7mL63lJDPOLQybsXY8WZhK8QMjvz5D/qM6KBtZAYmQAAAAAIyIAIPynXT2ph1cCtzZ2E+fD0d6vmuZPc8BQvMyVxOjcK+c1/f///wJMiwYAAAAAABepFGdxKLPj9gk9IONcwMW/kz2S7YYIh6TOAAAAAAAAIgAg9ZFXIhxr0C/u7qGjb+y5bdnmVPnY3tH583t2S8HyPqp+hR0AAQErpM4AAAAAAAAiACD1kVciHGvQL+7uoaNv7Llt2eZU+dje0fnze3ZLwfI+qgEFR1IhApKznFtt8+fKlGOcjgKzwmEgy8O2et7atlNfdA5bb80uIQN9dFnXvgcdA4fmLWblwKJbuzazugS3dzc6PrlDq2fd4FKuIgYCkrOcW23z58qUY5yOArPCYSDLw7Z63tq2U190DltvzS4couvgTjAAAIABAACAAAAAgAIAAIAAAAAAAAAAACIGA310Wde+Bx0Dh+YtZuXAolu7NrO6BLd3Nzo+uUOrZ93gHB9eQ9gwAACAAQAAgAAAAIACAACAAAAAAAAAAAAAAAEBR1IhAuOCnowHNpvquGET8SUCHqm7lSymqSslu2U4B2VdZ9hAIQOo4hJeqVo5DnlJPz/2YUn3odyLWIHI1GBOEbzdokJRf1KuIgIC44KejAc2m+q4YRPxJQIeqbuVLKapKyW7ZTgHZV1n2EAcouvgTjAAAIABAACAAAAAgAIAAIAAAAAAAQAAACICA6jiEl6pWjkOeUk/P/ZhSfeh3ItYgcjUYE4RvN2iQlF/HB9eQ9gwAACAAQAAgAAAAIACAACAAAAAAAEAAAAA";
        let (_, mut psbt) = psbt_from_base64(with_name).unwrap();
        let name = context.save_psbt(&mut psbt).unwrap();
        assert_eq!(name, "to-carol");
    }
}