hw-wallet 0.1.2

Shared wallet orchestration utilities for CLI and FFI
Documentation
use serde::Deserialize;
use sha2::{Digest, Sha256};
use trezor_connect::thp::{BtcHDNode, BtcHDNodePath};

use crate::error::{WalletError, WalletResult};
use crate::hex::decode;

#[derive(Debug, Deserialize)]
pub struct TxInputHDNode {
    pub depth: u32,
    pub fingerprint: u32,
    pub child_num: u32,
    pub chain_code: String,
    pub public_key: String,
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum TxInputHDNodeRef {
    Node(TxInputHDNode),
    Xpub(String),
}

#[derive(Debug, Deserialize)]
pub struct TxInputHDNodePath {
    pub node: TxInputHDNodeRef,
    pub address_n: Vec<u32>,
}

impl TryFrom<&TxInputHDNode> for BtcHDNode {
    type Error = WalletError;

    fn try_from(node: &TxInputHDNode) -> WalletResult<Self> {
        let chain_code = decode(&node.chain_code)?;
        if chain_code.len() != 32 {
            return Err(WalletError::Signing(format!(
                "invalid hd_node.chain_code length: expected 32 bytes, got {}",
                chain_code.len()
            )));
        }

        let public_key = decode(&node.public_key)?;
        if public_key.len() != 33 {
            return Err(WalletError::Signing(format!(
                "invalid hd_node.public_key length: expected 33 bytes, got {}",
                public_key.len()
            )));
        }

        Ok(Self {
            depth: node.depth,
            fingerprint: node.fingerprint,
            child_num: node.child_num,
            chain_code,
            public_key,
        })
    }
}

impl TryFrom<&TxInputHDNodeRef> for BtcHDNode {
    type Error = WalletError;

    fn try_from(node: &TxInputHDNodeRef) -> WalletResult<Self> {
        match node {
            TxInputHDNodeRef::Node(node) => Self::try_from(node),
            TxInputHDNodeRef::Xpub(xpub) => hd_node_from_xpub(xpub),
        }
    }
}

impl TryFrom<TxInputHDNodePath> for BtcHDNodePath {
    type Error = WalletError;

    fn try_from(path: TxInputHDNodePath) -> WalletResult<Self> {
        Ok(Self {
            node: BtcHDNode::try_from(&path.node)?,
            address_n: path.address_n,
        })
    }
}

fn hd_node_from_xpub(xpub: &str) -> WalletResult<BtcHDNode> {
    let invalid = |reason: String| WalletError::Signing(format!("invalid xpub '{xpub}': {reason}"));
    let decoded = bs58::decode(xpub)
        .into_vec()
        .map_err(|err| invalid(err.to_string()))?;
    if decoded.len() < 4 {
        return Err(invalid("payload is too short".into()));
    }

    let (payload, checksum) = decoded.split_at(decoded.len() - 4);
    let expected_checksum = Sha256::digest(Sha256::digest(payload));
    if checksum != &expected_checksum[..4] {
        return Err(invalid("checksum mismatch".into()));
    }
    let Ok(payload) = <&[u8; 78]>::try_from(payload) else {
        return Err(invalid(format!(
            "expected 78-byte payload, got {} bytes",
            payload.len()
        )));
    };

    let key_prefix = payload[45];
    if key_prefix != 0x02 && key_prefix != 0x03 {
        return Err(invalid(
            "compressed public key must start with 0x02 or 0x03".into(),
        ));
    }

    let [_, _, _, _, depth, f0, f1, f2, f3, c0, c1, c2, c3, ..] = *payload;
    Ok(BtcHDNode {
        depth: u32::from(depth),
        fingerprint: u32::from_be_bytes([f0, f1, f2, f3]),
        child_num: u32::from_be_bytes([c0, c1, c2, c3]),
        chain_code: payload[13..45].to_vec(),
        public_key: payload[45..78].to_vec(),
    })
}