hw-wallet 0.1.2

Shared wallet orchestration utilities for CLI and FFI
Documentation
use serde::Deserialize;
use trezor_connect::thp::{BtcMultisig, BtcOutputScriptType, BtcSignOutput};

use super::multisig::TxInputMultisig;
use super::owner::TxOwner;
use super::sats::parse_sats;
use super::script_type::OutputScriptTypeExt;
use crate::bip32::parse_bip32_path;
use crate::error::{WalletError, WalletResult};
use crate::hex::{decode, decode_hash32};

#[derive(Debug, Deserialize)]
pub struct TxInputOutput {
    pub address: Option<String>,
    pub path: Option<String>,
    pub amount: String,
    pub script_type: Option<String>,
    pub multisig: Option<TxInputMultisig>,
    pub op_return_data: Option<String>,
    pub orig_hash: Option<String>,
    pub orig_index: Option<u32>,
    #[serde(default)]
    pub payment_req_index: Option<u32>,
}

impl TxInputOutput {
    pub(super) fn into_sign_output(self, owner: TxOwner) -> WalletResult<BtcSignOutput> {
        let label = owner.output_label();
        let explicit_script_type = self
            .script_type
            .as_deref()
            .map(BtcOutputScriptType::parse)
            .transpose()?;
        let path = self.path.as_deref().map(parse_bip32_path).transpose()?;
        let script_type = match (self.address.as_deref(), path.as_deref()) {
            (Some(_), Some(_)) => {
                return Err(WalletError::Signing(format!(
                    "{label} cannot specify both address and path"
                )));
            }
            (Some(_), None) => BtcOutputScriptType::for_address(explicit_script_type, label)?,
            (None, Some(path)) => {
                BtcOutputScriptType::for_change(explicit_script_type, path, label)?
            }
            (None, None) => match explicit_script_type {
                Some(BtcOutputScriptType::PayToOpReturn) => BtcOutputScriptType::PayToOpReturn,
                Some(
                    BtcOutputScriptType::PayToAddress
                    | BtcOutputScriptType::PayToScriptHash
                    | BtcOutputScriptType::PayToMultisig
                    | BtcOutputScriptType::PayToWitness
                    | BtcOutputScriptType::PayToP2shWitness
                    | BtcOutputScriptType::PayToTaproot,
                )
                | None => {
                    return Err(WalletError::Signing(format!(
                        "{label} requires either address or path"
                    )));
                }
            },
        };

        let amount = parse_sats(&self.amount)?;
        let op_return_data = self.op_return_data.as_deref().map(decode).transpose()?;
        if script_type == BtcOutputScriptType::PayToOpReturn && op_return_data.is_none() {
            return Err(WalletError::Signing(format!(
                "{label} with script_type PayToOpReturn requires op_return_data"
            )));
        }
        if script_type == BtcOutputScriptType::PayToOpReturn && amount != 0 {
            return Err(WalletError::Signing(format!(
                "{label} with script_type PayToOpReturn must have zero amount, got {amount}"
            )));
        }
        let multisig = self.multisig.map(BtcMultisig::try_from).transpose()?;
        if script_type == BtcOutputScriptType::PayToMultisig && multisig.is_none() {
            return Err(WalletError::Signing(
                "bitcoin PayToMultisig output requires multisig metadata".into(),
            ));
        }
        Ok(BtcSignOutput {
            address: self.address,
            path: path.unwrap_or_default(),
            amount,
            script_type,
            multisig,
            op_return_data,
            orig_hash: self
                .orig_hash
                .as_deref()
                .map(|value| decode_hash32(owner.output_orig_hash_field(), value))
                .transpose()?,
            orig_index: self.orig_index,
            payment_req_index: self.payment_req_index,
        })
    }
}