hw-wallet 0.1.2

Shared wallet orchestration utilities for CLI and FFI
Documentation
use serde::Deserialize;
use trezor_connect::thp::{BtcPaymentRequest, BtcPaymentRequestAmount, BtcPaymentRequestMemo};

use super::sats::parse_sats;
use crate::bip32::parse_bip32_path;
use crate::error::{WalletError, WalletResult};
use crate::hex::decode;

#[derive(Debug, Deserialize)]
pub struct TxInputPaymentRequest {
    pub nonce: Option<String>,
    pub recipient_name: String,
    #[serde(default)]
    pub memos: Vec<TxInputPaymentRequestMemo>,
    pub amount: Option<String>,
    pub signature: String,
}

#[derive(Debug, Deserialize)]
pub struct TxInputPaymentRequestMemo {
    #[serde(rename = "type")]
    pub memo_type: String,
    pub title: Option<String>,
    pub text: Option<String>,
    pub address: Option<String>,
    pub path: Option<String>,
    pub mac: Option<String>,
    pub coin_type: Option<u32>,
    pub amount: Option<String>,
}

impl TryFrom<TxInputPaymentRequest> for BtcPaymentRequest {
    type Error = WalletError;

    fn try_from(request: TxInputPaymentRequest) -> WalletResult<Self> {
        let memos = request
            .memos
            .into_iter()
            .map(BtcPaymentRequestMemo::try_from)
            .collect::<WalletResult<_>>()?;
        Ok(Self {
            nonce: request.nonce.as_deref().map(decode).transpose()?,
            recipient_name: request.recipient_name,
            memos,
            amount: request
                .amount
                .as_deref()
                .map(parse_sats)
                .transpose()?
                .map(BtcPaymentRequestAmount::from_sats),
            signature: decode(&request.signature)?,
        })
    }
}

impl TryFrom<TxInputPaymentRequestMemo> for BtcPaymentRequestMemo {
    type Error = WalletError;

    fn try_from(memo: TxInputPaymentRequestMemo) -> WalletResult<Self> {
        let memo_type = memo.memo_type.as_str();
        let missing = |field: &str| {
            WalletError::Signing(format!(
                "bitcoin payment request {memo_type} memo requires {field}"
            ))
        };
        match memo_type {
            "text" => Ok(Self::Text {
                text: memo.text.ok_or_else(|| missing("text"))?,
            }),
            "text_details" => Ok(Self::TextDetails {
                title: memo.title.ok_or_else(|| missing("title"))?,
                text: memo.text.ok_or_else(|| missing("text"))?,
            }),
            "refund" => Ok(Self::Refund {
                address: memo.address.ok_or_else(|| missing("address"))?,
                path: parse_bip32_path(memo.path.as_deref().ok_or_else(|| missing("path"))?)?,
                mac: decode(memo.mac.as_deref().ok_or_else(|| missing("mac"))?)?,
            }),
            "coin_purchase" => Ok(Self::CoinPurchase {
                coin_type: memo.coin_type.ok_or_else(|| missing("coin_type"))?,
                amount: memo.amount.ok_or_else(|| missing("amount"))?,
                address: memo.address.ok_or_else(|| missing("address"))?,
                path: parse_bip32_path(memo.path.as_deref().ok_or_else(|| missing("path"))?)?,
                mac: decode(memo.mac.as_deref().ok_or_else(|| missing("mac"))?)?,
            }),
            other => Err(WalletError::Signing(format!(
                "unsupported bitcoin payment request memo type '{other}'"
            ))),
        }
    }
}