diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index 26cc07d3f..4bd9db305 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -32,7 +32,7 @@ jobs: run: | cargo update -p log --precise "0.4.18" cargo update -p tempfile --precise "3.6.0" - cargo update -p rustls:0.21.8 --precise "0.21.1" + cargo update -p rustls:0.21.9 --precise "0.21.1" cargo update -p rustls:0.20.9 --precise "0.20.8" cargo update -p tokio --precise "1.29.1" cargo update -p tokio-util --precise "0.7.8" diff --git a/README.md b/README.md index 2968fc653..9c6ada208 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ cargo update -p log --precise "0.4.18" # tempfile 3.7.0 has MSRV 1.63.0+ cargo update -p tempfile --precise "3.6.0" # rustls 0.21.7 has MSRV 1.60.0+ -cargo update -p rustls:0.21.8 --precise "0.21.1" +cargo update -p rustls:0.21.9 --precise "0.21.1" # rustls 0.20.9 has MSRV 1.60.0+ cargo update -p rustls:0.20.9 --precise "0.20.8" # tokio 1.33 has MSRV 1.63.0+ diff --git a/crates/bdk/Cargo.toml b/crates/bdk/Cargo.toml index 8c519d891..17efd65c6 100644 --- a/crates/bdk/Cargo.toml +++ b/crates/bdk/Cargo.toml @@ -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 diff --git a/crates/bdk/examples/mnemonic_to_descriptors.rs b/crates/bdk/examples/mnemonic_to_descriptors.rs index 7d2dd6013..4e1d5061d 100644 --- a/crates/bdk/examples/mnemonic_to_descriptors.rs +++ b/crates/bdk/examples/mnemonic_to_descriptors.rs @@ -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> { +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> { // 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); diff --git a/crates/bdk/src/descriptor/error.rs b/crates/bdk/src/descriptor/error.rs index 07a874efe..b36e69e63 100644 --- a/crates/bdk/src/descriptor/error.rs +++ b/crates/bdk/src/descriptor/error.rs @@ -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 for Error { + fn from(err: bitcoin::bip32::Error) -> Self { + Error::Bip32(err) + } +} + +impl From for Error { + fn from(err: bitcoin::base58::Error) -> Self { + Error::Base58(err) + } +} + +impl From for Error { + fn from(err: bitcoin::key::Error) -> Self { + Error::Pk(err) + } +} + +impl From for Error { + fn from(err: miniscript::Error) -> Self { + Error::Miniscript(err) + } +} + +impl From for Error { + fn from(err: bitcoin::hashes::hex::Error) -> Self { + Error::Hex(err) + } +} + +impl From for Error { + fn from(err: crate::descriptor::policy::PolicyError) -> Self { + Error::Policy(err) + } +} diff --git a/crates/bdk/src/descriptor/policy.rs b/crates/bdk/src/descriptor/policy.rs index 008bbe9ae..29a0d1029 100644 --- a/crates/bdk/src/descriptor/policy.rs +++ b/crates/bdk/src/descriptor/policy.rs @@ -33,13 +33,14 @@ //! 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 index fcb5a6f7b..000000000 --- a/crates/bdk/src/error.rs +++ /dev/null @@ -1,201 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , 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 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); diff --git a/crates/bdk/src/keys/mod.rs b/crates/bdk/src/keys/mod.rs index b47c4b86d..541d439a6 100644 --- a/crates/bdk/src/keys/mod.rs +++ b/crates/bdk/src/keys/mod.rs @@ -413,7 +413,7 @@ impl From for ExtendedKey { /// } /// ``` /// -/// 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 for KeyError { + fn from(err: miniscript::Error) -> Self { + KeyError::Miniscript(err) + } +} + +impl From 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 { diff --git a/crates/bdk/src/lib.rs b/crates/bdk/src/lib.rs index 012a868a6..3f0c7a262 100644 --- a/crates/bdk/src/lib.rs +++ b/crates/bdk/src/lib.rs @@ -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; diff --git a/crates/bdk/src/wallet/coin_selection.rs b/crates/bdk/src/wallet/coin_selection.rs index a0179d31b..a29456fa8 100644 --- a/crates/bdk/src/wallet/coin_selection.rs +++ b/crates/bdk/src/wallet/coin_selection.rs @@ -26,9 +26,12 @@ //! ``` //! # 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 { +//! ) -> Result { //! 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, //! }); @@ -94,19 +97,20 @@ //! //! // 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 index 000000000..db58fef06 --- /dev/null +++ b/crates/bdk/src/wallet/error.rs @@ -0,0 +1,292 @@ +// Bitcoin Dev Kit +// Written in 2020 by Alekos Filini +// +// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , 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

{ + /// 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

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::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

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: MiniscriptPsbtError) -> Self { + CreateTxError::MiniscriptPsbt(err) + } +} + +impl

From for CreateTxError

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

From for CreateTxError

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

{} + +#[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 {} diff --git a/crates/bdk/src/wallet/mod.rs b/crates/bdk/src/wallet/mod.rs index d3bc116e2..240cc1050 100644 --- a/crates/bdk/src/wallet/mod.rs +++ b/crates/bdk/src/wallet/mod.rs @@ -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, network: Network, - ) -> Result { + ) -> Result { Self::new(descriptor, change_descriptor, (), network).map_err(|e| match e { NewError::Descriptor(e) => e, NewError::Write(_) => unreachable!("mock-write must always succeed"), @@ -257,6 +259,29 @@ impl Wallet { } } +impl Wallet +where + D: PersistBackend, +{ + /// Infallibly return a derived address using the external descriptor, see [`AddressIndex`] for + /// available address index selection strategies. If none of the keys in the descriptor are derivable + /// (i.e. does not end with /*) then the same address will always be returned for any [`AddressIndex`]. + pub fn get_address(&mut self, address_index: AddressIndex) -> AddressInfo { + self.try_get_address(address_index).unwrap() + } + + /// Infallibly return a derived address using the internal (change) descriptor. + /// + /// If the wallet doesn't have an internal descriptor it will use the external descriptor. + /// + /// see [`AddressIndex`] for available address index selection strategies. If none of the keys + /// in the descriptor are derivable (i.e. does not end with /*) then the same address will always + /// be returned for any [`AddressIndex`]. + pub fn get_internal_address(&mut self, address_index: AddressIndex) -> AddressInfo { + self.try_get_internal_address(address_index).unwrap() + } +} + /// The error type when constructing a fresh [`Wallet`]. /// /// Methods [`new`] and [`new_with_genesis_hash`] may return this error. @@ -609,27 +634,37 @@ impl Wallet { /// Return a derived address using the external descriptor, see [`AddressIndex`] for /// available address index selection strategies. If none of the keys in the descriptor are derivable /// (i.e. does not end with /*) then the same address will always be returned for any [`AddressIndex`]. - pub fn get_address(&mut self, address_index: AddressIndex) -> AddressInfo + /// + /// A `PersistBackend::WriteError` will result if unable to persist the new address + /// to the `PersistBackend`. + pub fn try_get_address( + &mut self, + address_index: AddressIndex, + ) -> Result where D: PersistBackend, { self._get_address(KeychainKind::External, address_index) - .expect("persistence backend must not fail") } /// Return a derived address using the internal (change) descriptor. /// /// If the wallet doesn't have an internal descriptor it will use the external descriptor. /// + /// A `PersistBackend::WriteError` will result if unable to persist the new address + /// to the `PersistBackend`. + /// /// see [`AddressIndex`] for available address index selection strategies. If none of the keys /// in the descriptor are derivable (i.e. does not end with /*) then the same address will always /// be returned for any [`AddressIndex`]. - pub fn get_internal_address(&mut self, address_index: AddressIndex) -> AddressInfo + pub fn try_get_internal_address( + &mut self, + address_index: AddressIndex, + ) -> Result where D: PersistBackend, { self._get_address(KeychainKind::Internal, address_index) - .expect("persistence backend must not fail") } /// Return a derived address using the specified `keychain` (external/internal). @@ -1092,6 +1127,10 @@ impl Wallet { /// # 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 +1142,7 @@ impl Wallet { /// }; /// /// // sign and broadcast ... - /// # Ok::<(), bdk::Error>(()) + /// # Ok::<(), anyhow::Error>(()) /// ``` /// /// [`TxBuilder`]: crate::TxBuilder @@ -1120,7 +1159,7 @@ impl Wallet { &mut self, coin_selection: Cs, params: TxParams, - ) -> Result + ) -> Result> where D: PersistBackend, { @@ -1142,7 +1181,7 @@ impl Wallet { let internal_policy = internal_descriptor .as_ref() .map(|desc| { - Ok::<_, Error>( + Ok::<_, CreateTxError>( desc.extract_policy(&self.change_signers, BuildSatisfaction::None, &self.secp)? .unwrap(), ) @@ -1155,7 +1194,9 @@ impl Wallet { && 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 +1204,9 @@ impl Wallet { && internal_policy.requires_path() && params.internal_policy_path.is_none() { - return Err(Error::SpendingPolicyRequired(KeychainKind::Internal)); + return Err(CreateTxError::SpendingPolicyRequired( + KeychainKind::Internal, + )); }; } @@ -1175,7 +1218,7 @@ impl Wallet { )?; let internal_requirements = internal_policy .map(|policy| { - Ok::<_, Error>( + Ok::<_, CreateTxError>( policy.get_condition( params .internal_policy_path @@ -1191,14 +1234,9 @@ impl Wallet { debug!("Policy requirements: {:?}", requirements); let version = match params.version { - Some(tx_builder::Version(0)) => { - return Err(Error::Generic("Invalid version `0`".into())) - } + 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 +1267,9 @@ impl Wallet { // No requirement, just use the fee_sniping_height None => fee_sniping_height, // There's a block-based requirement, but the value is lower than the fee_sniping_height - Some(value @ absolute::LockTime::Blocks(_)) if value < fee_sniping_height => fee_sniping_height, + Some(value @ absolute::LockTime::Blocks(_)) if value < fee_sniping_height => { + fee_sniping_height + } // There's a time-based requirement or a block-based requirement greater // than the fee_sniping_height use that value Some(value) => value, @@ -1238,9 +1278,19 @@ impl Wallet { // Specific nLockTime required and we have no constraints, so just set to that value Some(x) if requirements.timelock.is_none() => x, // Specific nLockTime required and it's compatible with the constraints - Some(x) if requirements.timelock.unwrap().is_same_unit(x) && x >= requirements.timelock.unwrap() => x, + Some(x) + if requirements.timelock.unwrap().is_same_unit(x) + && x >= requirements.timelock.unwrap() => + { + x + } // Invalid nLockTime required - Some(x) => return Err(Error::Generic(format!("TxBuilder requested timelock of `{:?}`, but at least `{:?}` is required to spend from this script", x, requirements.timelock.unwrap()))) + Some(x) => { + return Err(CreateTxError::LockTime { + requested: x, + required: requirements.timelock.unwrap(), + }) + } }; let n_sequence = match (params.rbf, requirements.csv) { @@ -1258,18 +1308,13 @@ impl Wallet { // RBF with a specific value but that value is too high (Some(tx_builder::RbfValue::Value(rbf)), _) if !rbf.is_rbf() => { - return Err(Error::Generic( - "Cannot enable RBF with a nSequence >= 0xFFFFFFFE".into(), - )) + 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 +1333,7 @@ impl Wallet { FeePolicy::FeeAmount(fee) => { if let Some(previous_fee) = params.bumping_fee { if *fee < previous_fee.absolute { - return Err(Error::FeeTooLow { + return Err(CreateTxError::FeeTooLow { required: previous_fee.absolute, }); } @@ -1299,7 +1344,7 @@ impl Wallet { if let Some(previous_fee) = params.bumping_fee { let required_feerate = FeeRate::from_sat_per_vb(previous_fee.rate + 1.0); if *rate < required_feerate { - return Err(Error::FeeRateTooLow { + return Err(CreateTxError::FeeRateTooLow { required: required_feerate, }); } @@ -1316,7 +1361,7 @@ impl Wallet { }; 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 +1375,7 @@ impl Wallet { && 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 +1408,7 @@ impl Wallet { if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed && internal_descriptor.is_none() { - return Err(Error::Generic( - "The `change_policy` can be set only if the wallet has a change_descriptor".into(), - )); + return Err(CreateTxError::ChangePolicyDescriptor); } let (required_utxos, optional_utxos) = self.preselect_utxos( @@ -1391,7 +1434,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 } }; @@ -1432,13 +1475,13 @@ impl Wallet { 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 +1528,10 @@ impl Wallet { /// # 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 +1555,27 @@ 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::<(), anyhow::Error>(()) /// ``` // TODO: support for merging multiple transactions while bumping the fees pub fn build_fee_bump( &mut self, txid: Txid, - ) -> Result, Error> { + ) -> Result, 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 +1583,29 @@ impl Wallet { .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::>(); 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 +1702,9 @@ impl Wallet { /// # 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 +1713,18 @@ impl Wallet { /// 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 { + ) -> Result { // 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 +1736,7 @@ impl Wallet { .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 +1749,7 @@ impl Wallet { || i.sighash_type == Some(TapSighashType::Default.into()) }) { - return Err(Error::Signer(signer::SignerError::NonStandardSighash)); + return Err(SignerError::NonStandardSighash); } for signer in self @@ -1719,7 +1770,7 @@ impl Wallet { } /// Return the spending policies for the wallet's descriptor - pub fn policies(&self, keychain: KeychainKind) -> Result, Error> { + pub fn policies(&self, keychain: KeychainKind) -> Result, DescriptorError> { let signers = match keychain { KeychainKind::External => &self.signers, KeychainKind::Internal => &self.change_signers, @@ -1751,7 +1802,7 @@ impl Wallet { &self, psbt: &mut psbt::PartiallySignedTransaction, sign_options: SignOptions, - ) -> Result { + ) -> Result { let chain_tip = self.chain.tip().block_id(); let tx = &psbt.unsigned_tx; @@ -1761,7 +1812,7 @@ impl Wallet { 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 +2061,10 @@ impl Wallet { tx: Transaction, selected: Vec, params: TxParams, - ) -> Result { + ) -> Result> + where + D: PersistBackend, + { let mut psbt = psbt::PartiallySignedTransaction::from_unsigned_tx(tx)?; if params.add_global_xpubs { @@ -2026,7 +2080,7 @@ impl Wallet { None if xpub.xkey.depth == 0 => { (xpub.root_fingerprint(&self.secp), vec![].into()) } - _ => return Err(Error::MissingKeyOrigin(xpub.xkey.to_string())), + _ => return Err(CreateTxError::MissingKeyOrigin(xpub.xkey.to_string())), }; psbt.xpub.insert(xpub.xkey, origin); @@ -2051,7 +2105,7 @@ impl Wallet { match self.get_psbt_input(utxo, params.sighash, params.only_witness_utxo) { Ok(psbt_input) => psbt_input, Err(e) => match e { - Error::UnknownUtxo => psbt::Input { + CreateTxError::UnknownUtxo => psbt::Input { sighash_type: params.sighash, ..psbt::Input::default() }, @@ -2072,10 +2126,7 @@ impl Wallet { && !params.only_witness_utxo && foreign_psbt_input.non_witness_utxo.is_none() { - return Err(Error::Generic(format!( - "Missing non_witness_utxo on foreign utxo {}", - outpoint - ))); + return Err(CreateTxError::MissingNonWitnessUtxo(outpoint)); } *psbt_input = *foreign_psbt_input; } @@ -2093,14 +2144,17 @@ impl Wallet { utxo: LocalUtxo, sighash_type: Option, only_witness_utxo: bool, - ) -> Result { + ) -> Result> + where + D: PersistBackend, + { // Try to find the prev_script in our db to figure out if this is internal or external, // and the derivation index let &(keychain, child) = self .indexed_graph .index .index_of_spk(&utxo.txout.script_pubkey) - .ok_or(Error::UnknownUtxo)?; + .ok_or(CreateTxError::UnknownUtxo)?; let mut psbt_input = psbt::Input { sighash_type, @@ -2131,7 +2185,7 @@ impl Wallet { fn update_psbt_with_descriptor( &self, psbt: &mut psbt::PartiallySignedTransaction, - ) -> Result<(), Error> { + ) -> Result<(), MiniscriptPsbtError> { // We need to borrow `psbt` mutably within the loops, so we have to allocate a vec for all // the input utxos and outputs // @@ -2271,7 +2325,7 @@ pub fn wallet_name_from_descriptor( change_descriptor: Option, network: Network, secp: &SecpCtx, -) -> Result +) -> Result where T: IntoWalletDescriptor, { diff --git a/crates/bdk/src/wallet/signer.rs b/crates/bdk/src/wallet/signer.rs index dfd4e78d0..e1e003c61 100644 --- a/crates/bdk/src/wallet/signer.rs +++ b/crates/bdk/src/wallet/signer.rs @@ -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), } diff --git a/crates/bdk/src/wallet/tx_builder.rs b/crates/bdk/src/wallet/tx_builder.rs index 3b88073a6..e99d2fe25 100644 --- a/crates/bdk/src/wallet/tx_builder.rs +++ b/crates/bdk/src/wallet/tx_builder.rs @@ -17,7 +17,11 @@ //! # 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::, _>>()?; 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( @@ -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 + pub fn finish(self) -> Result> where D: PersistBackend, { @@ -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)), } } } diff --git a/crates/bdk/tests/wallet.rs b/crates/bdk/tests/wallet.rs index 15a80f8c1..77ec1c8b1 100644 --- a/crates/bdk/tests/wallet.rs +++ b/crates/bdk/tests/wallet.rs @@ -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(); diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 3348fb40e..f84c3a3dc 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -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 TxGraph { /// Iterate over all tx outputs known by [`TxGraph`]. /// diff --git a/example-crates/wallet_electrum/Cargo.toml b/example-crates/wallet_electrum/Cargo.toml index 37a0f926a..847cd90d6 100644 --- a/example-crates/wallet_electrum/Cargo.toml +++ b/example-crates/wallet_electrum/Cargo.toml @@ -7,3 +7,4 @@ edition = "2021" bdk = { path = "../../crates/bdk" } bdk_electrum = { path = "../../crates/electrum" } bdk_file_store = { path = "../../crates/file_store" } +anyhow = "1" diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index 9c77d5df0..9d4c6c5a4 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -16,7 +16,7 @@ use bdk_electrum::{ }; use bdk_file_store::Store; -fn main() -> Result<(), Box> { +fn main() -> Result<(), anyhow::Error> { let db_path = std::env::temp_dir().join("bdk-electrum-example"); let db = Store::::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?; let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; @@ -29,7 +29,7 @@ fn main() -> Result<(), Box> { Network::Testnet, )?; - let address = wallet.get_address(bdk::wallet::AddressIndex::New); + let address = wallet.try_get_address(bdk::wallet::AddressIndex::New)?; println!("Generated Address: {}", address); let balance = wallet.get_balance(); diff --git a/example-crates/wallet_esplora_async/Cargo.toml b/example-crates/wallet_esplora_async/Cargo.toml index f67cecb48..c588a87aa 100644 --- a/example-crates/wallet_esplora_async/Cargo.toml +++ b/example-crates/wallet_esplora_async/Cargo.toml @@ -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" diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/wallet_esplora_async/src/main.rs index 56c13b774..fb8f7b510 100644 --- a/example-crates/wallet_esplora_async/src/main.rs +++ b/example-crates/wallet_esplora_async/src/main.rs @@ -14,7 +14,7 @@ const STOP_GAP: usize = 50; const PARALLEL_REQUESTS: usize = 5; #[tokio::main] -async fn main() -> Result<(), Box> { +async fn main() -> Result<(), anyhow::Error> { let db_path = std::env::temp_dir().join("bdk-esplora-async-example"); let db = Store::::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?; let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; @@ -27,7 +27,7 @@ async fn main() -> Result<(), Box> { Network::Testnet, )?; - let address = wallet.get_address(AddressIndex::New); + let address = wallet.try_get_address(AddressIndex::New)?; println!("Generated Address: {}", address); let balance = wallet.get_balance(); diff --git a/example-crates/wallet_esplora_blocking/Cargo.toml b/example-crates/wallet_esplora_blocking/Cargo.toml index f07f64e48..0679bd8f3 100644 --- a/example-crates/wallet_esplora_blocking/Cargo.toml +++ b/example-crates/wallet_esplora_blocking/Cargo.toml @@ -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" diff --git a/example-crates/wallet_esplora_blocking/src/main.rs b/example-crates/wallet_esplora_blocking/src/main.rs index e6173baef..09e7c3ad4 100644 --- a/example-crates/wallet_esplora_blocking/src/main.rs +++ b/example-crates/wallet_esplora_blocking/src/main.rs @@ -13,7 +13,7 @@ use bdk::{ use bdk_esplora::{esplora_client, EsploraExt}; use bdk_file_store::Store; -fn main() -> Result<(), Box> { +fn main() -> Result<(), anyhow::Error> { let db_path = std::env::temp_dir().join("bdk-esplora-example"); let db = Store::::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?; let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; @@ -26,7 +26,7 @@ fn main() -> Result<(), Box> { Network::Testnet, )?; - let address = wallet.get_address(AddressIndex::New); + let address = wallet.try_get_address(AddressIndex::New)?; println!("Generated Address: {}", address); let balance = wallet.get_balance();