diff --git a/crates/bdk/src/error.rs b/crates/bdk/src/error.rs index fcb5a6f7b1..105e642a04 100644 --- a/crates/bdk/src/error.rs +++ b/crates/bdk/src/error.rs @@ -20,25 +20,6 @@ use core::fmt; pub enum Error { /// Generic error Generic(String), - /// Cannot build a tx without recipients - NoRecipients, - /// `manually_selected_only` option is selected but no utxo has been passed - NoUtxosSelected, - /// Output created is under the dust limit, 546 satoshis - OutputBelowDustLimit(usize), - /// Wallet's UTXO set is not enough to cover recipient's requested plus fee - InsufficientFunds { - /// Sats needed for some transaction - needed: u64, - /// Sats available for spending - available: u64, - }, - /// Branch and bound coin selection possible attempts with sufficiently big UTXO set could grow - /// exponentially, thus a limit is set, and when hit, this error is thrown - BnBTotalTriesExceeded, - /// Branch and bound coin selection tries to avoid needing a change by finding the right inputs for - /// the desired outputs plus fee, if there is not such combination this error is thrown - BnBNoExactMatch, /// Happens when trying to spend an UTXO that is not in the internal database UnknownUtxo, /// Thrown when a tx is not found in the internal database @@ -47,32 +28,12 @@ pub enum Error { TransactionConfirmed, /// Trying to replace a tx that has a sequence >= `0xFFFFFFFE` IrreplaceableTransaction, - /// When bumping a tx the fee rate requested is lower than required - FeeRateTooLow { - /// Required fee rate (satoshi/vbyte) - required: crate::types::FeeRate, - }, - /// When bumping a tx the absolute fee requested is lower than replaced tx absolute fee - FeeTooLow { - /// Required fee absolute value (satoshi) - required: u64, - }, /// Node doesn't have data to estimate a fee rate FeeRateUnavailable, - /// In order to use the [`TxBuilder::add_global_xpubs`] option every extended - /// key in the descriptor must either be a master key itself (having depth = 0) or have an - /// explicit origin provided - /// - /// [`TxBuilder::add_global_xpubs`]: crate::wallet::tx_builder::TxBuilder::add_global_xpubs - MissingKeyOrigin(String), /// Error while working with [`keys`](crate::keys) Key(crate::keys::KeyError), /// Descriptor checksum mismatch ChecksumMismatch, - /// Spending policy is not compatible with this [`KeychainKind`](crate::types::KeychainKind) - SpendingPolicyRequired(crate::types::KeychainKind), - /// Error while extracting and manipulating policies - InvalidPolicyPathError(crate::descriptor::policy::PolicyError), /// Signing error Signer(crate::wallet::signer::SignerError), /// Requested outpoint doesn't exist in the tx (vout greater than available outputs) @@ -115,40 +76,15 @@ impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Generic(err) => write!(f, "Generic error: {}", err), - Self::NoRecipients => write!(f, "Cannot build tx without recipients"), - Self::NoUtxosSelected => write!(f, "No UTXO selected"), - Self::OutputBelowDustLimit(limit) => { - write!(f, "Output below the dust limit: {}", limit) - } - Self::InsufficientFunds { needed, available } => write!( - f, - "Insufficient funds: {} sat available of {} sat needed", - available, needed - ), - Self::BnBTotalTriesExceeded => { - write!(f, "Branch and bound coin selection: total tries exceeded") - } - Self::BnBNoExactMatch => write!(f, "Branch and bound coin selection: not exact match"), Self::UnknownUtxo => write!(f, "UTXO not found in the internal database"), Self::TransactionNotFound => { write!(f, "Transaction not found in the internal database") } Self::TransactionConfirmed => write!(f, "Transaction already confirmed"), Self::IrreplaceableTransaction => write!(f, "Transaction can't be replaced"), - Self::FeeRateTooLow { required } => write!( - f, - "Fee rate too low: required {} sat/vbyte", - required.as_sat_per_vb() - ), - Self::FeeTooLow { required } => write!(f, "Fee to low: required {} sat", required), Self::FeeRateUnavailable => write!(f, "Fee rate unavailable"), - Self::MissingKeyOrigin(err) => write!(f, "Missing key origin: {}", err), Self::Key(err) => write!(f, "Key error: {}", err), Self::ChecksumMismatch => write!(f, "Descriptor checksum mismatch"), - Self::SpendingPolicyRequired(keychain_kind) => { - write!(f, "Spending policy required: {:?}", keychain_kind) - } - Self::InvalidPolicyPathError(err) => write!(f, "Invalid policy path: {}", err), Self::Signer(err) => write!(f, "Signer error: {}", err), Self::InvalidOutpoint(outpoint) => write!( f, @@ -181,7 +117,6 @@ macro_rules! impl_error { } impl_error!(descriptor::error::Error, Descriptor); -impl_error!(descriptor::policy::PolicyError, InvalidPolicyPathError); impl_error!(wallet::signer::SignerError, Signer); impl From for Error { diff --git a/crates/bdk/src/wallet/coin_selection.rs b/crates/bdk/src/wallet/coin_selection.rs index 10309a6602..0ce38242fd 100644 --- a/crates/bdk/src/wallet/coin_selection.rs +++ b/crates/bdk/src/wallet/coin_selection.rs @@ -26,7 +26,7 @@ //! ``` //! # use std::str::FromStr; //! # use bitcoin::*; -//! # use bdk::wallet::{self, ChangeSet, coin_selection::*, CreateTxError}; +//! # use bdk::wallet::{self, ChangeSet, coin_selection::*, coin_selection, CreateTxError}; //! # use bdk_chain::PersistBackend; //! # use bdk::*; //! # use bdk::wallet::coin_selection::decide_change; @@ -42,7 +42,7 @@ //! fee_rate: bdk::FeeRate, //! target_amount: u64, //! drain_script: &Script, -//! ) -> Result { +//! ) -> Result { //! let mut selected_amount = 0; //! let mut additional_weight = Weight::ZERO; //! let all_utxos_selected = required_utxos @@ -62,7 +62,7 @@ //! let additional_fees = fee_rate.fee_wu(additional_weight); //! let amount_needed_with_fees = additional_fees + target_amount; //! if selected_amount < amount_needed_with_fees { -//! return Err(bdk::Error::InsufficientFunds { +//! return Err(coin_selection::Error::InsufficientFunds { //! needed: amount_needed_with_fees, //! available: selected_amount, //! }); @@ -100,14 +100,16 @@ use crate::types::FeeRate; use crate::wallet::utils::IsDust; +use crate::Utxo; use crate::WeightedUtxo; -use crate::{error::Error, Utxo}; use alloc::vec::Vec; use bitcoin::consensus::encode::serialize; use bitcoin::{Script, Weight}; use core::convert::TryInto; +use core::fmt; +use core::fmt::Formatter; use rand::seq::SliceRandom; /// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not @@ -118,6 +120,43 @@ pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection; // prev_txid (32 bytes) + prev_vout (4 bytes) + sequence (4 bytes) pub(crate) const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4; +/// Errors that can be thrown by the [`coin_selection`](crate::wallet::coin_selection) module +#[derive(Debug)] +pub enum Error { + /// Wallet's UTXO set is not enough to cover recipient's requested plus fee + InsufficientFunds { + /// Sats needed for some transaction + needed: u64, + /// Sats available for spending + available: u64, + }, + /// Branch and bound coin selection tries to avoid needing a change by finding the right inputs for + /// the desired outputs plus fee, if there is not such combination this error is thrown + BnBNoExactMatch, + /// Branch and bound coin selection possible attempts with sufficiently big UTXO set could grow + /// exponentially, thus a limit is set, and when hit, this error is thrown + BnBTotalTriesExceeded, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::InsufficientFunds { needed, available } => write!( + f, + "Insufficient funds: {} sat available of {} sat needed", + available, needed + ), + Self::BnBTotalTriesExceeded => { + write!(f, "Branch and bound coin selection: total tries exceeded") + } + Self::BnBNoExactMatch => write!(f, "Branch and bound coin selection: not exact match"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for Error {} + #[derive(Debug)] /// Remaining amount after performing coin selection pub enum Excess { diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 5c8308b24d..58c5f1002c 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -293,8 +293,69 @@ pub enum CreateTxError

{ Persist(P), /// There was a problem while extracting and manipulating policies Policy(PolicyError), - /// TODO: replace this with specific error types - Bdk(Error), + /// Spending policy is not compatible with this [`KeychainKind`](crate::types::KeychainKind) + SpendingPolicyRequired(KeychainKind), + /// Requested invalid transaction version '0' + Version0, + /// Requested transaction version `1`, but at least `2` is needed to use OP_CSV + Version1Csv, + /// Requested `LockTime` is less than is required to spend from this script + LockTime { + /// Requested `LockTime` + requested: absolute::LockTime, + /// Required `LockTime` + required: absolute::LockTime, + }, + /// Cannot enable RBF with a `Sequence` >= 0xFFFFFFFE + RbfSequence, + /// Cannot enable RBF with `Sequence` given a required OP_CSV + RbfSequenceCsv { + /// Given RBF `Sequence` + rbf: Sequence, + /// Required OP_CSV `Sequence` + csv: Sequence, + }, + /// When bumping a tx the absolute fee requested is lower than replaced tx absolute fee + FeeTooLow { + /// Required fee absolute value (satoshi) + required: u64, + }, + /// When bumping a tx the fee rate requested is lower than required + FeeRateTooLow { + /// Required fee rate (satoshi/vbyte) + required: FeeRate, + }, + /// `manually_selected_only` option is selected but no utxo has been passed + NoUtxosSelected, + /// Output created is under the dust limit, 546 satoshis + OutputBelowDustLimit(usize), + /// The `change_policy` was set but the wallet does not have a change_descriptor + ChangePolicyDescriptor, + /// There was an error with coin selection + CoinSelection(coin_selection::Error), + /// Wallet's UTXO set is not enough to cover recipient's requested plus fee + InsufficientFunds { + /// Sats needed for some transaction + needed: u64, + /// Sats available for spending + available: u64, + }, + /// Cannot build a tx without recipients + NoRecipients, + /// Partially signed bitcoin transaction error + Psbt(psbt::Error), + /// In order to use the [`TxBuilder::add_global_xpubs`] option every extended + /// key in the descriptor must either be a master key itself (having depth = 0) or have an + /// explicit origin provided + /// + /// [`TxBuilder::add_global_xpubs`]: crate::wallet::tx_builder::TxBuilder::add_global_xpubs + MissingKeyOrigin(String), + /// Happens when trying to spend an UTXO that is not in the internal database + UnknownUtxo, + /// Missing non_witness_utxo on foreign utxo for given `OutPoint` + MissingNonWitnessUtxo(OutPoint), + /// Miniscript PSBT error + MiniscriptPsbt(MiniscriptPsbtError), } #[cfg(feature = "std")] @@ -312,8 +373,81 @@ where e ) } - Self::Bdk(e) => e.fmt(f), Self::Policy(e) => e.fmt(f), + CreateTxError::SpendingPolicyRequired(keychain_kind) => { + write!(f, "Spending policy required: {:?}", keychain_kind) + } + CreateTxError::Version0 => { + write!(f, "Invalid version `0`") + } + CreateTxError::Version1Csv => { + write!( + f, + "TxBuilder requested version `1`, but at least `2` is needed to use OP_CSV" + ) + } + CreateTxError::LockTime { + requested, + required, + } => { + write!(f, "TxBuilder requested timelock of `{:?}`, but at least `{:?}` is required to spend from this script", required, requested) + } + CreateTxError::RbfSequence => { + write!(f, "Cannot enable RBF with a nSequence >= 0xFFFFFFFE") + } + CreateTxError::RbfSequenceCsv { rbf, csv } => { + write!( + f, + "Cannot enable RBF with nSequence `{:?}` given a required OP_CSV of `{:?}`", + rbf, csv + ) + } + CreateTxError::FeeTooLow { required } => { + write!(f, "Fee to low: required {} sat", required) + } + CreateTxError::FeeRateTooLow { required } => { + write!( + f, + "Fee rate too low: required {} sat/vbyte", + required.as_sat_per_vb() + ) + } + CreateTxError::NoUtxosSelected => { + write!(f, "No UTXO selected") + } + CreateTxError::OutputBelowDustLimit(limit) => { + write!(f, "Output below the dust limit: {}", limit) + } + CreateTxError::ChangePolicyDescriptor => { + write!( + f, + "The `change_policy` can be set only if the wallet has a change_descriptor" + ) + } + CreateTxError::CoinSelection(e) => e.fmt(f), + CreateTxError::InsufficientFunds { needed, available } => { + write!( + f, + "Insufficient funds: {} sat available of {} sat needed", + available, needed + ) + } + CreateTxError::NoRecipients => { + write!(f, "Cannot build tx without recipients") + } + CreateTxError::Psbt(e) => e.fmt(f), + CreateTxError::MissingKeyOrigin(err) => { + write!(f, "Missing key origin: {}", err) + } + CreateTxError::UnknownUtxo => { + write!(f, "UTXO not found in the internal database") + } + CreateTxError::MissingNonWitnessUtxo(outpoint) => { + write!(f, "Missing non_witness_utxo on foreign utxo {}", outpoint) + } + CreateTxError::MiniscriptPsbt(err) => { + write!(f, "Miniscript PSBT error: {}", err) + } } } } @@ -330,9 +464,21 @@ impl

From for CreateTxError

{ } } -impl

From for CreateTxError

{ - fn from(err: Error) -> Self { - CreateTxError::Bdk(err) +impl

From for CreateTxError

{ + fn from(err: MiniscriptPsbtError) -> Self { + CreateTxError::MiniscriptPsbt(err) + } +} + +impl

From for CreateTxError

{ + fn from(err: psbt::Error) -> Self { + CreateTxError::Psbt(err) + } +} + +impl

From for CreateTxError

{ + fn from(err: coin_selection::Error) -> Self { + CreateTxError::CoinSelection(err) } } @@ -937,7 +1083,7 @@ impl Wallet { &mut self, coin_selection: Cs, params: TxParams, - ) -> Result + ) -> Result> where D: PersistBackend, { @@ -959,7 +1105,7 @@ impl Wallet { let internal_policy = internal_descriptor .as_ref() .map(|desc| { - Ok::<_, Error>( + Ok::<_, CreateTxError>( desc.extract_policy(&self.change_signers, BuildSatisfaction::None, &self.secp)? .unwrap(), ) @@ -972,7 +1118,9 @@ impl Wallet { && external_policy.requires_path() && params.external_policy_path.is_none() { - return Err(Error::SpendingPolicyRequired(KeychainKind::External).into()); + return Err(CreateTxError::SpendingPolicyRequired( + KeychainKind::External, + )); }; // Same for the internal_policy path, if present if let Some(internal_policy) = &internal_policy { @@ -980,7 +1128,9 @@ impl Wallet { && internal_policy.requires_path() && params.internal_policy_path.is_none() { - return Err(Error::SpendingPolicyRequired(KeychainKind::Internal).into()); + return Err(CreateTxError::SpendingPolicyRequired( + KeychainKind::Internal, + )); }; } @@ -992,7 +1142,7 @@ impl Wallet { )?; let internal_requirements = internal_policy .map(|policy| { - Ok::<_, Error>( + Ok::<_, CreateTxError>( policy.get_condition( params .internal_policy_path @@ -1008,15 +1158,9 @@ impl Wallet { debug!("Policy requirements: {:?}", requirements); let version = match params.version { - Some(tx_builder::Version(0)) => { - return Err(Error::Generic("Invalid version `0`".into()).into()) - } + Some(tx_builder::Version(0)) => return Err(CreateTxError::Version0), Some(tx_builder::Version(1)) if requirements.csv.is_some() => { - return Err(Error::Generic( - "TxBuilder requested version `1`, but at least `2` is needed to use OP_CSV" - .into(), - ) - .into()) + return Err(CreateTxError::Version1Csv) } Some(tx_builder::Version(x)) => x, None if requirements.csv.is_some() => 2, @@ -1047,7 +1191,9 @@ impl Wallet { // No requirement, just use the fee_sniping_height None => fee_sniping_height, // There's a block-based requirement, but the value is lower than the fee_sniping_height - Some(value @ absolute::LockTime::Blocks(_)) if value < fee_sniping_height => fee_sniping_height, + Some(value @ absolute::LockTime::Blocks(_)) if value < fee_sniping_height => { + fee_sniping_height + } // There's a time-based requirement or a block-based requirement greater // than the fee_sniping_height use that value Some(value) => value, @@ -1056,9 +1202,19 @@ impl Wallet { // Specific nLockTime required and we have no constraints, so just set to that value Some(x) if requirements.timelock.is_none() => x, // Specific nLockTime required and it's compatible with the constraints - Some(x) if requirements.timelock.unwrap().is_same_unit(x) && x >= requirements.timelock.unwrap() => x, + Some(x) + if requirements.timelock.unwrap().is_same_unit(x) + && x >= requirements.timelock.unwrap() => + { + x + } // Invalid nLockTime required - Some(x) => return Err(Error::Generic(format!("TxBuilder requested timelock of `{:?}`, but at least `{:?}` is required to spend from this script", x, requirements.timelock.unwrap())).into()) + Some(x) => { + return Err(CreateTxError::LockTime { + requested: x, + required: requirements.timelock.unwrap(), + }) + } }; let n_sequence = match (params.rbf, requirements.csv) { @@ -1076,20 +1232,13 @@ impl Wallet { // RBF with a specific value but that value is too high (Some(tx_builder::RbfValue::Value(rbf)), _) if !rbf.is_rbf() => { - return Err(Error::Generic( - "Cannot enable RBF with a nSequence >= 0xFFFFFFFE".into(), - ) - .into()) + return Err(CreateTxError::RbfSequence) } // RBF with a specific value requested, but the value is incompatible with CSV (Some(tx_builder::RbfValue::Value(rbf)), Some(csv)) if !check_nsequence_rbf(rbf, csv) => { - return Err(Error::Generic(format!( - "Cannot enable RBF with nSequence `{:?}` given a required OP_CSV of `{:?}`", - rbf, csv - )) - .into()) + return Err(CreateTxError::RbfSequenceCsv { rbf, csv }) } // RBF enabled with the default value with CSV also enabled. CSV takes precedence @@ -1108,10 +1257,9 @@ impl Wallet { FeePolicy::FeeAmount(fee) => { if let Some(previous_fee) = params.bumping_fee { if *fee < previous_fee.absolute { - return Err(Error::FeeTooLow { + return Err(CreateTxError::FeeTooLow { required: previous_fee.absolute, - } - .into()); + }); } } (FeeRate::from_sat_per_vb(0.0), *fee) @@ -1120,10 +1268,9 @@ impl Wallet { if let Some(previous_fee) = params.bumping_fee { let required_feerate = FeeRate::from_sat_per_vb(previous_fee.rate + 1.0); if *rate < required_feerate { - return Err(Error::FeeRateTooLow { + return Err(CreateTxError::FeeRateTooLow { required: required_feerate, - } - .into()); + }); } } (*rate, 0) @@ -1138,7 +1285,7 @@ impl Wallet { }; if params.manually_selected_only && params.utxos.is_empty() { - return Err(Error::NoUtxosSelected.into()); + return Err(CreateTxError::NoUtxosSelected); } // we keep it as a float while we accumulate it, and only round it at the end @@ -1152,7 +1299,7 @@ impl Wallet { && value.is_dust(script_pubkey) && !script_pubkey.is_provably_unspendable() { - return Err(Error::OutputBelowDustLimit(index).into()); + return Err(CreateTxError::OutputBelowDustLimit(index)); } if self.is_mine(script_pubkey) { @@ -1185,10 +1332,7 @@ impl Wallet { if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed && internal_descriptor.is_none() { - return Err(Error::Generic( - "The `change_policy` can be set only if the wallet has a change_descriptor".into(), - ) - .into()); + return Err(CreateTxError::ChangePolicyDescriptor); } let (required_utxos, optional_utxos) = self.preselect_utxos( @@ -1255,14 +1399,13 @@ impl Wallet { change_fee, } = excess { - return Err(Error::InsufficientFunds { + return Err(CreateTxError::InsufficientFunds { needed: *dust_threshold, available: remaining_amount.saturating_sub(*change_fee), - } - .into()); + }); } } else { - return Err(Error::NoRecipients.into()); + return Err(CreateTxError::NoRecipients); } } @@ -1319,7 +1462,7 @@ impl Wallet { /// builder /// .add_recipient(to_address.script_pubkey(), 50_000) /// .enable_rbf(); - /// builder.finish()? + /// builder.finish().expect("psbt") /// }; /// let _ = wallet.sign(&mut psbt, SignOptions::default())?; /// let tx = psbt.extract_tx(); @@ -1328,13 +1471,13 @@ impl Wallet { /// let mut builder = wallet.build_fee_bump(tx.txid())?; /// builder /// .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0)); - /// builder.finish()? + /// builder.finish().expect("psbt") /// }; /// /// let _ = wallet.sign(&mut psbt, SignOptions::default())?; /// let fee_bumped_tx = psbt.extract_tx(); /// // broadcast fee_bumped_tx to replace original - /// # Ok::<(), CreateTxError<<() as PersistBackend>::WriteError>>(()) + /// # Ok::<(), bdk::Error>(()) /// ``` // TODO: support for merging multiple transactions while bumping the fees pub fn build_fee_bump( @@ -1489,11 +1632,11 @@ impl Wallet { /// let mut psbt = { /// let mut builder = wallet.build_tx(); /// builder.add_recipient(to_address.script_pubkey(), 50_000); - /// builder.finish()? + /// builder.finish().expect("psbt") /// }; /// let finalized = wallet.sign(&mut psbt, SignOptions::default())?; /// assert!(finalized, "we should have signed all the inputs"); - /// # Ok::<(), CreateTxError<<() as PersistBackend>::WriteError>>(()) + /// # Ok::<(), bdk::Error>(()) pub fn sign( &self, psbt: &mut psbt::PartiallySignedTransaction, @@ -1838,7 +1981,10 @@ impl Wallet { tx: Transaction, selected: Vec, params: TxParams, - ) -> Result { + ) -> Result> + where + D: PersistBackend, + { let mut psbt = psbt::PartiallySignedTransaction::from_unsigned_tx(tx)?; if params.add_global_xpubs { @@ -1854,7 +2000,7 @@ impl Wallet { None if xpub.xkey.depth == 0 => { (xpub.root_fingerprint(&self.secp), vec![].into()) } - _ => return Err(Error::MissingKeyOrigin(xpub.xkey.to_string())), + _ => return Err(CreateTxError::MissingKeyOrigin(xpub.xkey.to_string())), }; psbt.xpub.insert(xpub.xkey, origin); @@ -1879,7 +2025,7 @@ impl Wallet { match self.get_psbt_input(utxo, params.sighash, params.only_witness_utxo) { Ok(psbt_input) => psbt_input, Err(e) => match e { - Error::UnknownUtxo => psbt::Input { + CreateTxError::UnknownUtxo => psbt::Input { sighash_type: params.sighash, ..psbt::Input::default() }, @@ -1900,10 +2046,7 @@ impl Wallet { && !params.only_witness_utxo && foreign_psbt_input.non_witness_utxo.is_none() { - return Err(Error::Generic(format!( - "Missing non_witness_utxo on foreign utxo {}", - outpoint - ))); + return Err(CreateTxError::MissingNonWitnessUtxo(outpoint)); } *psbt_input = *foreign_psbt_input; } @@ -1921,14 +2064,17 @@ impl Wallet { utxo: LocalUtxo, sighash_type: Option, only_witness_utxo: bool, - ) -> Result { + ) -> Result> + where + D: PersistBackend, + { // Try to find the prev_script in our db to figure out if this is internal or external, // and the derivation index let &(keychain, child) = self .indexed_graph .index .index_of_spk(&utxo.txout.script_pubkey) - .ok_or(Error::UnknownUtxo)?; + .ok_or(CreateTxError::UnknownUtxo)?; let mut psbt_input = psbt::Input { sighash_type, @@ -1959,7 +2105,7 @@ impl Wallet { fn update_psbt_with_descriptor( &self, psbt: &mut psbt::PartiallySignedTransaction, - ) -> Result<(), Error> { + ) -> Result<(), MiniscriptPsbtError> { // We need to borrow `psbt` mutably within the loops, so we have to allocate a vec for all // the input utxos and outputs // diff --git a/crates/bdk/src/wallet/tx_builder.rs b/crates/bdk/src/wallet/tx_builder.rs index be8a7d7da7..367e0be247 100644 --- a/crates/bdk/src/wallet/tx_builder.rs +++ b/crates/bdk/src/wallet/tx_builder.rs @@ -50,8 +50,8 @@ use bitcoin::{absolute, script::PushBytes, OutPoint, ScriptBuf, Sequence, Transa use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm}; use super::ChangeSet; -use crate::wallet::CreateTxError; use crate::types::{FeeRate, KeychainKind, LocalUtxo, WeightedUtxo}; +use crate::wallet::CreateTxError; use crate::{Error, Utxo, Wallet}; /// Context in which the [`TxBuilder`] is valid pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {} @@ -545,7 +545,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D, /// Returns the [`BIP174`] "PSBT" and summary details about the transaction. /// /// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki - pub fn finish(self) -> Result> + pub fn finish(self) -> Result> where D: PersistBackend, { diff --git a/crates/bdk/tests/wallet.rs b/crates/bdk/tests/wallet.rs index 0ade431be0..d0cee40597 100644 --- a/crates/bdk/tests/wallet.rs +++ b/crates/bdk/tests/wallet.rs @@ -2,10 +2,10 @@ use assert_matches::assert_matches; use bdk::descriptor::calc_checksum; use bdk::psbt::PsbtUtils; use bdk::signer::{SignOptions, SignerError}; -use bdk::wallet::coin_selection::LargestFirstCoinSelection; +use bdk::wallet::coin_selection::{self, LargestFirstCoinSelection}; use bdk::wallet::AddressIndex::*; use bdk::wallet::{AddressIndex, AddressInfo, Balance, CreateTxError, Wallet}; -use bdk::{Error, FeeRate, KeychainKind}; +use bdk::{FeeRate, KeychainKind}; use bdk_chain::COINBASE_MATURITY; use bdk_chain::{BlockId, ConfirmationTime}; use bitcoin::hashes::Hash; @@ -213,7 +213,6 @@ fn test_create_tx_manually_selected_empty_utxos() { } #[test] -#[should_panic(expected = "Invalid version `0`")] fn test_create_tx_version_0() { let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_address(New); @@ -221,13 +220,10 @@ fn test_create_tx_version_0() { builder .add_recipient(addr.script_pubkey(), 25_000) .version(0); - builder.finish().unwrap(); + assert!(matches!(builder.finish(), Err(CreateTxError::Version0))); } #[test] -#[should_panic( - expected = "TxBuilder requested version `1`, but at least `2` is needed to use OP_CSV" -)] fn test_create_tx_version_1_csv() { let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); let addr = wallet.get_address(New); @@ -235,7 +231,7 @@ fn test_create_tx_version_1_csv() { builder .add_recipient(addr.script_pubkey(), 25_000) .version(1); - builder.finish().unwrap(); + assert!(matches!(builder.finish(), Err(CreateTxError::Version1Csv))); } #[test] @@ -323,9 +319,6 @@ fn test_create_tx_custom_locktime_compatible_with_cltv() { } #[test] -#[should_panic( - expected = "TxBuilder requested timelock of `Blocks(Height(50000))`, but at least `Blocks(Height(100000))` is required to spend from this script" -)] fn test_create_tx_custom_locktime_incompatible_with_cltv() { let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); let addr = wallet.get_address(New); @@ -333,7 +326,9 @@ fn test_create_tx_custom_locktime_incompatible_with_cltv() { builder .add_recipient(addr.script_pubkey(), 25_000) .nlocktime(absolute::LockTime::from_height(50000).unwrap()); - builder.finish().unwrap(); + assert!(matches!(builder.finish(), + Err(CreateTxError::LockTime { requested, required }) + if requested.to_consensus_u32() == 50_000 && required.to_consensus_u32() == 100_000)); } #[test] @@ -362,9 +357,9 @@ fn test_create_tx_with_default_rbf_csv() { } #[test] -#[should_panic( - expected = "Cannot enable RBF with nSequence `Sequence(3)` given a required OP_CSV of `Sequence(6)`" -)] +// #[should_panic( +// expected = "Cannot enable RBF with nSequence `Sequence(3)` given a required OP_CSV of `Sequence(6)`" +// )] fn test_create_tx_with_custom_rbf_csv() { let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); let addr = wallet.get_address(New); @@ -372,7 +367,9 @@ fn test_create_tx_with_custom_rbf_csv() { builder .add_recipient(addr.script_pubkey(), 25_000) .enable_rbf_with_sequence(Sequence(3)); - builder.finish().unwrap(); + assert!(matches!(builder.finish(), + Err(CreateTxError::RbfSequenceCsv { rbf, csv }) + if rbf.to_consensus_u32() == 3 && csv.to_consensus_u32() == 6)); } #[test] @@ -387,7 +384,6 @@ fn test_create_tx_no_rbf_cltv() { } #[test] -#[should_panic(expected = "Cannot enable RBF with a nSequence >= 0xFFFFFFFE")] fn test_create_tx_invalid_rbf_sequence() { let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_address(New); @@ -395,7 +391,7 @@ fn test_create_tx_invalid_rbf_sequence() { builder .add_recipient(addr.script_pubkey(), 25_000) .enable_rbf_with_sequence(Sequence(0xFFFFFFFE)); - builder.finish().unwrap(); + assert!(matches!(builder.finish(), Err(CreateTxError::RbfSequence))); } #[test] @@ -423,9 +419,6 @@ fn test_create_tx_default_sequence() { } #[test] -#[should_panic( - expected = "The `change_policy` can be set only if the wallet has a change_descriptor" -)] fn test_create_tx_change_policy_no_internal() { let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); let addr = wallet.get_address(New); @@ -433,7 +426,10 @@ fn test_create_tx_change_policy_no_internal() { builder .add_recipient(addr.script_pubkey(), 25_000) .do_not_spend_change(); - builder.finish().unwrap(); + assert!(matches!( + builder.finish(), + Err(CreateTxError::ChangePolicyDescriptor) + )); } macro_rules! check_fee { @@ -2852,7 +2848,7 @@ fn test_taproot_sign_missing_witness_utxo() { ); assert_matches!( result, - Err(Error::Signer(SignerError::MissingWitnessUtxo)), + Err(bdk::Error::Signer(SignerError::MissingWitnessUtxo)), "Signing should have failed with the correct error because the witness_utxo is missing" ); @@ -3193,7 +3189,7 @@ fn test_taproot_sign_non_default_sighash() { ); assert_matches!( result, - Err(Error::Signer(SignerError::NonStandardSighash)), + Err(bdk::Error::Signer(SignerError::NonStandardSighash)), "Signing failed with the wrong error type" ); @@ -3211,7 +3207,7 @@ fn test_taproot_sign_non_default_sighash() { ); assert_matches!( result, - Err(Error::Signer(SignerError::MissingWitnessUtxo)), + Err(bdk::Error::Signer(SignerError::MissingWitnessUtxo)), "Signing failed with the wrong error type" ); @@ -3299,10 +3295,12 @@ fn test_spend_coinbase() { .current_height(confirmation_height); assert!(matches!( builder.finish(), - Err(CreateTxError::Bdk(Error::InsufficientFunds { - needed: _, - available: 0 - })) + Err(CreateTxError::CoinSelection( + coin_selection::Error::InsufficientFunds { + needed: _, + available: 0 + } + )) )); // Still unspendable... @@ -3312,10 +3310,12 @@ fn test_spend_coinbase() { .current_height(not_yet_mature_time); assert_matches!( builder.finish(), - Err(CreateTxError::Bdk(Error::InsufficientFunds { - needed: _, - available: 0 - })) + Err(CreateTxError::CoinSelection( + coin_selection::Error::InsufficientFunds { + needed: _, + available: 0 + } + )) ); wallet @@ -3353,7 +3353,7 @@ fn test_allow_dust_limit() { assert_matches!( builder.finish(), - Err(CreateTxError::Bdk(Error::OutputBelowDustLimit(0))) + Err(CreateTxError::OutputBelowDustLimit(0)) ); let mut builder = wallet.build_tx();