refactor(bdk)!: add context specific error types, remove top level error mod

refactor(bdk)!: remove impl_error macro
refactor(wallet)!: add MiniscriptPsbtError, CreateTxError, BuildFeeBumpError error enums
refactor(coin_selection)!: add module Error enum
test(bdk): use anyhow dev-dependency for all tests
This commit is contained in:
Steve Myers 2023-11-16 10:22:37 -06:00
parent cc552c5f91
commit 9e7d99e3bf
No known key found for this signature in database
GPG Key ID: 8105A46B22C2D051
20 changed files with 678 additions and 360 deletions

View File

@ -49,6 +49,7 @@ env_logger = "0.7"
assert_matches = "1.5.0" assert_matches = "1.5.0"
tempfile = "3" tempfile = "3"
bdk_file_store = { path = "../file_store" } bdk_file_store = { path = "../file_store" }
anyhow = "1"
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true

View File

@ -6,6 +6,7 @@
// You may not use this file except in accordance with one or both of these // You may not use this file except in accordance with one or both of these
// licenses. // licenses.
use anyhow::anyhow;
use bdk::bitcoin::bip32::DerivationPath; use bdk::bitcoin::bip32::DerivationPath;
use bdk::bitcoin::secp256k1::Secp256k1; use bdk::bitcoin::secp256k1::Secp256k1;
use bdk::bitcoin::Network; use bdk::bitcoin::Network;
@ -14,13 +15,11 @@ use bdk::descriptor::IntoWalletDescriptor;
use bdk::keys::bip39::{Language, Mnemonic, WordCount}; use bdk::keys::bip39::{Language, Mnemonic, WordCount};
use bdk::keys::{GeneratableKey, GeneratedKey}; use bdk::keys::{GeneratableKey, GeneratedKey};
use bdk::miniscript::Tap; use bdk::miniscript::Tap;
use bdk::Error as BDK_Error;
use std::error::Error;
use std::str::FromStr; use std::str::FromStr;
/// This example demonstrates how to generate a mnemonic phrase /// This example demonstrates how to generate a mnemonic phrase
/// using BDK and use that to generate a descriptor string. /// 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(); let secp = Secp256k1::new();
// In this example we are generating a 12 words mnemonic phrase // In this example we are generating a 12 words mnemonic phrase
@ -28,7 +27,7 @@ fn main() -> Result<(), Box<dyn Error>> {
// using their respective `WordCount` variant. // using their respective `WordCount` variant.
let mnemonic: GeneratedKey<_, Tap> = let mnemonic: GeneratedKey<_, Tap> =
Mnemonic::generate((WordCount::Words12, Language::English)) 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); println!("Mnemonic phrase: {}", *mnemonic);
let mnemonic_with_passphrase = (mnemonic, None); let mnemonic_with_passphrase = (mnemonic, None);

View File

@ -10,7 +10,6 @@
// licenses. // licenses.
//! Descriptor errors //! Descriptor errors
use core::fmt; use core::fmt;
/// Errors related to the parsing and usage of descriptors /// Errors related to the parsing and usage of descriptors
@ -87,9 +86,38 @@ impl fmt::Display for Error {
#[cfg(feature = "std")] #[cfg(feature = "std")]
impl std::error::Error for Error {} impl std::error::Error for Error {}
impl_error!(bitcoin::bip32::Error, Bip32); impl From<bitcoin::bip32::Error> for Error {
impl_error!(bitcoin::base58::Error, Base58); fn from(err: bitcoin::bip32::Error) -> Self {
impl_error!(bitcoin::key::Error, Pk); Error::Bip32(err)
impl_error!(miniscript::Error, Miniscript); }
impl_error!(bitcoin::hashes::hex::Error, Hex); }
impl_error!(crate::descriptor::policy::PolicyError, Policy);
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)
}
}

View File

@ -33,13 +33,14 @@
//! let signers = Arc::new(SignersContainer::build(key_map, &extended_desc, &secp)); //! let signers = Arc::new(SignersContainer::build(key_map, &extended_desc, &secp));
//! let policy = extended_desc.extract_policy(&signers, BuildSatisfaction::None, &secp)?; //! let policy = extended_desc.extract_policy(&signers, BuildSatisfaction::None, &secp)?;
//! println!("policy: {}", serde_json::to_string(&policy).unwrap()); //! println!("policy: {}", serde_json::to_string(&policy).unwrap());
//! # Ok::<(), bdk::Error>(()) //! # Ok::<(), anyhow::Error>(())
//! ``` //! ```
use crate::collections::{BTreeMap, HashSet, VecDeque}; use crate::collections::{BTreeMap, HashSet, VecDeque};
use alloc::string::String; use alloc::string::String;
use alloc::vec::Vec; use alloc::vec::Vec;
use core::cmp::max; use core::cmp::max;
use core::fmt; use core::fmt;
use serde::ser::SerializeMap; use serde::ser::SerializeMap;

View File

@ -1,201 +0,0 @@
// Bitcoin Dev Kit
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
//
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.
use crate::bitcoin::Network;
use crate::{descriptor, wallet};
use alloc::{string::String, vec::Vec};
use bitcoin::{OutPoint, Txid};
use core::fmt;
/// Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
#[derive(Debug)]
pub enum Error {
/// Generic error
Generic(String),
/// Cannot build a tx without recipients
NoRecipients,
/// `manually_selected_only` option is selected but no utxo has been passed
NoUtxosSelected,
/// Output created is under the dust limit, 546 satoshis
OutputBelowDustLimit(usize),
/// Wallet's UTXO set is not enough to cover recipient's requested plus fee
InsufficientFunds {
/// Sats needed for some transaction
needed: u64,
/// Sats available for spending
available: u64,
},
/// Branch and bound coin selection possible attempts with sufficiently big UTXO set could grow
/// exponentially, thus a limit is set, and when hit, this error is thrown
BnBTotalTriesExceeded,
/// Branch and bound coin selection tries to avoid needing a change by finding the right inputs for
/// the desired outputs plus fee, if there is not such combination this error is thrown
BnBNoExactMatch,
/// Happens when trying to spend an UTXO that is not in the internal database
UnknownUtxo,
/// Thrown when a tx is not found in the internal database
TransactionNotFound,
/// Happens when trying to bump a transaction that is already confirmed
TransactionConfirmed,
/// Trying to replace a tx that has a sequence >= `0xFFFFFFFE`
IrreplaceableTransaction,
/// When bumping a tx the fee rate requested is lower than required
FeeRateTooLow {
/// Required fee rate (satoshi/vbyte)
required: crate::types::FeeRate,
},
/// When bumping a tx the absolute fee requested is lower than replaced tx absolute fee
FeeTooLow {
/// Required fee absolute value (satoshi)
required: u64,
},
/// Node doesn't have data to estimate a fee rate
FeeRateUnavailable,
/// In order to use the [`TxBuilder::add_global_xpubs`] option every extended
/// key in the descriptor must either be a master key itself (having depth = 0) or have an
/// explicit origin provided
///
/// [`TxBuilder::add_global_xpubs`]: crate::wallet::tx_builder::TxBuilder::add_global_xpubs
MissingKeyOrigin(String),
/// Error while working with [`keys`](crate::keys)
Key(crate::keys::KeyError),
/// Descriptor checksum mismatch
ChecksumMismatch,
/// Spending policy is not compatible with this [`KeychainKind`](crate::types::KeychainKind)
SpendingPolicyRequired(crate::types::KeychainKind),
/// Error while extracting and manipulating policies
InvalidPolicyPathError(crate::descriptor::policy::PolicyError),
/// Signing error
Signer(crate::wallet::signer::SignerError),
/// Requested outpoint doesn't exist in the tx (vout greater than available outputs)
InvalidOutpoint(OutPoint),
/// Error related to the parsing and usage of descriptors
Descriptor(crate::descriptor::error::Error),
/// Miniscript error
Miniscript(miniscript::Error),
/// Miniscript PSBT error
MiniscriptPsbt(MiniscriptPsbtError),
/// BIP32 error
Bip32(bitcoin::bip32::Error),
/// Partially signed bitcoin transaction error
Psbt(bitcoin::psbt::Error),
}
/// Errors returned by miniscript when updating inconsistent PSBTs
#[derive(Debug, Clone)]
pub enum MiniscriptPsbtError {
Conversion(miniscript::descriptor::ConversionError),
UtxoUpdate(miniscript::psbt::UtxoUpdateError),
OutputUpdate(miniscript::psbt::OutputUpdateError),
}
impl fmt::Display for MiniscriptPsbtError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Conversion(err) => write!(f, "Conversion error: {}", err),
Self::UtxoUpdate(err) => write!(f, "UTXO update error: {}", err),
Self::OutputUpdate(err) => write!(f, "Output update error: {}", err),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for MiniscriptPsbtError {}
#[cfg(feature = "std")]
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Generic(err) => write!(f, "Generic error: {}", err),
Self::NoRecipients => write!(f, "Cannot build tx without recipients"),
Self::NoUtxosSelected => write!(f, "No UTXO selected"),
Self::OutputBelowDustLimit(limit) => {
write!(f, "Output below the dust limit: {}", limit)
}
Self::InsufficientFunds { needed, available } => write!(
f,
"Insufficient funds: {} sat available of {} sat needed",
available, needed
),
Self::BnBTotalTriesExceeded => {
write!(f, "Branch and bound coin selection: total tries exceeded")
}
Self::BnBNoExactMatch => write!(f, "Branch and bound coin selection: not exact match"),
Self::UnknownUtxo => write!(f, "UTXO not found in the internal database"),
Self::TransactionNotFound => {
write!(f, "Transaction not found in the internal database")
}
Self::TransactionConfirmed => write!(f, "Transaction already confirmed"),
Self::IrreplaceableTransaction => write!(f, "Transaction can't be replaced"),
Self::FeeRateTooLow { required } => write!(
f,
"Fee rate too low: required {} sat/vbyte",
required.as_sat_per_vb()
),
Self::FeeTooLow { required } => write!(f, "Fee to low: required {} sat", required),
Self::FeeRateUnavailable => write!(f, "Fee rate unavailable"),
Self::MissingKeyOrigin(err) => write!(f, "Missing key origin: {}", err),
Self::Key(err) => write!(f, "Key error: {}", err),
Self::ChecksumMismatch => write!(f, "Descriptor checksum mismatch"),
Self::SpendingPolicyRequired(keychain_kind) => {
write!(f, "Spending policy required: {:?}", keychain_kind)
}
Self::InvalidPolicyPathError(err) => write!(f, "Invalid policy path: {}", err),
Self::Signer(err) => write!(f, "Signer error: {}", err),
Self::InvalidOutpoint(outpoint) => write!(
f,
"Requested outpoint doesn't exist in the tx: {}",
outpoint
),
Self::Descriptor(err) => write!(f, "Descriptor error: {}", err),
Self::Miniscript(err) => write!(f, "Miniscript error: {}", err),
Self::MiniscriptPsbt(err) => write!(f, "Miniscript PSBT error: {}", err),
Self::Bip32(err) => write!(f, "BIP32 error: {}", err),
Self::Psbt(err) => write!(f, "PSBT error: {}", err),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for Error {}
macro_rules! impl_error {
( $from:ty, $to:ident ) => {
impl_error!($from, $to, Error);
};
( $from:ty, $to:ident, $impl_for:ty ) => {
impl core::convert::From<$from> for $impl_for {
fn from(err: $from) -> Self {
<$impl_for>::$to(err)
}
}
};
}
impl_error!(descriptor::error::Error, Descriptor);
impl_error!(descriptor::policy::PolicyError, InvalidPolicyPathError);
impl_error!(wallet::signer::SignerError, Signer);
impl From<crate::keys::KeyError> for Error {
fn from(key_error: crate::keys::KeyError) -> Error {
match key_error {
crate::keys::KeyError::Miniscript(inner) => Error::Miniscript(inner),
crate::keys::KeyError::Bip32(inner) => Error::Bip32(inner),
crate::keys::KeyError::InvalidChecksum => Error::ChecksumMismatch,
e => Error::Key(e),
}
}
}
impl_error!(miniscript::Error, Miniscript);
impl_error!(MiniscriptPsbtError, MiniscriptPsbt);
impl_error!(bitcoin::bip32::Error, Bip32);
impl_error!(bitcoin::psbt::Error, Psbt);

View File

@ -413,7 +413,7 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
/// } /// }
/// ``` /// ```
/// ///
/// Types that don't internally encode the [`Network`](bitcoin::Network) in which they are valid need some extra /// Types that don't internally encode the [`Network`] in which they are valid need some extra
/// steps to override the set of valid networks, otherwise only the network specified in the /// steps to override the set of valid networks, otherwise only the network specified in the
/// [`ExtendedPrivKey`] or [`ExtendedPubKey`] will be considered valid. /// [`ExtendedPrivKey`] or [`ExtendedPubKey`] will be considered valid.
/// ///
@ -932,8 +932,17 @@ pub enum KeyError {
Miniscript(miniscript::Error), Miniscript(miniscript::Error),
} }
impl_error!(miniscript::Error, Miniscript, KeyError); impl From<miniscript::Error> for KeyError {
impl_error!(bitcoin::bip32::Error, Bip32, 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 { impl fmt::Display for KeyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {

View File

@ -27,9 +27,6 @@ extern crate serde_json;
#[cfg(feature = "keys-bip39")] #[cfg(feature = "keys-bip39")]
extern crate bip39; extern crate bip39;
#[allow(unused_imports)]
#[macro_use]
pub(crate) mod error;
pub mod descriptor; pub mod descriptor;
pub mod keys; pub mod keys;
pub mod psbt; pub mod psbt;
@ -38,7 +35,6 @@ pub mod wallet;
pub use descriptor::template; pub use descriptor::template;
pub use descriptor::HdKeyPaths; pub use descriptor::HdKeyPaths;
pub use error::Error;
pub use types::*; pub use types::*;
pub use wallet::signer; pub use wallet::signer;
pub use wallet::signer::SignOptions; pub use wallet::signer::SignOptions;

View File

@ -26,9 +26,12 @@
//! ``` //! ```
//! # use std::str::FromStr; //! # use std::str::FromStr;
//! # use bitcoin::*; //! # 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::*;
//! # use bdk::wallet::coin_selection::decide_change; //! # use bdk::wallet::coin_selection::decide_change;
//! # use anyhow::Error;
//! # const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4; //! # const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4;
//! #[derive(Debug)] //! #[derive(Debug)]
//! struct AlwaysSpendEverything; //! struct AlwaysSpendEverything;
@ -41,7 +44,7 @@
//! fee_rate: bdk::FeeRate, //! fee_rate: bdk::FeeRate,
//! target_amount: u64, //! target_amount: u64,
//! drain_script: &Script, //! drain_script: &Script,
//! ) -> Result<CoinSelectionResult, bdk::Error> { //! ) -> Result<CoinSelectionResult, coin_selection::Error> {
//! let mut selected_amount = 0; //! let mut selected_amount = 0;
//! let mut additional_weight = Weight::ZERO; //! let mut additional_weight = Weight::ZERO;
//! let all_utxos_selected = required_utxos //! let all_utxos_selected = required_utxos
@ -61,7 +64,7 @@
//! let additional_fees = fee_rate.fee_wu(additional_weight); //! let additional_fees = fee_rate.fee_wu(additional_weight);
//! let amount_needed_with_fees = additional_fees + target_amount; //! let amount_needed_with_fees = additional_fees + target_amount;
//! if selected_amount < amount_needed_with_fees { //! if selected_amount < amount_needed_with_fees {
//! return Err(bdk::Error::InsufficientFunds { //! return Err(coin_selection::Error::InsufficientFunds {
//! needed: amount_needed_with_fees, //! needed: amount_needed_with_fees,
//! available: selected_amount, //! available: selected_amount,
//! }); //! });
@ -94,19 +97,20 @@
//! //!
//! // inspect, sign, broadcast, ... //! // inspect, sign, broadcast, ...
//! //!
//! # Ok::<(), bdk::Error>(()) //! # Ok::<(), anyhow::Error>(())
//! ``` //! ```
use crate::types::FeeRate; use crate::types::FeeRate;
use crate::wallet::utils::IsDust; use crate::wallet::utils::IsDust;
use crate::Utxo;
use crate::WeightedUtxo; use crate::WeightedUtxo;
use crate::{error::Error, Utxo};
use alloc::vec::Vec; use alloc::vec::Vec;
use bitcoin::consensus::encode::serialize; use bitcoin::consensus::encode::serialize;
use bitcoin::{Script, Weight}; use bitcoin::{Script, Weight};
use core::convert::TryInto; use core::convert::TryInto;
use core::fmt::{self, Formatter};
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
/// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not /// 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) // prev_txid (32 bytes) + prev_vout (4 bytes) + sequence (4 bytes)
pub(crate) const TXIN_BASE_WEIGHT: usize = (32 + 4 + 4) * 4; 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)] #[derive(Debug)]
/// Remaining amount after performing coin selection /// Remaining amount after performing coin selection
pub enum Excess { pub enum Excess {

View File

@ -0,0 +1,292 @@
// Bitcoin Dev Kit
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
//
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.
//! Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
use crate::descriptor::policy::PolicyError;
use crate::descriptor::DescriptorError;
use crate::wallet::coin_selection;
use crate::{descriptor, FeeRate, KeychainKind};
use alloc::string::String;
use bitcoin::{absolute, psbt, OutPoint, Sequence, Txid};
use core::fmt;
/// Errors returned by miniscript when updating inconsistent PSBTs
#[derive(Debug, Clone)]
pub enum MiniscriptPsbtError {
/// Descriptor key conversion error
Conversion(miniscript::descriptor::ConversionError),
/// Return error type for PsbtExt::update_input_with_descriptor
UtxoUpdate(miniscript::psbt::UtxoUpdateError),
/// Return error type for PsbtExt::update_output_with_descriptor
OutputUpdate(miniscript::psbt::OutputUpdateError),
}
impl fmt::Display for MiniscriptPsbtError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Conversion(err) => write!(f, "Conversion error: {}", err),
Self::UtxoUpdate(err) => write!(f, "UTXO update error: {}", err),
Self::OutputUpdate(err) => write!(f, "Output update error: {}", err),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for MiniscriptPsbtError {}
#[derive(Debug)]
/// Error returned from [`TxBuilder::finish`]
///
/// [`TxBuilder::finish`]: crate::wallet::tx_builder::TxBuilder::finish
pub enum CreateTxError<P> {
/// There was a problem with the descriptors passed in
Descriptor(DescriptorError),
/// We were unable to write wallet data to the persistence backend
Persist(P),
/// There was a problem while extracting and manipulating policies
Policy(PolicyError),
/// Spending policy is not compatible with this [`KeychainKind`]
SpendingPolicyRequired(KeychainKind),
/// Requested invalid transaction version '0'
Version0,
/// Requested transaction version `1`, but at least `2` is needed to use OP_CSV
Version1Csv,
/// Requested `LockTime` is less than is required to spend from this script
LockTime {
/// Requested `LockTime`
requested: absolute::LockTime,
/// Required `LockTime`
required: absolute::LockTime,
},
/// Cannot enable RBF with a `Sequence` >= 0xFFFFFFFE
RbfSequence,
/// Cannot enable RBF with `Sequence` given a required OP_CSV
RbfSequenceCsv {
/// Given RBF `Sequence`
rbf: Sequence,
/// Required OP_CSV `Sequence`
csv: Sequence,
},
/// When bumping a tx the absolute fee requested is lower than replaced tx absolute fee
FeeTooLow {
/// Required fee absolute value (satoshi)
required: u64,
},
/// When bumping a tx the fee rate requested is lower than required
FeeRateTooLow {
/// Required fee rate (satoshi/vbyte)
required: FeeRate,
},
/// `manually_selected_only` option is selected but no utxo has been passed
NoUtxosSelected,
/// Output created is under the dust limit, 546 satoshis
OutputBelowDustLimit(usize),
/// The `change_policy` was set but the wallet does not have a change_descriptor
ChangePolicyDescriptor,
/// There was an error with coin selection
CoinSelection(coin_selection::Error),
/// Wallet's UTXO set is not enough to cover recipient's requested plus fee
InsufficientFunds {
/// Sats needed for some transaction
needed: u64,
/// Sats available for spending
available: u64,
},
/// Cannot build a tx without recipients
NoRecipients,
/// Partially signed bitcoin transaction error
Psbt(psbt::Error),
/// In order to use the [`TxBuilder::add_global_xpubs`] option every extended
/// key in the descriptor must either be a master key itself (having depth = 0) or have an
/// explicit origin provided
///
/// [`TxBuilder::add_global_xpubs`]: crate::wallet::tx_builder::TxBuilder::add_global_xpubs
MissingKeyOrigin(String),
/// Happens when trying to spend an UTXO that is not in the internal database
UnknownUtxo,
/// Missing non_witness_utxo on foreign utxo for given `OutPoint`
MissingNonWitnessUtxo(OutPoint),
/// Miniscript PSBT error
MiniscriptPsbt(MiniscriptPsbtError),
}
impl<P> fmt::Display for CreateTxError<P>
where
P: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Descriptor(e) => e.fmt(f),
Self::Persist(e) => {
write!(
f,
"failed to write wallet data to persistence backend: {}",
e
)
}
Self::Policy(e) => e.fmt(f),
CreateTxError::SpendingPolicyRequired(keychain_kind) => {
write!(f, "Spending policy required: {:?}", keychain_kind)
}
CreateTxError::Version0 => {
write!(f, "Invalid version `0`")
}
CreateTxError::Version1Csv => {
write!(
f,
"TxBuilder requested version `1`, but at least `2` is needed to use OP_CSV"
)
}
CreateTxError::LockTime {
requested,
required,
} => {
write!(f, "TxBuilder requested timelock of `{:?}`, but at least `{:?}` is required to spend from this script", required, requested)
}
CreateTxError::RbfSequence => {
write!(f, "Cannot enable RBF with a nSequence >= 0xFFFFFFFE")
}
CreateTxError::RbfSequenceCsv { rbf, csv } => {
write!(
f,
"Cannot enable RBF with nSequence `{:?}` given a required OP_CSV of `{:?}`",
rbf, csv
)
}
CreateTxError::FeeTooLow { required } => {
write!(f, "Fee to low: required {} sat", required)
}
CreateTxError::FeeRateTooLow { required } => {
write!(
f,
"Fee rate too low: required {} sat/vbyte",
required.as_sat_per_vb()
)
}
CreateTxError::NoUtxosSelected => {
write!(f, "No UTXO selected")
}
CreateTxError::OutputBelowDustLimit(limit) => {
write!(f, "Output below the dust limit: {}", limit)
}
CreateTxError::ChangePolicyDescriptor => {
write!(
f,
"The `change_policy` can be set only if the wallet has a change_descriptor"
)
}
CreateTxError::CoinSelection(e) => e.fmt(f),
CreateTxError::InsufficientFunds { needed, available } => {
write!(
f,
"Insufficient funds: {} sat available of {} sat needed",
available, needed
)
}
CreateTxError::NoRecipients => {
write!(f, "Cannot build tx without recipients")
}
CreateTxError::Psbt(e) => e.fmt(f),
CreateTxError::MissingKeyOrigin(err) => {
write!(f, "Missing key origin: {}", err)
}
CreateTxError::UnknownUtxo => {
write!(f, "UTXO not found in the internal database")
}
CreateTxError::MissingNonWitnessUtxo(outpoint) => {
write!(f, "Missing non_witness_utxo on foreign utxo {}", outpoint)
}
CreateTxError::MiniscriptPsbt(err) => {
write!(f, "Miniscript PSBT error: {}", err)
}
}
}
}
impl<P> From<descriptor::error::Error> for CreateTxError<P> {
fn from(err: descriptor::error::Error) -> Self {
CreateTxError::Descriptor(err)
}
}
impl<P> From<PolicyError> for CreateTxError<P> {
fn from(err: PolicyError) -> Self {
CreateTxError::Policy(err)
}
}
impl<P> From<MiniscriptPsbtError> for CreateTxError<P> {
fn from(err: MiniscriptPsbtError) -> Self {
CreateTxError::MiniscriptPsbt(err)
}
}
impl<P> From<psbt::Error> for CreateTxError<P> {
fn from(err: psbt::Error) -> Self {
CreateTxError::Psbt(err)
}
}
impl<P> From<coin_selection::Error> for CreateTxError<P> {
fn from(err: coin_selection::Error) -> Self {
CreateTxError::CoinSelection(err)
}
}
#[cfg(feature = "std")]
impl<P: core::fmt::Display + core::fmt::Debug> std::error::Error for CreateTxError<P> {}
#[derive(Debug)]
/// Error returned from [`Wallet::build_fee_bump`]
///
/// [`Wallet::build_fee_bump`]: super::Wallet::build_fee_bump
pub enum BuildFeeBumpError {
/// Happens when trying to spend an UTXO that is not in the internal database
UnknownUtxo(OutPoint),
/// Thrown when a tx is not found in the internal database
TransactionNotFound(Txid),
/// Happens when trying to bump a transaction that is already confirmed
TransactionConfirmed(Txid),
/// Trying to replace a tx that has a sequence >= `0xFFFFFFFE`
IrreplaceableTransaction(Txid),
/// Node doesn't have data to estimate a fee rate
FeeRateUnavailable,
}
impl fmt::Display for BuildFeeBumpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownUtxo(outpoint) => write!(
f,
"UTXO not found in the internal database with txid: {}, vout: {}",
outpoint.txid, outpoint.vout
),
Self::TransactionNotFound(txid) => {
write!(
f,
"Transaction not found in the internal database with txid: {}",
txid
)
}
Self::TransactionConfirmed(txid) => {
write!(f, "Transaction already confirmed with txid: {}", txid)
}
Self::IrreplaceableTransaction(txid) => {
write!(f, "Transaction can't be replaced with txid: {}", txid)
}
Self::FeeRateUnavailable => write!(f, "Fee rate unavailable"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for BuildFeeBumpError {}

View File

@ -38,6 +38,7 @@ use bitcoin::{consensus::encode::serialize, BlockHash};
use bitcoin::{constants::genesis_block, psbt}; use bitcoin::{constants::genesis_block, psbt};
use core::fmt; use core::fmt;
use core::ops::Deref; use core::ops::Deref;
use descriptor::error::Error as DescriptorError;
use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier}; use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier};
use bdk_chain::tx_graph::CalculateFeeError; use bdk_chain::tx_graph::CalculateFeeError;
@ -50,6 +51,7 @@ pub mod signer;
pub mod tx_builder; pub mod tx_builder;
pub(crate) mod utils; pub(crate) mod utils;
pub mod error;
#[cfg(feature = "hardware-signer")] #[cfg(feature = "hardware-signer")]
#[cfg_attr(docsrs, doc(cfg(feature = "hardware-signer")))] #[cfg_attr(docsrs, doc(cfg(feature = "hardware-signer")))]
pub mod hardwaresigner; pub mod hardwaresigner;
@ -64,14 +66,14 @@ use utils::{check_nsequence_rbf, After, Older, SecpCtx};
use crate::descriptor::policy::BuildSatisfaction; use crate::descriptor::policy::BuildSatisfaction;
use crate::descriptor::{ 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, ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, Policy, XKeyUtils,
}; };
use crate::error::{Error, MiniscriptPsbtError};
use crate::psbt::PsbtUtils; use crate::psbt::PsbtUtils;
use crate::signer::SignerError; use crate::signer::SignerError;
use crate::types::*; use crate::types::*;
use crate::wallet::coin_selection::Excess::{Change, NoChange}; use crate::wallet::coin_selection::Excess::{Change, NoChange};
use crate::wallet::error::{BuildFeeBumpError, CreateTxError, MiniscriptPsbtError};
const COINBASE_MATURITY: u32 = 100; const COINBASE_MATURITY: u32 = 100;
@ -235,7 +237,7 @@ impl Wallet {
descriptor: E, descriptor: E,
change_descriptor: Option<E>, change_descriptor: Option<E>,
network: Network, network: Network,
) -> Result<Self, crate::descriptor::DescriptorError> { ) -> Result<Self, DescriptorError> {
Self::new(descriptor, change_descriptor, (), network).map_err(|e| match e { Self::new(descriptor, change_descriptor, (), network).map_err(|e| match e {
NewError::Descriptor(e) => e, NewError::Descriptor(e) => e,
NewError::Write(_) => unreachable!("mock-write must always succeed"), NewError::Write(_) => unreachable!("mock-write must always succeed"),
@ -1092,6 +1094,10 @@ impl<D> Wallet<D> {
/// # use std::str::FromStr; /// # use std::str::FromStr;
/// # use bitcoin::*; /// # use bitcoin::*;
/// # use bdk::*; /// # use bdk::*;
/// # use bdk::wallet::ChangeSet;
/// # use bdk::wallet::error::CreateTxError;
/// # use bdk_chain::PersistBackend;
/// # use anyhow::Error;
/// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
/// # let mut wallet = doctest_wallet!(); /// # let mut wallet = doctest_wallet!();
/// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
@ -1103,7 +1109,7 @@ impl<D> Wallet<D> {
/// }; /// };
/// ///
/// // sign and broadcast ... /// // sign and broadcast ...
/// # Ok::<(), bdk::Error>(()) /// # Ok::<(), anyhow::Error>(())
/// ``` /// ```
/// ///
/// [`TxBuilder`]: crate::TxBuilder /// [`TxBuilder`]: crate::TxBuilder
@ -1120,7 +1126,7 @@ impl<D> Wallet<D> {
&mut self, &mut self,
coin_selection: Cs, coin_selection: Cs,
params: TxParams, params: TxParams,
) -> Result<psbt::PartiallySignedTransaction, Error> ) -> Result<psbt::PartiallySignedTransaction, CreateTxError<D::WriteError>>
where where
D: PersistBackend<ChangeSet>, D: PersistBackend<ChangeSet>,
{ {
@ -1142,7 +1148,7 @@ impl<D> Wallet<D> {
let internal_policy = internal_descriptor let internal_policy = internal_descriptor
.as_ref() .as_ref()
.map(|desc| { .map(|desc| {
Ok::<_, Error>( Ok::<_, CreateTxError<D::WriteError>>(
desc.extract_policy(&self.change_signers, BuildSatisfaction::None, &self.secp)? desc.extract_policy(&self.change_signers, BuildSatisfaction::None, &self.secp)?
.unwrap(), .unwrap(),
) )
@ -1155,7 +1161,9 @@ impl<D> Wallet<D> {
&& external_policy.requires_path() && external_policy.requires_path()
&& params.external_policy_path.is_none() && 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 // Same for the internal_policy path, if present
if let Some(internal_policy) = &internal_policy { if let Some(internal_policy) = &internal_policy {
@ -1163,7 +1171,9 @@ impl<D> Wallet<D> {
&& internal_policy.requires_path() && internal_policy.requires_path()
&& params.internal_policy_path.is_none() && params.internal_policy_path.is_none()
{ {
return Err(Error::SpendingPolicyRequired(KeychainKind::Internal)); return Err(CreateTxError::SpendingPolicyRequired(
KeychainKind::Internal,
));
}; };
} }
@ -1175,7 +1185,7 @@ impl<D> Wallet<D> {
)?; )?;
let internal_requirements = internal_policy let internal_requirements = internal_policy
.map(|policy| { .map(|policy| {
Ok::<_, Error>( Ok::<_, CreateTxError<D::WriteError>>(
policy.get_condition( policy.get_condition(
params params
.internal_policy_path .internal_policy_path
@ -1191,14 +1201,9 @@ impl<D> Wallet<D> {
debug!("Policy requirements: {:?}", requirements); debug!("Policy requirements: {:?}", requirements);
let version = match params.version { let version = match params.version {
Some(tx_builder::Version(0)) => { Some(tx_builder::Version(0)) => return Err(CreateTxError::Version0),
return Err(Error::Generic("Invalid version `0`".into()))
}
Some(tx_builder::Version(1)) if requirements.csv.is_some() => { Some(tx_builder::Version(1)) if requirements.csv.is_some() => {
return Err(Error::Generic( return Err(CreateTxError::Version1Csv)
"TxBuilder requested version `1`, but at least `2` is needed to use OP_CSV"
.into(),
))
} }
Some(tx_builder::Version(x)) => x, Some(tx_builder::Version(x)) => x,
None if requirements.csv.is_some() => 2, None if requirements.csv.is_some() => 2,
@ -1229,7 +1234,9 @@ impl<D> Wallet<D> {
// No requirement, just use the fee_sniping_height // No requirement, just use the fee_sniping_height
None => fee_sniping_height, None => fee_sniping_height,
// There's a block-based requirement, but the value is lower than the 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 // There's a time-based requirement or a block-based requirement greater
// than the fee_sniping_height use that value // than the fee_sniping_height use that value
Some(value) => value, Some(value) => value,
@ -1238,9 +1245,19 @@ impl<D> Wallet<D> {
// Specific nLockTime required and we have no constraints, so just set to that value // Specific nLockTime required and we have no constraints, so just set to that value
Some(x) if requirements.timelock.is_none() => x, Some(x) if requirements.timelock.is_none() => x,
// Specific nLockTime required and it's compatible with the constraints // 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 // 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) { let n_sequence = match (params.rbf, requirements.csv) {
@ -1258,18 +1275,13 @@ impl<D> Wallet<D> {
// RBF with a specific value but that value is too high // RBF with a specific value but that value is too high
(Some(tx_builder::RbfValue::Value(rbf)), _) if !rbf.is_rbf() => { (Some(tx_builder::RbfValue::Value(rbf)), _) if !rbf.is_rbf() => {
return Err(Error::Generic( return Err(CreateTxError::RbfSequence)
"Cannot enable RBF with a nSequence >= 0xFFFFFFFE".into(),
))
} }
// RBF with a specific value requested, but the value is incompatible with CSV // RBF with a specific value requested, but the value is incompatible with CSV
(Some(tx_builder::RbfValue::Value(rbf)), Some(csv)) (Some(tx_builder::RbfValue::Value(rbf)), Some(csv))
if !check_nsequence_rbf(rbf, csv) => if !check_nsequence_rbf(rbf, csv) =>
{ {
return Err(Error::Generic(format!( return Err(CreateTxError::RbfSequenceCsv { rbf, csv })
"Cannot enable RBF with nSequence `{:?}` given a required OP_CSV of `{:?}`",
rbf, csv
)))
} }
// RBF enabled with the default value with CSV also enabled. CSV takes precedence // RBF enabled with the default value with CSV also enabled. CSV takes precedence
@ -1288,7 +1300,7 @@ impl<D> Wallet<D> {
FeePolicy::FeeAmount(fee) => { FeePolicy::FeeAmount(fee) => {
if let Some(previous_fee) = params.bumping_fee { if let Some(previous_fee) = params.bumping_fee {
if *fee < previous_fee.absolute { if *fee < previous_fee.absolute {
return Err(Error::FeeTooLow { return Err(CreateTxError::FeeTooLow {
required: previous_fee.absolute, required: previous_fee.absolute,
}); });
} }
@ -1299,7 +1311,7 @@ impl<D> Wallet<D> {
if let Some(previous_fee) = params.bumping_fee { if let Some(previous_fee) = params.bumping_fee {
let required_feerate = FeeRate::from_sat_per_vb(previous_fee.rate + 1.0); let required_feerate = FeeRate::from_sat_per_vb(previous_fee.rate + 1.0);
if *rate < required_feerate { if *rate < required_feerate {
return Err(Error::FeeRateTooLow { return Err(CreateTxError::FeeRateTooLow {
required: required_feerate, required: required_feerate,
}); });
} }
@ -1316,7 +1328,7 @@ impl<D> Wallet<D> {
}; };
if params.manually_selected_only && params.utxos.is_empty() { 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 // we keep it as a float while we accumulate it, and only round it at the end
@ -1330,7 +1342,7 @@ impl<D> Wallet<D> {
&& value.is_dust(script_pubkey) && value.is_dust(script_pubkey)
&& !script_pubkey.is_provably_unspendable() && !script_pubkey.is_provably_unspendable()
{ {
return Err(Error::OutputBelowDustLimit(index)); return Err(CreateTxError::OutputBelowDustLimit(index));
} }
if self.is_mine(script_pubkey) { if self.is_mine(script_pubkey) {
@ -1363,9 +1375,7 @@ impl<D> Wallet<D> {
if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed
&& internal_descriptor.is_none() && internal_descriptor.is_none()
{ {
return Err(Error::Generic( return Err(CreateTxError::ChangePolicyDescriptor);
"The `change_policy` can be set only if the wallet has a change_descriptor".into(),
));
} }
let (required_utxos, optional_utxos) = self.preselect_utxos( let (required_utxos, optional_utxos) = self.preselect_utxos(
@ -1391,7 +1401,7 @@ impl<D> Wallet<D> {
.stage(ChangeSet::from(indexed_tx_graph::ChangeSet::from( .stage(ChangeSet::from(indexed_tx_graph::ChangeSet::from(
index_changeset, index_changeset,
))); )));
self.persist.commit().expect("TODO"); self.persist.commit().map_err(CreateTxError::Persist)?;
spk spk
} }
}; };
@ -1432,13 +1442,13 @@ impl<D> Wallet<D> {
change_fee, change_fee,
} = excess } = excess
{ {
return Err(Error::InsufficientFunds { return Err(CreateTxError::InsufficientFunds {
needed: *dust_threshold, needed: *dust_threshold,
available: remaining_amount.saturating_sub(*change_fee), available: remaining_amount.saturating_sub(*change_fee),
}); });
} }
} else { } else {
return Err(Error::NoRecipients); return Err(CreateTxError::NoRecipients);
} }
} }
@ -1485,6 +1495,10 @@ impl<D> Wallet<D> {
/// # use std::str::FromStr; /// # use std::str::FromStr;
/// # use bitcoin::*; /// # use bitcoin::*;
/// # use bdk::*; /// # use bdk::*;
/// # use bdk::wallet::ChangeSet;
/// # use bdk::wallet::error::CreateTxError;
/// # use bdk_chain::PersistBackend;
/// # use anyhow::Error;
/// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
/// # let mut wallet = doctest_wallet!(); /// # let mut wallet = doctest_wallet!();
/// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
@ -1508,27 +1522,27 @@ impl<D> Wallet<D> {
/// let _ = wallet.sign(&mut psbt, SignOptions::default())?; /// let _ = wallet.sign(&mut psbt, SignOptions::default())?;
/// let fee_bumped_tx = psbt.extract_tx(); /// let fee_bumped_tx = psbt.extract_tx();
/// // broadcast fee_bumped_tx to replace original /// // broadcast fee_bumped_tx to replace original
/// # Ok::<(), bdk::Error>(()) /// # Ok::<(), anyhow::Error>(())
/// ``` /// ```
// TODO: support for merging multiple transactions while bumping the fees // TODO: support for merging multiple transactions while bumping the fees
pub fn build_fee_bump( pub fn build_fee_bump(
&mut self, &mut self,
txid: Txid, txid: Txid,
) -> Result<TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, BumpFee>, Error> { ) -> Result<TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, BumpFee>, BuildFeeBumpError> {
let graph = self.indexed_graph.graph(); let graph = self.indexed_graph.graph();
let txout_index = &self.indexed_graph.index; let txout_index = &self.indexed_graph.index;
let chain_tip = self.chain.tip().block_id(); let chain_tip = self.chain.tip().block_id();
let mut tx = graph let mut tx = graph
.get_tx(txid) .get_tx(txid)
.ok_or(Error::TransactionNotFound)? .ok_or(BuildFeeBumpError::TransactionNotFound(txid))?
.clone(); .clone();
let pos = graph let pos = graph
.get_chain_position(&self.chain, chain_tip, txid) .get_chain_position(&self.chain, chain_tip, txid)
.ok_or(Error::TransactionNotFound)?; .ok_or(BuildFeeBumpError::TransactionNotFound(txid))?;
if let ChainPosition::Confirmed(_) = pos { if let ChainPosition::Confirmed(_) = pos {
return Err(Error::TransactionConfirmed); return Err(BuildFeeBumpError::TransactionConfirmed(txid));
} }
if !tx if !tx
@ -1536,29 +1550,29 @@ impl<D> Wallet<D> {
.iter() .iter()
.any(|txin| txin.sequence.to_consensus_u32() <= 0xFFFFFFFD) .any(|txin| txin.sequence.to_consensus_u32() <= 0xFFFFFFFD)
{ {
return Err(Error::IrreplaceableTransaction); return Err(BuildFeeBumpError::IrreplaceableTransaction(tx.txid()));
} }
let fee = self let fee = self
.calculate_fee(&tx) .calculate_fee(&tx)
.map_err(|_| Error::FeeRateUnavailable)?; .map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?;
let fee_rate = self let fee_rate = self
.calculate_fee_rate(&tx) .calculate_fee_rate(&tx)
.map_err(|_| Error::FeeRateUnavailable)?; .map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?;
// remove the inputs from the tx and process them // remove the inputs from the tx and process them
let original_txin = tx.input.drain(..).collect::<Vec<_>>(); let original_txin = tx.input.drain(..).collect::<Vec<_>>();
let original_utxos = original_txin let original_utxos = original_txin
.iter() .iter()
.map(|txin| -> Result<_, Error> { .map(|txin| -> Result<_, BuildFeeBumpError> {
let prev_tx = graph let prev_tx = graph
.get_tx(txin.previous_output.txid) .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 txout = &prev_tx.output[txin.previous_output.vout as usize];
let confirmation_time: ConfirmationTime = graph let confirmation_time: ConfirmationTime = graph
.get_chain_position(&self.chain, chain_tip, txin.previous_output.txid) .get_chain_position(&self.chain, chain_tip, txin.previous_output.txid)
.ok_or(Error::UnknownUtxo)? .ok_or(BuildFeeBumpError::UnknownUtxo(txin.previous_output))?
.cloned() .cloned()
.into(); .into();
@ -1655,6 +1669,9 @@ impl<D> Wallet<D> {
/// # use std::str::FromStr; /// # use std::str::FromStr;
/// # use bitcoin::*; /// # use bitcoin::*;
/// # use bdk::*; /// # use bdk::*;
/// # use bdk::wallet::ChangeSet;
/// # use bdk::wallet::error::CreateTxError;
/// # use bdk_chain::PersistBackend;
/// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
/// # let mut wallet = doctest_wallet!(); /// # let mut wallet = doctest_wallet!();
/// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
@ -1663,17 +1680,18 @@ impl<D> Wallet<D> {
/// builder.add_recipient(to_address.script_pubkey(), 50_000); /// builder.add_recipient(to_address.script_pubkey(), 50_000);
/// builder.finish()? /// 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"); /// assert!(finalized, "we should have signed all the inputs");
/// # Ok::<(), bdk::Error>(()) /// # Ok::<(),anyhow::Error>(())
pub fn sign( pub fn sign(
&self, &self,
psbt: &mut psbt::PartiallySignedTransaction, psbt: &mut psbt::PartiallySignedTransaction,
sign_options: SignOptions, 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 // This adds all the PSBT metadata for the inputs, which will help us later figure out how
// to derive our keys // 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) // If we aren't allowed to use `witness_utxo`, ensure that every input (except p2tr and finalized ones)
// has the `non_witness_utxo` // has the `non_witness_utxo`
@ -1685,7 +1703,7 @@ impl<D> Wallet<D> {
.filter(|i| i.tap_internal_key.is_none() && i.tap_merkle_root.is_none()) .filter(|i| i.tap_internal_key.is_none() && i.tap_merkle_root.is_none())
.any(|i| i.non_witness_utxo.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 // If the user hasn't explicitly opted-in, refuse to sign the transaction unless every input
@ -1698,7 +1716,7 @@ impl<D> Wallet<D> {
|| i.sighash_type == Some(TapSighashType::Default.into()) || i.sighash_type == Some(TapSighashType::Default.into())
}) })
{ {
return Err(Error::Signer(signer::SignerError::NonStandardSighash)); return Err(SignerError::NonStandardSighash);
} }
for signer in self for signer in self
@ -1719,7 +1737,7 @@ impl<D> Wallet<D> {
} }
/// Return the spending policies for the wallet's descriptor /// 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 { let signers = match keychain {
KeychainKind::External => &self.signers, KeychainKind::External => &self.signers,
KeychainKind::Internal => &self.change_signers, KeychainKind::Internal => &self.change_signers,
@ -1751,7 +1769,7 @@ impl<D> Wallet<D> {
&self, &self,
psbt: &mut psbt::PartiallySignedTransaction, psbt: &mut psbt::PartiallySignedTransaction,
sign_options: SignOptions, sign_options: SignOptions,
) -> Result<bool, Error> { ) -> Result<bool, SignerError> {
let chain_tip = self.chain.tip().block_id(); let chain_tip = self.chain.tip().block_id();
let tx = &psbt.unsigned_tx; let tx = &psbt.unsigned_tx;
@ -1761,7 +1779,7 @@ impl<D> Wallet<D> {
let psbt_input = &psbt let psbt_input = &psbt
.inputs .inputs
.get(n) .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() { if psbt_input.final_script_sig.is_some() || psbt_input.final_script_witness.is_some() {
continue; continue;
} }
@ -2010,7 +2028,10 @@ impl<D> Wallet<D> {
tx: Transaction, tx: Transaction,
selected: Vec<Utxo>, selected: Vec<Utxo>,
params: TxParams, 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)?; let mut psbt = psbt::PartiallySignedTransaction::from_unsigned_tx(tx)?;
if params.add_global_xpubs { if params.add_global_xpubs {
@ -2026,7 +2047,7 @@ impl<D> Wallet<D> {
None if xpub.xkey.depth == 0 => { None if xpub.xkey.depth == 0 => {
(xpub.root_fingerprint(&self.secp), vec![].into()) (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); psbt.xpub.insert(xpub.xkey, origin);
@ -2051,7 +2072,7 @@ impl<D> Wallet<D> {
match self.get_psbt_input(utxo, params.sighash, params.only_witness_utxo) { match self.get_psbt_input(utxo, params.sighash, params.only_witness_utxo) {
Ok(psbt_input) => psbt_input, Ok(psbt_input) => psbt_input,
Err(e) => match e { Err(e) => match e {
Error::UnknownUtxo => psbt::Input { CreateTxError::UnknownUtxo => psbt::Input {
sighash_type: params.sighash, sighash_type: params.sighash,
..psbt::Input::default() ..psbt::Input::default()
}, },
@ -2072,10 +2093,7 @@ impl<D> Wallet<D> {
&& !params.only_witness_utxo && !params.only_witness_utxo
&& foreign_psbt_input.non_witness_utxo.is_none() && foreign_psbt_input.non_witness_utxo.is_none()
{ {
return Err(Error::Generic(format!( return Err(CreateTxError::MissingNonWitnessUtxo(outpoint));
"Missing non_witness_utxo on foreign utxo {}",
outpoint
)));
} }
*psbt_input = *foreign_psbt_input; *psbt_input = *foreign_psbt_input;
} }
@ -2093,14 +2111,17 @@ impl<D> Wallet<D> {
utxo: LocalUtxo, utxo: LocalUtxo,
sighash_type: Option<psbt::PsbtSighashType>, sighash_type: Option<psbt::PsbtSighashType>,
only_witness_utxo: bool, 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, // Try to find the prev_script in our db to figure out if this is internal or external,
// and the derivation index // and the derivation index
let &(keychain, child) = self let &(keychain, child) = self
.indexed_graph .indexed_graph
.index .index
.index_of_spk(&utxo.txout.script_pubkey) .index_of_spk(&utxo.txout.script_pubkey)
.ok_or(Error::UnknownUtxo)?; .ok_or(CreateTxError::UnknownUtxo)?;
let mut psbt_input = psbt::Input { let mut psbt_input = psbt::Input {
sighash_type, sighash_type,
@ -2131,7 +2152,7 @@ impl<D> Wallet<D> {
fn update_psbt_with_descriptor( fn update_psbt_with_descriptor(
&self, &self,
psbt: &mut psbt::PartiallySignedTransaction, 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 // We need to borrow `psbt` mutably within the loops, so we have to allocate a vec for all
// the input utxos and outputs // the input utxos and outputs
// //
@ -2271,7 +2292,7 @@ pub fn wallet_name_from_descriptor<T>(
change_descriptor: Option<T>, change_descriptor: Option<T>,
network: Network, network: Network,
secp: &SecpCtx, secp: &SecpCtx,
) -> Result<String, Error> ) -> Result<String, DescriptorError>
where where
T: IntoWalletDescriptor, T: IntoWalletDescriptor,
{ {

View File

@ -76,7 +76,7 @@
//! Arc::new(custom_signer) //! Arc::new(custom_signer)
//! ); //! );
//! //!
//! # Ok::<_, bdk::Error>(()) //! # Ok::<_, anyhow::Error>(())
//! ``` //! ```
use crate::collections::BTreeMap; use crate::collections::BTreeMap;
@ -103,6 +103,7 @@ use miniscript::{Legacy, Segwitv0, SigType, Tap, ToPublicKey};
use super::utils::SecpCtx; use super::utils::SecpCtx;
use crate::descriptor::{DescriptorMeta, XKeyUtils}; use crate::descriptor::{DescriptorMeta, XKeyUtils};
use crate::psbt::PsbtUtils; 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 /// Identifier of a signer in the `SignersContainers`. Used as a key to find the right signer among
/// multiple of them /// multiple of them
@ -159,6 +160,8 @@ pub enum SignerError {
InvalidSighash, InvalidSighash,
/// Error while computing the hash to sign /// Error while computing the hash to sign
SighashError(sighash::Error), SighashError(sighash::Error),
/// Miniscript PSBT error
MiniscriptPsbt(MiniscriptPsbtError),
/// Error while signing using hardware wallets /// Error while signing using hardware wallets
#[cfg(feature = "hardware-signer")] #[cfg(feature = "hardware-signer")]
HWIError(hwi::error::Error), 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::NonStandardSighash => write!(f, "The psbt contains a non standard sighash"),
Self::InvalidSighash => write!(f, "Invalid SIGHASH for the signing context in use"), 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::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")] #[cfg(feature = "hardware-signer")]
Self::HWIError(err) => write!(f, "Error while signing using hardware wallets: {}", err), Self::HWIError(err) => write!(f, "Error while signing using hardware wallets: {}", err),
} }

View File

@ -17,7 +17,11 @@
//! # use std::str::FromStr; //! # use std::str::FromStr;
//! # use bitcoin::*; //! # use bitcoin::*;
//! # use bdk::*; //! # use bdk::*;
//! # use bdk::wallet::ChangeSet;
//! # use bdk::wallet::error::CreateTxError;
//! # use bdk::wallet::tx_builder::CreateTx; //! # use bdk::wallet::tx_builder::CreateTx;
//! # use bdk_chain::PersistBackend;
//! # use anyhow::Error;
//! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); //! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
//! # let mut wallet = doctest_wallet!(); //! # let mut wallet = doctest_wallet!();
//! // create a TxBuilder from a wallet //! // create a TxBuilder from a wallet
@ -33,7 +37,7 @@
//! // Turn on RBF signaling //! // Turn on RBF signaling
//! .enable_rbf(); //! .enable_rbf();
//! let psbt = tx_builder.finish()?; //! let psbt = tx_builder.finish()?;
//! # Ok::<(), bdk::Error>(()) //! # Ok::<(), anyhow::Error>(())
//! ``` //! ```
use crate::collections::BTreeMap; use crate::collections::BTreeMap;
@ -41,15 +45,18 @@ use crate::collections::HashSet;
use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec}; use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
use bdk_chain::PersistBackend; use bdk_chain::PersistBackend;
use core::cell::RefCell; use core::cell::RefCell;
use core::fmt;
use core::marker::PhantomData; use core::marker::PhantomData;
use bitcoin::psbt::{self, PartiallySignedTransaction as Psbt}; 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::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
use super::ChangeSet; use super::ChangeSet;
use crate::types::{FeeRate, KeychainKind, LocalUtxo, WeightedUtxo}; 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 /// Context in which the [`TxBuilder`] is valid
pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {} pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {}
@ -78,6 +85,10 @@ impl TxBuilderContext for BumpFee {}
/// # use bdk::wallet::tx_builder::*; /// # use bdk::wallet::tx_builder::*;
/// # use bitcoin::*; /// # use bitcoin::*;
/// # use core::str::FromStr; /// # 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 mut wallet = doctest_wallet!();
/// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); /// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
/// # let addr2 = addr1.clone(); /// # let addr2 = addr1.clone();
@ -102,7 +113,7 @@ impl TxBuilderContext for BumpFee {}
/// }; /// };
/// ///
/// assert_eq!(psbt1.unsigned_tx.output[..2], psbt2.unsigned_tx.output[..2]); /// 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`. /// 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) /// .add_recipient(to_address.script_pubkey(), 50_000)
/// .policy_path(path, KeychainKind::External); /// .policy_path(path, KeychainKind::External);
/// ///
/// # Ok::<(), bdk::Error>(()) /// # Ok::<(), anyhow::Error>(())
/// ``` /// ```
pub fn policy_path( pub fn policy_path(
&mut self, &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 /// 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. /// 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 wallet = self.wallet.borrow();
let utxos = outpoints let utxos = outpoints
.iter() .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<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
for utxo in utxos { 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 /// 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. /// 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]) self.add_utxos(&[outpoint])
} }
@ -366,23 +381,22 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
outpoint: OutPoint, outpoint: OutPoint,
psbt_input: psbt::Input, psbt_input: psbt::Input,
satisfaction_weight: usize, satisfaction_weight: usize,
) -> Result<&mut Self, Error> { ) -> Result<&mut Self, AddForeignUtxoError> {
if psbt_input.witness_utxo.is_none() { if psbt_input.witness_utxo.is_none() {
match psbt_input.non_witness_utxo.as_ref() { match psbt_input.non_witness_utxo.as_ref() {
Some(tx) => { Some(tx) => {
if tx.txid() != outpoint.txid { if tx.txid() != outpoint.txid {
return Err(Error::Generic( return Err(AddForeignUtxoError::InvalidTxid {
"Foreign utxo outpoint does not match PSBT input".into(), input_txid: tx.txid(),
)); foreign_utxo: outpoint,
});
} }
if tx.output.len() <= outpoint.vout as usize { if tx.output.len() <= outpoint.vout as usize {
return Err(Error::InvalidOutpoint(outpoint)); return Err(AddForeignUtxoError::InvalidOutpoint(outpoint));
} }
} }
None => { None => {
return Err(Error::Generic( return Err(AddForeignUtxoError::MissingUtxo);
"Foreign utxo missing witness_utxo or non_witness_utxo".into(),
))
} }
} }
} }
@ -520,7 +534,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
/// Choose the coin selection algorithm /// 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. /// 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>( pub fn coin_selection<P: CoinSelectionAlgorithm>(
@ -537,10 +551,10 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
/// Finish building the transaction. /// 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 /// [`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 where
D: PersistBackend<ChangeSet>, D: PersistBackend<ChangeSet>,
{ {
@ -595,6 +609,90 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
} }
} }
#[derive(Debug)]
/// Error returned from [`TxBuilder::add_utxo`] and [`TxBuilder::add_utxos`]
pub enum AddUtxoError {
/// Happens when trying to spend an UTXO that is not in the internal database
UnknownUtxo(OutPoint),
}
impl fmt::Display for AddUtxoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownUtxo(outpoint) => write!(
f,
"UTXO not found in the internal database for txid: {} with vout: {}",
outpoint.txid, outpoint.vout
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for AddUtxoError {}
#[derive(Debug)]
/// Error returned from [`TxBuilder::add_foreign_utxo`].
pub enum AddForeignUtxoError {
/// Foreign utxo outpoint txid does not match PSBT input txid
InvalidTxid {
/// PSBT input txid
input_txid: Txid,
/// Foreign UTXO outpoint
foreign_utxo: OutPoint,
},
/// Requested outpoint doesn't exist in the tx (vout greater than available outputs)
InvalidOutpoint(OutPoint),
/// Foreign utxo missing witness_utxo or non_witness_utxo
MissingUtxo,
}
impl fmt::Display for AddForeignUtxoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidTxid {
input_txid,
foreign_utxo,
} => write!(
f,
"Foreign UTXO outpoint txid: {} does not match PSBT input txid: {}",
foreign_utxo.txid, input_txid,
),
Self::InvalidOutpoint(outpoint) => write!(
f,
"Requested outpoint doesn't exist for txid: {} with vout: {}",
outpoint.txid, outpoint.vout,
),
Self::MissingUtxo => write!(f, "Foreign utxo missing witness_utxo or non_witness_utxo"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for AddForeignUtxoError {}
#[derive(Debug)]
/// Error returned from [`TxBuilder::allow_shrinking`]
pub enum AllowShrinkingError {
/// Script/PubKey was not in the original transaction
MissingScriptPubKey(ScriptBuf),
}
impl fmt::Display for AllowShrinkingError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingScriptPubKey(script_buf) => write!(
f,
"Script/PubKey was not in the original transaction: {}",
script_buf,
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for AllowShrinkingError {}
impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> { impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> {
/// Replace the recipients already added with a new list /// Replace the recipients already added with a new list
pub fn set_recipients(&mut self, recipients: Vec<(ScriptBuf, u64)>) -> &mut Self { 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 std::str::FromStr;
/// # use bitcoin::*; /// # use bitcoin::*;
/// # use bdk::*; /// # use bdk::*;
/// # use bdk::wallet::ChangeSet;
/// # use bdk::wallet::error::CreateTxError;
/// # use bdk::wallet::tx_builder::CreateTx; /// # use bdk::wallet::tx_builder::CreateTx;
/// # use bdk_chain::PersistBackend;
/// # use anyhow::Error;
/// # let to_address = /// # let to_address =
/// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt") /// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
/// .unwrap() /// .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)) /// .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0))
/// .enable_rbf(); /// .enable_rbf();
/// let psbt = tx_builder.finish()?; /// let psbt = tx_builder.finish()?;
/// # Ok::<(), bdk::Error>(()) /// # Ok::<(), anyhow::Error>(())
/// ``` /// ```
/// ///
/// [`allow_shrinking`]: Self::allow_shrinking /// [`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 /// Returns an `Err` if `script_pubkey` can't be found among the recipients of the
/// transaction we are bumping. /// 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 match self
.params .params
.recipients .recipients
@ -692,10 +797,7 @@ impl<'a, D> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpFee> {
self.params.drain_to = Some(script_pubkey); self.params.drain_to = Some(script_pubkey);
Ok(self) Ok(self)
} }
None => Err(Error::Generic(format!( None => Err(AllowShrinkingError::MissingScriptPubKey(script_pubkey)),
"{} was not in the original transaction",
script_pubkey
))),
} }
} }
} }

View File

@ -4,10 +4,12 @@ use assert_matches::assert_matches;
use bdk::descriptor::calc_checksum; use bdk::descriptor::calc_checksum;
use bdk::psbt::PsbtUtils; use bdk::psbt::PsbtUtils;
use bdk::signer::{SignOptions, SignerError}; 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::*;
use bdk::wallet::{AddressIndex, AddressInfo, Balance, Wallet}; use bdk::wallet::{AddressIndex, AddressInfo, Balance, Wallet};
use bdk::{Error, FeeRate, KeychainKind}; use bdk::{FeeRate, KeychainKind};
use bdk_chain::COINBASE_MATURITY; use bdk_chain::COINBASE_MATURITY;
use bdk_chain::{BlockId, ConfirmationTime}; use bdk_chain::{BlockId, ConfirmationTime};
use bitcoin::hashes::Hash; use bitcoin::hashes::Hash;
@ -309,7 +311,6 @@ fn test_create_tx_manually_selected_empty_utxos() {
} }
#[test] #[test]
#[should_panic(expected = "Invalid version `0`")]
fn test_create_tx_version_0() { fn test_create_tx_version_0() {
let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New); let addr = wallet.get_address(New);
@ -317,13 +318,10 @@ fn test_create_tx_version_0() {
builder builder
.add_recipient(addr.script_pubkey(), 25_000) .add_recipient(addr.script_pubkey(), 25_000)
.version(0); .version(0);
builder.finish().unwrap(); assert!(matches!(builder.finish(), Err(CreateTxError::Version0)));
} }
#[test] #[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() { fn test_create_tx_version_1_csv() {
let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv());
let addr = wallet.get_address(New); let addr = wallet.get_address(New);
@ -331,7 +329,7 @@ fn test_create_tx_version_1_csv() {
builder builder
.add_recipient(addr.script_pubkey(), 25_000) .add_recipient(addr.script_pubkey(), 25_000)
.version(1); .version(1);
builder.finish().unwrap(); assert!(matches!(builder.finish(), Err(CreateTxError::Version1Csv)));
} }
#[test] #[test]
@ -419,9 +417,6 @@ fn test_create_tx_custom_locktime_compatible_with_cltv() {
} }
#[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() { fn test_create_tx_custom_locktime_incompatible_with_cltv() {
let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv());
let addr = wallet.get_address(New); let addr = wallet.get_address(New);
@ -429,7 +424,9 @@ fn test_create_tx_custom_locktime_incompatible_with_cltv() {
builder builder
.add_recipient(addr.script_pubkey(), 25_000) .add_recipient(addr.script_pubkey(), 25_000)
.nlocktime(absolute::LockTime::from_height(50000).unwrap()); .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]
@ -458,9 +455,6 @@ fn test_create_tx_with_default_rbf_csv() {
} }
#[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() { fn test_create_tx_with_custom_rbf_csv() {
let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv());
let addr = wallet.get_address(New); let addr = wallet.get_address(New);
@ -468,7 +462,9 @@ fn test_create_tx_with_custom_rbf_csv() {
builder builder
.add_recipient(addr.script_pubkey(), 25_000) .add_recipient(addr.script_pubkey(), 25_000)
.enable_rbf_with_sequence(Sequence(3)); .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]
@ -483,7 +479,6 @@ fn test_create_tx_no_rbf_cltv() {
} }
#[test] #[test]
#[should_panic(expected = "Cannot enable RBF with a nSequence >= 0xFFFFFFFE")]
fn test_create_tx_invalid_rbf_sequence() { fn test_create_tx_invalid_rbf_sequence() {
let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New); let addr = wallet.get_address(New);
@ -491,7 +486,7 @@ fn test_create_tx_invalid_rbf_sequence() {
builder builder
.add_recipient(addr.script_pubkey(), 25_000) .add_recipient(addr.script_pubkey(), 25_000)
.enable_rbf_with_sequence(Sequence(0xFFFFFFFE)); .enable_rbf_with_sequence(Sequence(0xFFFFFFFE));
builder.finish().unwrap(); assert!(matches!(builder.finish(), Err(CreateTxError::RbfSequence)));
} }
#[test] #[test]
@ -519,9 +514,6 @@ fn test_create_tx_default_sequence() {
} }
#[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() { fn test_create_tx_change_policy_no_internal() {
let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let addr = wallet.get_address(New); let addr = wallet.get_address(New);
@ -529,7 +521,10 @@ fn test_create_tx_change_policy_no_internal() {
builder builder
.add_recipient(addr.script_pubkey(), 25_000) .add_recipient(addr.script_pubkey(), 25_000)
.do_not_spend_change(); .do_not_spend_change();
builder.finish().unwrap(); assert!(matches!(
builder.finish(),
Err(CreateTxError::ChangePolicyDescriptor)
));
} }
macro_rules! check_fee { macro_rules! check_fee {
@ -1236,7 +1231,6 @@ fn test_calculate_fee_with_missing_foreign_utxo() {
} }
#[test] #[test]
#[should_panic(expected = "Generic(\"Foreign utxo missing witness_utxo or non_witness_utxo\")")]
fn test_add_foreign_utxo_invalid_psbt_input() { fn test_add_foreign_utxo_invalid_psbt_input() {
let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let outpoint = wallet.list_unspent().next().expect("must exist").outpoint; let outpoint = wallet.list_unspent().next().expect("must exist").outpoint;
@ -1247,9 +1241,9 @@ fn test_add_foreign_utxo_invalid_psbt_input() {
.unwrap(); .unwrap();
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder let result =
.add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction) builder.add_foreign_utxo(outpoint, psbt::Input::default(), foreign_utxo_satisfaction);
.unwrap(); assert!(matches!(result, Err(AddForeignUtxoError::MissingUtxo)));
} }
#[test] #[test]
@ -2531,7 +2525,7 @@ fn test_sign_nonstandard_sighash() {
); );
assert_matches!( assert_matches!(
result, result,
Err(bdk::Error::Signer(SignerError::NonStandardSighash)), Err(SignerError::NonStandardSighash),
"Signing failed with the wrong error type" "Signing failed with the wrong error type"
); );
@ -2948,7 +2942,7 @@ fn test_taproot_sign_missing_witness_utxo() {
); );
assert_matches!( assert_matches!(
result, result,
Err(Error::Signer(SignerError::MissingWitnessUtxo)), Err(SignerError::MissingWitnessUtxo),
"Signing should have failed with the correct error because the witness_utxo is missing" "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!( assert_matches!(
result, result,
Err(Error::Signer(SignerError::NonStandardSighash)), Err(SignerError::NonStandardSighash),
"Signing failed with the wrong error type" "Signing failed with the wrong error type"
); );
@ -3307,7 +3301,7 @@ fn test_taproot_sign_non_default_sighash() {
); );
assert_matches!( assert_matches!(
result, result,
Err(Error::Signer(SignerError::MissingWitnessUtxo)), Err(SignerError::MissingWitnessUtxo),
"Signing failed with the wrong error type" "Signing failed with the wrong error type"
); );
@ -3395,10 +3389,12 @@ fn test_spend_coinbase() {
.current_height(confirmation_height); .current_height(confirmation_height);
assert!(matches!( assert!(matches!(
builder.finish(), builder.finish(),
Err(Error::InsufficientFunds { Err(CreateTxError::CoinSelection(
needed: _, coin_selection::Error::InsufficientFunds {
available: 0 needed: _,
}) available: 0
}
))
)); ));
// Still unspendable... // Still unspendable...
@ -3408,10 +3404,12 @@ fn test_spend_coinbase() {
.current_height(not_yet_mature_time); .current_height(not_yet_mature_time);
assert_matches!( assert_matches!(
builder.finish(), builder.finish(),
Err(Error::InsufficientFunds { Err(CreateTxError::CoinSelection(
needed: _, coin_selection::Error::InsufficientFunds {
available: 0 needed: _,
}) available: 0
}
))
); );
wallet wallet
@ -3447,7 +3445,10 @@ fn test_allow_dust_limit() {
builder.add_recipient(addr.script_pubkey(), 0); 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(); let mut builder = wallet.build_tx();

View File

@ -57,6 +57,7 @@ use crate::{
use alloc::collections::vec_deque::VecDeque; use alloc::collections::vec_deque::VecDeque;
use alloc::vec::Vec; use alloc::vec::Vec;
use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid}; use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid};
use core::fmt::{self, Formatter};
use core::{ use core::{
convert::Infallible, convert::Infallible,
ops::{Deref, RangeInclusive}, ops::{Deref, RangeInclusive},
@ -145,6 +146,26 @@ pub enum CalculateFeeError {
NegativeFee(i64), 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> { impl<A> TxGraph<A> {
/// Iterate over all tx outputs known by [`TxGraph`]. /// Iterate over all tx outputs known by [`TxGraph`].
/// ///

View File

@ -7,3 +7,4 @@ edition = "2021"
bdk = { path = "../../crates/bdk" } bdk = { path = "../../crates/bdk" }
bdk_electrum = { path = "../../crates/electrum" } bdk_electrum = { path = "../../crates/electrum" }
bdk_file_store = { path = "../../crates/file_store" } bdk_file_store = { path = "../../crates/file_store" }
anyhow = "1"

View File

@ -16,7 +16,7 @@ use bdk_electrum::{
}; };
use bdk_file_store::Store; 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_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 db = Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";

View File

@ -10,3 +10,4 @@ bdk = { path = "../../crates/bdk" }
bdk_esplora = { path = "../../crates/esplora", features = ["async-https"] } bdk_esplora = { path = "../../crates/esplora", features = ["async-https"] }
bdk_file_store = { path = "../../crates/file_store" } bdk_file_store = { path = "../../crates/file_store" }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
anyhow = "1"

View File

@ -14,7 +14,7 @@ const STOP_GAP: usize = 50;
const PARALLEL_REQUESTS: usize = 5; const PARALLEL_REQUESTS: usize = 5;
#[tokio::main] #[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_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 db = Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";

View File

@ -10,3 +10,4 @@ publish = false
bdk = { path = "../../crates/bdk" } bdk = { path = "../../crates/bdk" }
bdk_esplora = { path = "../../crates/esplora", features = ["blocking"] } bdk_esplora = { path = "../../crates/esplora", features = ["blocking"] }
bdk_file_store = { path = "../../crates/file_store" } bdk_file_store = { path = "../../crates/file_store" }
anyhow = "1"

View File

@ -13,7 +13,7 @@ use bdk::{
use bdk_esplora::{esplora_client, EsploraExt}; use bdk_esplora::{esplora_client, EsploraExt};
use bdk_file_store::Store; 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_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 db = Store::<bdk::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";