diff --git a/crates/bdk/src/wallet/coin_selection.rs b/crates/bdk/src/wallet/coin_selection.rs index c3e84af2ba..10309a6602 100644 --- a/crates/bdk/src/wallet/coin_selection.rs +++ b/crates/bdk/src/wallet/coin_selection.rs @@ -26,7 +26,8 @@ //! ``` //! # use std::str::FromStr; //! # use bitcoin::*; -//! # use bdk::wallet::{self, coin_selection::*}; +//! # use bdk::wallet::{self, ChangeSet, coin_selection::*, CreateTxError}; +//! # use bdk_chain::PersistBackend; //! # use bdk::*; //! # use bdk::wallet::coin_selection::decide_change; //! # const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4; @@ -94,7 +95,7 @@ //! //! // inspect, sign, broadcast, ... //! -//! # Ok::<(), bdk::Error>(()) +//! # Ok::<(), CreateTxError<<() as PersistBackend>::WriteError>>(()) //! ``` use crate::types::FeeRate; diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index 659da90a26..472369d980 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -56,16 +56,17 @@ pub mod hardwaresigner; pub use utils::IsDust; +use crate::descriptor; #[allow(deprecated)] use coin_selection::DefaultCoinSelectionAlgorithm; use signer::{SignOptions, SignerOrdering, SignersContainer, TransactionSigner}; use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams}; use utils::{check_nsequence_rbf, After, Older, SecpCtx}; -use crate::descriptor::policy::BuildSatisfaction; +use crate::descriptor::policy::{BuildSatisfaction, PolicyError}; use crate::descriptor::{ - calc_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DescriptorMeta, - ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, Policy, XKeyUtils, + calc_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DescriptorError, + DescriptorMeta, ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, Policy, XKeyUtils, }; use crate::error::{Error, MiniscriptPsbtError}; use crate::psbt::PsbtUtils; @@ -265,6 +266,61 @@ pub enum InsertTxError { #[cfg(feature = "std")] impl std::error::Error for NewError

{} +#[derive(Debug)] +/// Error returned from [`TxBuilder::finish`] +pub enum CreateTxError

{ + /// There was a problem with the descriptors passed in + Descriptor(DescriptorError), + /// We were unable to write wallet data to the persistence backend + Persist(P), + /// There was a problem while extracting and manipulating policies + Policy(PolicyError), + /// TODO: replace this with specific error types + Bdk(Error), +} + +#[cfg(feature = "std")] +impl

fmt::Display for CreateTxError

+where + P: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Descriptor(e) => e.fmt(f), + Self::Persist(e) => { + write!( + f, + "failed to write wallet data to persistence backend: {}", + e + ) + } + Self::Bdk(e) => e.fmt(f), + Self::Policy(e) => e.fmt(f), + } + } +} + +impl

From for CreateTxError

{ + fn from(err: descriptor::error::Error) -> Self { + CreateTxError::Descriptor(err) + } +} + +impl

From for CreateTxError

{ + fn from(err: PolicyError) -> Self { + CreateTxError::Policy(err) + } +} + +impl

From for CreateTxError

{ + fn from(err: Error) -> Self { + CreateTxError::Bdk(err) + } +} + +#[cfg(feature = "std")] +impl std::error::Error for CreateTxError

{} + impl Wallet { /// Create a wallet from a `descriptor` (and an optional `change_descriptor`) and load related /// transaction data from `db`. @@ -823,6 +879,8 @@ impl Wallet { /// # use std::str::FromStr; /// # use bitcoin::*; /// # use bdk::*; + /// # use bdk::wallet::{ChangeSet,CreateTxError}; + /// # use bdk_chain::PersistBackend; /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; /// # let mut wallet = doctest_wallet!(); /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); @@ -834,7 +892,7 @@ impl Wallet { /// }; /// /// // sign and broadcast ... - /// # Ok::<(), bdk::Error>(()) + /// # Ok::<(), CreateTxError<<() as PersistBackend>::WriteError>>(()) /// ``` /// /// [`TxBuilder`]: crate::TxBuilder @@ -886,7 +944,7 @@ impl Wallet { && external_policy.requires_path() && params.external_policy_path.is_none() { - return Err(Error::SpendingPolicyRequired(KeychainKind::External)); + return Err(Error::SpendingPolicyRequired(KeychainKind::External).into()); }; // Same for the internal_policy path, if present if let Some(internal_policy) = &internal_policy { @@ -894,7 +952,7 @@ impl Wallet { && internal_policy.requires_path() && params.internal_policy_path.is_none() { - return Err(Error::SpendingPolicyRequired(KeychainKind::Internal)); + return Err(Error::SpendingPolicyRequired(KeychainKind::Internal).into()); }; } @@ -923,13 +981,14 @@ impl Wallet { let version = match params.version { Some(tx_builder::Version(0)) => { - return Err(Error::Generic("Invalid version `0`".into())) + return Err(Error::Generic("Invalid version `0`".into()).into()) } 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()) } Some(tx_builder::Version(x)) => x, None if requirements.csv.is_some() => 2, @@ -971,7 +1030,7 @@ impl Wallet { // 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, // 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()))) + 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()) }; let n_sequence = match (params.rbf, requirements.csv) { @@ -991,7 +1050,8 @@ impl Wallet { (Some(tx_builder::RbfValue::Value(rbf)), _) if !rbf.is_rbf() => { return Err(Error::Generic( "Cannot enable RBF with a nSequence >= 0xFFFFFFFE".into(), - )) + ) + .into()) } // RBF with a specific value requested, but the value is incompatible with CSV (Some(tx_builder::RbfValue::Value(rbf)), Some(csv)) @@ -1000,7 +1060,8 @@ impl Wallet { return Err(Error::Generic(format!( "Cannot enable RBF with nSequence `{:?}` given a required OP_CSV of `{:?}`", rbf, csv - ))) + )) + .into()) } // RBF enabled with the default value with CSV also enabled. CSV takes precedence @@ -1021,7 +1082,8 @@ impl Wallet { if *fee < previous_fee.absolute { return Err(Error::FeeTooLow { required: previous_fee.absolute, - }); + } + .into()); } } (FeeRate::from_sat_per_vb(0.0), *fee) @@ -1032,7 +1094,8 @@ impl Wallet { if *rate < required_feerate { return Err(Error::FeeRateTooLow { required: required_feerate, - }); + } + .into()); } } (*rate, 0) @@ -1047,7 +1110,7 @@ impl Wallet { }; if params.manually_selected_only && params.utxos.is_empty() { - return Err(Error::NoUtxosSelected); + return Err(Error::NoUtxosSelected.into()); } // we keep it as a float while we accumulate it, and only round it at the end @@ -1061,7 +1124,7 @@ impl Wallet { && value.is_dust(script_pubkey) && !script_pubkey.is_provably_unspendable() { - return Err(Error::OutputBelowDustLimit(index)); + return Err(Error::OutputBelowDustLimit(index).into()); } if self.is_mine(script_pubkey) { @@ -1096,7 +1159,8 @@ impl Wallet { { return Err(Error::Generic( "The `change_policy` can be set only if the wallet has a change_descriptor".into(), - )); + ) + .into()); } let (required_utxos, optional_utxos) = self.preselect_utxos( @@ -1122,7 +1186,7 @@ impl Wallet { .stage(ChangeSet::from(indexed_tx_graph::ChangeSet::from( index_changeset, ))); - self.persist.commit().expect("TODO"); + self.persist.commit().map_err(CreateTxError::Persist)?; spk } }; @@ -1166,10 +1230,11 @@ impl Wallet { return Err(Error::InsufficientFunds { needed: *dust_threshold, available: remaining_amount.saturating_sub(*change_fee), - }); + } + .into()); } } else { - return Err(Error::NoRecipients); + return Err(Error::NoRecipients.into()); } } @@ -1216,6 +1281,8 @@ impl Wallet { /// # use std::str::FromStr; /// # use bitcoin::*; /// # use bdk::*; + /// # use bdk::wallet::{ChangeSet, CreateTxError}; + /// # use bdk_chain::PersistBackend; /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; /// # let mut wallet = doctest_wallet!(); /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); @@ -1239,7 +1306,7 @@ impl Wallet { /// let _ = wallet.sign(&mut psbt, SignOptions::default())?; /// let fee_bumped_tx = psbt.extract_tx(); /// // broadcast fee_bumped_tx to replace original - /// # Ok::<(), bdk::Error>(()) + /// # Ok::<(), CreateTxError<<() as PersistBackend>::WriteError>>(()) /// ``` // TODO: support for merging multiple transactions while bumping the fees pub fn build_fee_bump( @@ -1386,6 +1453,8 @@ impl Wallet { /// # use std::str::FromStr; /// # use bitcoin::*; /// # use bdk::*; + /// # use bdk::wallet::{ChangeSet, CreateTxError}; + /// # use bdk_chain::PersistBackend; /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; /// # let mut wallet = doctest_wallet!(); /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); @@ -1396,7 +1465,7 @@ impl Wallet { /// }; /// let finalized = wallet.sign(&mut psbt, SignOptions::default())?; /// assert!(finalized, "we should have signed all the inputs"); - /// # Ok::<(), bdk::Error>(()) + /// # Ok::<(), CreateTxError<<() as PersistBackend>::WriteError>>(()) pub fn sign( &self, psbt: &mut psbt::PartiallySignedTransaction, diff --git a/crates/bdk/src/wallet/tx_builder.rs b/crates/bdk/src/wallet/tx_builder.rs index 37e85a1240..be8a7d7da7 100644 --- a/crates/bdk/src/wallet/tx_builder.rs +++ b/crates/bdk/src/wallet/tx_builder.rs @@ -17,8 +17,10 @@ //! # use std::str::FromStr; //! # use bitcoin::*; //! # use bdk::*; +//! # use bdk::wallet::{ChangeSet, CreateTxError}; //! # use bdk::wallet::tx_builder::CreateTx; //! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); +//! # use bdk_chain::PersistBackend; //! # let mut wallet = doctest_wallet!(); //! // create a TxBuilder from a wallet //! let mut tx_builder = wallet.build_tx(); @@ -33,7 +35,7 @@ //! // Turn on RBF signaling //! .enable_rbf(); //! let psbt = tx_builder.finish()?; -//! # Ok::<(), bdk::Error>(()) +//! # Ok::<(), CreateTxError<<() as PersistBackend>::WriteError>>(()) //! ``` use crate::collections::BTreeMap; @@ -48,6 +50,7 @@ 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::{Error, Utxo, Wallet}; /// Context in which the [`TxBuilder`] is valid @@ -78,6 +81,8 @@ impl TxBuilderContext for BumpFee {} /// # use bdk::wallet::tx_builder::*; /// # use bitcoin::*; /// # use core::str::FromStr; +/// # use bdk::wallet::{ChangeSet, CreateTxError}; +/// # use bdk_chain::PersistBackend; /// # let mut wallet = doctest_wallet!(); /// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); /// # let addr2 = addr1.clone(); @@ -102,7 +107,7 @@ impl TxBuilderContext for BumpFee {} /// }; /// /// assert_eq!(psbt1.unsigned_tx.output[..2], psbt2.unsigned_tx.output[..2]); -/// # Ok::<(), bdk::Error>(()) +/// # Ok::<(), CreateTxError<<() as PersistBackend>::WriteError>>(()) /// ``` /// /// At the moment [`coin_selection`] is an exception to the rule as it consumes `self`. @@ -540,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, { @@ -639,7 +644,9 @@ impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> { /// # use std::str::FromStr; /// # use bitcoin::*; /// # use bdk::*; + /// # use bdk::wallet::{ChangeSet, CreateTxError}; /// # use bdk::wallet::tx_builder::CreateTx; + /// # use bdk_chain::PersistBackend; /// # let to_address = /// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt") /// .unwrap() @@ -655,7 +662,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> { /// .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0)) /// .enable_rbf(); /// let psbt = tx_builder.finish()?; - /// # Ok::<(), bdk::Error>(()) + /// # Ok::<(), CreateTxError<<() as PersistBackend>::WriteError>>(()) /// ``` /// /// [`allow_shrinking`]: Self::allow_shrinking diff --git a/crates/bdk/tests/wallet.rs b/crates/bdk/tests/wallet.rs index aad8c2db25..0ade431be0 100644 --- a/crates/bdk/tests/wallet.rs +++ b/crates/bdk/tests/wallet.rs @@ -4,7 +4,7 @@ use bdk::psbt::PsbtUtils; use bdk::signer::{SignOptions, SignerError}; use bdk::wallet::coin_selection::LargestFirstCoinSelection; use bdk::wallet::AddressIndex::*; -use bdk::wallet::{AddressIndex, AddressInfo, Balance, Wallet}; +use bdk::wallet::{AddressIndex, AddressInfo, Balance, CreateTxError, Wallet}; use bdk::{Error, FeeRate, KeychainKind}; use bdk_chain::COINBASE_MATURITY; use bdk_chain::{BlockId, ConfirmationTime}; @@ -3299,10 +3299,10 @@ fn test_spend_coinbase() { .current_height(confirmation_height); assert!(matches!( builder.finish(), - Err(Error::InsufficientFunds { + Err(CreateTxError::Bdk(Error::InsufficientFunds { needed: _, available: 0 - }) + })) )); // Still unspendable... @@ -3312,10 +3312,10 @@ fn test_spend_coinbase() { .current_height(not_yet_mature_time); assert_matches!( builder.finish(), - Err(Error::InsufficientFunds { + Err(CreateTxError::Bdk(Error::InsufficientFunds { needed: _, available: 0 - }) + })) ); wallet @@ -3351,7 +3351,10 @@ fn test_allow_dust_limit() { builder.add_recipient(addr.script_pubkey(), 0); - assert_matches!(builder.finish(), Err(Error::OutputBelowDustLimit(0))); + assert_matches!( + builder.finish(), + Err(CreateTxError::Bdk(Error::OutputBelowDustLimit(0))) + ); let mut builder = wallet.build_tx();