]> Untitled Git - bdk/commitdiff
refactor(bdk)!: add context specific error types, remove top level error mod
authorSteve Myers <steve@notmandatory.org>
Thu, 16 Nov 2023 16:22:37 +0000 (10:22 -0600)
committerSteve Myers <steve@notmandatory.org>
Thu, 16 Nov 2023 16:24:35 +0000 (10:24 -0600)
refactor(bdk)!: remove impl_error macro
refactor(wallet)!: add MiniscriptPsbtError, CreateTxError, BuildFeeBumpError error enums
refactor(coin_selection)!: add module Error enum
test(bdk): use anyhow dev-dependency for all tests

20 files changed:
crates/bdk/Cargo.toml
crates/bdk/examples/mnemonic_to_descriptors.rs
crates/bdk/src/descriptor/error.rs
crates/bdk/src/descriptor/policy.rs
crates/bdk/src/error.rs [deleted file]
crates/bdk/src/keys/mod.rs
crates/bdk/src/lib.rs
crates/bdk/src/wallet/coin_selection.rs
crates/bdk/src/wallet/error.rs [new file with mode: 0644]
crates/bdk/src/wallet/mod.rs
crates/bdk/src/wallet/signer.rs
crates/bdk/src/wallet/tx_builder.rs
crates/bdk/tests/wallet.rs
crates/chain/src/tx_graph.rs
example-crates/wallet_electrum/Cargo.toml
example-crates/wallet_electrum/src/main.rs
example-crates/wallet_esplora_async/Cargo.toml
example-crates/wallet_esplora_async/src/main.rs
example-crates/wallet_esplora_blocking/Cargo.toml
example-crates/wallet_esplora_blocking/src/main.rs

index 8c519d8916a94d7c6ff4ce88ceb3880f7c0e497d..17efd65c63b28b68bfed836eb06631202e0a9404 100644 (file)
@@ -49,6 +49,7 @@ env_logger = "0.7"
 assert_matches = "1.5.0"
 tempfile = "3"
 bdk_file_store = { path = "../file_store" }
+anyhow = "1"
 
 [package.metadata.docs.rs]
 all-features = true
index 7d2dd6013ffba96c769e2c4167603c301c6e9937..4e1d5061d45569a875df996df13ba81273b6838e 100644 (file)
@@ -6,6 +6,7 @@
 // You may not use this file except in accordance with one or both of these
 // licenses.
 
+use anyhow::anyhow;
 use bdk::bitcoin::bip32::DerivationPath;
 use bdk::bitcoin::secp256k1::Secp256k1;
 use bdk::bitcoin::Network;
@@ -14,13 +15,11 @@ use bdk::descriptor::IntoWalletDescriptor;
 use bdk::keys::bip39::{Language, Mnemonic, WordCount};
 use bdk::keys::{GeneratableKey, GeneratedKey};
 use bdk::miniscript::Tap;
-use bdk::Error as BDK_Error;
-use std::error::Error;
 use std::str::FromStr;
 
 /// This example demonstrates how to generate a mnemonic phrase
 /// using BDK and use that to generate a descriptor string.
-fn main() -> Result<(), Box<dyn Error>> {
+fn main() -> Result<(), anyhow::Error> {
     let secp = Secp256k1::new();
 
     // In this example we are generating a 12 words mnemonic phrase
@@ -28,7 +27,7 @@ fn main() -> Result<(), Box<dyn Error>> {
     // using their respective `WordCount` variant.
     let mnemonic: GeneratedKey<_, Tap> =
         Mnemonic::generate((WordCount::Words12, Language::English))
-            .map_err(|_| BDK_Error::Generic("Mnemonic generation error".to_string()))?;
+            .map_err(|_| anyhow!("Mnemonic generation error"))?;
 
     println!("Mnemonic phrase: {}", *mnemonic);
     let mnemonic_with_passphrase = (mnemonic, None);
index 07a874efed93a54a2e5feb81a250b208be004ef1..b36e69e6334697e6595d8fbb69740b25711836df 100644 (file)
@@ -10,7 +10,6 @@
 // licenses.
 
 //! Descriptor errors
-
 use core::fmt;
 
 /// Errors related to the parsing and usage of descriptors
@@ -87,9 +86,38 @@ impl fmt::Display for Error {
 #[cfg(feature = "std")]
 impl std::error::Error for Error {}
 
-impl_error!(bitcoin::bip32::Error, Bip32);
-impl_error!(bitcoin::base58::Error, Base58);
-impl_error!(bitcoin::key::Error, Pk);
-impl_error!(miniscript::Error, Miniscript);
-impl_error!(bitcoin::hashes::hex::Error, Hex);
-impl_error!(crate::descriptor::policy::PolicyError, Policy);
+impl From<bitcoin::bip32::Error> for Error {
+    fn from(err: bitcoin::bip32::Error) -> Self {
+        Error::Bip32(err)
+    }
+}
+
+impl From<bitcoin::base58::Error> for Error {
+    fn from(err: bitcoin::base58::Error) -> Self {
+        Error::Base58(err)
+    }
+}
+
+impl From<bitcoin::key::Error> for Error {
+    fn from(err: bitcoin::key::Error) -> Self {
+        Error::Pk(err)
+    }
+}
+
+impl From<miniscript::Error> for Error {
+    fn from(err: miniscript::Error) -> Self {
+        Error::Miniscript(err)
+    }
+}
+
+impl From<bitcoin::hashes::hex::Error> for Error {
+    fn from(err: bitcoin::hashes::hex::Error) -> Self {
+        Error::Hex(err)
+    }
+}
+
+impl From<crate::descriptor::policy::PolicyError> for Error {
+    fn from(err: crate::descriptor::policy::PolicyError) -> Self {
+        Error::Policy(err)
+    }
+}
index 008bbe9aedfa39c3c44413ee6b5e52df014275fa..29a0d10298933fbfa43d1f21cfd78072cb872008 100644 (file)
 //! let signers = Arc::new(SignersContainer::build(key_map, &extended_desc, &secp));
 //! let policy = extended_desc.extract_policy(&signers, BuildSatisfaction::None, &secp)?;
 //! println!("policy: {}", serde_json::to_string(&policy).unwrap());
-//! # Ok::<(), bdk::Error>(())
+//! # Ok::<(), anyhow::Error>(())
 //! ```
 
 use crate::collections::{BTreeMap, HashSet, VecDeque};
 use alloc::string::String;
 use alloc::vec::Vec;
 use core::cmp::max;
+
 use core::fmt;
 
 use serde::ser::SerializeMap;
diff --git a/crates/bdk/src/error.rs b/crates/bdk/src/error.rs
deleted file mode 100644 (file)
index fcb5a6f..0000000
+++ /dev/null
@@ -1,201 +0,0 @@
-// Bitcoin Dev Kit
-// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
-//
-// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
-//
-// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
-// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
-// You may not use this file except in accordance with one or both of these
-// licenses.
-
-use crate::bitcoin::Network;
-use crate::{descriptor, wallet};
-use alloc::{string::String, vec::Vec};
-use bitcoin::{OutPoint, Txid};
-use core::fmt;
-
-/// Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
-#[derive(Debug)]
-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
-    TransactionNotFound,
-    /// Happens when trying to bump a transaction that is already confirmed
-    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)
-    InvalidOutpoint(OutPoint),
-    /// Error related to the parsing and usage of descriptors
-    Descriptor(crate::descriptor::error::Error),
-    /// Miniscript error
-    Miniscript(miniscript::Error),
-    /// Miniscript PSBT error
-    MiniscriptPsbt(MiniscriptPsbtError),
-    /// BIP32 error
-    Bip32(bitcoin::bip32::Error),
-    /// Partially signed bitcoin transaction error
-    Psbt(bitcoin::psbt::Error),
-}
-
-/// Errors returned by miniscript when updating inconsistent PSBTs
-#[derive(Debug, Clone)]
-pub enum MiniscriptPsbtError {
-    Conversion(miniscript::descriptor::ConversionError),
-    UtxoUpdate(miniscript::psbt::UtxoUpdateError),
-    OutputUpdate(miniscript::psbt::OutputUpdateError),
-}
-
-impl fmt::Display for MiniscriptPsbtError {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        match self {
-            Self::Conversion(err) => write!(f, "Conversion error: {}", err),
-            Self::UtxoUpdate(err) => write!(f, "UTXO update error: {}", err),
-            Self::OutputUpdate(err) => write!(f, "Output update error: {}", err),
-        }
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for MiniscriptPsbtError {}
-
-#[cfg(feature = "std")]
-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,
-                "Requested outpoint doesn't exist in the tx: {}",
-                outpoint
-            ),
-            Self::Descriptor(err) => write!(f, "Descriptor error: {}", err),
-            Self::Miniscript(err) => write!(f, "Miniscript error: {}", err),
-            Self::MiniscriptPsbt(err) => write!(f, "Miniscript PSBT error: {}", err),
-            Self::Bip32(err) => write!(f, "BIP32 error: {}", err),
-            Self::Psbt(err) => write!(f, "PSBT error: {}", err),
-        }
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for Error {}
-
-macro_rules! impl_error {
-    ( $from:ty, $to:ident ) => {
-        impl_error!($from, $to, Error);
-    };
-    ( $from:ty, $to:ident, $impl_for:ty ) => {
-        impl core::convert::From<$from> for $impl_for {
-            fn from(err: $from) -> Self {
-                <$impl_for>::$to(err)
-            }
-        }
-    };
-}
-
-impl_error!(descriptor::error::Error, Descriptor);
-impl_error!(descriptor::policy::PolicyError, InvalidPolicyPathError);
-impl_error!(wallet::signer::SignerError, Signer);
-
-impl From<crate::keys::KeyError> for Error {
-    fn from(key_error: crate::keys::KeyError) -> Error {
-        match key_error {
-            crate::keys::KeyError::Miniscript(inner) => Error::Miniscript(inner),
-            crate::keys::KeyError::Bip32(inner) => Error::Bip32(inner),
-            crate::keys::KeyError::InvalidChecksum => Error::ChecksumMismatch,
-            e => Error::Key(e),
-        }
-    }
-}
-
-impl_error!(miniscript::Error, Miniscript);
-impl_error!(MiniscriptPsbtError, MiniscriptPsbt);
-impl_error!(bitcoin::bip32::Error, Bip32);
-impl_error!(bitcoin::psbt::Error, Psbt);
index b47c4b86d47ef7f7bbefb7dcf36954c3c24c897f..541d439a62f4c7fea339fef85282bb9df5c804d7 100644 (file)
@@ -413,7 +413,7 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
 /// }
 /// ```
 ///
-/// Types that don't internally encode the [`Network`](bitcoin::Network) in which they are valid need some extra
+/// Types that don't internally encode the [`Network`] in which they are valid need some extra
 /// steps to override the set of valid networks, otherwise only the network specified in the
 /// [`ExtendedPrivKey`] or [`ExtendedPubKey`] will be considered valid.
 ///
@@ -932,8 +932,17 @@ pub enum KeyError {
     Miniscript(miniscript::Error),
 }
 
-impl_error!(miniscript::Error, Miniscript, KeyError);
-impl_error!(bitcoin::bip32::Error, Bip32, KeyError);
+impl From<miniscript::Error> for KeyError {
+    fn from(err: miniscript::Error) -> Self {
+        KeyError::Miniscript(err)
+    }
+}
+
+impl From<bip32::Error> for KeyError {
+    fn from(err: bip32::Error) -> Self {
+        KeyError::Bip32(err)
+    }
+}
 
 impl fmt::Display for KeyError {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
index 012a868a6179229a25bed09d8be0bb839a46f0c3..3f0c7a26263da5add67a616b849f148843b177e8 100644 (file)
@@ -27,9 +27,6 @@ extern crate serde_json;
 #[cfg(feature = "keys-bip39")]
 extern crate bip39;
 
-#[allow(unused_imports)]
-#[macro_use]
-pub(crate) mod error;
 pub mod descriptor;
 pub mod keys;
 pub mod psbt;
@@ -38,7 +35,6 @@ pub mod wallet;
 
 pub use descriptor::template;
 pub use descriptor::HdKeyPaths;
-pub use error::Error;
 pub use types::*;
 pub use wallet::signer;
 pub use wallet::signer::SignOptions;
index a0179d31b3681e8f55d0343202ebbbf2baf55004..a29456fa8324d09b1e6bbf5f921f243ee72648d3 100644 (file)
 //! ```
 //! # use std::str::FromStr;
 //! # use bitcoin::*;
-//! # use bdk::wallet::{self, coin_selection::*};
+//! # use bdk::wallet::{self, ChangeSet, coin_selection::*, coin_selection};
+//! # use bdk::wallet::error::CreateTxError;
+//! # use bdk_chain::PersistBackend;
 //! # use bdk::*;
 //! # use bdk::wallet::coin_selection::decide_change;
+//! # use anyhow::Error;
 //! # const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4;
 //! #[derive(Debug)]
 //! struct AlwaysSpendEverything;
@@ -41,7 +44,7 @@
 //!         fee_rate: bdk::FeeRate,
 //!         target_amount: u64,
 //!         drain_script: &Script,
-//!     ) -> Result<CoinSelectionResult, bdk::Error> {
+//!     ) -> Result<CoinSelectionResult, coin_selection::Error> {
 //!         let mut selected_amount = 0;
 //!         let mut additional_weight = Weight::ZERO;
 //!         let all_utxos_selected = required_utxos
@@ -61,7 +64,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,
 //!             });
 //!
 //! // inspect, sign, broadcast, ...
 //!
-//! # Ok::<(), bdk::Error>(())
+//! # Ok::<(), anyhow::Error>(())
 //! ```
 
 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::{self, Formatter};
 use rand::seq::SliceRandom;
 
 /// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not
@@ -117,6 +121,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/error.rs b/crates/bdk/src/wallet/error.rs
new file mode 100644 (file)
index 0000000..db58fef
--- /dev/null
@@ -0,0 +1,292 @@
+// Bitcoin Dev Kit
+// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
+//
+// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
+
+use crate::descriptor::policy::PolicyError;
+use crate::descriptor::DescriptorError;
+use crate::wallet::coin_selection;
+use crate::{descriptor, FeeRate, KeychainKind};
+use alloc::string::String;
+use bitcoin::{absolute, psbt, OutPoint, Sequence, Txid};
+use core::fmt;
+
+/// Errors returned by miniscript when updating inconsistent PSBTs
+#[derive(Debug, Clone)]
+pub enum MiniscriptPsbtError {
+    /// Descriptor key conversion error
+    Conversion(miniscript::descriptor::ConversionError),
+    /// Return error type for PsbtExt::update_input_with_descriptor
+    UtxoUpdate(miniscript::psbt::UtxoUpdateError),
+    /// Return error type for PsbtExt::update_output_with_descriptor
+    OutputUpdate(miniscript::psbt::OutputUpdateError),
+}
+
+impl fmt::Display for MiniscriptPsbtError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Conversion(err) => write!(f, "Conversion error: {}", err),
+            Self::UtxoUpdate(err) => write!(f, "UTXO update error: {}", err),
+            Self::OutputUpdate(err) => write!(f, "Output update error: {}", err),
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for MiniscriptPsbtError {}
+
+#[derive(Debug)]
+/// Error returned from [`TxBuilder::finish`]
+///
+/// [`TxBuilder::finish`]: crate::wallet::tx_builder::TxBuilder::finish
+pub enum CreateTxError<P> {
+    /// 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),
+    /// Spending policy is not compatible with this [`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),
+}
+
+impl<P> fmt::Display for CreateTxError<P>
+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::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)
+            }
+        }
+    }
+}
+
+impl<P> From<descriptor::error::Error> for CreateTxError<P> {
+    fn from(err: descriptor::error::Error) -> Self {
+        CreateTxError::Descriptor(err)
+    }
+}
+
+impl<P> From<PolicyError> for CreateTxError<P> {
+    fn from(err: PolicyError) -> Self {
+        CreateTxError::Policy(err)
+    }
+}
+
+impl<P> From<MiniscriptPsbtError> for CreateTxError<P> {
+    fn from(err: MiniscriptPsbtError) -> Self {
+        CreateTxError::MiniscriptPsbt(err)
+    }
+}
+
+impl<P> From<psbt::Error> for CreateTxError<P> {
+    fn from(err: psbt::Error) -> Self {
+        CreateTxError::Psbt(err)
+    }
+}
+
+impl<P> From<coin_selection::Error> for CreateTxError<P> {
+    fn from(err: coin_selection::Error) -> Self {
+        CreateTxError::CoinSelection(err)
+    }
+}
+
+#[cfg(feature = "std")]
+impl<P: core::fmt::Display + core::fmt::Debug> std::error::Error for CreateTxError<P> {}
+
+#[derive(Debug)]
+/// Error returned from [`Wallet::build_fee_bump`]
+///
+/// [`Wallet::build_fee_bump`]: super::Wallet::build_fee_bump
+pub enum BuildFeeBumpError {
+    /// Happens when trying to spend an UTXO that is not in the internal database
+    UnknownUtxo(OutPoint),
+    /// Thrown when a tx is not found in the internal database
+    TransactionNotFound(Txid),
+    /// Happens when trying to bump a transaction that is already confirmed
+    TransactionConfirmed(Txid),
+    /// Trying to replace a tx that has a sequence >= `0xFFFFFFFE`
+    IrreplaceableTransaction(Txid),
+    /// Node doesn't have data to estimate a fee rate
+    FeeRateUnavailable,
+}
+
+impl fmt::Display for BuildFeeBumpError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::UnknownUtxo(outpoint) => write!(
+                f,
+                "UTXO not found in the internal database with txid: {}, vout: {}",
+                outpoint.txid, outpoint.vout
+            ),
+            Self::TransactionNotFound(txid) => {
+                write!(
+                    f,
+                    "Transaction not found in the internal database with txid: {}",
+                    txid
+                )
+            }
+            Self::TransactionConfirmed(txid) => {
+                write!(f, "Transaction already confirmed with txid: {}", txid)
+            }
+            Self::IrreplaceableTransaction(txid) => {
+                write!(f, "Transaction can't be replaced with txid: {}", txid)
+            }
+            Self::FeeRateUnavailable => write!(f, "Fee rate unavailable"),
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for BuildFeeBumpError {}
index d3bc116e26e7c81a678d201119a7f53596facf23..005d75f9e79a12b914db9baac91d1d5f0e2b6047 100644 (file)
@@ -38,6 +38,7 @@ use bitcoin::{consensus::encode::serialize, BlockHash};
 use bitcoin::{constants::genesis_block, psbt};
 use core::fmt;
 use core::ops::Deref;
+use descriptor::error::Error as DescriptorError;
 use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier};
 
 use bdk_chain::tx_graph::CalculateFeeError;
@@ -50,6 +51,7 @@ pub mod signer;
 pub mod tx_builder;
 pub(crate) mod utils;
 
+pub mod error;
 #[cfg(feature = "hardware-signer")]
 #[cfg_attr(docsrs, doc(cfg(feature = "hardware-signer")))]
 pub mod hardwaresigner;
@@ -64,14 +66,14 @@ use utils::{check_nsequence_rbf, After, Older, SecpCtx};
 
 use crate::descriptor::policy::BuildSatisfaction;
 use crate::descriptor::{
-    calc_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DescriptorMeta,
+    self, calc_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DescriptorMeta,
     ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, Policy, XKeyUtils,
 };
-use crate::error::{Error, MiniscriptPsbtError};
 use crate::psbt::PsbtUtils;
 use crate::signer::SignerError;
 use crate::types::*;
 use crate::wallet::coin_selection::Excess::{Change, NoChange};
+use crate::wallet::error::{BuildFeeBumpError, CreateTxError, MiniscriptPsbtError};
 
 const COINBASE_MATURITY: u32 = 100;
 
@@ -235,7 +237,7 @@ impl Wallet {
         descriptor: E,
         change_descriptor: Option<E>,
         network: Network,
-    ) -> Result<Self, crate::descriptor::DescriptorError> {
+    ) -> Result<Self, DescriptorError> {
         Self::new(descriptor, change_descriptor, (), network).map_err(|e| match e {
             NewError::Descriptor(e) => e,
             NewError::Write(_) => unreachable!("mock-write must always succeed"),
@@ -1092,6 +1094,10 @@ impl<D> Wallet<D> {
     /// # use std::str::FromStr;
     /// # use bitcoin::*;
     /// # use bdk::*;
+    /// # use bdk::wallet::ChangeSet;
+    /// # use bdk::wallet::error::CreateTxError;
+    /// # use bdk_chain::PersistBackend;
+    /// # use anyhow::Error;
     /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
     /// # let mut wallet = doctest_wallet!();
     /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
@@ -1103,7 +1109,7 @@ impl<D> Wallet<D> {
     /// };
     ///
     /// // sign and broadcast ...
-    /// # Ok::<(), bdk::Error>(())
+    /// # Ok::<(), anyhow::Error>(())
     /// ```
     ///
     /// [`TxBuilder`]: crate::TxBuilder
@@ -1120,7 +1126,7 @@ impl<D> Wallet<D> {
         &mut self,
         coin_selection: Cs,
         params: TxParams,
-    ) -> Result<psbt::PartiallySignedTransaction, Error>
+    ) -> Result<psbt::PartiallySignedTransaction, CreateTxError<D::WriteError>>
     where
         D: PersistBackend<ChangeSet>,
     {
@@ -1142,7 +1148,7 @@ impl<D> Wallet<D> {
         let internal_policy = internal_descriptor
             .as_ref()
             .map(|desc| {
-                Ok::<_, Error>(
+                Ok::<_, CreateTxError<D::WriteError>>(
                     desc.extract_policy(&self.change_signers, BuildSatisfaction::None, &self.secp)?
                         .unwrap(),
                 )
@@ -1155,7 +1161,9 @@ impl<D> Wallet<D> {
             && external_policy.requires_path()
             && params.external_policy_path.is_none()
         {
-            return Err(Error::SpendingPolicyRequired(KeychainKind::External));
+            return Err(CreateTxError::SpendingPolicyRequired(
+                KeychainKind::External,
+            ));
         };
         // Same for the internal_policy path, if present
         if let Some(internal_policy) = &internal_policy {
@@ -1163,7 +1171,9 @@ impl<D> Wallet<D> {
                 && internal_policy.requires_path()
                 && params.internal_policy_path.is_none()
             {
-                return Err(Error::SpendingPolicyRequired(KeychainKind::Internal));
+                return Err(CreateTxError::SpendingPolicyRequired(
+                    KeychainKind::Internal,
+                ));
             };
         }
 
@@ -1175,7 +1185,7 @@ impl<D> Wallet<D> {
         )?;
         let internal_requirements = internal_policy
             .map(|policy| {
-                Ok::<_, Error>(
+                Ok::<_, CreateTxError<D::WriteError>>(
                     policy.get_condition(
                         params
                             .internal_policy_path
@@ -1191,14 +1201,9 @@ impl<D> Wallet<D> {
         debug!("Policy requirements: {:?}", requirements);
 
         let version = match params.version {
-            Some(tx_builder::Version(0)) => {
-                return Err(Error::Generic("Invalid version `0`".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(),
-                ))
+                return Err(CreateTxError::Version1Csv)
             }
             Some(tx_builder::Version(x)) => x,
             None if requirements.csv.is_some() => 2,
@@ -1229,7 +1234,9 @@ impl<D> Wallet<D> {
                     // 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,
@@ -1238,9 +1245,19 @@ impl<D> Wallet<D> {
             // 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())))
+            Some(x) => {
+                return Err(CreateTxError::LockTime {
+                    requested: x,
+                    required: requirements.timelock.unwrap(),
+                })
+            }
         };
 
         let n_sequence = match (params.rbf, requirements.csv) {
@@ -1258,18 +1275,13 @@ impl<D> Wallet<D> {
 
             // 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(),
-                ))
+                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
-                )))
+                return Err(CreateTxError::RbfSequenceCsv { rbf, csv })
             }
 
             // RBF enabled with the default value with CSV also enabled. CSV takes precedence
@@ -1288,7 +1300,7 @@ impl<D> Wallet<D> {
             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,
                         });
                     }
@@ -1299,7 +1311,7 @@ impl<D> Wallet<D> {
                 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,
                         });
                     }
@@ -1316,7 +1328,7 @@ impl<D> Wallet<D> {
         };
 
         if params.manually_selected_only && params.utxos.is_empty() {
-            return Err(Error::NoUtxosSelected);
+            return Err(CreateTxError::NoUtxosSelected);
         }
 
         // we keep it as a float while we accumulate it, and only round it at the end
@@ -1330,7 +1342,7 @@ impl<D> Wallet<D> {
                 && value.is_dust(script_pubkey)
                 && !script_pubkey.is_provably_unspendable()
             {
-                return Err(Error::OutputBelowDustLimit(index));
+                return Err(CreateTxError::OutputBelowDustLimit(index));
             }
 
             if self.is_mine(script_pubkey) {
@@ -1363,9 +1375,7 @@ impl<D> Wallet<D> {
         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(),
-            ));
+            return Err(CreateTxError::ChangePolicyDescriptor);
         }
 
         let (required_utxos, optional_utxos) = self.preselect_utxos(
@@ -1391,7 +1401,7 @@ impl<D> Wallet<D> {
                     .stage(ChangeSet::from(indexed_tx_graph::ChangeSet::from(
                         index_changeset,
                     )));
-                self.persist.commit().expect("TODO");
+                self.persist.commit().map_err(CreateTxError::Persist)?;
                 spk
             }
         };
@@ -1432,13 +1442,13 @@ impl<D> Wallet<D> {
                     change_fee,
                 } = excess
                 {
-                    return Err(Error::InsufficientFunds {
+                    return Err(CreateTxError::InsufficientFunds {
                         needed: *dust_threshold,
                         available: remaining_amount.saturating_sub(*change_fee),
                     });
                 }
             } else {
-                return Err(Error::NoRecipients);
+                return Err(CreateTxError::NoRecipients);
             }
         }
 
@@ -1485,6 +1495,10 @@ impl<D> Wallet<D> {
     /// # use std::str::FromStr;
     /// # use bitcoin::*;
     /// # use bdk::*;
+    /// # use bdk::wallet::ChangeSet;
+    /// # use bdk::wallet::error::CreateTxError;
+    /// # use bdk_chain::PersistBackend;
+    /// # use anyhow::Error;
     /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
     /// # let mut wallet = doctest_wallet!();
     /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
@@ -1508,27 +1522,27 @@ impl<D> Wallet<D> {
     /// 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::<(), anyhow::Error>(())
     /// ```
     // TODO: support for merging multiple transactions while bumping the fees
     pub fn build_fee_bump(
         &mut self,
         txid: Txid,
-    ) -> Result<TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, BumpFee>, Error> {
+    ) -> Result<TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, BumpFee>, BuildFeeBumpError> {
         let graph = self.indexed_graph.graph();
         let txout_index = &self.indexed_graph.index;
         let chain_tip = self.chain.tip().block_id();
 
         let mut tx = graph
             .get_tx(txid)
-            .ok_or(Error::TransactionNotFound)?
+            .ok_or(BuildFeeBumpError::TransactionNotFound(txid))?
             .clone();
 
         let pos = graph
             .get_chain_position(&self.chain, chain_tip, txid)
-            .ok_or(Error::TransactionNotFound)?;
+            .ok_or(BuildFeeBumpError::TransactionNotFound(txid))?;
         if let ChainPosition::Confirmed(_) = pos {
-            return Err(Error::TransactionConfirmed);
+            return Err(BuildFeeBumpError::TransactionConfirmed(txid));
         }
 
         if !tx
@@ -1536,29 +1550,29 @@ impl<D> Wallet<D> {
             .iter()
             .any(|txin| txin.sequence.to_consensus_u32() <= 0xFFFFFFFD)
         {
-            return Err(Error::IrreplaceableTransaction);
+            return Err(BuildFeeBumpError::IrreplaceableTransaction(tx.txid()));
         }
 
         let fee = self
             .calculate_fee(&tx)
-            .map_err(|_| Error::FeeRateUnavailable)?;
+            .map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?;
         let fee_rate = self
             .calculate_fee_rate(&tx)
-            .map_err(|_| Error::FeeRateUnavailable)?;
+            .map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?;
 
         // remove the inputs from the tx and process them
         let original_txin = tx.input.drain(..).collect::<Vec<_>>();
         let original_utxos = original_txin
             .iter()
-            .map(|txin| -> Result<_, Error> {
+            .map(|txin| -> Result<_, BuildFeeBumpError> {
                 let prev_tx = graph
                     .get_tx(txin.previous_output.txid)
-                    .ok_or(Error::UnknownUtxo)?;
+                    .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?;
                 let txout = &prev_tx.output[txin.previous_output.vout as usize];
 
                 let confirmation_time: ConfirmationTime = graph
                     .get_chain_position(&self.chain, chain_tip, txin.previous_output.txid)
-                    .ok_or(Error::UnknownUtxo)?
+                    .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?
                     .cloned()
                     .into();
 
@@ -1655,6 +1669,9 @@ impl<D> Wallet<D> {
     /// # use std::str::FromStr;
     /// # use bitcoin::*;
     /// # use bdk::*;
+    /// # use bdk::wallet::ChangeSet;
+    /// # use bdk::wallet::error::CreateTxError;
+    /// # use bdk_chain::PersistBackend;
     /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
     /// # let mut wallet = doctest_wallet!();
     /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
@@ -1663,17 +1680,18 @@ impl<D> Wallet<D> {
     ///     builder.add_recipient(to_address.script_pubkey(), 50_000);
     ///     builder.finish()?
     /// };
-    /// let  finalized = wallet.sign(&mut psbt, SignOptions::default())?;
+    /// let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
     /// assert!(finalized, "we should have signed all the inputs");
-    /// # Ok::<(), bdk::Error>(())
+    /// # Ok::<(),anyhow::Error>(())
     pub fn sign(
         &self,
         psbt: &mut psbt::PartiallySignedTransaction,
         sign_options: SignOptions,
-    ) -> Result<bool, Error> {
+    ) -> Result<bool, SignerError> {
         // This adds all the PSBT metadata for the inputs, which will help us later figure out how
         // to derive our keys
-        self.update_psbt_with_descriptor(psbt)?;
+        self.update_psbt_with_descriptor(psbt)
+            .map_err(SignerError::MiniscriptPsbt)?;
 
         // If we aren't allowed to use `witness_utxo`, ensure that every input (except p2tr and finalized ones)
         // has the `non_witness_utxo`
@@ -1685,7 +1703,7 @@ impl<D> Wallet<D> {
                 .filter(|i| i.tap_internal_key.is_none() && i.tap_merkle_root.is_none())
                 .any(|i| i.non_witness_utxo.is_none())
         {
-            return Err(Error::Signer(signer::SignerError::MissingNonWitnessUtxo));
+            return Err(SignerError::MissingNonWitnessUtxo);
         }
 
         // If the user hasn't explicitly opted-in, refuse to sign the transaction unless every input
@@ -1698,7 +1716,7 @@ impl<D> Wallet<D> {
                     || i.sighash_type == Some(TapSighashType::Default.into())
             })
         {
-            return Err(Error::Signer(signer::SignerError::NonStandardSighash));
+            return Err(SignerError::NonStandardSighash);
         }
 
         for signer in self
@@ -1719,7 +1737,7 @@ impl<D> Wallet<D> {
     }
 
     /// Return the spending policies for the wallet's descriptor
-    pub fn policies(&self, keychain: KeychainKind) -> Result<Option<Policy>, Error> {
+    pub fn policies(&self, keychain: KeychainKind) -> Result<Option<Policy>, DescriptorError> {
         let signers = match keychain {
             KeychainKind::External => &self.signers,
             KeychainKind::Internal => &self.change_signers,
@@ -1751,7 +1769,7 @@ impl<D> Wallet<D> {
         &self,
         psbt: &mut psbt::PartiallySignedTransaction,
         sign_options: SignOptions,
-    ) -> Result<bool, Error> {
+    ) -> Result<bool, SignerError> {
         let chain_tip = self.chain.tip().block_id();
 
         let tx = &psbt.unsigned_tx;
@@ -1761,7 +1779,7 @@ impl<D> Wallet<D> {
             let psbt_input = &psbt
                 .inputs
                 .get(n)
-                .ok_or(Error::Signer(SignerError::InputIndexOutOfRange))?;
+                .ok_or(SignerError::InputIndexOutOfRange)?;
             if psbt_input.final_script_sig.is_some() || psbt_input.final_script_witness.is_some() {
                 continue;
             }
@@ -2010,7 +2028,10 @@ impl<D> Wallet<D> {
         tx: Transaction,
         selected: Vec<Utxo>,
         params: TxParams,
-    ) -> Result<psbt::PartiallySignedTransaction, Error> {
+    ) -> Result<psbt::PartiallySignedTransaction, CreateTxError<D::WriteError>>
+    where
+        D: PersistBackend<ChangeSet>,
+    {
         let mut psbt = psbt::PartiallySignedTransaction::from_unsigned_tx(tx)?;
 
         if params.add_global_xpubs {
@@ -2026,7 +2047,7 @@ impl<D> Wallet<D> {
                     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);
@@ -2051,7 +2072,7 @@ impl<D> Wallet<D> {
                         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()
                                 },
@@ -2072,10 +2093,7 @@ impl<D> Wallet<D> {
                         && !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;
                 }
@@ -2093,14 +2111,17 @@ impl<D> Wallet<D> {
         utxo: LocalUtxo,
         sighash_type: Option<psbt::PsbtSighashType>,
         only_witness_utxo: bool,
-    ) -> Result<psbt::Input, Error> {
+    ) -> Result<psbt::Input, CreateTxError<D::WriteError>>
+    where
+        D: PersistBackend<ChangeSet>,
+    {
         // 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,
@@ -2131,7 +2152,7 @@ impl<D> Wallet<D> {
     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
         //
@@ -2271,7 +2292,7 @@ pub fn wallet_name_from_descriptor<T>(
     change_descriptor: Option<T>,
     network: Network,
     secp: &SecpCtx,
-) -> Result<String, Error>
+) -> Result<String, DescriptorError>
 where
     T: IntoWalletDescriptor,
 {
index dfd4e78d0f74d33efd6cd70bd6d84154e5163482..e1e003c6172db31bc34dcd68342d7ab589e60467 100644 (file)
@@ -76,7 +76,7 @@
 //!     Arc::new(custom_signer)
 //! );
 //!
-//! # Ok::<_, bdk::Error>(())
+//! # Ok::<_, anyhow::Error>(())
 //! ```
 
 use crate::collections::BTreeMap;
@@ -103,6 +103,7 @@ use miniscript::{Legacy, Segwitv0, SigType, Tap, ToPublicKey};
 use super::utils::SecpCtx;
 use crate::descriptor::{DescriptorMeta, XKeyUtils};
 use crate::psbt::PsbtUtils;
+use crate::wallet::error::MiniscriptPsbtError;
 
 /// Identifier of a signer in the `SignersContainers`. Used as a key to find the right signer among
 /// multiple of them
@@ -159,6 +160,8 @@ pub enum SignerError {
     InvalidSighash,
     /// Error while computing the hash to sign
     SighashError(sighash::Error),
+    /// Miniscript PSBT error
+    MiniscriptPsbt(MiniscriptPsbtError),
     /// Error while signing using hardware wallets
     #[cfg(feature = "hardware-signer")]
     HWIError(hwi::error::Error),
@@ -192,6 +195,7 @@ impl fmt::Display for SignerError {
             Self::NonStandardSighash => write!(f, "The psbt contains a non standard sighash"),
             Self::InvalidSighash => write!(f, "Invalid SIGHASH for the signing context in use"),
             Self::SighashError(err) => write!(f, "Error while computing the hash to sign: {}", err),
+            Self::MiniscriptPsbt(err) => write!(f, "Miniscript PSBT error: {}", err),
             #[cfg(feature = "hardware-signer")]
             Self::HWIError(err) => write!(f, "Error while signing using hardware wallets: {}", err),
         }
index 3b88073a6c023e025e59661028da335585588327..e99d2fe2503b60b252030c8f6150c73fa7ad2443 100644 (file)
 //! # use std::str::FromStr;
 //! # use bitcoin::*;
 //! # use bdk::*;
+//! # use bdk::wallet::ChangeSet;
+//! # use bdk::wallet::error::CreateTxError;
 //! # use bdk::wallet::tx_builder::CreateTx;
+//! # use bdk_chain::PersistBackend;
+//! # use anyhow::Error;
 //! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
 //! # let mut wallet = doctest_wallet!();
 //! // create a TxBuilder from a wallet
@@ -33,7 +37,7 @@
 //!     // Turn on RBF signaling
 //!     .enable_rbf();
 //! let psbt = tx_builder.finish()?;
-//! # Ok::<(), bdk::Error>(())
+//! # Ok::<(), anyhow::Error>(())
 //! ```
 
 use crate::collections::BTreeMap;
@@ -41,15 +45,18 @@ use crate::collections::HashSet;
 use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
 use bdk_chain::PersistBackend;
 use core::cell::RefCell;
+use core::fmt;
 use core::marker::PhantomData;
 
 use bitcoin::psbt::{self, PartiallySignedTransaction as Psbt};
-use bitcoin::{absolute, script::PushBytes, OutPoint, ScriptBuf, Sequence, Transaction};
+use bitcoin::{absolute, script::PushBytes, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
 
 use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
 use super::ChangeSet;
 use crate::types::{FeeRate, KeychainKind, LocalUtxo, WeightedUtxo};
-use crate::{Error, Utxo, Wallet};
+use crate::wallet::CreateTxError;
+use crate::{Utxo, Wallet};
+
 /// Context in which the [`TxBuilder`] is valid
 pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {}
 
@@ -78,6 +85,10 @@ impl TxBuilderContext for BumpFee {}
 /// # use bdk::wallet::tx_builder::*;
 /// # use bitcoin::*;
 /// # use core::str::FromStr;
+/// # use bdk::wallet::ChangeSet;
+/// # use bdk::wallet::error::CreateTxError;
+/// # use bdk_chain::PersistBackend;
+/// # use anyhow::Error;
 /// # let mut wallet = doctest_wallet!();
 /// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
 /// # let addr2 = addr1.clone();
@@ -102,7 +113,7 @@ impl TxBuilderContext for BumpFee {}
 /// };
 ///
 /// assert_eq!(psbt1.unsigned_tx.output[..2], psbt2.unsigned_tx.output[..2]);
-/// # Ok::<(), bdk::Error>(())
+/// # Ok::<(), anyhow::Error>(())
 /// ```
 ///
 /// At the moment [`coin_selection`] is an exception to the rule as it consumes `self`.
@@ -263,7 +274,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
     ///     .add_recipient(to_address.script_pubkey(), 50_000)
     ///     .policy_path(path, KeychainKind::External);
     ///
-    /// # Ok::<(), bdk::Error>(())
+    /// # Ok::<(), anyhow::Error>(())
     /// ```
     pub fn policy_path(
         &mut self,
@@ -285,12 +296,16 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
     ///
     /// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in
     /// the "utxos" and the "unspendable" list, it will be spent.
-    pub fn add_utxos(&mut self, outpoints: &[OutPoint]) -> Result<&mut Self, Error> {
+    pub fn add_utxos(&mut self, outpoints: &[OutPoint]) -> Result<&mut Self, AddUtxoError> {
         {
             let wallet = self.wallet.borrow();
             let utxos = outpoints
                 .iter()
-                .map(|outpoint| wallet.get_utxo(*outpoint).ok_or(Error::UnknownUtxo))
+                .map(|outpoint| {
+                    wallet
+                        .get_utxo(*outpoint)
+                        .ok_or(AddUtxoError::UnknownUtxo(*outpoint))
+                })
                 .collect::<Result<Vec<_>, _>>()?;
 
             for utxo in utxos {
@@ -311,7 +326,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
     ///
     /// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in
     /// the "utxos" and the "unspendable" list, it will be spent.
-    pub fn add_utxo(&mut self, outpoint: OutPoint) -> Result<&mut Self, Error> {
+    pub fn add_utxo(&mut self, outpoint: OutPoint) -> Result<&mut Self, AddUtxoError> {
         self.add_utxos(&[outpoint])
     }
 
@@ -366,23 +381,22 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
         outpoint: OutPoint,
         psbt_input: psbt::Input,
         satisfaction_weight: usize,
-    ) -> Result<&mut Self, Error> {
+    ) -> Result<&mut Self, AddForeignUtxoError> {
         if psbt_input.witness_utxo.is_none() {
             match psbt_input.non_witness_utxo.as_ref() {
                 Some(tx) => {
                     if tx.txid() != outpoint.txid {
-                        return Err(Error::Generic(
-                            "Foreign utxo outpoint does not match PSBT input".into(),
-                        ));
+                        return Err(AddForeignUtxoError::InvalidTxid {
+                            input_txid: tx.txid(),
+                            foreign_utxo: outpoint,
+                        });
                     }
                     if tx.output.len() <= outpoint.vout as usize {
-                        return Err(Error::InvalidOutpoint(outpoint));
+                        return Err(AddForeignUtxoError::InvalidOutpoint(outpoint));
                     }
                 }
                 None => {
-                    return Err(Error::Generic(
-                        "Foreign utxo missing witness_utxo or non_witness_utxo".into(),
-                    ))
+                    return Err(AddForeignUtxoError::MissingUtxo);
                 }
             }
         }
@@ -520,7 +534,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
 
     /// Choose the coin selection algorithm
     ///
-    /// Overrides the [`DefaultCoinSelectionAlgorithm`](super::coin_selection::DefaultCoinSelectionAlgorithm).
+    /// Overrides the [`DefaultCoinSelectionAlgorithm`].
     ///
     /// Note that this function consumes the builder and returns it so it is usually best to put this as the first call on the builder.
     pub fn coin_selection<P: CoinSelectionAlgorithm>(
@@ -537,10 +551,10 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
 
     /// Finish building the transaction.
     ///
-    /// Returns the [`BIP174`] "PSBT" and summary details about the transaction.
+    /// Returns a new [`Psbt`] per [`BIP174`].
     ///
     /// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
-    pub fn finish(self) -> Result<Psbt, Error>
+    pub fn finish(self) -> Result<Psbt, CreateTxError<D::WriteError>>
     where
         D: PersistBackend<ChangeSet>,
     {
@@ -595,6 +609,90 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
     }
 }
 
+#[derive(Debug)]
+/// Error returned from [`TxBuilder::add_utxo`] and [`TxBuilder::add_utxos`]
+pub enum AddUtxoError {
+    /// Happens when trying to spend an UTXO that is not in the internal database
+    UnknownUtxo(OutPoint),
+}
+
+impl fmt::Display for AddUtxoError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::UnknownUtxo(outpoint) => write!(
+                f,
+                "UTXO not found in the internal database for txid: {} with vout: {}",
+                outpoint.txid, outpoint.vout
+            ),
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for AddUtxoError {}
+
+#[derive(Debug)]
+/// Error returned from [`TxBuilder::add_foreign_utxo`].
+pub enum AddForeignUtxoError {
+    /// Foreign utxo outpoint txid does not match PSBT input txid
+    InvalidTxid {
+        /// PSBT input txid
+        input_txid: Txid,
+        /// Foreign UTXO outpoint
+        foreign_utxo: OutPoint,
+    },
+    /// Requested outpoint doesn't exist in the tx (vout greater than available outputs)
+    InvalidOutpoint(OutPoint),
+    /// Foreign utxo missing witness_utxo or non_witness_utxo
+    MissingUtxo,
+}
+
+impl fmt::Display for AddForeignUtxoError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::InvalidTxid {
+                input_txid,
+                foreign_utxo,
+            } => write!(
+                f,
+                "Foreign UTXO outpoint txid: {} does not match PSBT input txid: {}",
+                foreign_utxo.txid, input_txid,
+            ),
+            Self::InvalidOutpoint(outpoint) => write!(
+                f,
+                "Requested outpoint doesn't exist for txid: {} with vout: {}",
+                outpoint.txid, outpoint.vout,
+            ),
+            Self::MissingUtxo => write!(f, "Foreign utxo missing witness_utxo or non_witness_utxo"),
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for AddForeignUtxoError {}
+
+#[derive(Debug)]
+/// Error returned from [`TxBuilder::allow_shrinking`]
+pub enum AllowShrinkingError {
+    /// Script/PubKey was not in the original transaction
+    MissingScriptPubKey(ScriptBuf),
+}
+
+impl fmt::Display for AllowShrinkingError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::MissingScriptPubKey(script_buf) => write!(
+                f,
+                "Script/PubKey was not in the original transaction: {}",
+                script_buf,
+            ),
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for AllowShrinkingError {}
+
 impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> {
     /// Replace the recipients already added with a new list
     pub fn set_recipients(&mut self, recipients: Vec<(ScriptBuf, u64)>) -> &mut Self {
@@ -639,7 +737,11 @@ impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> {
     /// # use std::str::FromStr;
     /// # use bitcoin::*;
     /// # use bdk::*;
+    /// # use bdk::wallet::ChangeSet;
+    /// # use bdk::wallet::error::CreateTxError;
     /// # use bdk::wallet::tx_builder::CreateTx;
+    /// # use bdk_chain::PersistBackend;
+    /// # use anyhow::Error;
     /// # let to_address =
     /// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
     ///     .unwrap()
@@ -655,7 +757,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::<(), anyhow::Error>(())
     /// ```
     ///
     /// [`allow_shrinking`]: Self::allow_shrinking
@@ -680,7 +782,10 @@ impl<'a, D> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpFee> {
     ///
     /// Returns an `Err` if `script_pubkey` can't be found among the recipients of the
     /// transaction we are bumping.
-    pub fn allow_shrinking(&mut self, script_pubkey: ScriptBuf) -> Result<&mut Self, Error> {
+    pub fn allow_shrinking(
+        &mut self,
+        script_pubkey: ScriptBuf,
+    ) -> Result<&mut Self, AllowShrinkingError> {
         match self
             .params
             .recipients
@@ -692,10 +797,7 @@ impl<'a, D> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpFee> {
                 self.params.drain_to = Some(script_pubkey);
                 Ok(self)
             }
-            None => Err(Error::Generic(format!(
-                "{} was not in the original transaction",
-                script_pubkey
-            ))),
+            None => Err(AllowShrinkingError::MissingScriptPubKey(script_pubkey)),
         }
     }
 }
index 15a80f8c11c47761fa44028e0876f2ff325f7d4c..77ec1c8b1a9980a922b8c53979d67afc55a9a9da 100644 (file)
@@ -4,10 +4,12 @@ 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::error::CreateTxError;
+use bdk::wallet::tx_builder::AddForeignUtxoError;
 use bdk::wallet::AddressIndex::*;
 use bdk::wallet::{AddressIndex, AddressInfo, Balance, Wallet};
-use bdk::{Error, FeeRate, KeychainKind};
+use bdk::{FeeRate, KeychainKind};
 use bdk_chain::COINBASE_MATURITY;
 use bdk_chain::{BlockId, ConfirmationTime};
 use bitcoin::hashes::Hash;
@@ -309,7 +311,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);
@@ -317,13 +318,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);
@@ -331,7 +329,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]
@@ -419,9 +417,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);
@@ -429,7 +424,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]
@@ -458,9 +455,6 @@ 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)`"
-)]
 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);
@@ -468,7 +462,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]
@@ -483,7 +479,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);
@@ -491,7 +486,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]
@@ -519,9 +514,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);
@@ -529,7 +521,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 {
@@ -1236,7 +1231,6 @@ fn test_calculate_fee_with_missing_foreign_utxo() {
 }
 
 #[test]
-#[should_panic(expected = "Generic(\"Foreign utxo missing witness_utxo or non_witness_utxo\")")]
 fn test_add_foreign_utxo_invalid_psbt_input() {
     let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
     let outpoint = wallet.list_unspent().next().expect("must exist").outpoint;
@@ -1247,9 +1241,9 @@ fn test_add_foreign_utxo_invalid_psbt_input() {
         .unwrap();
 
     let mut builder = wallet.build_tx();
-    builder
-        .add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction)
-        .unwrap();
+    let result =
+        builder.add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction);
+    assert!(matches!(result, Err(AddForeignUtxoError::MissingUtxo)));
 }
 
 #[test]
@@ -2531,7 +2525,7 @@ fn test_sign_nonstandard_sighash() {
     );
     assert_matches!(
         result,
-        Err(bdk::Error::Signer(SignerError::NonStandardSighash)),
+        Err(SignerError::NonStandardSighash),
         "Signing failed with the wrong error type"
     );
 
@@ -2948,7 +2942,7 @@ fn test_taproot_sign_missing_witness_utxo() {
     );
     assert_matches!(
         result,
-        Err(Error::Signer(SignerError::MissingWitnessUtxo)),
+        Err(SignerError::MissingWitnessUtxo),
         "Signing should have failed with the correct error because the witness_utxo is missing"
     );
 
@@ -3289,7 +3283,7 @@ fn test_taproot_sign_non_default_sighash() {
     );
     assert_matches!(
         result,
-        Err(Error::Signer(SignerError::NonStandardSighash)),
+        Err(SignerError::NonStandardSighash),
         "Signing failed with the wrong error type"
     );
 
@@ -3307,7 +3301,7 @@ fn test_taproot_sign_non_default_sighash() {
     );
     assert_matches!(
         result,
-        Err(Error::Signer(SignerError::MissingWitnessUtxo)),
+        Err(SignerError::MissingWitnessUtxo),
         "Signing failed with the wrong error type"
     );
 
@@ -3395,10 +3389,12 @@ fn test_spend_coinbase() {
         .current_height(confirmation_height);
     assert!(matches!(
         builder.finish(),
-        Err(Error::InsufficientFunds {
-            needed: _,
-            available: 0
-        })
+        Err(CreateTxError::CoinSelection(
+            coin_selection::Error::InsufficientFunds {
+                needed: _,
+                available: 0
+            }
+        ))
     ));
 
     // Still unspendable...
@@ -3408,10 +3404,12 @@ fn test_spend_coinbase() {
         .current_height(not_yet_mature_time);
     assert_matches!(
         builder.finish(),
-        Err(Error::InsufficientFunds {
-            needed: _,
-            available: 0
-        })
+        Err(CreateTxError::CoinSelection(
+            coin_selection::Error::InsufficientFunds {
+                needed: _,
+                available: 0
+            }
+        ))
     );
 
     wallet
@@ -3447,7 +3445,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::OutputBelowDustLimit(0))
+    );
 
     let mut builder = wallet.build_tx();
 
index 3348fb40e0650d5d2b280c47141f2c9290149a15..f84c3a3dc1f3b5fc8254ef1321534ee5c4f8a9d7 100644 (file)
@@ -57,6 +57,7 @@ use crate::{
 use alloc::collections::vec_deque::VecDeque;
 use alloc::vec::Vec;
 use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid};
+use core::fmt::{self, Formatter};
 use core::{
     convert::Infallible,
     ops::{Deref, RangeInclusive},
@@ -145,6 +146,26 @@ pub enum CalculateFeeError {
     NegativeFee(i64),
 }
 
+impl fmt::Display for CalculateFeeError {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        match self {
+            CalculateFeeError::MissingTxOut(outpoints) => write!(
+                f,
+                "missing `TxOut` for one or more of the inputs of the tx: {:?}",
+                outpoints
+            ),
+            CalculateFeeError::NegativeFee(fee) => write!(
+                f,
+                "transaction is invalid according to the graph and has negative fee: {}",
+                fee
+            ),
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for CalculateFeeError {}
+
 impl<A> TxGraph<A> {
     /// Iterate over all tx outputs known by [`TxGraph`].
     ///
index 37a0f926a396af5e89e8996e94d915c3272ab89c..847cd90d63b121eff78aa706cb4649fd52716613 100644 (file)
@@ -7,3 +7,4 @@ edition = "2021"
 bdk = { path = "../../crates/bdk" }
 bdk_electrum = { path = "../../crates/electrum" }
 bdk_file_store = { path = "../../crates/file_store" }
+anyhow = "1"
index 9c77d5df084707273ec8b896fc96b1b8d9f6d66b..6f9d93ea248d86311e49ecf693bd88599db9f5d0 100644 (file)
@@ -16,7 +16,7 @@ use bdk_electrum::{
 };
 use bdk_file_store::Store;
 
-fn main() -> Result<(), Box<dyn std::error::Error>> {
+fn main() -> Result<(), anyhow::Error> {
     let db_path = std::env::temp_dir().join("bdk-electrum-example");
     let db = Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
     let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
index f67cecb483883079945d35575612ff8924a5542e..c588a87aa630c5bc17ebdb8d77803c7fbd9db425 100644 (file)
@@ -10,3 +10,4 @@ bdk = { path = "../../crates/bdk" }
 bdk_esplora = { path = "../../crates/esplora", features = ["async-https"] }
 bdk_file_store = { path = "../../crates/file_store" }
 tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
+anyhow = "1"
index 56c13b774ae5e49cfcf5db2acc27d7582b792c99..c438505c0961975a5c467867f6e5d44cffa271f9 100644 (file)
@@ -14,7 +14,7 @@ const STOP_GAP: usize = 50;
 const PARALLEL_REQUESTS: usize = 5;
 
 #[tokio::main]
-async fn main() -> Result<(), Box<dyn std::error::Error>> {
+async fn main() -> Result<(), anyhow::Error> {
     let db_path = std::env::temp_dir().join("bdk-esplora-async-example");
     let db = Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
     let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
index f07f64e482db3928136eb10ca37ef589734b6a3e..0679bd8f38ccf2e6bd44d32e392d77ba109033ef 100644 (file)
@@ -10,3 +10,4 @@ publish = false
 bdk = { path = "../../crates/bdk" }
 bdk_esplora = { path = "../../crates/esplora", features = ["blocking"] }
 bdk_file_store = { path = "../../crates/file_store" }
+anyhow = "1"
index e6173baefcebc1a4081ea5449f0dab712430043d..b0c0a9382ea3fd764fe350a037ccd211abecd9cf 100644 (file)
@@ -13,7 +13,7 @@ use bdk::{
 use bdk_esplora::{esplora_client, EsploraExt};
 use bdk_file_store::Store;
 
-fn main() -> Result<(), Box<dyn std::error::Error>> {
+fn main() -> Result<(), anyhow::Error> {
     let db_path = std::env::temp_dir().join("bdk-esplora-example");
     let db = Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
     let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";