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:
parent
cc552c5f91
commit
9e7d99e3bf
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
292
crates/bdk/src/wallet/error.rs
Normal file
292
crates/bdk/src/wallet/error.rs
Normal 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 {}
|
@ -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,
|
||||||
{
|
{
|
||||||
|
@ -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),
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
))),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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`].
|
||||||
///
|
///
|
||||||
|
@ -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"
|
||||||
|
@ -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/*)";
|
||||||
|
@ -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"
|
||||||
|
@ -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/*)";
|
||||||
|
@ -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"
|
||||||
|
@ -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/*)";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user