pubport 0.4.0

A library for parsing hardware wallet export formats
Documentation
use serde::{Deserialize, Serialize};

use crate::{
    descriptor::{self, Descriptors},
    json::{self, GenericJson},
    key_expression::KeyExpression,
    xpub,
};

#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
pub enum Format {
    Descriptor(Descriptors),
    Json(Json),
    Wasabi(Descriptors),
    Electrum(Descriptors),
    KeyExpression(Descriptors),
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("Invalid descriptor: {0:?}")]
    InvalidDescriptor(#[from] descriptor::Error),

    #[error("Invalid json: {0}")]
    InvalidJsonParse(#[from] serde_json::Error),

    #[error("Unable to create descriptor from json")]
    InvalidDescriptorInJson,

    #[error("Invalid json, no xpubs or descriptor")]
    JsonNoDecriptor,

    #[error("Invalid xpub: {0}")]
    InvalidXpub(#[from] xpub::Error),
}

#[derive(Debug, Clone, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
pub struct Json {
    pub bip44: Option<Descriptors>,
    pub bip49: Option<Descriptors>,
    pub bip84: Option<Descriptors>,
}

impl TryFrom<GenericJson> for Json {
    type Error = Error;

    fn try_from(json: GenericJson) -> Result<Self, Self::Error> {
        if json.bip44.is_none() && json.bip49.is_none() && json.bip84.is_none() {
            return Err(Error::JsonNoDecriptor);
        }

        let bip44 = json
            .bip44
            .map(|single_sig| Descriptors::try_from_single_sig(single_sig, json.xfp.as_deref()))
            .transpose()?;

        let bip49 = json
            .bip49
            .map(|single_sig| Descriptors::try_from_single_sig(single_sig, json.xfp.as_deref()))
            .transpose()?;

        let bip84 = json
            .bip84
            .map(|single_sig| Descriptors::try_from_single_sig(single_sig, json.xfp.as_deref()))
            .transpose()?;

        if bip44.is_none() && bip49.is_none() && bip84.is_none() {
            return Err(Error::JsonNoDecriptor);
        }

        Ok(Json {
            bip44,
            bip49,
            bip84,
        })
    }
}

impl Format {
    pub fn try_new_from_str(string: &str) -> Result<Self, Error> {
        if let Ok(json) = serde_json::from_str::<json::GenericJson>(string) {
            if let Ok(json) = Json::try_from(json) {
                return Ok(Format::Json(json));
            }
        }

        if let Ok(json) = serde_json::from_str::<json::WasabiJson>(string) {
            if let Ok(desc) = Descriptors::try_from(json) {
                return Ok(Format::Wasabi(desc));
            }
        }

        if let Ok(json) = serde_json::from_str::<json::ElectrumJson>(string) {
            if let Ok(desc) = Descriptors::try_from(json) {
                return Ok(Format::Electrum(desc));
            }
        }

        if let Ok(desc) = Descriptors::try_from(string) {
            return Ok(Format::Descriptor(desc));
        }

        if let Ok(key_expression) = KeyExpression::try_from_str(string) {
            if let Ok(desc) = Descriptors::try_from_key_expression(&key_expression) {
                return Ok(Format::KeyExpression(desc));
            }

            let json = Json::try_from_child_xpub(key_expression.xpub)?;
            return Ok(Format::Json(json));
        }

        let json = Json::try_from_child_xpub_str(string)?;
        Ok(Format::Json(json))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_all_formats() {
        let files = std::fs::read_dir("test/data").unwrap();

        for file in files {
            let file = file.unwrap();
            let path = file.path();

            if !path.ends_with(".json") || path.ends_with(".txt") {
                continue;
            }

            let string = std::fs::read_to_string(&path).unwrap();

            let format = Format::try_new_from_str(&string);
            assert!(format.is_ok());
        }
    }

    #[test]
    fn test_parse_with_base_xpub() {
        let xpub = "xpub6CiKnWv7PPyyeb4kCwK4fidKqVjPfD9TP6MiXnzBVGZYNanNdY3mMvywcrdDc6wK82jyBSd95vsk26QujnJWPrSaPfYeyW7NyX37HHGtfQM";
        let format = Format::try_new_from_str(xpub);
        assert!(format.is_ok());
    }

    #[test]
    fn test_parse_krux() {
        let string = std::fs::read_to_string("test/data/krux.txt").unwrap();
        let krux = KeyExpression::try_from_str(&string);
        assert!(krux.is_ok());
    }
}