firma 0.20.0

Firma is a library to create bitcoin multisig wallets with private keys stored on offline devices.
Documentation
pub mod identifier;
pub mod persisted;

use crate::bitcoincore_rpc_json::WalletCreateFundedPsbtResult;
use crate::{psbt_from_base64, BitcoinPsbt, DaemonOpts, Result};
use bitcoin::bech32::FromBase32;
use bitcoin::util::bip32::{DerivationPath, Fingerprint};
use bitcoin::util::psbt::raw;
use bitcoin::{bech32, Address, Amount, OutPoint, Txid};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashSet;
use std::convert::TryInto;
use std::path::PathBuf;

use bitcoin::util::psbt::raw::ProprietaryKey;
pub use identifier::*;
pub use persisted::*;

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct BalanceSatBtc {
    pub satoshi: u64,
    pub btc: String,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct BalanceOutput {
    pub confirmed: BalanceSatBtc,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub pending: Option<BalanceSatBtc>,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct GetAddressOutput {
    pub address: Address,
    pub path: DerivationPath,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub qr_file: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct SendTxOutput {
    pub hex: String,
    pub txid: Txid,
    pub broadcasted: bool,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct CreateTxOutput {
    pub psbt_name: String,
    pub funded_psbt: Psbt,
    pub address_reused: HashSet<Address>,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
pub struct ListOutput {
    pub wallets: Vec<Wallet>,
    //pub wallets_indexes: Vec<IndexesJson>,
    pub wallets_signatures: Vec<WalletSignature>,
    pub master_secrets: Vec<MasterSecret>,
    //pub descriptor_public_keys: Vec<PublicMasterKey>,
    pub psbts: Vec<Psbt>,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct ListCoinsOutput {
    pub coins: Vec<Coin>,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Coin {
    pub outpoint: OutPoint,
    pub amount: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub unconfirmed: Option<bool>,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct ErrorJson {
    pub error: String,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct TxIn {
    pub outpoint: String,
    pub signatures: HashSet<Fingerprint>,
    #[serde(flatten)]
    pub common: TxCommonInOut,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct TxOut {
    pub address: String,
    #[serde(flatten)]
    pub common: TxCommonInOut,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct TxCommonInOut {
    pub value: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub wallet_with_path: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
pub struct PsbtPrettyPrint {
    pub inputs: Vec<TxIn>,
    pub outputs: Vec<TxOut>,
    pub size: Size,
    pub fee: Fee,
    pub info: Vec<String>,
    pub psbt_file: PathBuf,
    pub balances: String,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
pub struct Fee {
    pub absolute: Option<u64>,
    pub absolute_fmt: String,
    pub rate: Option<f64>,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
pub struct Size {
    pub unsigned: usize,
    pub estimated: Option<usize>,
    pub psbt: usize,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct VerifyWalletResult {
    pub descriptor: String,
    pub signature: WalletSignature,
    pub verified: bool,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct EncodedQrs {
    pub qrs: Vec<StringEncoding>,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "t", content = "c", rename_all = "lowercase")]
pub enum StringEncoding {
    Base64(String),
    Hex(String),
    Bech32(String),
    Plain(String),
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Balance {
    #[serde(with = "bitcoin::util::amount::serde::as_btc")]
    pub trusted: Amount,
    #[serde(with = "bitcoin::util::amount::serde::as_btc")]
    pub untrusted_pending: Amount,
    #[serde(with = "bitcoin::util::amount::serde::as_btc")]
    pub immature: Amount,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Balances {
    pub mine: Balance,
    pub watchonly: Balance,
}

impl StringEncoding {
    pub fn new_base64(content: &[u8]) -> Self {
        StringEncoding::Base64(base64::encode(content))
    }

    pub fn new_hex(content: &[u8]) -> Self {
        StringEncoding::Hex(hex::encode(content))
    }

    pub fn as_bytes(&self) -> Result<Vec<u8>> {
        Ok(match self {
            StringEncoding::Base64(s) => base64::decode(s)?,
            StringEncoding::Hex(s) => hex::decode(s)?,
            StringEncoding::Bech32(s) => {
                let (_, vec_u5, _) = bech32::decode(s)?;
                Vec::<u8>::from_base32(&vec_u5)?
            }
            StringEncoding::Plain(s) => s.as_bytes().to_vec(),
        })
    }

    pub fn get_exactly_32(&self) -> Result<[u8; 32]> {
        let bytes = self.as_bytes()?;
        if bytes.len() != 32 {
            return Err(crate::Error::EncryptionKeyNot32Bytes(bytes.len()));
        }
        let mut result = [0u8; 32];
        result.copy_from_slice(&bytes[..]);
        Ok(result)
    }

    pub fn kind(&self) -> String {
        match self {
            StringEncoding::Base64(_) => "base64",
            StringEncoding::Hex(_) => "hex",
            StringEncoding::Bech32(_) => "bech32",
            StringEncoding::Plain(_) => "plain",
        }
        .to_string()
    }
}

pub fn get_name_key() -> raw::ProprietaryKey {
    ProprietaryKey {
        prefix: b"firma".to_vec(),
        subtype: 0u8,
        key: b"name".to_vec(),
    }
}

pub fn psbt_from_rpc(psbt: &WalletCreateFundedPsbtResult, name: &str) -> Result<BitcoinPsbt> {
    let (_, mut psbt_with_name) = psbt_from_base64(&psbt.psbt)?;

    psbt_with_name
        .global
        .proprietary
        .insert(get_name_key(), name.as_bytes().to_vec());

    Ok(psbt_with_name)
}

macro_rules! impl_try_into {
    ( $for:ty ) => {
        impl TryInto<Value> for $for {
            type Error = crate::Error;

            fn try_into(self) -> std::result::Result<Value, Self::Error> {
                Ok(serde_json::to_value(self)?)
            }
        }
    };
}

impl_try_into!(CreateTxOutput);
impl_try_into!(SendTxOutput);
impl_try_into!(BalanceOutput);
impl_try_into!(ListCoinsOutput);
impl_try_into!(GetAddressOutput);
impl_try_into!(ListOutput);

impl_try_into!(PsbtPrettyPrint);
impl_try_into!(VerifyWalletResult);
impl_try_into!(DaemonOpts);

impl_try_into!(WalletSignature);
impl_try_into!(MasterSecret);
impl_try_into!(Wallet);

macro_rules! impl_traits {
    ( $for:ty, $val:expr, $k:expr  ) => {
        impl Identifiable for $for {
            fn id(&self) -> &Identifier {
                &self.id
            }
        }
        impl Overwritable for $for {
            fn can_overwrite() -> bool {
                $val
            }
        }
        impl WhichKind for $for {
            fn kind() -> Kind {
                $k
            }
        }
    };
}

impl_traits!(WalletSignature, false, Kind::WalletSignature);
impl_traits!(MasterSecret, false, Kind::MasterSecret);
impl_traits!(Wallet, false, Kind::Wallet);
impl_traits!(WalletIndexes, true, Kind::WalletIndexes);
impl_traits!(DescriptorPublicKey, false, Kind::DescriptorPublicKey);
impl_traits!(Psbt, true, Kind::Psbt);

#[cfg(test)]
mod tests {
    use crate::Wallet;

    #[test]
    fn test_cbor_wallet() {
        let wallet: Wallet = serde_json::from_str(r#"{"id":{"name":"name","kind":"Wallet","network": "bitcoin"},"descriptor":"wsh(multi(3,tpubD6NzVbkrYhZ4XuzR59W1JHQpXcufQVj64NDa4eiALMJxC2xAwpY7wy2J9RVQ7BHDYK3eWrVRsuMUcdwGn9xVBmC9wfpVawpNGLyrdgAhehd/0/*,tpubD6NzVbkrYhZ4WarEBpY5okLrjRQ8sgfoEsxZfprQDEbAjWM585LhNeT9GuSeFRGL7yLheiRgtCQCBb73y21EsLzRfwdrRmfaAT4yUTEKtu7/0/*,tpubD6NzVbkrYhZ4WRwbTYgdGDMxPUzq5WwX8HwnAR6PYB291uUH63pCU1WFV6RRWGyA2Xy8okiFAqfAXEErx1SVh7mKSVQa34hFaa8GcmuEeds/0/*,tpubD6NzVbkrYhZ4YkVm13NDmMPEHEWXHoqGXBPCrtHbB1hE6GoTjdvXEKrtRBMtSe4gQQUSyvU78jgyrK5AfwLewr1cTkkojQbYTuyNtgQFEDb/0/*,tpubD6NzVbkrYhZ4YGeACdA4t1ZjEfJm8ExF818xG2ndsNoT61PwPnotxVQXDLZAZ5ut7t1iHR2FLEYnTzJTN5DGxQTKgwQpt7ftPzRwjugwuYg/0/*))#we4l0t0l","fingerprints":["171f9233","37439b38","ab4343d4","deb8f1ba","214c5f36"],"required_sig":3,"created_at_height":1720730}"#).unwrap();

        let vec_json = serde_json::to_vec(&wallet).unwrap();
        let vec_cbor = serde_cbor::to_vec(&wallet).unwrap();
        let vec_packed_cbor = serde_cbor::ser::to_vec_packed(&wallet).unwrap();

        assert_eq!(vec_json.len(), 704);
        assert_eq!(vec_cbor.len(), 682);
        assert_eq!(vec_packed_cbor.len(), 632);
    }
}