assert_matches = "1.5.0"
tempfile = "3"
bdk_file_store = { path = "../file_store" }
+anyhow = "1"
[package.metadata.docs.rs]
all-features = true
// 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;
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
// 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);
// licenses.
//! Descriptor errors
-
use core::fmt;
/// Errors related to the parsing and usage of descriptors
#[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)
+ }
+}
//! 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;
+++ /dev/null
-// 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);
/// }
/// ```
///
-/// 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.
///
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 {
#[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;
pub use descriptor::template;
pub use descriptor::HdKeyPaths;
-pub use error::Error;
pub use types::*;
pub use wallet::signer;
pub use wallet::signer::SignOptions;
//! ```
//! # 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;
//! 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
//! 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
// 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 {
--- /dev/null
+// 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 {}
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;
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;
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;
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"),
/// # 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();
/// };
///
/// // sign and broadcast ...
- /// # Ok::<(), bdk::Error>(())
+ /// # Ok::<(), anyhow::Error>(())
/// ```
///
/// [`TxBuilder`]: crate::TxBuilder
&mut self,
coin_selection: Cs,
params: TxParams,
- ) -> Result<psbt::PartiallySignedTransaction, Error>
+ ) -> Result<psbt::PartiallySignedTransaction, CreateTxError<D::WriteError>>
where
D: PersistBackend<ChangeSet>,
{
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(),
)
&& 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 {
&& internal_policy.requires_path()
&& params.internal_policy_path.is_none()
{
- return Err(Error::SpendingPolicyRequired(KeychainKind::Internal));
+ return Err(CreateTxError::SpendingPolicyRequired(
+ KeychainKind::Internal,
+ ));
};
}
)?;
let internal_requirements = internal_policy
.map(|policy| {
- Ok::<_, Error>(
+ Ok::<_, CreateTxError<D::WriteError>>(
policy.get_condition(
params
.internal_policy_path
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,
// 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,
// 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) {
// 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
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,
});
}
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,
});
}
};
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
&& 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) {
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(
.stage(ChangeSet::from(indexed_tx_graph::ChangeSet::from(
index_changeset,
)));
- self.persist.commit().expect("TODO");
+ self.persist.commit().map_err(CreateTxError::Persist)?;
spk
}
};
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);
}
}
/// # 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();
/// 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
.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();
/// # 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();
/// 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`
.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
|| i.sighash_type == Some(TapSighashType::Default.into())
})
{
- return Err(Error::Signer(signer::SignerError::NonStandardSighash));
+ return Err(SignerError::NonStandardSighash);
}
for signer in self
}
/// 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,
&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;
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;
}
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 {
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);
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()
},
&& !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;
}
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,
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
//
change_descriptor: Option<T>,
network: Network,
secp: &SecpCtx,
-) -> Result<String, Error>
+) -> Result<String, DescriptorError>
where
T: IntoWalletDescriptor,
{
//! Arc::new(custom_signer)
//! );
//!
-//! # Ok::<_, bdk::Error>(())
+//! # Ok::<_, anyhow::Error>(())
//! ```
use crate::collections::BTreeMap;
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
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),
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),
}
//! # 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
//! // Turn on RBF signaling
//! .enable_rbf();
//! let psbt = tx_builder.finish()?;
-//! # Ok::<(), bdk::Error>(())
+//! # Ok::<(), anyhow::Error>(())
//! ```
use crate::collections::BTreeMap;
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 {}
/// # 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();
/// };
///
/// 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`.
/// .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,
///
/// 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 {
///
/// 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])
}
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);
}
}
}
/// 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>(
/// 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>,
{
}
}
+#[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 {
/// # 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()
/// .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
///
/// 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
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)),
}
}
}
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;
}
#[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);
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);
builder
.add_recipient(addr.script_pubkey(), 25_000)
.version(1);
- builder.finish().unwrap();
+ assert!(matches!(builder.finish(), Err(CreateTxError::Version1Csv)));
}
#[test]
}
#[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);
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]
}
#[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);
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]
}
#[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);
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]
}
#[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);
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 {
}
#[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;
.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]
);
assert_matches!(
result,
- Err(bdk::Error::Signer(SignerError::NonStandardSighash)),
+ Err(SignerError::NonStandardSighash),
"Signing failed with the wrong error type"
);
);
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"
);
);
assert_matches!(
result,
- Err(Error::Signer(SignerError::NonStandardSighash)),
+ Err(SignerError::NonStandardSighash),
"Signing failed with the wrong error type"
);
);
assert_matches!(
result,
- Err(Error::Signer(SignerError::MissingWitnessUtxo)),
+ Err(SignerError::MissingWitnessUtxo),
"Signing failed with the wrong error type"
);
.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...
.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
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();
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},
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`].
///
bdk = { path = "../../crates/bdk" }
bdk_electrum = { path = "../../crates/electrum" }
bdk_file_store = { path = "../../crates/file_store" }
+anyhow = "1"
};
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/*)";
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"
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/*)";
bdk = { path = "../../crates/bdk" }
bdk_esplora = { path = "../../crates/esplora", features = ["blocking"] }
bdk_file_store = { path = "../../crates/file_store" }
+anyhow = "1"
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/*)";