diff --git a/bdk-ffi/src/error.rs b/bdk-ffi/src/error.rs index 4f718a76..eb0097e7 100644 --- a/bdk-ffi/src/error.rs +++ b/bdk-ffi/src/error.rs @@ -723,6 +723,21 @@ pub enum SighashParseError { Invalid { error_message: String }, } +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum GetPsbtInputError { + #[error("invalid sighash type: {error_message}")] + InvalidSighash { error_message: String }, + + #[error("reference to an unknown utxo: {outpoint}")] + UnknownUtxo { outpoint: String }, + + #[error("miniscript psbt error: {error_message}")] + MiniscriptPsbt { error_message: String }, + + #[error("create tx error: {error_message}")] + CreateTx { error_message: String }, +} + #[derive(Debug, thiserror::Error, uniffi::Error)] pub enum PsbtFinalizeError { #[error("an input at index {index} is invalid: {reason}")] @@ -1066,6 +1081,32 @@ impl From for CreateTxError { } } +impl GetPsbtInputError { + pub(crate) fn from_bdk_create_tx_error(error: BdkCreateTxError, outpoint: OutPoint) -> Self { + match error { + BdkCreateTxError::UnknownUtxo => GetPsbtInputError::UnknownUtxo { + outpoint: format!("{}:{}", outpoint.txid, outpoint.vout), + }, + BdkCreateTxError::MiniscriptPsbt(error) => GetPsbtInputError::MiniscriptPsbt { + error_message: error.to_string(), + }, + other => GetPsbtInputError::CreateTx { + error_message: CreateTxError::from(other).to_string(), + }, + } + } +} + +impl From for GetPsbtInputError { + fn from(error: SighashParseError) -> Self { + match error { + SighashParseError::Invalid { error_message } => { + GetPsbtInputError::InvalidSighash { error_message } + } + } + } +} + impl From for CreateTxError { fn from(_: PushBytesError) -> Self { CreateTxError::PushBytesError diff --git a/bdk-ffi/src/tests/wallet.rs b/bdk-ffi/src/tests/wallet.rs index 6219fa7b..4046a351 100644 --- a/bdk-ffi/src/tests/wallet.rs +++ b/bdk-ffi/src/tests/wallet.rs @@ -1,10 +1,19 @@ -use crate::bitcoin::{Network, NetworkKind}; +use crate::bitcoin::{Network, NetworkKind, Transaction}; use crate::descriptor::Descriptor; +use crate::error::GetPsbtInputError; use crate::store::Persister; +use crate::types::{LocalOutput, UnconfirmedTx}; use crate::wallet::Wallet; +use bdk_wallet::bitcoin::absolute::LockTime; +use bdk_wallet::bitcoin::transaction::Version; +use bdk_wallet::bitcoin::{ + Amount as BdkAmount, OutPoint as BdkOutPoint, ScriptBuf, Sequence, + Transaction as BdkTransaction, TxIn as BdkTxIn, TxOut as BdkTxOut, Txid as BdkTxid, Witness, +}; use bdk_wallet::KeychainKind; +use std::str::FromStr; use std::sync::Arc; const EXTERNAL_DESCRIPTOR: &str = "wpkh(tprv8ZgxMBicQKsPf2qfrEygW6fdYseJDDrVnDv26PH5BHdvSuG6ecCbHqLVof9yZcMoM31z9ur3tTYbSnr1WBqbGX97CbXcmp5H6qeMpyvx35B/84h/1h/1h/0/*)"; @@ -35,6 +44,40 @@ fn build_wallet() -> Wallet { .unwrap() } +fn fund_wallet(wallet: &Wallet) -> LocalOutput { + let address_info = wallet.reveal_next_address(KeychainKind::External); + let script_pubkey = ScriptBuf::from_bytes(address_info.address.script_pubkey().to_bytes()); + let tx = BdkTransaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![BdkTxIn { + previous_output: BdkOutPoint { + txid: BdkTxid::from_str( + "0101010101010101010101010101010101010101010101010101010101010101", + ) + .unwrap(), + vout: 0, + }, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::default(), + }], + output: vec![BdkTxOut { + value: BdkAmount::from_sat(50_000), + script_pubkey, + }], + }; + + wallet.apply_unconfirmed_txs(vec![UnconfirmedTx { + tx: Arc::new(Transaction::from(tx)), + last_seen: 1, + }]); + + let utxos = wallet.list_unspent(); + assert_eq!(utxos.len(), 1); + utxos.into_iter().next().unwrap() +} + #[test] fn test_create_wallet() { let wallet = build_wallet(); @@ -150,3 +193,47 @@ fn test_create_two_path_wallet() { assert_eq!(wallet.derivation_index(KeychainKind::External), Some(0)); assert_eq!(wallet.derivation_index(KeychainKind::Internal), Some(0)); } + +#[test] +fn test_get_psbt_input_for_local_utxo() { + let wallet = build_wallet(); + let utxo = fund_wallet(&wallet); + + let psbt_input = wallet.get_psbt_input(utxo, None, false).unwrap(); + + assert!(psbt_input.witness_utxo.is_some() || psbt_input.non_witness_utxo.is_some()); + assert_eq!(psbt_input.sighash_type, None); + assert!(!psbt_input.bip32_derivation.is_empty()); +} + +#[test] +fn test_get_psbt_input_parses_sighash() { + let wallet = build_wallet(); + let utxo = fund_wallet(&wallet); + + let psbt_input = wallet + .get_psbt_input( + utxo, + Some("SIGHASH_SINGLE|SIGHASH_ANYONECANPAY".to_string()), + false, + ) + .unwrap(); + + assert_eq!( + psbt_input.sighash_type.as_deref(), + Some("SIGHASH_SINGLE|SIGHASH_ANYONECANPAY") + ); +} + +#[test] +fn test_get_psbt_input_invalid_sighash_returns_error() { + let wallet = build_wallet(); + let utxo = fund_wallet(&wallet); + + let result = wallet.get_psbt_input(utxo, Some("not-a-sighash".to_string()), false); + + assert!(matches!( + result, + Err(GetPsbtInputError::InvalidSighash { .. }) + )); +} diff --git a/bdk-ffi/src/types.rs b/bdk-ffi/src/types.rs index 0cfe874c..4bda185f 100644 --- a/bdk-ffi/src/types.rs +++ b/bdk-ffi/src/types.rs @@ -107,6 +107,24 @@ impl From> for ChainPosition { } } +impl From for BdkChainPosition { + fn from(chain_position: ChainPosition) -> Self { + match chain_position { + ChainPosition::Confirmed { + confirmation_block_time, + transitively, + } => BdkChainPosition::Confirmed { + anchor: confirmation_block_time.into(), + transitively: transitively.map(|txid| txid.0), + }, + ChainPosition::Unconfirmed { timestamp } => BdkChainPosition::Unconfirmed { + first_seen: timestamp, + last_seen: timestamp, + }, + } + } +} + /// Represents the confirmation block and time of a transaction. #[derive(Debug, Clone, PartialEq, Eq, std::hash::Hash, uniffi::Record)] pub struct ConfirmationBlockTime { @@ -294,6 +312,19 @@ impl From for LocalOutput { } } +impl From for BdkLocalOutput { + fn from(local_utxo: LocalOutput) -> Self { + BdkLocalOutput { + outpoint: local_utxo.outpoint.into(), + txout: local_utxo.txout.into(), + keychain: local_utxo.keychain, + is_spent: local_utxo.is_spent, + derivation_index: local_utxo.derivation_index, + chain_position: local_utxo.chain_position.into(), + } + } +} + // Callback for the FullScanRequest #[uniffi::export(with_foreign)] pub trait FullScanScriptInspector: Sync + Send { diff --git a/bdk-ffi/src/wallet.rs b/bdk-ffi/src/wallet.rs index acd73c6a..c7127657 100644 --- a/bdk-ffi/src/wallet.rs +++ b/bdk-ffi/src/wallet.rs @@ -1,8 +1,9 @@ -use crate::bitcoin::{Amount, FeeRate, OutPoint, Psbt, Script, Transaction, TxOut, Txid}; +use crate::bitcoin::{Amount, FeeRate, Input, OutPoint, Psbt, Script, Transaction, TxOut, Txid}; use crate::descriptor::Descriptor; use crate::error::{ CalculateFeeError, CannotConnectError, CreateWithPersistError, DescriptorError, - LoadWithPersistError, PersistenceError, SignerError, TxidParseError, + GetPsbtInputError, LoadWithPersistError, PersistenceError, SighashParseError, SignerError, + TxidParseError, }; use crate::store::{PersistenceType, Persister}; use crate::types::{ @@ -11,6 +12,7 @@ use crate::types::{ SyncRequestBuilder, UnconfirmedTx, Update, WalletEvent, WalletKeychain, }; +use bdk_wallet::bitcoin::psbt::PsbtSighashType as BdkPsbtSighashType; use bdk_wallet::bitcoin::Network; use bdk_wallet::keys::KeyMap; #[allow(deprecated)] @@ -18,6 +20,7 @@ use bdk_wallet::signer::SignOptions as BdkSignOptions; use bdk_wallet::{PersistedWallet, Wallet as BdkWallet}; use std::ops::DerefMut; +use std::str::FromStr; use std::sync::{Arc, Mutex, MutexGuard}; /// A Bitcoin wallet. @@ -214,6 +217,26 @@ impl Wallet { .map(|local_output| local_output.into()) } + /// Get the corresponding PSBT Input for a `LocalOutput`. + #[uniffi::method(default(sighash_type = None, only_witness_utxo = false))] + pub fn get_psbt_input( + &self, + utxo: LocalOutput, + sighash_type: Option, + only_witness_utxo: bool, + ) -> Result { + let outpoint = utxo.outpoint.clone(); + let sighash_type = sighash_type + .map(|sighash_type| parse_sighash_type(&sighash_type)) + .transpose()?; + let psbt_input = self + .get_wallet() + .get_psbt_input(utxo.into(), sighash_type, only_witness_utxo) + .map_err(|error| GetPsbtInputError::from_bdk_create_tx_error(error, outpoint))?; + + Ok(Input::from(&psbt_input)) + } + /// Attempt to reveal the next address of the given `keychain`. /// /// This will increment the keychain's derivation index. If the keychain's descriptor doesn't @@ -744,3 +767,9 @@ impl Wallet { self.inner_mutex.lock().expect("wallet") } } + +fn parse_sighash_type(sighash: &str) -> Result { + BdkPsbtSighashType::from_str(sighash).map_err(|error| SighashParseError::Invalid { + error_message: error.to_string(), + }) +}