From 5ad0f138c637159facaa37ef15fd55b0be894400 Mon Sep 17 00:00:00 2001 From: thunderbiscuit Date: Thu, 30 Jul 2026 10:56:24 -0400 Subject: [PATCH 1/7] feat(keyring): add KeyRing --- src/keyring/error.rs | 49 +++++++++ src/keyring/mod.rs | 230 +++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 3 files changed, 280 insertions(+) create mode 100644 src/keyring/error.rs create mode 100644 src/keyring/mod.rs diff --git a/src/keyring/error.rs b/src/keyring/error.rs new file mode 100644 index 00000000..af8a7015 --- /dev/null +++ b/src/keyring/error.rs @@ -0,0 +1,49 @@ +//! Errors produced while building a [`KeyRing`](crate::keyring::KeyRing). + +use crate::descriptor::DescriptorError; +use alloc::boxed::Box; +use core::fmt; +use miniscript::{Descriptor, DescriptorPublicKey}; + +/// Error returned when a descriptor cannot be added to a [`KeyRing`](crate::keyring::KeyRing). +#[derive(Debug)] +#[non_exhaustive] +pub enum KeyRingError { + /// The descriptor is invalid, does not match the keyring's network, or fails the checks + /// applied to every wallet descriptor. + Descriptor(DescriptorError), + /// The keychain is already assigned to a different descriptor. + /// + /// A keychain identifies exactly one descriptor for the life of the wallet. + KeychainAlreadyAssigned(K), + /// The descriptor is already assigned to a different keychain. + /// + /// Two keychains sharing a descriptor cannot be told apart when attributing discovered + /// outputs, so the indexer rejects it. + DescriptorAlreadyAssigned(Box>), +} + +impl From for KeyRingError { + fn from(err: DescriptorError) -> Self { + Self::Descriptor(err) + } +} + +impl fmt::Display for KeyRingError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Descriptor(e) => e.fmt(f), + Self::KeychainAlreadyAssigned(keychain) => { + write!(f, "keychain {keychain} is already assigned to a descriptor") + } + Self::DescriptorAlreadyAssigned(descriptor) => { + write!( + f, + "descriptor {descriptor} is already assigned to a keychain" + ) + } + } + } +} + +impl core::error::Error for KeyRingError {} diff --git a/src/keyring/mod.rs b/src/keyring/mod.rs new file mode 100644 index 00000000..a6a39db8 --- /dev/null +++ b/src/keyring/mod.rs @@ -0,0 +1,230 @@ +//! A validated set of keychains for constructing a [`Wallet`](crate::Wallet). +//! +//! A [`KeyRing`] pairs each keychain identifier `K` with the descriptor that keychain tracks, on a +//! single [`Network`]. It is a *construction-time* value: [`Wallet::create`](crate::Wallet::create) +//! consumes it, hands the descriptors to the wallet's indexer, and the `KeyRing` itself is not +//! retained. +//! +//! # Invariant +//! +//! A `KeyRing` is a proof that a valid `Wallet` can be constructed from it: +//! +//! - every descriptor parses and passes [`check_wallet_descriptor`] +//! - every descriptor matches the keyring's network +//! - each keychain maps to exactly one descriptor, and each descriptor to exactly one keychain +//! - at least one keychain exists +//! +//! Because [`KeyRing::new`] requires a descriptor, an empty keyring cannot be represented. All +//! validation happens in [`KeyRing::new`] and [`KeyRing::add_descriptor`]; there is no other way to +//! build one. Do not add a `Deserialize` implementation without validating on the way in — it would +//! reconstruct a `KeyRing` while skipping these checks. +//! +//! ``` +//! # use bdk_wallet::keyring::KeyRing; +//! # use bdk_wallet::KeychainKind; +//! # use bitcoin::Network; +//! # const EXTERNAL: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; +//! # const INTERNAL: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; +//! let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, EXTERNAL)?; +//! keyring.add_descriptor(KeychainKind::Internal, INTERNAL)?; +//! +//! assert_eq!(keyring.keychains().count(), 2); +//! # Ok::<(), bdk_wallet::keyring::KeyRingError>(()) +//! ``` +//! +//! [`check_wallet_descriptor`]: mod@crate::descriptor + +pub mod error; + +pub use error::KeyRingError; + +use crate::collections::BTreeMap; +use crate::descriptor::{IntoWalletDescriptor, check_wallet_descriptor}; +use crate::wallet::utils::SecpCtx; +use bitcoin::Network; +use bitcoin::secp256k1::Secp256k1; +use core::fmt::Debug; +use miniscript::{Descriptor, DescriptorPublicKey}; + +/// A validated set of keychains, used to construct a [`Wallet`](crate::Wallet). +/// +/// See the [module documentation](self) for the invariant this type upholds. +#[derive(Debug)] +pub struct KeyRing { + secp: SecpCtx, + network: Network, + keychains: BTreeMap>, +} + +impl KeyRing +where + K: Ord + Clone + Debug, +{ + /// Create a `KeyRing` for `network` holding a single `keychain`. + /// + /// More keychains can be added with [`add_descriptor`](Self::add_descriptor). A keyring always + /// holds at least one, so there is no "empty keyring" state to guard against later. + /// + /// # Errors + /// + /// If `descriptor` cannot be parsed, does not match `network`, or fails the checks applied to + /// every wallet descriptor (multipath, hardened derivation in a public descriptor, miniscript + /// sanity). + pub fn new( + network: Network, + keychain: K, + descriptor: impl IntoWalletDescriptor, + ) -> Result> { + let secp = Secp256k1::new(); + let descriptor = Self::validate(&secp, network, descriptor)?; + + Ok(Self { + secp, + network, + keychains: BTreeMap::from([(keychain, descriptor)]), + }) + } + + /// Assign `descriptor` to `keychain`. + /// + /// # Errors + /// + /// As [`new`](Self::new), and additionally if `keychain` is already assigned a descriptor, or + /// if `descriptor` is already assigned to another keychain. Both would make the wallet's + /// indexer unable to attribute discovered outputs unambiguously. + /// + /// On error the keyring is left unchanged. + pub fn add_descriptor( + &mut self, + keychain: K, + descriptor: impl IntoWalletDescriptor, + ) -> Result<(), KeyRingError> { + let descriptor = Self::validate(&self.secp, self.network, descriptor)?; + + if self.keychains.contains_key(&keychain) { + return Err(KeyRingError::KeychainAlreadyAssigned(keychain)); + } + if self.keychains.values().any(|d| d == &descriptor) { + return Err(KeyRingError::DescriptorAlreadyAssigned( + alloc::boxed::Box::new(descriptor), + )); + } + + self.keychains.insert(keychain, descriptor); + Ok(()) + } + + /// The network these descriptors are valid for. + pub fn network(&self) -> Network { + self.network + } + + /// Iterate over the keychains and their descriptors. + /// + /// # Ordering + /// + /// Keychains are yielded in the order defined by `K`'s [`Ord`] implementation. That order is + /// stable across runs but otherwise *arbitrary* — for a derived `Ord` on an enum it is + /// declaration order, so reordering variants changes what you see here. It carries no meaning; + /// sort explicitly if a particular order matters. + pub fn keychains(&self) -> impl Iterator)> { + self.keychains.iter() + } + + /// The descriptor assigned to `keychain`, if any. + pub fn descriptor(&self, keychain: &K) -> Option<&Descriptor> { + self.keychains.get(keychain) + } + + fn validate( + secp: &SecpCtx, + network: Network, + descriptor: impl IntoWalletDescriptor, + ) -> Result, KeyRingError> { + let (descriptor, _keymap) = descriptor.into_wallet_descriptor(secp, network.into())?; + check_wallet_descriptor(&descriptor)?; + Ok(descriptor) + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +#[cfg(test)] +mod test { + use super::*; + use crate::KeychainKind; + use alloc::vec; + use alloc::vec::Vec; + + const EXTERNAL: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; + const INTERNAL: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; + + #[test] + fn new_holds_one_keychain() { + let keyring = KeyRing::new(Network::Testnet, KeychainKind::External, EXTERNAL).unwrap(); + assert_eq!(keyring.network(), Network::Testnet); + assert_eq!(keyring.keychains().count(), 1); + assert!(keyring.descriptor(&KeychainKind::External).is_some()); + assert!(keyring.descriptor(&KeychainKind::Internal).is_none()); + } + + #[test] + fn add_descriptor_extends() { + let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, EXTERNAL).unwrap(); + keyring + .add_descriptor(KeychainKind::Internal, INTERNAL) + .unwrap(); + assert_eq!(keyring.keychains().count(), 2); + } + + #[test] + fn rejects_duplicate_keychain() { + let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, EXTERNAL).unwrap(); + assert!(matches!( + keyring.add_descriptor(KeychainKind::External, INTERNAL), + Err(KeyRingError::KeychainAlreadyAssigned( + KeychainKind::External + )) + )); + // unchanged on error + assert_eq!(keyring.keychains().count(), 1); + } + + #[test] + fn rejects_duplicate_descriptor() { + let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, EXTERNAL).unwrap(); + assert!(matches!( + keyring.add_descriptor(KeychainKind::Internal, EXTERNAL), + Err(KeyRingError::DescriptorAlreadyAssigned(_)) + )); + assert_eq!(keyring.keychains().count(), 1); + } + + #[test] + fn rejects_wrong_network() { + // A testnet xpriv in a keyring declared for Bitcoin. + assert!(matches!( + KeyRing::new(Network::Bitcoin, KeychainKind::External, EXTERNAL), + Err(KeyRingError::Descriptor(_)) + )); + } + + #[test] + fn rejects_multipath_descriptor() { + const MULTIPATH: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/<0;1>/*)"; + assert!(matches!( + KeyRing::new(Network::Testnet, KeychainKind::External, MULTIPATH), + Err(KeyRingError::Descriptor(_)) + )); + } + + #[test] + fn keychains_iterate_in_ord_order() { + let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::Internal, INTERNAL).unwrap(); + keyring + .add_descriptor(KeychainKind::External, EXTERNAL) + .unwrap(); + // External < Internal by declaration order, regardless of insertion order. + let order: Vec<_> = keyring.keychains().map(|(k, _)| *k).collect(); + assert_eq!(order, vec![KeychainKind::External, KeychainKind::Internal]); + } +} diff --git a/src/lib.rs b/src/lib.rs index ce6ccd13..3974d324 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,7 @@ pub extern crate serde; pub extern crate serde_json; pub mod descriptor; +pub mod keyring; pub mod keys; pub mod psbt; #[cfg(feature = "test-utils")] From 13224aee81ade745889a4a0fe1ad56ce9a31f4d0 Mon Sep 17 00:00:00 2001 From: thunderbiscuit Date: Thu, 30 Jul 2026 14:54:40 -0400 Subject: [PATCH 2/7] Add multi-keychain feature --- README.md | 10 +- examples/bitcoind_rpc.rs | 23 +- examples/compiler.rs | 10 +- examples/electrum.rs | 13 +- examples/esplora_async.rs | 12 +- examples/esplora_blocking.rs | 12 +- examples/psbt.rs | 16 +- examples/replace_by_fee.rs | 13 +- src/descriptor/template.rs | 149 +- src/keyring/mod.rs | 14 + src/lib.rs | 1 + src/persist_test_utils.rs | 74 +- src/test_utils.rs | 59 +- src/types.rs | 49 +- src/wallet/changeset.rs | 330 ++- src/wallet/error.rs | 30 +- src/wallet/event.rs | 4 +- src/wallet/export.rs | 76 +- src/wallet/mod.rs | 5334 ++++++++++++++++------------------ src/wallet/params.rs | 180 +- src/wallet/persisted.rs | 134 +- src/wallet/signer.rs | 9 +- src/wallet/tx_builder.rs | 55 +- tests/common.rs | 9 +- tests/create_psbt.rs | 172 +- tests/persisted_wallet.rs | 91 +- tests/wallet.rs | 272 +- 27 files changed, 3717 insertions(+), 3434 deletions(-) diff --git a/README.md b/README.md index 71b0265d..2e45a17c 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ To persist `Wallet` state use a data storage crate that reads and writes [`Chang ```rust,no_run use bdk_wallet::rusqlite; -use bdk_wallet::{KeychainKind, Wallet}; +use bdk_wallet::{KeyRing, KeychainKind, Wallet}; // Open or create a new SQLite database for wallet data. let db_path = "my_wallet.sqlite"; @@ -96,9 +96,11 @@ let mut wallet = match Wallet::load() .load_wallet(&mut conn)? { Some(wallet) => wallet, - None => Wallet::create(descriptor, change_descriptor) - .network(network) - .create_wallet(&mut conn)?, + None => { + let mut keyring = KeyRing::new(network, KeychainKind::External, descriptor)?; + keyring.add_descriptor(KeychainKind::Internal, change_descriptor)?; + Wallet::create(keyring).create_wallet(&mut conn)? + } }; // Get a new address to receive bitcoin! diff --git a/examples/bitcoind_rpc.rs b/examples/bitcoind_rpc.rs index c6e20cda..9219e841 100644 --- a/examples/bitcoind_rpc.rs +++ b/examples/bitcoind_rpc.rs @@ -2,6 +2,7 @@ use bdk_bitcoind_rpc::{ Emitter, MempoolEvent, bitcoincore_rpc::{Auth, Client, RpcApi}, }; +use bdk_wallet::KeyRing; use bdk_wallet::rusqlite::Connection; use bdk_wallet::{ KeychainKind, Wallet, @@ -101,14 +102,20 @@ fn main() -> anyhow::Result<()> { .load_wallet(&mut db)?; let mut wallet = match wallet_opt { Some(wallet) => wallet, - None => match &args.change_descriptor { - Some(change_desc) => Wallet::create(args.descriptor.clone(), change_desc.clone()) - .network(args.network) - .create_wallet(&mut db)?, - None => Wallet::create_single(args.descriptor.clone()) - .network(args.network) - .create_wallet(&mut db)?, - }, + None => { + let mut keyring = KeyRing::new( + args.network, + KeychainKind::External, + args.descriptor.clone(), + ) + .expect("valid descriptor"); + if let Some(change_desc) = &args.change_descriptor { + keyring + .add_descriptor(KeychainKind::Internal, change_desc.clone()) + .expect("valid change descriptor"); + } + Wallet::create(keyring).create_wallet(&mut db)? + } }; println!( "Loaded wallet in {}s", diff --git a/examples/compiler.rs b/examples/compiler.rs index 19eea3d2..a8d1e6f6 100644 --- a/examples/compiler.rs +++ b/examples/compiler.rs @@ -24,7 +24,7 @@ use miniscript::policy::Concrete; use bdk_wallet::descriptor::ExtractPolicy; use bdk_wallet::descriptor::policy::BuildSatisfaction; use bdk_wallet::signer::SignersContainer; -use bdk_wallet::{KeychainKind, Wallet}; +use bdk_wallet::{KeyRing, KeychainKind, Wallet}; /// Miniscript policy is a high level abstraction of spending conditions. Defined in the /// rust-miniscript library here https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html @@ -60,9 +60,11 @@ fn main() -> Result<(), Box> { println!("Compiled into Descriptor: \n{descriptor}"); // Create a new wallet from descriptors - let mut wallet = Wallet::create_single(descriptor) - .network(Network::Regtest) - .create_wallet_no_persist()?; + let mut wallet = Wallet::create( + KeyRing::new(Network::Regtest, KeychainKind::External, descriptor) + .expect("valid descriptors"), + ) + .create_wallet_no_persist(); println!( "First derived address from the descriptor: \n{}", diff --git a/examples/electrum.rs b/examples/electrum.rs index 2a667ecb..c195a7ca 100644 --- a/examples/electrum.rs +++ b/examples/electrum.rs @@ -10,7 +10,7 @@ use bdk_wallet::descriptor::IntoWalletDescriptor; use bdk_wallet::miniscript::descriptor::KeyMapWrapper; use bdk_wallet::psbt::PsbtUtils; use bdk_wallet::rusqlite::Connection; -use bdk_wallet::{KeychainKind, SignOptions}; +use bdk_wallet::{KeyRing, KeychainKind, SignOptions}; use std::io::Write; use std::thread::sleep; use std::time::Duration; @@ -43,9 +43,14 @@ fn main() -> Result<(), anyhow::Error> { .load_wallet(&mut db)?; let mut wallet = match wallet_opt { Some(wallet) => wallet, - None => Wallet::create(external_descriptor, internal_descriptor) - .network(NETWORK) - .create_wallet(&mut db)?, + None => { + let mut keyring = KeyRing::new(NETWORK, KeychainKind::External, external_descriptor) + .expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, internal_descriptor) + .expect("valid change descriptor"); + Wallet::create(keyring).create_wallet(&mut db)? + } }; let address = wallet.next_unused_address(KeychainKind::External); diff --git a/examples/esplora_async.rs b/examples/esplora_async.rs index b1c37fdd..e1d0afd2 100644 --- a/examples/esplora_async.rs +++ b/examples/esplora_async.rs @@ -1,4 +1,5 @@ use bdk_esplora::{EsploraAsyncExt, esplora_client}; +use bdk_wallet::KeyRing; use bdk_wallet::bitcoin::secp256k1::Secp256k1; use bdk_wallet::descriptor::IntoWalletDescriptor; use bdk_wallet::miniscript::descriptor::KeyMapWrapper; @@ -40,9 +41,14 @@ async fn main() -> Result<(), anyhow::Error> { .load_wallet(&mut db)?; let mut wallet = match wallet_opt { Some(wallet) => wallet, - None => Wallet::create(external_descriptor, internal_descriptor) - .network(NETWORK) - .create_wallet(&mut db)?, + None => { + let mut keyring = KeyRing::new(NETWORK, KeychainKind::External, external_descriptor) + .expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, internal_descriptor) + .expect("valid change descriptor"); + Wallet::create(keyring).create_wallet(&mut db)? + } }; let address = wallet.next_unused_address(KeychainKind::External); diff --git a/examples/esplora_blocking.rs b/examples/esplora_blocking.rs index bbd593aa..16382f93 100644 --- a/examples/esplora_blocking.rs +++ b/examples/esplora_blocking.rs @@ -1,4 +1,5 @@ use bdk_esplora::{EsploraExt, esplora_client}; +use bdk_wallet::KeyRing; use bdk_wallet::bitcoin::secp256k1::Secp256k1; use bdk_wallet::descriptor::IntoWalletDescriptor; use bdk_wallet::miniscript::descriptor::KeyMapWrapper; @@ -40,9 +41,14 @@ fn main() -> Result<(), anyhow::Error> { .load_wallet(&mut db)?; let mut wallet = match wallet_opt { Some(wallet) => wallet, - None => Wallet::create(external_descriptor, internal_descriptor) - .network(NETWORK) - .create_wallet(&mut db)?, + None => { + let mut keyring = KeyRing::new(NETWORK, KeychainKind::External, external_descriptor) + .expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, internal_descriptor) + .expect("valid change descriptor"); + Wallet::create(keyring).create_wallet(&mut db)? + } }; let address = wallet.next_unused_address(KeychainKind::External); diff --git a/examples/psbt.rs b/examples/psbt.rs index 5b8368bd..3adf123c 100644 --- a/examples/psbt.rs +++ b/examples/psbt.rs @@ -7,7 +7,11 @@ use bdk_chain::BlockId; use bdk_chain::ConfirmationBlockTime; use bdk_wallet::psbt::{PsbtParams, SelectionStrategy::*}; use bdk_wallet::test_utils::*; -use bdk_wallet::{KeychainKind::External, Wallet}; +use bdk_wallet::{ + KeyRing, KeychainKind, + KeychainKind::{External, Internal}, + Wallet, +}; use bitcoin::{Address, Amount, TxIn, TxOut, consensus, secp256k1::rand}; use rand::Rng; @@ -22,9 +26,11 @@ fn main() -> anyhow::Result<()> { let (desc, change_desc) = get_test_wpkh_and_change_desc(); // Create wallet and fund it. - let mut wallet = Wallet::create(desc, change_desc) - .network(NETWORK) - .create_wallet_no_persist()?; + let mut keyring = KeyRing::new(NETWORK, External, desc).expect("valid descriptor"); + keyring + .add_descriptor(Internal, change_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); fund_wallet(&mut wallet)?; @@ -80,7 +86,7 @@ fn main() -> anyhow::Result<()> { Ok(()) } -fn fund_wallet(wallet: &mut Wallet) -> anyhow::Result<()> { +fn fund_wallet(wallet: &mut Wallet) -> anyhow::Result<()> { let anchor = ConfirmationBlockTime { block_id: BlockId { height: 260071, diff --git a/examples/replace_by_fee.rs b/examples/replace_by_fee.rs index f69cfd39..33a79b70 100644 --- a/examples/replace_by_fee.rs +++ b/examples/replace_by_fee.rs @@ -6,7 +6,7 @@ use bdk_chain::BlockId; use bdk_tx::ChangeScript; use bdk_wallet::psbt::PsbtParams; use bdk_wallet::test_utils::*; -use bdk_wallet::{KeychainKind, Wallet}; +use bdk_wallet::{KeyRing, KeychainKind, Wallet}; use bitcoin::{Amount, FeeRate, TxIn, TxOut}; use miniscript::{DefiniteDescriptorKey, Descriptor}; @@ -19,9 +19,12 @@ fn main() -> anyhow::Result<()> { let (desc, change_desc) = get_test_wpkh_and_change_desc(); // Create wallet and "fund" it with a single UTXO. - let mut wallet = Wallet::create(desc, change_desc) - .network(NETWORK) - .create_wallet_no_persist()?; + let mut keyring = + KeyRing::new(NETWORK, KeychainKind::External, desc).expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); fund_wallet(&mut wallet)?; @@ -156,7 +159,7 @@ fn main() -> anyhow::Result<()> { Ok(()) } -fn fund_wallet(wallet: &mut Wallet) -> anyhow::Result<()> { +fn fund_wallet(wallet: &mut Wallet) -> anyhow::Result<()> { let anchor_block = BlockId { height: 1, hash: "3bcc1c447c6b3886f43e416b5c21cf5c139dc4829a71dc78609bc8f6235611c5".parse()?, diff --git a/src/descriptor/template.rs b/src/descriptor/template.rs index 86c5fa89..5959502a 100644 --- a/src/descriptor/template.rs +++ b/src/descriptor/template.rs @@ -77,6 +77,7 @@ impl IntoWalletDescriptor for T { /// ``` /// # use bdk_wallet::bitcoin::{PrivateKey, Network}; /// # use bdk_wallet::Wallet; +/// # use bdk_wallet::KeyRing; /// # use bdk_wallet::KeychainKind; /// use bdk_wallet::template::P2Pkh; /// @@ -84,9 +85,12 @@ impl IntoWalletDescriptor for T { /// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; /// let key_internal = /// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?; -/// let mut wallet = Wallet::create(P2Pkh(key_external), P2Pkh(key_internal)) -/// .network(Network::Testnet) -/// .create_wallet_no_persist()?; +/// let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, P2Pkh(key_external)) +/// .expect("valid descriptor"); +/// keyring +/// .add_descriptor(KeychainKind::Internal, P2Pkh(key_internal)) +/// .expect("valid change descriptor"); +/// let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); /// /// assert_eq!( /// wallet @@ -112,6 +116,7 @@ impl> DescriptorTemplate for P2Pkh { /// ``` /// # use bdk_wallet::bitcoin::{PrivateKey, Network}; /// # use bdk_wallet::Wallet; +/// # use bdk_wallet::KeyRing; /// # use bdk_wallet::KeychainKind; /// use bdk_wallet::template::P2Wpkh_P2Sh; /// @@ -119,9 +124,12 @@ impl> DescriptorTemplate for P2Pkh { /// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; /// let key_internal = /// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?; -/// let mut wallet = Wallet::create(P2Wpkh_P2Sh(key_external), P2Wpkh_P2Sh(key_internal)) -/// .network(Network::Testnet) -/// .create_wallet_no_persist()?; +/// let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, P2Wpkh_P2Sh(key_external)) +/// .expect("valid descriptor"); +/// keyring +/// .add_descriptor(KeychainKind::Internal, P2Wpkh_P2Sh(key_internal)) +/// .expect("valid change descriptor"); +/// let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); /// /// assert_eq!( /// wallet @@ -148,6 +156,7 @@ impl> DescriptorTemplate for P2Wpkh_P2Sh { /// ``` /// # use bdk_wallet::bitcoin::{PrivateKey, Network}; /// # use bdk_wallet::Wallet; +/// # use bdk_wallet::KeyRing; /// # use bdk_wallet::KeychainKind; /// use bdk_wallet::template::P2Wpkh; /// @@ -155,9 +164,12 @@ impl> DescriptorTemplate for P2Wpkh_P2Sh { /// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; /// let key_internal = /// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?; -/// let mut wallet = Wallet::create(P2Wpkh(key_external), P2Wpkh(key_internal)) -/// .network(Network::Testnet) -/// .create_wallet_no_persist()?; +/// let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, P2Wpkh(key_external)) +/// .expect("valid descriptor"); +/// keyring +/// .add_descriptor(KeychainKind::Internal, P2Wpkh(key_internal)) +/// .expect("valid change descriptor"); +/// let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); /// /// assert_eq!( /// wallet @@ -183,6 +195,7 @@ impl> DescriptorTemplate for P2Wpkh { /// ``` /// # use bdk_wallet::bitcoin::{PrivateKey, Network}; /// # use bdk_wallet::Wallet; +/// # use bdk_wallet::KeyRing; /// # use bdk_wallet::KeychainKind; /// use bdk_wallet::template::P2TR; /// @@ -190,9 +203,12 @@ impl> DescriptorTemplate for P2Wpkh { /// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?; /// let key_internal = /// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?; -/// let mut wallet = Wallet::create(P2TR(key_external), P2TR(key_internal)) -/// .network(Network::Testnet) -/// .create_wallet_no_persist()?; +/// let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, P2TR(key_external)) +/// .expect("valid descriptor"); +/// keyring +/// .add_descriptor(KeychainKind::Internal, P2TR(key_internal)) +/// .expect("valid change descriptor"); +/// let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); /// /// assert_eq!( /// wallet @@ -223,13 +239,16 @@ impl> DescriptorTemplate for P2TR { /// ```rust /// # use std::str::FromStr; /// # use bdk_wallet::bitcoin::{PrivateKey, Network}; -/// # use bdk_wallet::{Wallet, KeychainKind}; +/// # use bdk_wallet::{Wallet, KeychainKind, KeyRing}; /// use bdk_wallet::template::Bip44; /// /// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; -/// let mut wallet = Wallet::create(Bip44(key.clone(), KeychainKind::External), Bip44(key, KeychainKind::Internal)) -/// .network(Network::Testnet) -/// .create_wallet_no_persist()?; +/// let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, Bip44(key.clone(), KeychainKind::External)) +/// .expect("valid descriptor"); +/// keyring +/// .add_descriptor(KeychainKind::Internal, Bip44(key, KeychainKind::Internal)) +/// .expect("valid change descriptor"); +/// let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); /// /// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89"); /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt"); @@ -265,17 +284,17 @@ impl> DescriptorTemplate for Bip44 { /// ``` /// # use std::str::FromStr; /// # use bdk_wallet::bitcoin::{PrivateKey, Network}; -/// # use bdk_wallet::{KeychainKind, Wallet}; +/// # use bdk_wallet::{KeychainKind, KeyRing, Wallet}; /// use bdk_wallet::template::Bip44Public; /// /// let key = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?; /// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?; -/// let mut wallet = Wallet::create( -/// Bip44Public(key.clone(), fingerprint, KeychainKind::External), -/// Bip44Public(key, fingerprint, KeychainKind::Internal), -/// ) -/// .network(Network::Testnet) -/// .create_wallet_no_persist()?; +/// let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, Bip44Public(key.clone(), fingerprint, KeychainKind::External)) +/// .expect("valid descriptor"); +/// keyring +/// .add_descriptor(KeychainKind::Internal, Bip44Public(key, fingerprint, KeychainKind::Internal)) +/// .expect("valid change descriptor"); +/// let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); /// /// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR"); /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz"); @@ -309,16 +328,16 @@ impl> DescriptorTemplate for Bip44Public { /// ``` /// # use std::str::FromStr; /// # use bdk_wallet::bitcoin::{PrivateKey, Network}; -/// # use bdk_wallet::{Wallet, KeychainKind}; +/// # use bdk_wallet::{Wallet, KeychainKind, KeyRing}; /// use bdk_wallet::template::Bip49; /// /// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; -/// let mut wallet = Wallet::create( -/// Bip49(key.clone(), KeychainKind::External), -/// Bip49(key, KeychainKind::Internal), -/// ) -/// .network(Network::Testnet) -/// .create_wallet_no_persist()?; +/// let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, Bip49(key.clone(), KeychainKind::External)) +/// .expect("valid descriptor"); +/// keyring +/// .add_descriptor(KeychainKind::Internal, Bip49(key, KeychainKind::Internal)) +/// .expect("valid change descriptor"); +/// let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); /// /// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB"); /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e"); @@ -354,17 +373,17 @@ impl> DescriptorTemplate for Bip49 { /// ``` /// # use std::str::FromStr; /// # use bdk_wallet::bitcoin::{PrivateKey, Network}; -/// # use bdk_wallet::{Wallet, KeychainKind}; +/// # use bdk_wallet::{Wallet, KeychainKind, KeyRing}; /// use bdk_wallet::template::Bip49Public; /// /// let key = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?; /// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?; -/// let mut wallet = Wallet::create( -/// Bip49Public(key.clone(), fingerprint, KeychainKind::External), -/// Bip49Public(key, fingerprint, KeychainKind::Internal), -/// ) -/// .network(Network::Testnet) -/// .create_wallet_no_persist()?; +/// let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, Bip49Public(key.clone(), fingerprint, KeychainKind::External)) +/// .expect("valid descriptor"); +/// keyring +/// .add_descriptor(KeychainKind::Internal, Bip49Public(key, fingerprint, KeychainKind::Internal)) +/// .expect("valid change descriptor"); +/// let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); /// /// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt"); /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q"); @@ -398,16 +417,16 @@ impl> DescriptorTemplate for Bip49Public { /// ``` /// # use std::str::FromStr; /// # use bdk_wallet::bitcoin::{PrivateKey, Network}; -/// # use bdk_wallet::{Wallet, KeychainKind}; +/// # use bdk_wallet::{Wallet, KeychainKind, KeyRing}; /// use bdk_wallet::template::Bip84; /// /// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; -/// let mut wallet = Wallet::create( -/// Bip84(key.clone(), KeychainKind::External), -/// Bip84(key, KeychainKind::Internal), -/// ) -/// .network(Network::Testnet) -/// .create_wallet_no_persist()?; +/// let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, Bip84(key.clone(), KeychainKind::External)) +/// .expect("valid descriptor"); +/// keyring +/// .add_descriptor(KeychainKind::Internal, Bip84(key, KeychainKind::Internal)) +/// .expect("valid change descriptor"); +/// let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); /// /// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n"); /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr"); @@ -443,17 +462,17 @@ impl> DescriptorTemplate for Bip84 { /// ``` /// # use std::str::FromStr; /// # use bdk_wallet::bitcoin::{PrivateKey, Network}; -/// # use bdk_wallet::{Wallet, KeychainKind}; +/// # use bdk_wallet::{Wallet, KeychainKind, KeyRing}; /// use bdk_wallet::template::Bip84Public; /// /// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?; /// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?; -/// let mut wallet = Wallet::create( -/// Bip84Public(key.clone(), fingerprint, KeychainKind::External), -/// Bip84Public(key, fingerprint, KeychainKind::Internal), -/// ) -/// .network(Network::Testnet) -/// .create_wallet_no_persist()?; +/// let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, Bip84Public(key.clone(), fingerprint, KeychainKind::External)) +/// .expect("valid descriptor"); +/// keyring +/// .add_descriptor(KeychainKind::Internal, Bip84Public(key, fingerprint, KeychainKind::Internal)) +/// .expect("valid change descriptor"); +/// let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); /// /// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7"); /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv"); @@ -487,16 +506,16 @@ impl> DescriptorTemplate for Bip84Public { /// ``` /// # use std::str::FromStr; /// # use bdk_wallet::bitcoin::{PrivateKey, Network}; -/// # use bdk_wallet::{Wallet, KeychainKind}; +/// # use bdk_wallet::{Wallet, KeychainKind, KeyRing}; /// use bdk_wallet::template::Bip86; /// /// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?; -/// let mut wallet = Wallet::create( -/// Bip86(key.clone(), KeychainKind::External), -/// Bip86(key, KeychainKind::Internal), -/// ) -/// .network(Network::Testnet) -/// .create_wallet_no_persist()?; +/// let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, Bip86(key.clone(), KeychainKind::External)) +/// .expect("valid descriptor"); +/// keyring +/// .add_descriptor(KeychainKind::Internal, Bip86(key, KeychainKind::Internal)) +/// .expect("valid change descriptor"); +/// let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); /// /// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1p5unlj09djx8xsjwe97269kqtxqpwpu2epeskgqjfk4lnf69v4tnqpp35qu"); /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "tr([c55b303f/86'/1'/0']tpubDCiHofpEs47kx358bPdJmTZHmCDqQ8qw32upCSxHrSEdeeBs2T5Mq6QMB2ukeMqhNBiyhosBvJErteVhfURPGXPv3qLJPw5MVpHUewsbP2m/0/*)#dkgvr5hm"); @@ -532,17 +551,17 @@ impl> DescriptorTemplate for Bip86 { /// ``` /// # use std::str::FromStr; /// # use bdk_wallet::bitcoin::{PrivateKey, Network}; -/// # use bdk_wallet::{Wallet, KeychainKind}; +/// # use bdk_wallet::{Wallet, KeychainKind, KeyRing}; /// use bdk_wallet::template::Bip86Public; /// /// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?; /// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?; -/// let mut wallet = Wallet::create( -/// Bip86Public(key.clone(), fingerprint, KeychainKind::External), -/// Bip86Public(key, fingerprint, KeychainKind::Internal), -/// ) -/// .network(Network::Testnet) -/// .create_wallet_no_persist()?; +/// let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, Bip86Public(key.clone(), fingerprint, KeychainKind::External)) +/// .expect("valid descriptor"); +/// keyring +/// .add_descriptor(KeychainKind::Internal, Bip86Public(key, fingerprint, KeychainKind::Internal)) +/// .expect("valid change descriptor"); +/// let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); /// /// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1pwjp9f2k5n0xq73ecuu0c5njvgqr3vkh7yaylmpqvsuuaafymh0msvcmh37"); /// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "tr([c55b303f/86'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#2p65srku"); diff --git a/src/keyring/mod.rs b/src/keyring/mod.rs index a6a39db8..d2e7d7bb 100644 --- a/src/keyring/mod.rs +++ b/src/keyring/mod.rs @@ -136,6 +136,20 @@ where self.keychains.get(keychain) } + /// Convert into the parameters used to create a [`Wallet`](crate::Wallet). + /// + /// Prefer [`Wallet::create`](crate::Wallet::create), which calls this for you. + pub fn into_params(self) -> crate::CreateParams { + crate::CreateParams { + secp: self.secp, + descriptors: self.keychains, + network: self.network, + genesis_hash: None, + lookahead: bdk_chain::keychain_txout::DEFAULT_LOOKAHEAD, + use_spk_cache: false, + } + } + fn validate( secp: &SecpCtx, network: Network, diff --git a/src/lib.rs b/src/lib.rs index 3974d324..bf59c83e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -51,6 +51,7 @@ pub use bdk_chain::rusqlite; pub use bdk_chain::rusqlite_impl; pub use descriptor::HdKeyPaths; pub use descriptor::template; +pub use keyring::KeyRing; pub use psbt::*; pub use signer; pub use signer::SignOptions; diff --git a/src/persist_test_utils.rs b/src/persist_test_utils.rs index 1981af0f..998885a2 100644 --- a/src/persist_test_utils.rs +++ b/src/persist_test_utils.rs @@ -14,7 +14,7 @@ use bitcoin::{ }; use miniscript::{Descriptor, DescriptorPublicKey}; -use crate::{AsyncWalletPersister, ChangeSet, WalletPersister, locked_outpoints}; +use crate::{AsyncWalletPersister, ChangeSet, KeychainKind, WalletPersister, locked_outpoints}; macro_rules! block_id { ($height:expr, $hash:literal) => {{ @@ -68,7 +68,7 @@ fn spk_at_index(descriptor: &Descriptor, index: u32) -> Scr pub fn persist_wallet_changeset(create_store: F) -> Result<(), PersistError> where F: FnOnce() -> Result, - P: WalletPersister, + P: WalletPersister, P::Error: core::error::Error + 'static, { let mut persister = init_wallet_persister(create_store)?; @@ -90,7 +90,7 @@ where pub fn persist_multiple_wallet_changesets(create_stores: F) -> Result<(), PersistError> where F: Fn() -> Result<(P, P), P::Error>, - P: WalletPersister, + P: WalletPersister, P::Error: core::error::Error + 'static, { use PersistError as E; @@ -113,8 +113,11 @@ where let change_descriptor: Descriptor = DESCRIPTORS[1].parse().unwrap(); let changeset1 = ChangeSet { - descriptor: Some(descriptor.clone()), - change_descriptor: Some(change_descriptor.clone()), + descriptors: [ + (KeychainKind::External, descriptor.clone()), + (KeychainKind::Internal, change_descriptor.clone()), + ] + .into(), network: Some(Network::Testnet), ..ChangeSet::default() }; @@ -137,8 +140,11 @@ where let change_descriptor: Descriptor = DESCRIPTORS[3].parse().unwrap(); let changeset2 = ChangeSet { - descriptor: Some(descriptor.clone()), - change_descriptor: Some(change_descriptor.clone()), + descriptors: [ + (KeychainKind::External, descriptor.clone()), + (KeychainKind::Internal, change_descriptor.clone()), + ] + .into(), network: Some(Network::Testnet), ..ChangeSet::default() }; @@ -175,7 +181,7 @@ where pub fn persist_network(create_store: F) -> Result<(), PersistError> where F: FnOnce() -> Result, - P: WalletPersister, + P: WalletPersister, P::Error: core::error::Error + 'static, { let mut persister = init_wallet_persister(create_store)?; @@ -190,7 +196,7 @@ where pub fn persist_keychains(create_store: F) -> Result<(), PersistError> where F: FnOnce() -> Result, - P: WalletPersister, + P: WalletPersister, P::Error: core::error::Error + 'static, { let mut persister = init_wallet_persister(create_store)?; @@ -215,7 +221,7 @@ where fn init_wallet_persister(create_store: F) -> Result where F: FnOnce() -> Result, - P: WalletPersister, + P: WalletPersister, P::Error: core::error::Error + 'static, { let mut persister = create_store().map_err(PersistError::persister)?; @@ -238,11 +244,11 @@ where /// - If the newly initialized [`ChangeSet`] doesn't match `expected` fn check_changeset_is_persisted

( persister: &mut P, - changeset: &ChangeSet, - expected: &ChangeSet, + changeset: &ChangeSet, + expected: &ChangeSet, ) -> Result<(), PersistError> where - P: WalletPersister, + P: WalletPersister, P::Error: core::error::Error + 'static, { WalletPersister::persist(persister, changeset).map_err(PersistError::persister)?; @@ -256,31 +262,31 @@ where Ok(()) } -fn network_changeset() -> ChangeSet { +fn network_changeset() -> ChangeSet { ChangeSet { network: Some(Network::Bitcoin), ..Default::default() } } -fn descriptor_changeset() -> ChangeSet { +fn descriptor_changeset() -> ChangeSet { let descriptor: Descriptor = DESCRIPTORS[0].parse().unwrap(); ChangeSet { - descriptor: Some(descriptor), + descriptors: [(KeychainKind::External, descriptor)].into(), ..Default::default() } } -fn change_descriptor_changeset() -> ChangeSet { +fn change_descriptor_changeset() -> ChangeSet { let change_descriptor: Descriptor = DESCRIPTORS[1].parse().unwrap(); ChangeSet { - change_descriptor: Some(change_descriptor), + descriptors: [(KeychainKind::Internal, change_descriptor)].into(), ..Default::default() } } /// Creates a [`ChangeSet`]. -fn get_changeset(tx1: Transaction) -> ChangeSet { +fn get_changeset(tx1: Transaction) -> ChangeSet { let descriptor: Descriptor = DESCRIPTORS[0].parse().unwrap(); let change_descriptor: Descriptor = DESCRIPTORS[1].parse().unwrap(); @@ -351,8 +357,11 @@ fn get_changeset(tx1: Transaction) -> ChangeSet { }; ChangeSet { - descriptor: Some(descriptor.clone()), - change_descriptor: Some(change_descriptor.clone()), + descriptors: [ + (KeychainKind::External, descriptor.clone()), + (KeychainKind::Internal, change_descriptor.clone()), + ] + .into(), network: Some(Network::Testnet), local_chain: local_chain_changeset, tx_graph: tx_graph_changeset, @@ -365,7 +374,7 @@ fn get_changeset(tx1: Transaction) -> ChangeSet { /// /// To correctly test a wallet persister this should return a different /// [`ChangeSet`] than the one returned by [`get_changeset`]. -fn get_changeset_two(tx2: Transaction) -> ChangeSet { +fn get_changeset_two(tx2: Transaction) -> ChangeSet { let descriptor: Descriptor = DESCRIPTORS[0].parse().unwrap(); let local_chain_changeset = local_chain::ChangeSet { @@ -411,8 +420,7 @@ fn get_changeset_two(tx2: Transaction) -> ChangeSet { }; ChangeSet { - descriptor: None, - change_descriptor: None, + descriptors: Default::default(), network: None, local_chain: local_chain_changeset, tx_graph: tx_graph_changeset, @@ -428,9 +436,9 @@ pub enum PersistError { /// Change set mismatch ChangeSetMismatch { /// the resulting changeset - got: Box, + got: Box>, /// the expected changeset - expected: Box, + expected: Box>, }, /// The wallet persister implementation failed Persister(Box), @@ -471,7 +479,7 @@ impl PersistError { pub async fn persist_wallet_changeset_async(create_store: F) -> Result<(), PersistError> where F: AsyncFnOnce() -> Result, - P: AsyncWalletPersister, + P: AsyncWalletPersister, P::Error: core::error::Error + 'static, { let mut persister = init_async_wallet_persister(create_store).await?; @@ -492,7 +500,7 @@ where pub async fn persist_keychains_async(create_store: F) -> Result<(), PersistError> where F: AsyncFnOnce() -> Result, - P: AsyncWalletPersister, + P: AsyncWalletPersister, P::Error: core::error::Error + 'static, { let mut persister = init_async_wallet_persister(create_store).await?; @@ -512,7 +520,7 @@ where pub async fn persist_network_async(create_store: F) -> Result<(), PersistError> where F: AsyncFnOnce() -> Result, - P: AsyncWalletPersister, + P: AsyncWalletPersister, P::Error: core::error::Error + 'static, { let mut persister = init_async_wallet_persister(create_store).await?; @@ -532,7 +540,7 @@ where async fn init_async_wallet_persister(create_store: F) -> Result where F: AsyncFnOnce() -> Result, - P: AsyncWalletPersister, + P: AsyncWalletPersister, P::Error: core::error::Error + 'static, { let mut persister = create_store().await.map_err(PersistError::persister)?; @@ -557,11 +565,11 @@ where /// - If the newly initialized [`ChangeSet`] doesn't match `expected` async fn check_changeset_is_persisted_async

( persister: &mut P, - changeset: &ChangeSet, - expected: &ChangeSet, + changeset: &ChangeSet, + expected: &ChangeSet, ) -> Result<(), PersistError> where - P: AsyncWalletPersister, + P: AsyncWalletPersister, P::Error: core::error::Error + 'static, { AsyncWalletPersister::persist(persister, changeset) diff --git a/src/test_utils.rs b/src/test_utils.rs index a00c176c..f32fc93d 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -10,18 +10,24 @@ use bitcoin::{ absolute, hashes::Hash, transaction, }; -use crate::{KeychainKind, Update, Wallet}; +use crate::{KeyRing, KeychainKind, Update, Wallet}; /// Return a fake wallet that appears to be funded for testing. /// /// The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 /// to a foreign address and one returning 50_000 back to the wallet. The remaining 1000 /// sats are the transaction fee. -pub fn get_funded_wallet(descriptor: &str, change_descriptor: &str) -> (Wallet, Txid) { +pub fn get_funded_wallet( + descriptor: &str, + change_descriptor: &str, +) -> (Wallet, Txid) { new_funded_wallet(descriptor, Some(change_descriptor)) } -fn new_funded_wallet(descriptor: &str, change_descriptor: Option<&str>) -> (Wallet, Txid) { +fn new_funded_wallet( + descriptor: &str, + change_descriptor: Option<&str>, +) -> (Wallet, Txid) { let (mut wallet, txid, update) = new_wallet_and_funding_update(descriptor, change_descriptor); wallet.apply_update(update).unwrap(); (wallet, txid) @@ -32,12 +38,12 @@ fn new_funded_wallet(descriptor: &str, change_descriptor: Option<&str>) -> (Wall /// The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 /// to a foreign address and one returning 50_000 back to the wallet. The remaining 1000 /// sats are the transaction fee. -pub fn get_funded_wallet_single(descriptor: &str) -> (Wallet, Txid) { +pub fn get_funded_wallet_single(descriptor: &str) -> (Wallet, Txid) { new_funded_wallet(descriptor, None) } /// Get funded segwit wallet -pub fn get_funded_wallet_wpkh() -> (Wallet, Txid) { +pub fn get_funded_wallet_wpkh() -> (Wallet, Txid) { let (desc, change_desc) = get_test_wpkh_and_change_desc(); get_funded_wallet(desc, change_desc) } @@ -50,17 +56,21 @@ pub fn get_funded_wallet_wpkh() -> (Wallet, Txid) { pub fn new_wallet_and_funding_update( descriptor: &str, change_descriptor: Option<&str>, -) -> (Wallet, Txid, Update) { - let params = if let Some(change_desc) = change_descriptor { - Wallet::create(descriptor.to_string(), change_desc.to_string()) - } else { - Wallet::create_single(descriptor.to_string()) - }; +) -> (Wallet, Txid, Update) { + let mut keyring = KeyRing::new( + Network::Regtest, + KeychainKind::External, + descriptor.to_string(), + ) + .expect("descriptor must be valid"); - let wallet = params - .network(Network::Regtest) - .create_wallet_no_persist() - .expect("descriptors must be valid"); + if let Some(change_desc) = change_descriptor { + keyring + .add_descriptor(KeychainKind::Internal, change_desc.to_string()) + .expect("change descriptor must be valid"); + } + + let wallet = Wallet::create(keyring).create_wallet_no_persist(); let receive_address = wallet.peek_address(KeychainKind::External, 0).address; let sendto_address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5") @@ -257,7 +267,10 @@ impl From for ReceiveTo { } /// Receive a tx output with the given value in the latest block -pub fn receive_output_in_latest_block(wallet: &mut Wallet, value: Amount) -> OutPoint { +pub fn receive_output_in_latest_block( + wallet: &mut Wallet, + value: Amount, +) -> OutPoint { let latest_cp = wallet.latest_checkpoint(); let height = latest_cp.height(); assert!(height > 0, "cannot receive tx into genesis block"); @@ -273,7 +286,7 @@ pub fn receive_output_in_latest_block(wallet: &mut Wallet, value: Amount) -> Out /// Receive a tx output with the given value and chain position pub fn receive_output( - wallet: &mut Wallet, + wallet: &mut Wallet, value: Amount, receive_to: impl Into, ) -> OutPoint { @@ -283,7 +296,7 @@ pub fn receive_output( /// Receive a tx output to an address with the given value and chain position pub fn receive_output_to_address( - wallet: &mut Wallet, + wallet: &mut Wallet, addr: Address, value: Amount, receive_to: impl Into, @@ -312,7 +325,7 @@ pub fn receive_output_to_address( /// Insert a checkpoint into the wallet. This can be used to extend the wallet's local chain /// or to insert a block that did not exist previously. Note that if replacing a block with /// a different one at the same height, then all later blocks are evicted as well. -pub fn insert_checkpoint(wallet: &mut Wallet, block: BlockId) { +pub fn insert_checkpoint(wallet: &mut Wallet, block: BlockId) { let mut cp = wallet.latest_checkpoint(); cp = cp.insert(block); wallet @@ -328,7 +341,7 @@ pub fn insert_checkpoint(wallet: &mut Wallet, block: BlockId) { /// must always appear confirmed. /// /// This will also insert the anchor `block_id`. See [`insert_anchor`] for more. -pub fn insert_tx_anchor(wallet: &mut Wallet, tx: Transaction, block_id: BlockId) { +pub fn insert_tx_anchor(wallet: &mut Wallet, tx: Transaction, block_id: BlockId) { insert_checkpoint(wallet, block_id); let anchor = ConfirmationBlockTime { block_id, @@ -351,7 +364,7 @@ pub fn insert_tx_anchor(wallet: &mut Wallet, tx: Transaction, block_id: BlockId) /// Inserts a transaction into the local view, assuming it is currently present in the mempool. /// /// This can be used, for example, to track a transaction immediately after it is broadcast. -pub fn insert_tx(wallet: &mut Wallet, tx: Transaction) { +pub fn insert_tx(wallet: &mut Wallet, tx: Transaction) { let txid = tx.compute_txid(); let seen_at = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); let mut tx_update = TxUpdate::default(); @@ -368,7 +381,7 @@ pub fn insert_tx(wallet: &mut Wallet, tx: Transaction) { /// Simulates confirming a tx with `txid` by applying an update to the wallet containing /// the given `anchor`. Note: to be considered confirmed the anchor block must exist in /// the current active chain. -pub fn insert_anchor(wallet: &mut Wallet, txid: Txid, anchor: ConfirmationBlockTime) { +pub fn insert_anchor(wallet: &mut Wallet, txid: Txid, anchor: ConfirmationBlockTime) { let mut tx_update = TxUpdate::default(); tx_update.anchors = [(anchor, txid)].into(); wallet @@ -380,7 +393,7 @@ pub fn insert_anchor(wallet: &mut Wallet, txid: Txid, anchor: ConfirmationBlockT } /// Marks the given `txid` seen as unconfirmed at `seen_at` -pub fn insert_seen_at(wallet: &mut Wallet, txid: Txid, seen_at: u64) { +pub fn insert_seen_at(wallet: &mut Wallet, txid: Txid, seen_at: u64) { let mut tx_update = TxUpdate::default(); tx_update.seen_ats = [(txid, seen_at)].into(); wallet diff --git a/src/types.rs b/src/types.rs index da785e77..efa60c3e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -30,6 +30,49 @@ pub enum KeychainKind { Internal = 1, } +/// Stored as the text `"external"` / `"internal"` so a database is readable and stays valid if +/// the enum's discriminants ever change. +#[cfg(feature = "rusqlite")] +impl bdk_chain::rusqlite::ToSql for KeychainKind { + fn to_sql(&self) -> bdk_chain::rusqlite::Result> { + let s = match self { + KeychainKind::External => "external", + KeychainKind::Internal => "internal", + }; + Ok(bdk_chain::rusqlite::types::ToSqlOutput::from(s)) + } +} + +#[cfg(feature = "rusqlite")] +impl bdk_chain::rusqlite::types::FromSql for KeychainKind { + fn column_result( + value: bdk_chain::rusqlite::types::ValueRef<'_>, + ) -> bdk_chain::rusqlite::types::FromSqlResult { + match value.as_str()? { + "external" => Ok(KeychainKind::External), + "internal" => Ok(KeychainKind::Internal), + other => Err(bdk_chain::rusqlite::types::FromSqlError::Other( + alloc::boxed::Box::new(UnknownKeychain(alloc::string::String::from(other))), + )), + } + } +} + +/// A keychain column held a value this wallet does not recognise. +#[cfg(feature = "rusqlite")] +#[derive(Debug)] +pub struct UnknownKeychain(pub alloc::string::String); + +#[cfg(feature = "rusqlite")] +impl core::fmt::Display for UnknownKeychain { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "unknown keychain: {}", self.0) + } +} + +#[cfg(feature = "rusqlite")] +impl core::error::Error for UnknownKeychain {} + impl KeychainKind { /// Return [`KeychainKind`] as a byte pub fn as_byte(&self) -> u8 { @@ -62,13 +105,13 @@ impl AsRef<[u8]> for KeychainKind { /// /// [`Wallet`]: crate::Wallet #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] -pub struct LocalOutput { +pub struct LocalOutput { /// Reference to a transaction output pub outpoint: OutPoint, /// Transaction output pub txout: TxOut, /// Type of keychain - pub keychain: KeychainKind, + pub keychain: K, /// Whether this UTXO is spent or not pub is_spent: bool, /// The derivation index for the script pubkey in the wallet @@ -94,7 +137,7 @@ pub struct WeightedUtxo { /// An unspent transaction output (UTXO). pub enum Utxo { /// A UTXO owned by the local wallet. - Local(LocalOutput), + Local(LocalOutput), /// A UTXO owned by another wallet. Foreign { /// The location of the output. diff --git a/src/wallet/changeset.rs b/src/wallet/changeset.rs index c385ccf9..8372292c 100644 --- a/src/wallet/changeset.rs +++ b/src/wallet/changeset.rs @@ -1,3 +1,5 @@ +use crate::collections::BTreeMap; +use alloc::collections::btree_map::Entry; use bdk_chain::{ ConfirmationBlockTime, Merge, indexed_tx_graph, keychain_txout, local_chain, tx_graph, }; @@ -37,13 +39,13 @@ type IndexedTxGraphChangeSet = /// ## Members and required fields /// /// The change set has certain required fields without which a [`Wallet`] cannot function. -/// These include the [`descriptor`] and the [`bitcoin::Network`] in use. These are required to be +/// These include the [`descriptors`] and the [`bitcoin::Network`] in use. These are required to be /// non-empty *in the aggregate*, meaning the field must be present and non-null in the union of all /// persisted changes, but may be empty in any one change set, where "empty" is defined by the /// [`Merge`](Merge::is_empty) implementation of that change set. This requirement also applies to /// the [`local_chain`] field in that the aggregate change set must include a genesis block. /// -/// For example, the [`descriptor`] and [`bitcoin::Network`] are present in the first change set +/// For example, the [`descriptors`] and [`bitcoin::Network`] are present in the first change set /// after wallet creation, but are usually omitted in subsequent updates, as they are not permitted /// to change at any point thereafter. /// @@ -53,10 +55,9 @@ type IndexedTxGraphChangeSet = /// * [`tx_graph`](Self::tx_graph) /// * [`indexer`](Self::indexer) /// -/// The [`change_descriptor`] is special in that its presence is optional, however the value of the -/// change descriptor should be defined at wallet creation time and respected for the life of the -/// wallet, meaning that if a change descriptor is originally defined, it must also be present in -/// the aggregate change set. +/// A keychain may be introduced by a later change set — a wallet can start tracking a new one at +/// any time — but the descriptor bound to a keychain is fixed at the point that keychain first +/// appears, and must be identical in every change set thereafter. /// /// ## Staging /// @@ -121,8 +122,7 @@ type IndexedTxGraphChangeSet = /// please refer to the documentation for [`WalletPersister`] and [`PersistedWallet`] for more /// information. /// -/// [`change_descriptor`]: Self::change_descriptor -/// [`descriptor`]: Self::descriptor +/// [`descriptors`]: Self::descriptors /// [`local_chain`]: Self::local_chain /// [merged]: bdk_chain::Merge /// [`network`]: Self::network @@ -133,12 +133,13 @@ type IndexedTxGraphChangeSet = /// [`Wallet::staged`]: crate::Wallet::staged /// [`Wallet`]: crate::Wallet /// [Semantic Versioning]: -#[derive(Default, Debug, Clone, PartialEq, Deserialize, Serialize)] -pub struct ChangeSet { - /// Descriptor for recipient addresses. - pub descriptor: Option>, - /// Descriptor for change addresses. - pub change_descriptor: Option>, +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct ChangeSet { + /// The descriptor tracked by each keychain. + /// + /// A keychain's descriptor is fixed for the life of the wallet: [`Merge`] will accept a + /// keychain it has not seen before, but never a different descriptor for one it has. + pub descriptors: BTreeMap>, /// Stores the network type of the transaction data. pub network: Option, /// Changes to the [`LocalChain`](local_chain::LocalChain). @@ -152,25 +153,37 @@ pub struct ChangeSet { pub locked_outpoints: locked_outpoints::ChangeSet, } -impl Merge for ChangeSet { +impl Default for ChangeSet { + fn default() -> Self { + Self { + descriptors: BTreeMap::new(), + network: None, + local_chain: Default::default(), + tx_graph: Default::default(), + indexer: Default::default(), + locked_outpoints: Default::default(), + } + } +} + +impl Merge for ChangeSet { /// Merge another [`ChangeSet`] into itself. fn merge(&mut self, other: Self) { - if self.descriptor.is_none() && other.descriptor.is_some() { - self.descriptor = other.descriptor; - } else { - debug_assert!( - other.descriptor.is_none() || self.descriptor == other.descriptor, - "descriptor must never change" - ); - } - if self.change_descriptor.is_none() && other.change_descriptor.is_some() { - self.change_descriptor = other.change_descriptor; - } else { - debug_assert!( - other.change_descriptor.is_none() - || self.change_descriptor == other.change_descriptor, - "change descriptor must never change" - ); + // A keychain may be introduced by a later changeset, but the descriptor it is bound to + // must never change. `extend` would silently overwrite, which would let a corrupt or + // hostile changeset swap a descriptor out from under a loaded wallet. + for (keychain, descriptor) in other.descriptors { + match self.descriptors.entry(keychain) { + Entry::Vacant(slot) => { + slot.insert(descriptor); + } + Entry::Occupied(slot) => { + debug_assert!( + *slot.get() == descriptor, + "a keychain's descriptor must never change" + ); + } + } } if self.network.is_none() && other.network.is_some() { self.network = other.network; @@ -190,8 +203,7 @@ impl Merge for ChangeSet { } fn is_empty(&self) -> bool { - self.descriptor.is_none() - && self.change_descriptor.is_none() + self.descriptors.is_empty() && self.network.is_none() && self.local_chain.is_empty() && self.tx_graph.is_empty() @@ -201,7 +213,10 @@ impl Merge for ChangeSet { } #[cfg(feature = "rusqlite")] -impl ChangeSet { +impl ChangeSet +where + K: Ord + Clone + chain::rusqlite::ToSql + chain::rusqlite::types::FromSql, +{ /// Schema name for wallet. pub const WALLET_SCHEMA_NAME: &'static str = "bdk_wallet"; /// Name of table to store wallet descriptors and network. @@ -234,12 +249,29 @@ impl ChangeSet { ) } + /// Name of table storing one descriptor per keychain. + pub const WALLET_KEYCHAIN_TABLE_NAME: &'static str = "bdk_wallet_keychain"; + + /// Get v2 sqlite [`ChangeSet`] schema. + /// + /// Schema v2 replaces the single-row `descriptor` / `change_descriptor` columns with one row + /// per keychain, so a wallet may track any number of them. + pub fn schema_v2() -> alloc::string::String { + format!( + "CREATE TABLE {} ( \ + keychain TEXT PRIMARY KEY NOT NULL, \ + descriptor TEXT NOT NULL \ + ) STRICT;", + Self::WALLET_KEYCHAIN_TABLE_NAME, + ) + } + /// Initialize sqlite tables for wallet tables. pub fn init_sqlite_tables(db_tx: &chain::rusqlite::Transaction) -> chain::rusqlite::Result<()> { crate::rusqlite_impl::migrate_schema( db_tx, Self::WALLET_SCHEMA_NAME, - &[&Self::schema_v0(), &Self::schema_v1()], + &[&Self::schema_v0(), &Self::schema_v1(), &Self::schema_v2()], )?; bdk_chain::local_chain::ChangeSet::init_sqlite_tables(db_tx)?; @@ -257,25 +289,30 @@ impl ChangeSet { let mut changeset = Self::default(); - let mut wallet_statement = db_tx.prepare(&format!( - "SELECT descriptor, change_descriptor, network FROM {}", - Self::WALLET_TABLE_NAME, - ))?; - let row = wallet_statement + let mut network_statement = + db_tx.prepare(&format!("SELECT network FROM {}", Self::WALLET_TABLE_NAME,))?; + let row = network_statement .query_row([], |row| { - Ok(( - row.get::<_, Option>>>("descriptor")?, - row.get::<_, Option>>>( - "change_descriptor", - )?, - row.get::<_, Option>>("network")?, - )) + row.get::<_, Option>>("network") }) .optional()?; - if let Some((desc, change_desc, network)) = row { - changeset.descriptor = desc.map(Impl::into_inner); - changeset.change_descriptor = change_desc.map(Impl::into_inner); - changeset.network = network.map(Impl::into_inner); + if let Some(network) = row.flatten() { + changeset.network = Some(network.into_inner()); + } + + let mut keychain_statement = db_tx.prepare(&format!( + "SELECT keychain, descriptor FROM {}", + Self::WALLET_KEYCHAIN_TABLE_NAME, + ))?; + let rows = keychain_statement.query_map([], |row| { + Ok(( + row.get::<_, K>("keychain")?, + row.get::<_, Impl>>("descriptor")?, + )) + })?; + for row in rows { + let (keychain, Impl(descriptor)) = row?; + changeset.descriptors.insert(keychain, descriptor); } // Select locked outpoints. @@ -311,30 +348,18 @@ impl ChangeSet { use chain::Impl; use chain::rusqlite::named_params; - let mut descriptor_statement = db_tx.prepare_cached(&format!( - "INSERT INTO {}(id, descriptor) VALUES(:id, :descriptor) ON CONFLICT(id) DO UPDATE SET descriptor=COALESCE({}.descriptor, :descriptor)", - Self::WALLET_TABLE_NAME, - Self::WALLET_TABLE_NAME, + // A keychain's descriptor never changes once written, so first write wins. + let mut keychain_statement = db_tx.prepare_cached(&format!( + "INSERT OR IGNORE INTO {}(keychain, descriptor) VALUES(:keychain, :descriptor)", + Self::WALLET_KEYCHAIN_TABLE_NAME, ))?; - if let Some(descriptor) = &self.descriptor { - descriptor_statement.execute(named_params! { - ":id": 0, + for (keychain, descriptor) in &self.descriptors { + keychain_statement.execute(named_params! { + ":keychain": keychain, ":descriptor": Impl(descriptor.clone()), })?; } - let mut change_descriptor_statement = db_tx.prepare_cached(&format!( - "INSERT INTO {}(id, change_descriptor) VALUES(:id, :change_descriptor) ON CONFLICT(id) DO UPDATE SET change_descriptor=COALESCE({}.change_descriptor, :change_descriptor)", - Self::WALLET_TABLE_NAME, - Self::WALLET_TABLE_NAME, - ))?; - if let Some(change_descriptor) = &self.change_descriptor { - change_descriptor_statement.execute(named_params! { - ":id": 0, - ":change_descriptor": Impl(change_descriptor.clone()), - })?; - } - let mut network_statement = db_tx.prepare_cached(&format!( "INSERT INTO {}(id, network) VALUES(:id, :network) ON CONFLICT(id) DO UPDATE SET network=COALESCE({}.network, :network)", Self::WALLET_TABLE_NAME, @@ -378,7 +403,66 @@ impl ChangeSet { } } -impl From for ChangeSet { +#[cfg(feature = "rusqlite")] +impl ChangeSet { + /// Recover descriptors written by schema v0 or v1. + /// + /// Those versions stored the wallet's two descriptors as `descriptor` and `change_descriptor` + /// columns on a single row, rather than one row per keychain. This reads them and maps them + /// onto [`External`](crate::KeychainKind::External) and + /// [`Internal`](crate::KeychainKind::Internal). + /// + /// Only meaningful for wallets keyed by [`KeychainKind`](crate::KeychainKind): interpreting + /// the two legacy columns *requires* knowing they mean external and change. A wallet using a + /// custom keychain type has no v0/v1 database to recover, since those schemas predate custom + /// keychains entirely. + /// + /// Descriptors already present are left alone, so this never overwrites what schema v2 holds. + pub fn read_legacy_descriptors( + db_tx: &chain::rusqlite::Transaction, + changeset: &mut Self, + ) -> chain::rusqlite::Result<()> { + use crate::KeychainKind; + use chain::Impl; + use chain::rusqlite::OptionalExtension; + + let mut statement = db_tx.prepare(&format!( + "SELECT descriptor, change_descriptor FROM {}", + Self::WALLET_TABLE_NAME, + ))?; + let row = statement + .query_row([], |row| { + Ok(( + row.get::<_, Option>>>("descriptor")?, + row.get::<_, Option>>>( + "change_descriptor", + )?, + )) + }) + .optional()?; + + let Some((descriptor, change_descriptor)) = row else { + return Ok(()); + }; + + if let Some(Impl(descriptor)) = descriptor { + changeset + .descriptors + .entry(KeychainKind::External) + .or_insert(descriptor); + } + if let Some(Impl(change_descriptor)) = change_descriptor { + changeset + .descriptors + .entry(KeychainKind::Internal) + .or_insert(change_descriptor); + } + + Ok(()) + } +} + +impl From for ChangeSet { fn from(chain: local_chain::ChangeSet) -> Self { Self { local_chain: chain, @@ -387,7 +471,7 @@ impl From for ChangeSet { } } -impl From for ChangeSet { +impl From for ChangeSet { fn from(indexed_tx_graph: IndexedTxGraphChangeSet) -> Self { Self { tx_graph: indexed_tx_graph.tx_graph, @@ -397,7 +481,7 @@ impl From for ChangeSet { } } -impl From> for ChangeSet { +impl From> for ChangeSet { fn from(tx_graph: tx_graph::ChangeSet) -> Self { Self { tx_graph, @@ -406,7 +490,7 @@ impl From> for ChangeSet { } } -impl From for ChangeSet { +impl From for ChangeSet { fn from(indexer: keychain_txout::ChangeSet) -> Self { Self { indexer, @@ -415,7 +499,7 @@ impl From for ChangeSet { } } -impl From for ChangeSet { +impl From for ChangeSet { fn from(locked_outpoints: locked_outpoints::ChangeSet) -> Self { Self { locked_outpoints, @@ -427,6 +511,8 @@ impl From for ChangeSet { #[cfg_attr(coverage_nightly, coverage(off))] #[cfg(test)] mod test { + use super::*; + // Tests that merging `ChangeSet`s with write-once fields follows "first write wins" semantics // // Verifies three scenarios: @@ -525,4 +611,98 @@ mod test { "network must not change when merging other value" ); } + + #[cfg(feature = "rusqlite")] + #[test] + fn reads_descriptors_from_a_legacy_v0_database() { + use crate::KeychainKind; + use bitcoin::Network; + use chain::rusqlite::{Connection, named_params}; + + const EXTERNAL: &str = "wpkh([41f2aed0/84h/1h/0h]tpubDDFSdQWw75hk1ewbwnNpPp5DvXFRKt68ioPoyJDY752cNHKkFxPWqkqCyCf4hxrEfpuxh46QisehL3m8Bi6MsAv394QVLopwbtfvryFQNUH/0/*)"; + const INTERNAL: &str = "wpkh([41f2aed0/84h/1h/0h]tpubDDFSdQWw75hk1ewbwnNpPp5DvXFRKt68ioPoyJDY752cNHKkFxPWqkqCyCf4hxrEfpuxh46QisehL3m8Bi6MsAv394QVLopwbtfvryFQNUH/1/*)"; + + let mut conn = Connection::open_in_memory().unwrap(); + let db_tx = conn.transaction().unwrap(); + ChangeSet::::init_sqlite_tables(&db_tx).unwrap(); + + // Simulate a wallet written before schema v2: descriptors live in the columns on the + // single wallet row, and the per-keychain table is empty. + let external: Descriptor = EXTERNAL.parse().unwrap(); + let internal: Descriptor = INTERNAL.parse().unwrap(); + db_tx + .execute( + &format!( + "INSERT INTO {}(id, descriptor, change_descriptor, network) \ + VALUES(:id, :descriptor, :change_descriptor, :network)", + ChangeSet::::WALLET_TABLE_NAME + ), + named_params! { + ":id": 0, + ":descriptor": chain::Impl(external.clone()), + ":change_descriptor": chain::Impl(internal.clone()), + ":network": chain::Impl(Network::Testnet), + }, + ) + .unwrap(); + + let mut changeset = ChangeSet::::from_sqlite(&db_tx).unwrap(); + // Nothing in the v2 table yet. + assert!(changeset.descriptors.is_empty()); + assert_eq!(changeset.network, Some(Network::Testnet)); + + ChangeSet::::read_legacy_descriptors(&db_tx, &mut changeset).unwrap(); + + assert_eq!( + changeset.descriptors.get(&KeychainKind::External), + Some(&external), + "legacy `descriptor` column must map to the external keychain" + ); + assert_eq!( + changeset.descriptors.get(&KeychainKind::Internal), + Some(&internal), + "legacy `change_descriptor` column must map to the internal keychain" + ); + } + + #[cfg(feature = "rusqlite")] + #[test] + fn legacy_read_never_overwrites_v2_descriptors() { + use crate::KeychainKind; + use chain::rusqlite::{Connection, named_params}; + + const V2_DESC: &str = "wpkh([41f2aed0/84h/1h/0h]tpubDDFSdQWw75hk1ewbwnNpPp5DvXFRKt68ioPoyJDY752cNHKkFxPWqkqCyCf4hxrEfpuxh46QisehL3m8Bi6MsAv394QVLopwbtfvryFQNUH/0/*)"; + const LEGACY_DESC: &str = "wpkh([41f2aed0/84h/1h/0h]tpubDDFSdQWw75hk1ewbwnNpPp5DvXFRKt68ioPoyJDY752cNHKkFxPWqkqCyCf4hxrEfpuxh46QisehL3m8Bi6MsAv394QVLopwbtfvryFQNUH/1/*)"; + + let mut conn = Connection::open_in_memory().unwrap(); + let db_tx = conn.transaction().unwrap(); + ChangeSet::::init_sqlite_tables(&db_tx).unwrap(); + + let v2: Descriptor = V2_DESC.parse().unwrap(); + let legacy: Descriptor = LEGACY_DESC.parse().unwrap(); + + // A stale legacy column alongside an authoritative v2 row for the same keychain. + db_tx + .execute( + &format!( + "INSERT INTO {}(id, descriptor) VALUES(:id, :descriptor)", + ChangeSet::::WALLET_TABLE_NAME + ), + named_params! { ":id": 0, ":descriptor": chain::Impl(legacy) }, + ) + .unwrap(); + + let mut changeset = ChangeSet::::default(); + changeset + .descriptors + .insert(KeychainKind::External, v2.clone()); + + ChangeSet::::read_legacy_descriptors(&db_tx, &mut changeset).unwrap(); + + assert_eq!( + changeset.descriptors.get(&KeychainKind::External), + Some(&v2), + "schema v2 is authoritative; a legacy column must not overwrite it" + ); + } } diff --git a/src/wallet/error.rs b/src/wallet/error.rs index 1eb8fbc3..b29711bc 100644 --- a/src/wallet/error.rs +++ b/src/wallet/error.rs @@ -29,7 +29,7 @@ use core::fmt; /// [`Wallet`]: crate::wallet::Wallet /// [`ChangeSet`]: crate::wallet::ChangeSet #[derive(Debug, PartialEq)] -pub enum LoadError { +pub enum LoadError { /// There was a problem with the passed-in descriptor(s). Descriptor(crate::descriptor::DescriptorError), /// Data loaded from persistence is missing network type. @@ -37,32 +37,32 @@ pub enum LoadError { /// Data loaded from persistence is missing genesis hash. MissingGenesis, /// Data loaded from persistence is missing descriptor. - MissingDescriptor(KeychainKind), + MissingDescriptors, /// Data loaded is unexpected. - Mismatch(LoadMismatch), + Mismatch(LoadMismatch), } -impl fmt::Display for LoadError { +impl fmt::Display for LoadError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { LoadError::Descriptor(e) => e.fmt(f), LoadError::MissingNetwork => write!(f, "loaded data is missing network type"), LoadError::MissingGenesis => write!(f, "loaded data is missing genesis hash"), - LoadError::MissingDescriptor(k) => { - write!(f, "loaded data is missing descriptor for {k} keychain") + LoadError::MissingDescriptors => { + write!(f, "loaded data is missing descriptors") } LoadError::Mismatch(e) => write!(f, "{e}"), } } } -impl core::error::Error for LoadError {} +impl core::error::Error for LoadError {} /// Represents a mismatch with what is loaded and what is expected from [`LoadParams`]. /// /// [`LoadParams`]: crate::wallet::LoadParams #[derive(Debug, PartialEq)] -pub enum LoadMismatch { +pub enum LoadMismatch { /// Network does not match. Network { /// The network that is loaded. @@ -80,7 +80,7 @@ pub enum LoadMismatch { /// Descriptor's [`DescriptorId`](bdk_chain::DescriptorId) does not match. Descriptor { /// Keychain identifying the descriptor. - keychain: KeychainKind, + keychain: K, /// The loaded descriptor. loaded: Option>, /// The expected descriptor. @@ -88,7 +88,7 @@ pub enum LoadMismatch { }, } -impl fmt::Display for LoadMismatch { +impl fmt::Display for LoadMismatch { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { LoadMismatch::Network { loaded, expected } => { @@ -107,7 +107,7 @@ impl fmt::Display for LoadMismatch { } => { write!( f, - "Descriptor mismatch for {} keychain: loaded {}, expected {}", + "Descriptor mismatch for {:?} keychain: loaded {}, expected {}", keychain, loaded .as_ref() @@ -121,14 +121,14 @@ impl fmt::Display for LoadMismatch { } } -impl From for LoadWithPersistError { - fn from(mismatch: LoadMismatch) -> Self { +impl From> for LoadWithPersistError { + fn from(mismatch: LoadMismatch) -> Self { Self::InvalidChangeSet(LoadError::Mismatch(mismatch)) } } -impl From for LoadError { - fn from(mismatch: LoadMismatch) -> Self { +impl From> for LoadError { + fn from(mismatch: LoadMismatch) -> Self { Self::Mismatch(mismatch) } } diff --git a/src/wallet/event.rs b/src/wallet/event.rs index b53862db..651b0134 100644 --- a/src/wallet/event.rs +++ b/src/wallet/event.rs @@ -84,8 +84,8 @@ pub enum WalletEvent { /// Generate `WalletEvent`s by comparing the chain tip and wallet transactions before and after /// updating the state of the `Wallet`. -pub(crate) fn wallet_events( - wallet: &Wallet, +pub(crate) fn wallet_events( + wallet: &Wallet, chain_tip1: BlockId, chain_tip2: BlockId, wallet_txs1: BTreeMap, ChainPosition)>, diff --git a/src/wallet/export.rs b/src/wallet/export.rs index d34ddc7c..e725e5ef 100644 --- a/src/wallet/export.rs +++ b/src/wallet/export.rs @@ -32,12 +32,19 @@ //! }"#; //! //! let import = FullyNodedExport::from_str(import)?; -//! let wallet = Wallet::create( +//! let mut keyring = KeyRing::new( +//! Network::Testnet, +//! KeychainKind::External, //! import.descriptor(), -//! import.change_descriptor().expect("change descriptor"), //! ) -//! .network(Network::Testnet) -//! .create_wallet_no_persist()?; +//! .expect("valid descriptor"); +//! keyring +//! .add_descriptor( +//! KeychainKind::Internal, +//! import.change_descriptor().expect("change descriptor"), +//! ) +//! .expect("valid change descriptor"); +//! let wallet = Wallet::create(keyring).create_wallet_no_persist(); //! # Ok::<_, Box>(()) //! ``` //! @@ -48,9 +55,12 @@ //! # use bdk_wallet::*; //! const EXTERNAL: &str = "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)"; //! const INTERNAL: &str = "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"; -//! let wallet = Wallet::create(EXTERNAL, INTERNAL) -//! .network(Network::Testnet) -//! .create_wallet_no_persist()?; +//! let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, EXTERNAL) +//! .expect("valid descriptor"); +//! keyring +//! .add_descriptor(KeychainKind::Internal, INTERNAL) +//! .expect("valid change descriptor"); +//! let wallet = Wallet::create(keyring).create_wallet_no_persist(); //! // Keys are caller-owned: supply the keymaps explicitly. //! let secp = wallet.secp_ctx(); //! let (_, external_keymap) = miniscript::Descriptor::parse_descriptor(secp, EXTERNAL)?; @@ -73,12 +83,13 @@ //! # use bitcoin::*; //! # use bdk_wallet::export::*; //! # use bdk_wallet::*; -//! let wallet = Wallet::create( +//! let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, //! "wsh(sortedmulti(2,[73756c7f/48h/0h/0h/2h]tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/0/*,[f9f62194/48h/0h/0h/2h]tpubDDp3ZSH1yCwusRppH7zgSxq2t1VEUyXSeEp8E5aFS8m43MknUjiF1bSLo3CGWAxbDyhF1XowA5ukPzyJZjznYk3kYi6oe7QxtX2euvKWsk4/0/*))", +//! ).expect("valid descriptor"); +//! keyring.add_descriptor(KeychainKind::Internal, //! "wsh(sortedmulti(2,[73756c7f/48h/0h/0h/2h]tpubDCKxNyM3bLgbEX13Mcd8mYxbVg9ajDkWXMh29hMWBurKfVmBfWAM96QVP3zaUcN51HvkZ3ar4VwP82kC8JZhhux8vFQoJintSpVBwpFvyU3/1/*,[f9f62194/48h/0h/0h/2h]tpubDDp3ZSH1yCwusRppH7zgSxq2t1VEUyXSeEp8E5aFS8m43MknUjiF1bSLo3CGWAxbDyhF1XowA5ukPzyJZjznYk3kYi6oe7QxtX2euvKWsk4/1/*))", -//! ) -//! .network(Network::Testnet) -//! .create_wallet_no_persist()?; +//! ).expect("valid change descriptor"); +//! let wallet = Wallet::create(keyring).create_wallet_no_persist(); //! let export = CaravanExport::export_wallet(&wallet, "My Multisig Wallet").unwrap(); //! //! println!("Exported: {}", export.to_string()); @@ -206,7 +217,7 @@ impl FullyNodedExport { /// If the database is empty or `include_blockheight` is false, the `blockheight` field /// returned will be `0`. pub fn export_wallet_with_keymaps( - wallet: &Wallet, + wallet: &Wallet, external_keymap: &KeyMap, internal_keymap: &KeyMap, label: &str, @@ -512,7 +523,7 @@ impl CaravanExport { /// supported by Caravan or if the descriptor is not a multisig descriptor. /// /// Caravan supports P2SH, P2WSH, and P2SH-P2WSH multisig wallets. - pub fn export_wallet(wallet: &Wallet, name: &str) -> Result { + pub fn export_wallet(wallet: &Wallet, name: &str) -> Result { // Get the descriptor directly from the wallet let descriptor = wallet.public_descriptor(KeychainKind::External); @@ -716,6 +727,7 @@ impl CaravanExport { #[cfg_attr(coverage_nightly, coverage(off))] #[cfg(test)] mod test { + use crate::KeyRing; use alloc::string::ToString; use bitcoin::Amount; use core::str::FromStr; @@ -727,11 +739,17 @@ mod test { use crate::Wallet; use crate::test_utils::*; - fn get_test_wallet(descriptor: &str, change_descriptor: &str, network: Network) -> Wallet { - let mut wallet = Wallet::create(descriptor.to_string(), change_descriptor.to_string()) - .network(network) - .create_wallet_no_persist() - .expect("must create wallet"); + fn get_test_wallet( + descriptor: &str, + change_descriptor: &str, + network: Network, + ) -> Wallet { + let mut keyring = KeyRing::new(network, KeychainKind::External, descriptor.to_string()) + .expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_descriptor.to_string()) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); let block = BlockId { height: 5000, hash: BlockHash::all_zeros(), @@ -745,7 +763,7 @@ mod test { /// Export using keymaps parsed from the descriptors, mirroring what a caller does now that /// the wallet no longer owns key material. fn export_with_desc_keymaps( - wallet: &Wallet, + wallet: &Wallet, descriptor: &str, change_descriptor: &str, label: &str, @@ -1062,17 +1080,17 @@ mod test { let (external_desc, internal_desc) = import.to_descriptors().unwrap(); // Verify the descriptors can create a functional BDK Wallet - let wallet_result = Wallet::create(external_desc, internal_desc) - .network(bitcoin::Network::Testnet) - .create_wallet_no_persist(); - - assert!( - wallet_result.is_ok(), - "Failed to create wallet from Caravan export descriptors: {:?}", - wallet_result.err() - ); + let mut keyring = KeyRing::new( + bitcoin::Network::Testnet, + crate::types::KeychainKind::External, + external_desc, + ) + .expect("Failed to build keyring from Caravan export external descriptor"); + keyring + .add_descriptor(crate::types::KeychainKind::Internal, internal_desc) + .expect("Failed to add Caravan export internal descriptor to keyring"); - let mut wallet = wallet_result.unwrap(); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); // Verify basic wallet functionality assert_eq!(wallet.network(), bitcoin::Network::Testnet); diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 5090d0b2..96b56b2e 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -109,7 +109,7 @@ pub use utils::{IsDust, TxDetails}; /// Alias [`FullTxOut`] with associated keychain and derivation index. #[allow(unused)] -type IndexedTxOut = ((KeychainKind, u32), FullTxOut); +type IndexedTxOut = ((K, u32), FullTxOut); /// A Bitcoin wallet /// @@ -129,10 +129,10 @@ type IndexedTxOut = ((KeychainKind, u32), FullTxOut); /// [`signer`]: crate::signer /// [`take_staged`]: Wallet::take_staged #[derive(Debug)] -pub struct Wallet { +pub struct Wallet { chain: LocalChain, - tx_graph: IndexedTxGraph>, - stage: ChangeSet, + tx_graph: IndexedTxGraph>, + stage: ChangeSet, network: Network, secp: SecpCtx, locked_outpoints: HashSet, @@ -141,11 +141,11 @@ pub struct Wallet { /// An update to [`Wallet`]. /// /// It updates [`KeychainTxOutIndex`], [`bdk_chain::TxGraph`] and [`LocalChain`] atomically. -#[derive(Debug, Clone, Default)] -pub struct Update { +#[derive(Debug, Clone)] +pub struct Update { /// Contains the last active derivation indices per keychain (`K`), which is used to update the /// [`KeychainTxOutIndex`]. - pub last_active_indices: BTreeMap, + pub last_active_indices: BTreeMap, /// Update for the wallet's internal [`TxGraph`]. pub tx_update: TxUpdate, @@ -154,8 +154,18 @@ pub struct Update { pub chain: Option, } -impl From> for Update { - fn from(value: FullScanResponse) -> Self { +impl Default for Update { + fn default() -> Self { + Self { + last_active_indices: Default::default(), + tx_update: Default::default(), + chain: Default::default(), + } + } +} + +impl From> for Update { + fn from(value: FullScanResponse) -> Self { Self { last_active_indices: value.last_active_indices, tx_update: value.tx_update, @@ -164,7 +174,7 @@ impl From> for Update { } } -impl From for Update { +impl From for Update { fn from(value: SyncResponse) -> Self { Self { last_active_indices: BTreeMap::new(), @@ -177,16 +187,16 @@ impl From for Update { /// A derived address and the index it was found at. /// For convenience this automatically derefs to `Address` #[derive(Debug, Clone, PartialEq, Eq)] -pub struct AddressInfo { +pub struct AddressInfo { /// Child index of this address pub index: u32, /// Address pub address: Address, /// Type of keychain - pub keychain: KeychainKind, + pub keychain: K, } -impl Deref for AddressInfo { +impl Deref for AddressInfo { type Target = Address; fn deref(&self) -> &Self::Target { @@ -194,7 +204,7 @@ impl Deref for AddressInfo { } } -impl fmt::Display for AddressInfo { +impl fmt::Display for AddressInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.address) } @@ -203,159 +213,55 @@ impl fmt::Display for AddressInfo { /// A `CanonicalTx` managed by a `Wallet`. pub type WalletTx<'a> = CanonicalTx<'a, Arc, ConfirmationBlockTime>; -impl Wallet { - /// Build a new single descriptor [`Wallet`]. - /// - /// If you have previously created a wallet, use [`load`](Self::load) instead. - /// - /// # Note - /// - /// Only use this method when creating a wallet designed to be used with a single - /// descriptor and keychain. Otherwise the recommended way to construct a new wallet is - /// by using [`Wallet::create`]. It's worth noting that not all features are available - /// with single descriptor wallets, for example setting a [`change_policy`] on [`TxBuilder`] - /// and related methods such as [`do_not_spend_change`]. This is because all payments are - /// received on the external keychain (including change), and without a change keychain - /// BDK lacks enough information to distinguish between change and outside payments. - /// - /// Additionally because this wallet has no internal (change) keychain, all methods that - /// require a [`KeychainKind`] as input, e.g. [`reveal_next_address`] should only be called - /// using the [`External`] variant. In most cases passing [`Internal`] is treated as the - /// equivalent of [`External`] but this behavior must not be relied on. +impl Wallet +where + K: Ord + Clone + core::fmt::Debug, +{ + /// Start building a [`Wallet`] from a [`KeyRing`](crate::KeyRing). /// - /// # Example + /// The keyring supplies the network and every keychain's descriptor, all already validated. + /// The returned [`CreateParams`] lets you set the remaining, key-independent options before + /// creating the wallet. /// - /// ```rust - /// # use bdk_wallet::Wallet; - /// # use bitcoin::Network; - /// # const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; - /// # let temp_dir = tempfile::tempdir().expect("must create tempdir"); - /// # let file_path = temp_dir.path().join("store.db"); - /// // Create a wallet that is persisted to SQLite database. - /// use bdk_wallet::rusqlite::Connection; - /// let mut conn = Connection::open(file_path)?; - /// let wallet = Wallet::create_single(EXTERNAL_DESC) - /// .network(Network::Testnet) - /// .create_wallet(&mut conn)?; - /// # Ok::<_, anyhow::Error>(()) /// ``` - /// [`change_policy`]: TxBuilder::change_policy - /// [`do_not_spend_change`]: TxBuilder::do_not_spend_change - /// [`External`]: KeychainKind::External - /// [`Internal`]: KeychainKind::Internal - /// [`reveal_next_address`]: Self::reveal_next_address - pub fn create_single(descriptor: D) -> CreateParams - where - D: IntoWalletDescriptor + Send + Clone + 'static, - { - CreateParams::new_single(descriptor) - } - - /// Build a new [`Wallet`]. - /// - /// If you have previously created a wallet, use [`load`](Self::load) instead. - /// - /// # Synopsis - /// - /// ```rust - /// # use bdk_wallet::Wallet; + /// # use bdk_wallet::{KeyRing, KeychainKind, Wallet}; /// # use bitcoin::Network; - /// # fn main() -> anyhow::Result<()> { - /// # const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; - /// # const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; - /// // Create a non-persisted wallet. - /// let wallet = Wallet::create(EXTERNAL_DESC, INTERNAL_DESC) - /// .network(Network::Testnet) - /// .create_wallet_no_persist()?; - /// - /// // Create a wallet that is persisted to SQLite database. - /// # let temp_dir = tempfile::tempdir().expect("must create tempdir"); - /// # let file_path = temp_dir.path().join("store.db"); - /// use bdk_wallet::rusqlite::Connection; - /// let mut conn = Connection::open(file_path)?; - /// let wallet = Wallet::create(EXTERNAL_DESC, INTERNAL_DESC) - /// .network(Network::Testnet) - /// .create_wallet(&mut conn)?; - /// # Ok(()) - /// # } + /// # const EXTERNAL: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; + /// # const INTERNAL: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; + /// let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, EXTERNAL)?; + /// keyring.add_descriptor(KeychainKind::Internal, INTERNAL)?; + /// let wallet = Wallet::create(keyring) + /// .lookahead(50) + /// .create_wallet_no_persist(); + /// + /// assert_eq!(wallet.keychains().count(), 2); + /// # Ok::<(), bdk_wallet::keyring::KeyRingError>(()) /// ``` - pub fn create(descriptor: D, change_descriptor: D) -> CreateParams - where - D: IntoWalletDescriptor + Send + Clone + 'static, - { - CreateParams::new(descriptor, change_descriptor) + pub fn create(keyring: crate::KeyRing) -> CreateParams { + keyring.into_params() } - /// Build a new [`Wallet`] from a two-path descriptor. - /// - /// This function parses a multipath descriptor with exactly 2 paths and creates a wallet - /// using the existing receive and change wallet creation logic. Note that you can only use this - /// method with public extended keys (`xpub` prefix) to create watch-only wallets. - /// - /// Multipath descriptors follow [BIP 389] and allow defining both receive and change - /// derivation paths in a single descriptor using the `<0;1>` syntax. - /// - /// If you have previously created a wallet, use [`load`](Self::load) instead. - /// - /// # Errors - /// Returns an error if the descriptor is invalid, not a 2-path multipath descriptor, or if - /// the descriptor provided contains an extended private key (`xprv` prefix). - /// - /// # Synopsis - /// - /// ```rust - /// # use bdk_wallet::Wallet; - /// # use bitcoin::Network; - /// # use bdk_wallet::KeychainKind; - /// # const TWO_PATH_DESC: &str = "wpkh([9a6a2580/84'/1'/0']tpubDDnGNapGEY6AZAdQbfRJgMg9fvz8pUBrLwvyvUqEgcUfgzM6zc2eVK4vY9x9L5FJWdX8WumXuLEDV5zDZnTfbn87vLe9XceCFwTu9so9Kks/<0;1>/*)"; - /// let wallet = Wallet::create_from_two_path_descriptor(TWO_PATH_DESC) - /// .network(Network::Testnet) - /// .create_wallet_no_persist() - /// .unwrap(); + /// Build a [`Wallet`] from already-assembled [`CreateParams`]. /// - /// // The multipath descriptor automatically creates separate receive and change descriptors - /// let receive_addr = wallet.peek_address(KeychainKind::External, 0); // Uses path /0/* - /// let change_addr = wallet.peek_address(KeychainKind::Internal, 0); // Uses path /1/* - /// assert_ne!(receive_addr.address, change_addr.address); - /// ``` + /// This is infallible: `CreateParams` can only be produced from a [`KeyRing`], which has + /// already validated every descriptor against the network and rejected duplicate keychains. /// - /// [BIP 389]: https://github.com/bitcoin/bips/blob/master/bip-0389.mediawiki - pub fn create_from_two_path_descriptor(two_path_descriptor: D) -> CreateParams - where - D: IntoWalletDescriptor + Send + Clone + 'static, - { - CreateParams::new_two_path(two_path_descriptor) - } + /// [`KeyRing`]: crate::KeyRing + pub fn create_with_params(params: CreateParams) -> Self { + let CreateParams { + secp, + descriptors, + network, + genesis_hash, + lookahead, + use_spk_cache, + } = params; - /// Create a new [`Wallet`] with given `params`. - /// - /// Refer to [`Wallet::create`] for more. - pub fn create_with_params(params: CreateParams) -> Result { - let secp = SecpCtx::new(); - let network = params.network; - let network_kind = NetworkKind::from(network); - let genesis_hash = params - .genesis_hash - .unwrap_or(genesis_block(network).block_hash()); + let genesis_hash = genesis_hash.unwrap_or(genesis_block(network).block_hash()); let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash); - let (descriptor, _) = (params.descriptor)(&secp, network_kind)?; - check_wallet_descriptor(&descriptor)?; - - let change_descriptor = match params.change_descriptor { - Some(make_desc) => { - let (change_descriptor, _) = make_desc(&secp, network_kind)?; - check_wallet_descriptor(&change_descriptor)?; - Some(change_descriptor) - } - None => None, - }; - - let locked_outpoints = HashSet::new(); - let mut stage = ChangeSet { - descriptor: Some(descriptor.clone()), - change_descriptor: change_descriptor.clone(), + descriptors: descriptors.clone(), local_chain: chain_changeset, network: Some(network), ..Default::default() @@ -365,74 +271,28 @@ impl Wallet { &mut stage, Default::default(), Default::default(), - descriptor, - change_descriptor, - params.lookahead, - params.use_spk_cache, - )?; + descriptors, + lookahead, + use_spk_cache, + ); - Ok(Wallet { + Wallet { network, chain, tx_graph, stage, secp, - locked_outpoints, - }) - } - - /// Build [`Wallet`] by loading from persistence or [`ChangeSet`]. - /// - /// Note that descriptor secret keys are not persisted. The wallet does not hold key - /// material: keep your own [`KeyMap`](miniscript::descriptor::KeyMap) and sign with - /// [`bitcoin::Psbt::sign`], or build a - /// [`SignersContainer`](crate::signer::SignersContainer) and pass it to - /// [`Wallet::sign_with_signers`]. You can check the wallet's descriptors are what you expect - /// with [`LoadParams::descriptor`]. - /// - /// # Synopsis - /// - /// ```rust,no_run - /// # use bdk_wallet::{Wallet, ChangeSet, KeychainKind}; - /// # use bitcoin::{BlockHash, Network, hashes::Hash}; - /// # fn main() -> anyhow::Result<()> { - /// # const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; - /// # const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; - /// # let changeset = ChangeSet::default(); - /// // Load a wallet from changeset (no persistence). - /// let wallet = Wallet::load() - /// .load_wallet_no_persist(changeset)? - /// .expect("must have data to load wallet"); - /// - /// // Load a wallet that is persisted to SQLite database. - /// # let temp_dir = tempfile::tempdir().expect("must create tempdir"); - /// # let file_path = temp_dir.path().join("store.db"); - /// # let genesis_hash = BlockHash::all_zeros(); - /// let mut conn = bdk_wallet::rusqlite::Connection::open(file_path)?; - /// let mut wallet = Wallet::load() - /// // check loaded descriptors match these values - /// .descriptor(KeychainKind::External, Some(EXTERNAL_DESC)) - /// .descriptor(KeychainKind::Internal, Some(INTERNAL_DESC)) - /// // ensure loaded wallet's genesis hash matches this value - /// .check_genesis_hash(genesis_hash) - /// // set a lookahead for our indexer - /// .lookahead(101) - /// .load_wallet(&mut conn)? - /// .expect("must have data to load wallet"); - /// # Ok(()) - /// # } - /// ``` - pub fn load() -> LoadParams { - LoadParams::new() + locked_outpoints: HashSet::new(), + } } /// Load [`Wallet`] from the given previously persisted [`ChangeSet`] and `params`. /// /// Returns `Ok(None)` if the changeset is empty. Refer to [`Wallet::load`] for more. pub fn load_with_params( - changeset: ChangeSet, - params: LoadParams, - ) -> Result, LoadError> { + changeset: ChangeSet, + params: LoadParams, + ) -> Result, LoadError> { if changeset.is_empty() { return Ok(None); } @@ -459,77 +319,41 @@ impl Wallet { } } - let descriptor = changeset - .descriptor - .ok_or(LoadError::MissingDescriptor(KeychainKind::External))?; - check_wallet_descriptor(&descriptor).map_err(LoadError::Descriptor)?; - - if let Some(expected) = params.check_descriptor { - if let Some(make_desc) = expected { - let (exp_desc, _) = - make_desc(&secp, network_kind).map_err(LoadError::Descriptor)?; - if descriptor.descriptor_id() != exp_desc.descriptor_id() { - return Err(LoadError::Mismatch(LoadMismatch::Descriptor { - keychain: KeychainKind::External, - loaded: Some(Box::new(descriptor)), - expected: Some(Box::new(exp_desc)), - })); - } - } else { - return Err(LoadError::Mismatch(LoadMismatch::Descriptor { - keychain: KeychainKind::External, - loaded: Some(Box::new(descriptor)), - expected: None, - })); - } + let descriptors = changeset.descriptors; + if descriptors.is_empty() { + return Err(LoadError::MissingDescriptors); + } + for descriptor in descriptors.values() { + check_wallet_descriptor(descriptor).map_err(LoadError::Descriptor)?; } - let mut change_descriptor = None; - - match (changeset.change_descriptor, params.check_change_descriptor) { - // Empty signer. - (None, None) => {} - (None, Some(expect)) => { - // Expected descriptor, but none is loaded. - if let Some(make_desc) = expect { - let (exp_desc, _) = - make_desc(&secp, network_kind).map_err(LoadError::Descriptor)?; - return Err(LoadError::Mismatch(LoadMismatch::Descriptor { - keychain: KeychainKind::Internal, - loaded: None, - expected: Some(Box::new(exp_desc)), - })); - } - } - // Nothing expected. - (Some(desc), None) => { - check_wallet_descriptor(&desc).map_err(LoadError::Descriptor)?; - change_descriptor = Some(desc); - } - (Some(desc), Some(expect)) => match expect { - // Expected none for existing. - None => { - return Err(LoadError::Mismatch(LoadMismatch::Descriptor { - keychain: KeychainKind::Internal, - loaded: Some(Box::new(desc)), - expected: None, - })); - } - // Parameters must match. + // Each entry in `check_descriptors` is an assertion about one keychain: `Some(make_desc)` + // means "this keychain must be loaded and must match", `None` means "this keychain must + // not be loaded at all". Keychains absent from the map are not checked. + for (keychain, expected) in params.check_descriptors { + let loaded = descriptors.get(&keychain); + let expected = match expected { Some(make_desc) => { - check_wallet_descriptor(&desc).map_err(LoadError::Descriptor)?; let (exp_desc, _) = make_desc(&secp, network_kind).map_err(LoadError::Descriptor)?; - if desc.descriptor_id() != exp_desc.descriptor_id() { - return Err(LoadError::Mismatch(LoadMismatch::Descriptor { - keychain: KeychainKind::Internal, - loaded: Some(Box::new(desc)), - expected: Some(Box::new(exp_desc)), - })); - } - change_descriptor = Some(desc); + Some(exp_desc) } - }, + None => None, + }; + let matches = match (loaded, &expected) { + (None, None) => true, + (Some(loaded), Some(expected)) => { + loaded.descriptor_id() == expected.descriptor_id() + } + _ => false, + }; + if !matches { + return Err(LoadError::Mismatch(LoadMismatch::Descriptor { + keychain, + loaded: loaded.cloned().map(Box::new), + expected: expected.map(Box::new), + })); + } } // Apply locked outpoints @@ -540,18 +364,16 @@ impl Wallet { .map(|(op, _)| op) .collect(); - let mut stage = ChangeSet::default(); + let mut stage = ChangeSet::::default(); let tx_graph = make_indexed_graph( &mut stage, changeset.tx_graph, changeset.indexer, - descriptor, - change_descriptor, + descriptors, params.lookahead, params.use_spk_cache, - ) - .map_err(LoadError::Descriptor)?; + ); Ok(Some(Wallet { chain, @@ -569,7 +391,7 @@ impl Wallet { } /// Iterator over all keychains in this wallet - pub fn keychains(&self) -> impl Iterator { + pub fn keychains(&self) -> impl Iterator { self.tx_graph.index.keychains() } @@ -581,12 +403,11 @@ impl Wallet { /// /// This panics when the caller requests for an address of derivation index greater than the /// [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) max index. - pub fn peek_address(&self, keychain: KeychainKind, mut index: u32) -> AddressInfo { - let keychain = self.map_keychain(keychain); + pub fn peek_address(&self, keychain: K, mut index: u32) -> AddressInfo { let mut spk_iter = self .tx_graph .index - .unbounded_spk_iter(keychain) + .unbounded_spk_iter(keychain.clone()) .expect("keychain must exist"); if !spk_iter.descriptor().has_wildcard() { index = 0; @@ -598,7 +419,7 @@ impl Wallet { AddressInfo { index, address: Address::from_script(&spk, self.network).expect("must have address form"), - keychain, + keychain: keychain.clone(), } } @@ -627,13 +448,13 @@ impl Wallet { /// println!("Next address: {}", next_address.address); /// # Ok::<(), anyhow::Error>(()) /// ``` - pub fn reveal_next_address(&mut self, keychain: KeychainKind) -> AddressInfo { - let keychain = self.map_keychain(keychain); + pub fn reveal_next_address(&mut self, keychain: K) -> AddressInfo { + let keychain = keychain; let index = &mut self.tx_graph.index; let stage = &mut self.stage; let ((index, spk), index_changeset) = index - .reveal_next_spk(keychain) + .reveal_next_spk(keychain.clone()) .expect("keychain must exist"); stage.merge(index_changeset.into()); @@ -642,7 +463,7 @@ impl Wallet { index, address: Address::from_script(spk.as_script(), self.network) .expect("must have address form"), - keychain, + keychain: keychain.clone(), } } @@ -657,14 +478,14 @@ impl Wallet { /// calls to this method before closing the wallet. See [`Wallet::reveal_next_address`]. pub fn reveal_addresses_to( &mut self, - keychain: KeychainKind, + keychain: K, index: u32, - ) -> impl Iterator + '_ { - let keychain = self.map_keychain(keychain); + ) -> impl Iterator> + '_ { + let keychain = keychain; let (spks, index_changeset) = self .tx_graph .index - .reveal_to_target(keychain, index) + .reveal_to_target(keychain.clone(), index) .expect("keychain must exist"); self.stage.merge(index_changeset.into()); @@ -672,7 +493,7 @@ impl Wallet { spks.into_iter().map(move |(index, spk)| AddressInfo { index, address: Address::from_script(&spk, self.network).expect("must have address form"), - keychain, + keychain: keychain.clone(), }) } @@ -685,12 +506,12 @@ impl Wallet { /// /// **WARNING**: To avoid address reuse you must persist the changes resulting from one or more /// calls to this method before closing the wallet. See [`Wallet::reveal_next_address`]. - pub fn next_unused_address(&mut self, keychain: KeychainKind) -> AddressInfo { - let keychain = self.map_keychain(keychain); + pub fn next_unused_address(&mut self, keychain: K) -> AddressInfo { + let keychain = keychain; let index = &mut self.tx_graph.index; let ((index, spk), index_changeset) = index - .next_unused_spk(keychain) + .next_unused_spk(keychain.clone()) .expect("keychain must exist"); self.stage @@ -700,14 +521,14 @@ impl Wallet { index, address: Address::from_script(spk.as_script(), self.network) .expect("must have address form"), - keychain, + keychain: keychain.clone(), } } /// Marks an address used of the given `keychain` at `index`. /// /// Returns whether the given index was present and then removed from the unused set. - pub fn mark_used(&mut self, keychain: KeychainKind, index: u32) -> bool { + pub fn mark_used(&mut self, keychain: K, index: u32) -> bool { self.tx_graph.index.mark_used(keychain, index) } @@ -719,7 +540,7 @@ impl Wallet { /// derived spk. /// /// [`mark_used`]: Self::mark_used - pub fn unmark_used(&mut self, keychain: KeychainKind, index: u32) -> bool { + pub fn unmark_used(&mut self, keychain: K, index: u32) -> bool { self.tx_graph.index.unmark_used(keychain, index) } @@ -730,16 +551,16 @@ impl Wallet { /// [`reveal_addresses_to`](Self::reveal_addresses_to). pub fn list_unused_addresses( &self, - keychain: KeychainKind, - ) -> impl DoubleEndedIterator + '_ { + keychain: K, + ) -> impl DoubleEndedIterator> + '_ { self.tx_graph .index - .unused_keychain_spks(self.map_keychain(keychain)) + .unused_keychain_spks(keychain.clone()) .map(move |(index, spk)| AddressInfo { index, address: Address::from_script(spk.as_script(), self.network) .expect("must have address form"), - keychain, + keychain: keychain.clone(), }) } @@ -751,12 +572,12 @@ impl Wallet { /// Finds how the wallet derived the script pubkey `spk`. /// /// Will only return `Some(_)` if the wallet has given out the spk. - pub fn derivation_of_spk(&self, spk: ScriptBuf) -> Option<(KeychainKind, u32)> { + pub fn derivation_of_spk(&self, spk: ScriptBuf) -> Option<(K, u32)> { self.tx_graph.index.index_of_spk(spk).cloned() } /// Return the list of unspent outputs of this wallet - pub fn list_unspent(&self) -> impl Iterator + '_ { + pub fn list_unspent(&self) -> impl Iterator> + '_ { self.tx_graph .graph() .filter_chain_unspents( @@ -773,7 +594,7 @@ impl Wallet { fn list_indexed_txouts( &self, params: CanonicalizationParams, - ) -> impl Iterator + '_ { + ) -> impl Iterator> + '_ { self.tx_graph.graph().filter_chain_txouts( &self.chain, self.chain.tip().block_id(), @@ -812,7 +633,7 @@ impl Wallet { /// List all relevant outputs (includes both spent and unspent, confirmed and unconfirmed). /// /// To list only unspent outputs (UTXOs), use [`Wallet::list_unspent`] instead. - pub fn list_output(&self) -> impl Iterator + '_ { + pub fn list_output(&self) -> impl Iterator> + '_ { self.tx_graph .graph() .filter_chain_txouts( @@ -844,7 +665,7 @@ impl Wallet { /// script pubkeys the wallet is storing internally). pub fn all_unbounded_spk_iters( &self, - ) -> BTreeMap> + Clone> { + ) -> BTreeMap> + Clone> { self.tx_graph.index.all_unbounded_spk_iters() } @@ -855,18 +676,19 @@ impl Wallet { /// [`all_unbounded_spk_iters`]: Self::all_unbounded_spk_iters pub fn unbounded_spk_iter( &self, - keychain: KeychainKind, + keychain: K, ) -> impl Iterator> + Clone { self.tx_graph .index - .unbounded_spk_iter(self.map_keychain(keychain)) + .unbounded_spk_iter(keychain.clone()) .expect("keychain must exist") } /// Returns the utxo owned by this wallet corresponding to `outpoint` if it exists in the /// wallet's database. - pub fn get_utxo(&self, op: OutPoint) -> Option { + pub fn get_utxo(&self, op: OutPoint) -> Option> { let ((keychain, index), _) = self.tx_graph.index.txout(op)?; + let (keychain, index) = (keychain.clone(), index); self.tx_graph .graph() .filter_chain_unspents( @@ -875,7 +697,7 @@ impl Wallet { CanonicalizationParams::default(), core::iter::once(((), op)), ) - .map(|(_, full_txo)| new_local_utxo(keychain, index, full_txo)) + .map(|(_, full_txo)| new_local_utxo(keychain.clone(), index, full_txo)) .next() } @@ -913,8 +735,8 @@ impl Wallet { /// /// ```rust, no_run /// # use bitcoin::Txid; - /// # use bdk_wallet::Wallet; - /// # let mut wallet: Wallet = todo!(); + /// # use bdk_wallet::{KeychainKind, Wallet}; + /// # let mut wallet: Wallet = todo!(); /// # let txid:Txid = todo!(); /// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; /// let fee = wallet.calculate_fee(&tx).expect("fee"); @@ -922,8 +744,8 @@ impl Wallet { /// /// ```rust, no_run /// # use bitcoin::Psbt; - /// # use bdk_wallet::Wallet; - /// # let mut wallet: Wallet = todo!(); + /// # use bdk_wallet::{KeychainKind, Wallet}; + /// # let mut wallet: Wallet = todo!(); /// # let mut psbt: Psbt = todo!(); /// let tx = &psbt.clone().extract_tx().expect("tx"); /// let fee = wallet.calculate_fee(tx).expect("fee"); @@ -944,8 +766,8 @@ impl Wallet { /// /// ```rust, no_run /// # use bitcoin::Txid; - /// # use bdk_wallet::Wallet; - /// # let mut wallet: Wallet = todo!(); + /// # use bdk_wallet::{KeychainKind, Wallet}; + /// # let mut wallet: Wallet = todo!(); /// # let txid:Txid = todo!(); /// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; /// let fee_rate = wallet.calculate_fee_rate(&tx).expect("fee rate"); @@ -953,8 +775,8 @@ impl Wallet { /// /// ```rust, no_run /// # use bitcoin::Psbt; - /// # use bdk_wallet::Wallet; - /// # let mut wallet: Wallet = todo!(); + /// # use bdk_wallet::{KeychainKind, Wallet}; + /// # let mut wallet: Wallet = todo!(); /// # let mut psbt: Psbt = todo!(); /// let tx = &psbt.clone().extract_tx().expect("tx"); /// let fee_rate = wallet.calculate_fee_rate(tx).expect("fee rate"); @@ -974,8 +796,8 @@ impl Wallet { /// /// ```rust, no_run /// # use bitcoin::Txid; - /// # use bdk_wallet::Wallet; - /// # let mut wallet: Wallet = todo!(); + /// # use bdk_wallet::{KeychainKind, Wallet}; + /// # let mut wallet: Wallet = todo!(); /// # let txid:Txid = todo!(); /// let tx = wallet.get_tx(txid).expect("tx exists").tx_node.tx; /// let (sent, received) = wallet.sent_and_received(&tx); @@ -983,8 +805,8 @@ impl Wallet { /// /// ```rust, no_run /// # use bitcoin::Psbt; - /// # use bdk_wallet::Wallet; - /// # let mut wallet: Wallet = todo!(); + /// # use bdk_wallet::{KeychainKind, Wallet}; + /// # let mut wallet: Wallet = todo!(); /// # let mut psbt: Psbt = todo!(); /// let tx = &psbt.clone().extract_tx().expect("tx"); /// let (sent, received) = wallet.sent_and_received(tx); @@ -1005,8 +827,8 @@ impl Wallet { /// /// ```rust, no_run /// use bdk_chain::Anchor; - /// use bdk_wallet::{chain::ChainPosition, Wallet}; - /// # let wallet: Wallet = todo!(); + /// use bdk_wallet::{chain::ChainPosition, KeychainKind, Wallet}; + /// # let wallet: Wallet = todo!(); /// # let my_txid: bitcoin::Txid = todo!(); /// /// let wallet_tx = wallet.get_tx(my_txid).expect("panic if tx does not exist"); @@ -1089,8 +911,8 @@ impl Wallet { /// # Example /// /// ```rust,no_run - /// # use bdk_wallet::{LoadParams, Wallet, WalletTx}; - /// # let mut wallet:Wallet = todo!(); + /// # use bdk_wallet::{KeychainKind, LoadParams, Wallet, WalletTx}; + /// # let mut wallet: Wallet = todo!(); /// // Transactions by chain position: first unconfirmed then descending by confirmed height. /// let sorted_txs: Vec = /// wallet.transactions_sort_by(|tx1, tx2| tx2.chain_position.cmp(&tx1.chain_position)); @@ -1105,2638 +927,2689 @@ impl Wallet { txs } - /// Return the balance, separated into available, trusted-pending, untrusted-pending, and - /// immature values. - pub fn balance(&self) -> Balance { - self.tx_graph.graph().balance( - &self.chain, - self.chain.tip().block_id(), - CanonicalizationParams::default(), - self.tx_graph.index.outpoints().iter().cloned(), - |&(k, _), _| k == KeychainKind::Internal, - ) + /// Returns the descriptor used to create addresses for a particular `keychain`. + /// + /// It's the "public" version of the wallet's descriptor, meaning a new descriptor that has + /// the same structure but with the all secret keys replaced by their corresponding public key. + /// This can be used to build a watch-only version of a wallet. + pub fn public_descriptor(&self, keychain: K) -> &ExtendedDescriptor { + self.tx_graph + .index + .get_descriptor(keychain) + .expect("keychain must exist") } - /// Start building a transaction. + /// Finalize a PSBT, i.e., for each input determine if sufficient data is available to pass + /// validation and construct the respective `scriptSig` or `scriptWitness`. Please refer to + /// [BIP174](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#Input_Finalizer), + /// and [BIP371](https://github.com/bitcoin/bips/blob/master/bip-0371.mediawiki) + /// for further information. /// - /// This returns a blank [`TxBuilder`] from which you can specify the parameters for the - /// transaction. + /// Returns `true` if the PSBT could be finalized, and `false` otherwise. /// - /// ## Example + /// The [`SignOptions`] can be used to tweak the behavior of the finalizer. + pub fn finalize_psbt( + &self, + psbt: &mut Psbt, + sign_options: SignOptions, + ) -> Result { + let tx = &psbt.unsigned_tx; + let chain_tip = self.chain.tip().block_id(); + let prev_txids = tx + .input + .iter() + .map(|txin| txin.previous_output.txid) + .collect::>(); + let confirmation_heights = self + .tx_graph + .graph() + .list_canonical_txs(&self.chain, chain_tip, CanonicalizationParams::default()) + .filter(|canon_tx| prev_txids.contains(&canon_tx.tx_node.txid)) + // This is for a small performance gain. Although `.filter` filters out excess txs, it + // will still consume the internal `CanonicalIter` entirely. Having a `.take` here + // allows us to stop further unnecessary canonicalization. + .take(prev_txids.len()) + .map(|canon_tx| { + let txid = canon_tx.tx_node.txid; + match canon_tx.chain_position { + ChainPosition::Confirmed { anchor, .. } => (txid, anchor.block_id.height), + ChainPosition::Unconfirmed { .. } => (txid, u32::MAX), + } + }) + .collect::>(); + let current_height = sign_options + .assume_height + .unwrap_or_else(|| self.chain.tip().height()); + + Ok(self + .try_finalize_psbt_with( + psbt, + Some(current_height), + |_, input| { + confirmation_heights + .get(&input.previous_output.txid) + .copied() + }, + true, + )? + .is_finalized()) + } + + /// Attempt to finalize each input of a PSBT and return per-input finalization results. /// - /// ``` - /// # use std::str::FromStr; - /// # use bitcoin::*; - /// # use bdk_wallet::*; - /// # use bdk_wallet::ChangeSet; - /// # use bdk_wallet::error::CreateTxError; - /// # use bdk_wallet::descriptor::IntoWalletDescriptor; - /// # use bdk_wallet::signer::SignersContainer; - /// # use anyhow::Error; - /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; - /// # let mut wallet = doctest_wallet!(); - /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); - /// let psbt = { - /// let mut builder = wallet.build_tx(); - /// builder - /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)); - /// builder.finish()? - /// }; + /// Use this method when you need to inspect why a specific input could not be finalized. Call + /// [`FinalizePsbtOutcome::is_finalized`] on the returned value to check whether all inputs are + /// finalized after the call. /// - /// // sign and broadcast ... - /// # Ok::<(), anyhow::Error>(()) - /// ``` + /// Per-input finalization failures are reported as [`FinalizeInputOutcome`] values. This method + /// only returns `Err` when the PSBT is malformed, for example if its inputs are out of bounds. /// - /// [`TxBuilder`]: crate::TxBuilder - pub fn build_tx(&mut self) -> TxBuilder<'_, DefaultCoinSelectionAlgorithm> { - TxBuilder { - wallet: self, - params: TxParams::default(), - coin_selection: DefaultCoinSelectionAlgorithm::default(), - } + /// Timelock satisfaction is evaluated from the PSBT transaction fields. This method does not + /// redact or clear output metadata. + pub fn try_finalize_psbt( + &self, + psbt: &mut Psbt, + ) -> Result { + self.try_finalize_psbt_with(psbt, None, |_, _| None, false) } - pub(crate) fn create_tx( - &mut self, - coin_selection: Cs, - params: TxParams, - rng: &mut impl RngCore, - ) -> Result { - // The spending condition may be supplied by the caller via `TxBuilder::set_condition`. - // Otherwise we derive it from the descriptors themselves. Deriving needs no key material: - // signers only influence a policy's `contribution`/`satisfaction`, which `get_condition` - // ignores, so an empty container is sufficient. If a descriptor offers several ways to be - // satisfied, `get_condition` cannot choose between them and the caller must say which one - // it intends via `set_condition`. - let requirements = match params.condition { - Some(condition) => condition, - None => { - let keychains: BTreeMap<_, _> = self.tx_graph.index.keychains().collect(); - let no_signers = SignersContainer::default(); - let no_path = BTreeMap::new(); - - let mut requirements = Condition::default(); - for (keychain, skip) in [ - ( - KeychainKind::External, - tx_builder::ChangeSpendPolicy::OnlyChange, - ), - ( - KeychainKind::Internal, - tx_builder::ChangeSpendPolicy::ChangeForbidden, - ), - ] { - if params.change_policy == skip { - continue; - } - let Some(descriptor) = keychains.get(&keychain) else { - continue; - }; - let Some(policy) = descriptor.extract_policy( - &no_signers, - BuildSatisfaction::None, - &self.secp, - )? - else { - continue; - }; - // A policy that cannot be resolved without an explicit path needs the caller - // to pick one. - let condition = policy - .get_condition(&no_path) - .map_err(|_| CreateTxError::SpendingPolicyRequired(keychain))?; - requirements = requirements.merge(&condition)?; - } - requirements - } - }; + fn try_finalize_psbt_with( + &self, + psbt: &mut Psbt, + current_height: Option, + mut confirmation_height_for_input: F, + clear_output_derivations: bool, + ) -> Result + where + F: FnMut(usize, &bitcoin::TxIn) -> Option, + { + let tx = &psbt.unsigned_tx; + if psbt.inputs.len() < tx.input.len() { + return Err(IndexOutOfBoundsError::new( + psbt.inputs.len(), + psbt.inputs.len(), + )); + } - let version = match params.version { - Some(transaction::Version(0)) => return Err(CreateTxError::Version0), - Some(transaction::Version::ONE) if requirements.csv.is_some() => { - return Err(CreateTxError::Version1Csv); - } - Some(v) => v, - None => transaction::Version::TWO, - }; + let mut outcomes = BTreeMap::new(); - // We use a match here instead of a unwrap_or_else as it's way more readable :) - let current_height = match params.current_height { - // If they didn't tell us the current height, we assume it's the latest sync height. - None => { - let tip_height = self.chain.tip().height(); - absolute::LockTime::from_height(tip_height).expect("invalid height") + for (n, input) in tx.input.iter().enumerate() { + let psbt_input = &psbt + .inputs + .get(n) + .ok_or(IndexOutOfBoundsError::new(n, psbt.inputs.len()))?; + if psbt_input.final_script_sig.is_some() || psbt_input.final_script_witness.is_some() { + outcomes.insert(n, FinalizeInputOutcome::AlreadyFinalized); + continue; } - Some(h) => h, - }; - - let lock_time = match params.locktime { - // When no `nLockTime` is specified, we try to prevent fee sniping, if possible. - None => { - // Fee sniping can be partially prevented by setting the timelock - // to current_height. If we don't know the current_height, - // we default to 0. - let fee_sniping_height = current_height; - // We choose the biggest between the required nlocktime and the fee sniping - // height. - match requirements.timelock { - // No requirement, just use the fee_sniping_height. - None => fee_sniping_height, - // There's a block-based requirement, but the value is lower than the - // fee_sniping_height. - Some(value @ absolute::LockTime::Blocks(_)) if value < fee_sniping_height => { - fee_sniping_height - } - // There's a time-based requirement or a block-based requirement greater - // than the fee_sniping_height use that value. - Some(value) => value, - } - } - // Specific nLockTime required and we have no constraints, so just set to that value. - Some(x) if requirements.timelock.is_none() => x, - // Specific nLockTime required and it's compatible with the constraints. - Some(x) - if requirements.timelock.unwrap().is_same_unit(x) - && x >= requirements.timelock.unwrap() => - { - x - } - // Invalid nLockTime required. - Some(x) => { - return Err(CreateTxError::LockTime { - requested: x, - required: requirements.timelock.unwrap(), + // - Try to derive the descriptor by looking at the txout. If it's in our database, we + // know exactly which `keychain` to use, and which derivation index it is. + // - If that fails, try to derive it by looking at the psbt input: the complete logic is + // in `src/descriptor/mod.rs`, but it will basically look at `bip32_derivation`, + // `redeem_script` and `witness_script` to determine the right derivation. + // - If that also fails, it will try it on the internal descriptor, if present. + let desc = psbt + .get_utxo_for(n) + .and_then(|txout| self.get_descriptor_for_txout(&txout)) + .or_else(|| { + self.tx_graph.index.keychains().find_map(|(_, desc)| { + desc.derive_from_psbt_input(psbt_input, psbt.get_utxo_for(n), &self.secp) + }) }); - } - }; - // nSequence value for inputs. - // When not explicitly specified, it defaults to 0xFFFFFFFD, meaning RBF signaling is - // enabled. - let n_sequence = match (params.sequence, requirements.csv) { - // Enable RBF by default. - (None, None) => Sequence::ENABLE_RBF_NO_LOCKTIME, - // None requested, use required. - (None, Some(csv)) => csv, - // Requested sequence is incompatible with requirements. - (Some(sequence), Some(csv)) if !check_nsequence_rbf(sequence, csv) => { - return Err(CreateTxError::RbfSequenceCsv { sequence, csv }); - } - // Use requested nSequence value. - (Some(sequence), _) => sequence, - }; + match desc { + Some(desc) => { + let mut tmp_input = bitcoin::TxIn::default(); + let satisfy_result = if let Some(current_height) = current_height { + let confirmation_height = confirmation_height_for_input(n, input); + desc.satisfy( + &mut tmp_input, + ( + PsbtInputSatisfier::new(psbt, n), + After::new(Some(current_height), false), + Older::new(Some(current_height), confirmation_height, false), + ), + ) + } else { + desc.satisfy(&mut tmp_input, PsbtInputSatisfier::new(psbt, n)) + }; - let (fee_rate, mut fee_amount) = match params.fee_policy.unwrap_or_default() { - //FIXME: see https://github.com/bitcoindevkit/bdk/issues/256 - FeePolicy::FeeAmount(fee) => { - if let Some(previous_fee) = params.bumping_fee { - if fee < previous_fee.absolute { - return Err(CreateTxError::FeeTooLow { - required: previous_fee.absolute, - }); + match satisfy_result { + Ok(_) => { + let length = psbt.inputs.len(); + let psbt_input = psbt + .inputs + .get_mut(n) + .ok_or(IndexOutOfBoundsError::new(n, length))?; + let original = mem::take(psbt_input); + let final_script_sig = + (!tmp_input.script_sig.is_empty()).then_some(tmp_input.script_sig); + let final_script_witness = + (!tmp_input.witness.is_empty()).then_some(tmp_input.witness); + + // BIP174 finalization clears input metadata except UTXOs, final + // scripts, and opaque fields the finalizer does not understand. + *psbt_input = bitcoin::psbt::Input { + non_witness_utxo: original.non_witness_utxo, + witness_utxo: original.witness_utxo, + final_script_sig, + final_script_witness, + proprietary: original.proprietary, + unknown: original.unknown, + ..Default::default() + }; + outcomes.insert(n, FinalizeInputOutcome::Finalized); + } + Err(err) => { + outcomes.insert(n, FinalizeInputOutcome::CouldNotSatisfy(err)); + } } } - (FeeRate::ZERO, fee) - } - FeePolicy::FeeRate(rate) => { - if let Some(previous_fee) = params.bumping_fee { - let required_feerate = FeeRate::from_sat_per_kwu( - previous_fee.rate.to_sat_per_kwu() - + FeeRate::BROADCAST_MIN.to_sat_per_kwu(), // +1 sat/vb - ); - if rate < required_feerate { - return Err(CreateTxError::FeeRateTooLow { - required: required_feerate, - }); - } + None => { + outcomes.insert(n, FinalizeInputOutcome::MissingDescriptor); } - (rate, Amount::ZERO) } - }; - - let mut tx = Transaction { - version, - lock_time, - input: vec![], - output: vec![], - }; - - if params.manually_selected_only && params.utxos.is_empty() { - return Err(CreateTxError::NoUtxosSelected); } - let mut outgoing = Amount::ZERO; - let recipients = params.recipients.iter().map(|(r, v)| (r, *v)); - - for (index, (script_pubkey, value)) in recipients.enumerate() { - if !params.allow_dust && value.is_dust(script_pubkey) && !script_pubkey.is_op_return() { - return Err(CreateTxError::OutputBelowDustLimit(index)); + let finalized = FinalizePsbtOutcome::new(outcomes); + if clear_output_derivations && finalized.is_finalized() { + for output in &mut psbt.outputs { + output.bip32_derivation.clear(); + output.tap_key_origins.clear(); } + } - let new_out = TxOut { - script_pubkey: script_pubkey.clone(), - value, - }; - - tx.output.push(new_out); + Ok(finalized) + } - outgoing += value; - } + /// Return the secp256k1 context used for all signing operations. + pub fn secp_ctx(&self) -> &SecpCtx { + &self.secp + } - fee_amount += fee_rate * tx.weight(); + /// The derivation index of this wallet. It will return `None` if it has not derived any + /// addresses. Otherwise, it will return the index of the highest address it has derived. + pub fn derivation_index(&self, keychain: K) -> Option { + self.tx_graph.index.last_revealed_index(keychain) + } - let (required_utxos, optional_utxos) = { - // NOTE: manual selection overrides unspendable - let mut required: Vec = params.utxos.clone(); - let optional = self.filter_utxos(¶ms, current_height.to_consensus_u32(), version); + /// The index of the next address that you would get if you were to ask the wallet for a new + /// address. + pub fn next_derivation_index(&self, keychain: K) -> u32 { + self.tx_graph + .index + .next_index(keychain) + .expect("keychain must exist") + .0 + } - // If `drain_wallet` is true, all UTxOs are required. - if params.drain_wallet { - required.extend(optional); - (required, vec![]) - } else { - (required, optional) - } - }; + /// Return the checksum of the public descriptor associated to the `keychain`. + /// + /// Internally calls [`Self::public_descriptor`] to fetch the right descriptor. + pub fn descriptor_checksum(&self, keychain: K) -> String { + self.public_descriptor(keychain) + .to_string() + .split_once('#') + .unwrap() + .1 + .to_string() + } - // Get drain script. - let mut drain_index = Option::<(KeychainKind, u32)>::None; - let drain_script = match params.drain_to { - Some(ref drain_recipient) => drain_recipient.clone(), - None => { - let change_keychain = self.map_keychain(KeychainKind::Internal); - let (index, spk) = self - .tx_graph - .index - .unused_keychain_spks(change_keychain) - .next() - .unwrap_or_else(|| { - let (next_index, _) = self - .tx_graph - .index - .next_index(change_keychain) - .expect("keychain must exist"); - let spk = self - .peek_address(change_keychain, next_index) - .script_pubkey(); - (next_index, spk) - }); - drain_index = Some((change_keychain, index)); - spk - } + /// Applies an update to the wallet and stages the changes (but does not persist them). + /// + /// Usually you create an `update` by interacting with some blockchain data source and inserting + /// transactions related to your wallet into it. + /// + /// After applying updates you should persist the staged wallet changes. For an example of how + /// to persist staged wallet changes see [`Wallet::reveal_next_address`]. + pub fn apply_update(&mut self, update: impl Into>) -> Result<(), CannotConnectError> { + let update = update.into(); + let mut changeset = match update.chain { + Some(chain_update) => ChangeSet::from(self.chain.apply_update(chain_update)?), + None => ChangeSet::default(), }; - let coin_selection = coin_selection - .coin_select( - required_utxos, - optional_utxos, - fee_rate, - outgoing + fee_amount, - &drain_script, - rng, - ) - .map_err(CreateTxError::CoinSelection)?; - - let excess = &coin_selection.excess; - tx.input = coin_selection - .selected - .iter() - .map(|u| bitcoin::TxIn { - previous_output: u.outpoint(), - script_sig: ScriptBuf::default(), - sequence: u.sequence().unwrap_or(n_sequence), - witness: Witness::new(), - }) - .collect(); - - if tx.output.is_empty() { - // Uh oh, our transaction has no outputs. - // We allow this when we have a `drain_to` address and either: - // - `drain_wallet` is enabled - // - there are UTXOs we must spend (this happens, for example, when - // sweeping specific UTXOs to a given address) - // Otherwise, we don't know who we should send the funds to, and how much - // we should send! - if params.drain_to.is_some() && (params.drain_wallet || !params.utxos.is_empty()) { - if let Excess::NoChange { - dust_threshold, - remaining_amount, - change_fee, - } = excess - { - return Err(CreateTxError::CoinSelection(InsufficientFunds { - needed: *dust_threshold, - available: remaining_amount - .checked_sub(*change_fee) - .unwrap_or_default(), - })); - } - } else { - return Err(CreateTxError::NoRecipients); - } - } - - // If there's change, create and add a change output. - if let Excess::Change { amount, .. } = excess { - // Create drain output. - let drain_output = TxOut { - value: *amount, - script_pubkey: drain_script, - }; - - // TODO: We should pay attention when adding a new output: this might increase - // the length of the "number of vouts" parameter by 2 bytes, potentially making - // our feerate too low. - tx.output.push(drain_output); - } - - // Sort inputs/outputs according to the chosen algorithm. - params.ordering.sort_tx_with_aux_rand(&mut tx, rng); - - let psbt = self.complete_transaction(tx, coin_selection.selected, params)?; - - // Recording changes to the change keychain. - if let (Excess::Change { .. }, Some((keychain, index))) = (excess, drain_index) { - if let Some((_, index_changeset)) = - self.tx_graph.index.reveal_to_target(keychain, index) - { - self.stage.merge(index_changeset.into()); - self.mark_used(keychain, index); - } - } - - Ok(psbt) + let index_changeset = self + .tx_graph + .index + .reveal_to_target_multi(&update.last_active_indices); + changeset.merge(index_changeset.into()); + changeset.merge(self.tx_graph.apply_update(update.tx_update).into()); + self.stage.merge(changeset); + Ok(()) } - /// Bump the fee of a transaction previously created with this wallet. + /// Applies an update to the wallet, stages the changes, and returns events. /// - /// Returns an error if the transaction is already confirmed or doesn't explicitly signal - /// *replace by fee* (RBF). If the transaction can be fee bumped then it returns a [`TxBuilder`] - /// pre-populated with the inputs and outputs of the original transaction. + /// Usually you create an `update` by interacting with some blockchain data source and inserting + /// transactions related to your wallet into it. Staged changes are NOT persisted. /// - /// ## Example + /// After applying updates you should process the events in your app before persisting the + /// staged wallet changes. For an example of how to persist staged wallet changes see + /// [`Wallet::reveal_next_address`]. /// - /// ```no_run - /// # // TODO: remove norun -- bumping fee seems to need the tx in the wallet database first. - /// # use std::str::FromStr; + /// ```rust,no_run /// # use bitcoin::*; /// # use bdk_wallet::*; - /// # use bdk_wallet::ChangeSet; - /// # use bdk_wallet::error::CreateTxError; - /// # use bdk_wallet::descriptor::IntoWalletDescriptor; - /// # use bdk_wallet::signer::SignersContainer; - /// # use anyhow::Error; - /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; + /// use bdk_wallet::WalletEvent; + /// # let wallet_update = Update::default(); /// # let mut wallet = doctest_wallet!(); - /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); - /// let mut psbt = { - /// let mut builder = wallet.build_tx(); - /// builder - /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)); - /// builder.finish()? - /// }; - /// // Keys are caller-owned: build a signer container from the signing descriptor. - /// let (signing_desc, keymap) = - /// descriptor.into_wallet_descriptor(wallet.secp_ctx(), wallet.network().into())?; - /// let signers = SignersContainer::build(keymap, &signing_desc, wallet.secp_ctx()); - /// let _ = wallet.sign_with_signers(&mut psbt, &[&signers], SignOptions::default())?; - /// let tx = psbt.clone().extract_tx().expect("tx"); - /// // broadcast tx but it's taking too long to confirm so we want to bump the fee - /// let mut psbt = { - /// let mut builder = wallet.build_fee_bump(tx.compute_txid())?; - /// builder - /// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate")); - /// builder.finish()? - /// }; - /// - /// let _ = wallet.sign_with_signers(&mut psbt, &[&signers], SignOptions::default())?; - /// let fee_bumped_tx = psbt.extract_tx(); - /// // broadcast fee_bumped_tx to replace original - /// # Ok::<(), anyhow::Error>(()) - /// ``` - // TODO: support for merging multiple transactions while bumping the fees - pub fn build_fee_bump( - &mut self, - txid: Txid, - ) -> Result, BuildFeeBumpError> { - let tx_graph = self.tx_graph.graph(); - let txout_index = &self.tx_graph.index; - let chain_tip = self.chain.tip().block_id(); - let chain_positions: HashMap> = tx_graph - .list_canonical_txs(&self.chain, chain_tip, CanonicalizationParams::default()) - .map(|canon_tx| (canon_tx.tx_node.txid, canon_tx.chain_position)) - .collect(); - - let mut tx = tx_graph - .get_tx(txid) - .ok_or(BuildFeeBumpError::TransactionNotFound(txid))? - .as_ref() - .clone(); - - if chain_positions - .get(&txid) - .ok_or(BuildFeeBumpError::TransactionNotFound(txid))? - .is_confirmed() - { - return Err(BuildFeeBumpError::TransactionConfirmed(txid)); - } - - if !tx - .input - .iter() - .any(|txin| txin.sequence.to_consensus_u32() <= 0xFFFFFFFD) - { - return Err(BuildFeeBumpError::IrreplaceableTransaction( - tx.compute_txid(), - )); - } - - let fee = self - .calculate_fee(&tx) - .map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?; - let fee_rate = fee / tx.weight(); + /// let events = wallet.apply_update_events(wallet_update)?; + /// // Handle wallet relevant events from this update. + /// events.iter().for_each(|event| { + /// match event { + /// // The chain tip changed. + /// WalletEvent::ChainTipChanged { old_tip, new_tip } => { + /// todo!() // handle event + /// } + /// // An unconfirmed tx is now confirmed in a block. + /// WalletEvent::TxConfirmed { + /// txid, + /// tx, + /// block_time, + /// old_block_time: None, + /// } => { + /// todo!() // handle event + /// } + /// // A confirmed tx is now confirmed in a new block (reorg). + /// WalletEvent::TxConfirmed { + /// txid, + /// tx, + /// block_time, + /// old_block_time: Some(old_block_time), + /// } => { + /// todo!() // handle event + /// } + /// // A new unconfirmed tx was seen in the mempool. + /// WalletEvent::TxUnconfirmed { + /// txid, + /// tx, + /// old_block_time: None, + /// } => { + /// todo!() // handle event + /// } + /// // A previously confirmed tx in now unconfirmed in the mempool (reorg). + /// WalletEvent::TxUnconfirmed { + /// txid, + /// tx, + /// old_block_time: Some(old_block_time), + /// } => { + /// todo!() // handle event + /// } + /// // An unconfirmed tx was replaced in the mempool (RBF or double spent input). + /// WalletEvent::TxReplaced { + /// txid, + /// tx, + /// conflicts, + /// } => { + /// todo!() // handle event + /// } + /// // An unconfirmed tx was dropped from the mempool (fee too low). + /// WalletEvent::TxDropped { txid, tx } => { + /// todo!() // handle event + /// } + /// _ => { + /// // unexpected event, do nothing + /// } + /// } + /// // take staged wallet changes + /// let staged = wallet.take_staged(); + /// // persist staged changes + /// }); + /// # Ok::<(), anyhow::Error>(()) + /// ``` + /// [`TxBuilder`]: crate::TxBuilder + pub fn apply_update_events( + &mut self, + update: impl Into>, + ) -> Result, CannotConnectError> { + self.events_helper(|wallet| wallet.apply_update(update)) + } + + /// Get a reference of the staged [`ChangeSet`] that is yet to be committed (if any). + pub fn staged(&self) -> Option<&ChangeSet> { + if self.stage.is_empty() { + None + } else { + Some(&self.stage) + } + } + + /// Get a mutable reference of the staged [`ChangeSet`] that is yet to be committed (if any). + pub fn staged_mut(&mut self) -> Option<&mut ChangeSet> { + if self.stage.is_empty() { + None + } else { + Some(&mut self.stage) + } + } + + /// Take the staged [`ChangeSet`] to be persisted now (if any). + pub fn take_staged(&mut self) -> Option> { + self.stage.take() + } + + /// Get a reference to the inner [`TxGraph`]. + pub fn tx_graph(&self) -> &TxGraph { + self.tx_graph.graph() + } + + /// Get a reference to the inner [`KeychainTxOutIndex`]. + pub fn spk_index(&self) -> &KeychainTxOutIndex { + &self.tx_graph.index + } + + /// Get a reference to the inner [`LocalChain`]. + pub fn local_chain(&self) -> &LocalChain { + &self.chain + } + + /// List the locked outpoints. + pub fn list_locked_outpoints(&self) -> impl Iterator + '_ { + self.locked_outpoints.iter().copied() + } + + /// List unspent outpoints that are currently locked. + pub fn list_locked_unspent(&self) -> impl Iterator + '_ { + self.list_unspent() + .filter(|output| self.is_outpoint_locked(output.outpoint)) + .map(|output| output.outpoint) + } + + /// Whether the `outpoint` is locked. See [`Wallet::lock_outpoint`] for more. + pub fn is_outpoint_locked(&self, outpoint: OutPoint) -> bool { + self.locked_outpoints.contains(&outpoint) + } + + /// Lock a wallet output identified by the given `outpoint`. + /// + /// A locked UTXO will not be selected as an input to fund a transaction. This is useful + /// for excluding or reserving candidate inputs during transaction creation. + /// + /// **You must persist the staged change for the lock status to be persistent**. To unlock a + /// previously locked outpoint, see [`Wallet::unlock_outpoint`]. + pub fn lock_outpoint(&mut self, outpoint: OutPoint) { + if self.locked_outpoints.insert(outpoint) { + let changeset = locked_outpoints::ChangeSet { + outpoints: [(outpoint, true)].into(), + }; + self.stage.merge(changeset.into()); + } + } + + /// Unlock the wallet output of the specified `outpoint`. + /// + /// **You must persist the staged change for the lock status to be persistent**. + pub fn unlock_outpoint(&mut self, outpoint: OutPoint) { + if self.locked_outpoints.remove(&outpoint) { + let changeset = locked_outpoints::ChangeSet { + outpoints: [(outpoint, false)].into(), + }; + self.stage.merge(changeset.into()); + } + } + + /// Introduces a `block` of `height` to the wallet, and tries to connect it to the + /// `prev_blockhash` of the block's header. + /// + /// This is a convenience method that is equivalent to calling [`apply_block_connected_to`] + /// with `prev_blockhash` and `height-1` as the `connected_to` parameter. + /// + /// [`apply_block_connected_to`]: Self::apply_block_connected_to + pub fn apply_block(&mut self, block: &Block, height: u32) -> Result<(), CannotConnectError> { + let connected_to = match height.checked_sub(1) { + Some(prev_height) => BlockId { + height: prev_height, + hash: block.header.prev_blockhash, + }, + None => BlockId { + height, + hash: block.block_hash(), + }, + }; + self.apply_block_connected_to(block, height, connected_to) + .map_err(|err| match err { + ApplyHeaderError::InconsistentBlocks => { + unreachable!("connected_to is derived from the block so must be consistent") + } + ApplyHeaderError::CannotConnect(err) => err, + }) + } + + /// Introduces a `block` of `height` to the wallet, and tries to connect it to the + /// `prev_blockhash` of the block's header and returns events. + /// + /// This is a convenience method that is equivalent to calling + /// [`apply_block_connected_to_events`] with `prev_blockhash` and `height-1` as the + /// `connected_to` parameter. + /// + /// See [`apply_update_events`] for more information on the returned [`WalletEvent`]s. + /// + /// [`apply_block_connected_to_events`]: Self::apply_block_connected_to_events + /// [`apply_update_events`]: Self::apply_update_events + pub fn apply_block_events( + &mut self, + block: &Block, + height: u32, + ) -> Result, CannotConnectError> { + self.events_helper(|wallet| wallet.apply_block(block, height)) + } + + /// Applies relevant transactions from `block` of `height` to the wallet, and connects the + /// block to the internal chain. + /// + /// The `connected_to` parameter informs the wallet how this block connects to the internal + /// [`LocalChain`]. Relevant transactions are filtered from the `block` and inserted into the + /// internal [`TxGraph`]. + /// + /// **WARNING**: You must persist the changes resulting from one or more calls to this method + /// if you need the inserted block data to be reloaded after closing the wallet. + /// See [`Wallet::reveal_next_address`]. + pub fn apply_block_connected_to( + &mut self, + block: &Block, + height: u32, + connected_to: BlockId, + ) -> Result<(), ApplyHeaderError> { + let mut changeset = ChangeSet::default(); + changeset.merge( + self.chain + .apply_header_connected_to(&block.header, height, connected_to)? + .into(), + ); + changeset.merge(self.tx_graph.apply_block_relevant(block, height).into()); + self.stage.merge(changeset); + Ok(()) + } + + /// Applies relevant transactions from `block` of `height` to the wallet, connects the + /// block to the internal chain and returns events. + /// + /// See [`apply_block_connected_to`] for more information. + /// + /// See [`apply_update_events`] for more information on the returned [`WalletEvent`]s. + /// + /// [`apply_block_connected_to`]: Self::apply_block_connected_to + /// [`apply_update_events`]: Self::apply_update_events + pub fn apply_block_connected_to_events( + &mut self, + block: &Block, + height: u32, + connected_to: BlockId, + ) -> Result, ApplyHeaderError> { + self.events_helper(|wallet| wallet.apply_block_connected_to(block, height, connected_to)) + } + + /// Apply relevant unconfirmed transactions to the wallet. + /// + /// Transactions that are not relevant are filtered out. + /// + /// This method takes in an iterator of `(tx, last_seen)` where `last_seen` is the timestamp of + /// when the transaction was last seen in the mempool. This is used for conflict resolution + /// when there are conflicting unconfirmed transactions in the mempool. The transaction with the + /// later `last_seen` is prioritized. + /// + /// **WARNING**: You must persist the changes resulting from one or more calls to this method + /// if you need the applied unconfirmed transactions to be reloaded after closing the wallet. + /// See [`Wallet::reveal_next_address`]. + pub fn apply_unconfirmed_txs>>( + &mut self, + unconfirmed_txs: impl IntoIterator, + ) { + let indexed_graph_changeset = self + .tx_graph + .batch_insert_relevant_unconfirmed(unconfirmed_txs); + self.stage.merge(indexed_graph_changeset.into()); + } + + /// Apply relevant unconfirmed transactions to the wallet and returns events. + /// + /// See [`apply_unconfirmed_txs`] for more information. + /// + /// See [`apply_update_events`] for more information on the returned [`WalletEvent`]s. + /// + /// [`apply_unconfirmed_txs`]: Self::apply_unconfirmed_txs + /// [`apply_update_events`]: Self::apply_update_events + pub fn apply_unconfirmed_txs_events>>( + &mut self, + unconfirmed_txs: impl IntoIterator, + ) -> Vec { + self.events_helper::<_, _, core::convert::Infallible>(|wallet| { + wallet.apply_unconfirmed_txs(unconfirmed_txs); + Ok(()) + }) + .expect("`apply_unconfirmed_txs` should not fail") + } + + /// Apply evictions of the given transaction IDs with their associated timestamps. + /// + /// This function is used to mark specific unconfirmed transactions as evicted from the mempool. + /// Eviction means that these transactions are not considered canonical by default, and will + /// no longer be part of the wallet's [`transactions`] set. This can happen for example when + /// a transaction is dropped from the mempool due to low fees or conflicts with another + /// transaction. + /// + /// Only transactions that are currently unconfirmed and canonical are considered for eviction. + /// Transactions that are not relevant to the wallet are ignored. Note that an evicted + /// transaction can become canonical again if it is later observed on-chain or seen in the + /// mempool with a higher priority (e.g., due to a fee bump). + /// + /// ## Parameters + /// + /// `evicted_txs`: An iterator of `(Txid, u64)` tuples, where: + /// - `Txid`: The transaction ID of the transaction to be evicted. + /// - `u64`: The timestamp indicating when the transaction was evicted from the mempool. This + /// will usually correspond to the time of the latest chain sync. See docs for + /// [`start_sync_with_revealed_spks`]. + /// + /// ## Notes + /// + /// - Not all blockchain backends support automatic mempool eviction handling - this method may + /// be used in such cases. It can also be used to negate the effect of + /// [`apply_unconfirmed_txs`] for a particular transaction without the need for an additional + /// sync. + /// - The changes are staged in the wallet's internal state and must be persisted to ensure they + /// are retained across wallet restarts. Use [`Wallet::take_staged`] to retrieve the staged + /// changes and persist them to your database of choice. + /// - Evicted transactions are removed from the wallet's canonical transaction set, but the data + /// remains in the wallet's internal transaction graph for historical purposes. + /// - Ensure that the timestamps provided are accurate and monotonically increasing, as they + /// influence the wallet's canonicalization logic. + /// + /// [`transactions`]: Wallet::transactions + /// [`apply_unconfirmed_txs`]: Wallet::apply_unconfirmed_txs + /// [`start_sync_with_revealed_spks`]: Wallet::start_sync_with_revealed_spks + pub fn apply_evicted_txs(&mut self, evicted_txs: impl IntoIterator) { + let chain = &self.chain; + let canon_txids: Vec = self + .tx_graph + .graph() + .list_canonical_txs( + chain, + chain.tip().block_id(), + CanonicalizationParams::default(), + ) + .map(|c| c.tx_node.txid) + .collect(); + + let changeset = self.tx_graph.batch_insert_relevant_evicted_at( + evicted_txs + .into_iter() + .filter(|(txid, _)| canon_txids.contains(txid)), + ); + + self.stage.merge(changeset.into()); + } + + /// Apply evictions of the given transaction IDs with their associated timestamps and returns + /// events. + /// + /// See [`apply_evicted_txs`] for more information. + /// + /// See [`apply_update_events`] for more information on the returned [`WalletEvent`]s. + /// + /// [`apply_evicted_txs`]: Self::apply_evicted_txs + /// [`apply_update_events`]: Self::apply_update_events + pub fn apply_evicted_txs_events( + &mut self, + evicted_txs: impl IntoIterator, + ) -> Vec { + self.events_helper::<_, _, core::convert::Infallible>(|wallet| { + wallet.apply_evicted_txs(evicted_txs); + Ok(()) + }) + .expect("`apply_evicted_txs` should not fail") + } + + /// Returns a map of canonical transactions keyed by txid. + /// + /// This is used internally to help generate [`WalletEvent`]s. + fn map_transactions( + &self, + ) -> BTreeMap, ChainPosition)> { + self.transactions() + .map(|wtx| { + ( + wtx.tx_node.txid, + (wtx.tx_node.tx.clone(), wtx.chain_position), + ) + }) + .collect() + } + + /// Generates wallet events by executing a wallet-mutating function and surfacing internal + /// state changes. + /// + /// It works by taking some wallet operation that modifies state, capturing "before" and "after" + /// snapshots of the wallet's chain tip and transactions and comparing them in order to + /// generate a list of [`WalletEvent`]s representing what changed. + /// + /// Common kinds of events include: + /// + /// - [`WalletEvent::ChainTipChanged`]: The blockchain tip changed + /// - [`WalletEvent::TxConfirmed`]: A transaction was confirmed in a block + /// - [`WalletEvent::TxUnconfirmed`]: A transaction was newly unconfirmed + /// - [`WalletEvent::TxReplaced`]: An unconfirmed transaction was replaced (e.g., via RBF) + /// - [`WalletEvent::TxDropped`]: An unconfirmed transaction was dropped from the mempool + /// + /// This is useful when you need to track specific changes to your wallet state, such + /// as updating a UI to reflect transaction status changes, triggering notifications when + /// transactions confirm, logging state changes for debugging or auditing, or responding to + /// chain reorganizations. + /// + /// # Example + /// + /// ```rust,no_run + /// # use bdk_chain::local_chain::CannotConnectError; + /// # use bdk_wallet::{KeychainKind, Wallet, Update, WalletEvent}; + /// # let mut wallet: Wallet = todo!(); + /// // Apply an update and get events describing what changed + /// let update = Update::default(); + /// let func = |wallet: &mut Wallet| wallet.apply_update(update); + /// let events = wallet.events_helper(func)?; + /// # Ok::<(), anyhow::Error>(()) + /// ``` + /// + /// # Errors + /// + /// If `f` returns an error, then returns `E` of a type defined by the function + /// passed in. + pub fn events_helper(&mut self, f: F) -> Result, E> + where + F: FnOnce(&mut Self) -> Result, + E: Debug + Display, + { + // Snapshot of chain tip and transactions before + let chain_tip1 = self.chain.tip().block_id(); + let wallet_txs1 = self.map_transactions(); + + // Call `f` on self + f(self)?; + + // Chain tip and transactions after + let chain_tip2 = self.chain.tip().block_id(); + let wallet_txs2 = self.map_transactions(); + + Ok(wallet_events( + self, + chain_tip1, + chain_tip2, + wallet_txs1, + wallet_txs2, + )) + } + + fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option { + let (keychain, child) = self + .tx_graph + .index + .index_of_spk(txout.script_pubkey.clone())?; + let (keychain, child) = (keychain.clone(), *child); + let descriptor = self.public_descriptor(keychain); + descriptor.at_derivation_index(child).ok() + } - // Remove the inputs from the tx and process them. - let utxos: Vec = tx - .input - .drain(..) - .map(|txin| -> Result<_, BuildFeeBumpError> { - let outpoint = txin.previous_output; - let prev_txout = tx_graph - .get_txout(outpoint) - .cloned() - .ok_or(BuildFeeBumpError::UnknownUtxo(outpoint))?; - match txout_index.index_of_spk(prev_txout.script_pubkey.clone()) { - Some(&(keychain, derivation_index)) => { - let txout = prev_txout; - let chain_position = chain_positions - .get(&outpoint.txid) - .cloned() - .ok_or(BuildFeeBumpError::TransactionNotFound(outpoint.txid))?; - Ok(WeightedUtxo { - satisfaction_weight: self - .public_descriptor(keychain) - .max_weight_to_satisfy() - .expect("descriptor should be satisfiable"), - utxo: Utxo::Local(LocalOutput { - outpoint, - txout, - keychain, - is_spent: true, - derivation_index, - chain_position, - }), - }) - } - None => Ok(WeightedUtxo { - satisfaction_weight: Weight::from_wu_usize( - serialize(&txin.script_sig).len() * 4 + serialize(&txin.witness).len(), - ), - utxo: Utxo::Foreign { - outpoint, - sequence: txin.sequence, - psbt_input: Box::new(psbt::Input { - witness_utxo: prev_txout - .script_pubkey - .witness_version() - .map(|_| prev_txout), - non_witness_utxo: tx_graph - .get_tx(outpoint.txid) - .map(|tx| tx.as_ref().clone()), - ..Default::default() - }), - }, - }), - } - }) - .collect::>()?; + /// Return the balance, separated into available, trusted-pending, untrusted-pending, and + /// immature values. + /// + /// Nothing is trusted before it is mined: a wallet generic over `K` has no way to know which + /// of its keychains hold self-owned change, so all unconfirmed output counts as + /// untrusted-pending. + pub fn balance(&self) -> Balance { + self.tx_graph.graph().balance( + &self.chain, + self.chain.tip().block_id(), + CanonicalizationParams::default(), + self.tx_graph.index.outpoints().iter().cloned(), + |_, _| false, + ) + } +} - if tx.output.len() > 1 { - let mut change_index = None; - for (index, txout) in tx.output.iter().enumerate() { - let change_keychain = self.map_keychain(KeychainKind::Internal); - match txout_index.index_of_spk(txout.script_pubkey.clone()) { - Some((keychain, _)) if *keychain == change_keychain => { - change_index = Some(index) - } - _ => {} - } - } +/// Methods to construct sync/full-scan requests for spk-based chain sources. +impl Wallet +where + K: Ord + Clone + core::fmt::Debug, +{ + /// Create a partial [`SyncRequest`] for all revealed spks at `start_time`. + /// + /// The `start_time` is used to record the time that a mempool transaction was last seen + /// (or evicted). See [`Wallet::start_sync_with_revealed_spks`] for more. + pub fn start_sync_with_revealed_spks_at( + &self, + start_time: u64, + ) -> SyncRequestBuilder<(K, u32)> { + use bdk_chain::keychain_txout::SyncRequestBuilderExt; + SyncRequest::builder_at(start_time) + .chain_tip(self.chain.tip()) + .revealed_spks_from_indexer(&self.tx_graph.index, ..) + .expected_spk_txids(self.tx_graph.list_expected_spk_txids( + &self.chain, + self.chain.tip().block_id(), + .., + )) + } - if let Some(change_index) = change_index { - tx.output.remove(change_index); - } + /// Create a partial [`SyncRequest`] for this wallet for all revealed spks. + /// + /// This is the first step when performing a spk-based wallet partial sync, the returned + /// [`SyncRequest`] collects all revealed script pubkeys from the wallet keychain needed to + /// start a blockchain sync with a spk based blockchain client. + /// + /// The time of the sync is the current system time and is used to record the + /// tx last-seen for mempool transactions. Or if an expected transaction is missing + /// or evicted, it is the time of the eviction. Note that timestamps may only increase + /// to be counted by the tx graph. To supply your own start time see + /// [`Wallet::start_sync_with_revealed_spks_at`]. + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] + #[cfg(feature = "std")] + pub fn start_sync_with_revealed_spks(&self) -> SyncRequestBuilder<(K, u32)> { + use bdk_chain::keychain_txout::SyncRequestBuilderExt; + SyncRequest::builder() + .chain_tip(self.chain.tip()) + .revealed_spks_from_indexer(&self.tx_graph.index, ..) + .expected_spk_txids(self.tx_graph.list_expected_spk_txids( + &self.chain, + self.chain.tip().block_id(), + .., + )) + } + + /// Create a [`FullScanRequest] for this wallet. + /// + /// This is the first step when performing a spk-based wallet full scan, the returned + /// [`FullScanRequest] collects iterators for the wallet's keychain script pub keys needed to + /// start a blockchain full scan with a spk based blockchain client. + /// + /// This operation is generally only used when importing or restoring a previously used wallet + /// in which the list of used scripts is not known. + /// + /// The time of the scan is the current system time and is used to record the tx last-seen for + /// mempool transactions. To supply your own start time see [`Wallet::start_full_scan_at`]. + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] + #[cfg(feature = "std")] + pub fn start_full_scan(&self) -> FullScanRequestBuilder { + use bdk_chain::keychain_txout::FullScanRequestBuilderExt; + FullScanRequest::builder() + .chain_tip(self.chain.tip()) + .spks_from_indexer(&self.tx_graph.index) + } + + /// Create a [`FullScanRequest`] builder at `start_time`. + pub fn start_full_scan_at(&self, start_time: u64) -> FullScanRequestBuilder { + use bdk_chain::keychain_txout::FullScanRequestBuilderExt; + FullScanRequest::builder_at(start_time) + .chain_tip(self.chain.tip()) + .spks_from_indexer(&self.tx_graph.index) + } +} + +/// Maps a chain position to tx confirmation status, if `pos` is the confirmed +/// variant. +/// +/// - Returns None if the confirmation height or time is not a valid absolute [`Height`] or +/// [`Time`]. +/// +/// [`Height`]: bitcoin::absolute::Height +/// [`Time`]: bitcoin::absolute::Time +#[cfg(all(bdk_wallet_unstable, feature = "bdk-tx"))] +fn status_from_position(pos: ChainPosition) -> Option { + if let ChainPosition::Confirmed { anchor, .. } = pos { + let conf_height = anchor.confirmation_height_upper_bound(); + let height = absolute::Height::from_consensus(conf_height).ok()?; + // TODO: Currently BDK has no notion of MTP, we can use the confirmation block time for now. + let time = + absolute::Time::from_consensus(anchor.confirmation_time.try_into().ok()?).ok()?; + Some(ConfirmationStatus { + height, + prev_mtp: Some(time), + }) + } else { + None + } +} + +#[cfg(all(bdk_wallet_unstable, feature = "bdk-tx"))] +impl Wallet { + /// Return the "keys" assets, i.e. the ones we can trivially infer by scanning + /// the pubkeys of the wallet's descriptors. + fn assets(&self) -> Assets { + let mut pks = vec![]; + for (_, desc) in self.keychains() { + desc.for_each_key(|k| { + pks.extend(k.clone().into_single_keys()); + true + }); } - let params = TxParams { - version: Some(tx.version), - recipients: tx - .output - .into_iter() - .map(|txout| (txout.script_pubkey, txout.value)) - .collect(), - utxos, - bumping_fee: Some(tx_builder::PreviousFee { - absolute: fee, - rate: fee_rate, - }), - ..Default::default() + Assets::new().add(pks) + } + + /// Peek at the next change address without revealing it, returning the auto-derived + /// change info `(keychain, index, spk)` alongside the [`ChangeScript`]. + /// + /// The next change address is the next unused address of the change keychain, or the + /// next-to-be-revealed address **without** mutating wallet state. Revelation is deferred + /// until after all error paths have been cleared by the caller. + fn peek_change_info(&self) -> ((KeychainKind, u32, ScriptBuf), ChangeScript) { + let change_keychain = self.map_keychain(KeychainKind::Internal); + let (index, spk) = self + .tx_graph + .index + .unused_keychain_spks(change_keychain) + .next() + .unwrap_or_else(|| { + let (next_index, _) = self + .tx_graph + .index + .next_index(change_keychain) + .expect("keychain must exist"); + let spk = self + .peek_address(change_keychain, next_index) + .script_pubkey(); + (next_index, spk) + }); + let descriptor = self + .public_descriptor(change_keychain) + .at_derivation_index(index) + .expect("should be valid derivation index"); + ( + (change_keychain, index, spk), + ChangeScript::from_descriptor(descriptor), + ) + } + + /// Parses the common parameters used during PSBT creation and returns the spend assets + /// and a map of indexed tx outputs. + fn parse_params( + &self, + params: &PsbtParams, + ) -> (Assets, HashMap>) { + // Get spend assets. + let assets = match params.assets { + None => self.assets(), + Some(ref params_assets) => { + let mut assets = Assets::new(); + assets.extend(params_assets); + // Fill in the "keys" assets if none are provided. + if assets.keys.is_empty() { + assets.extend(&self.assets()); + } + assets + } }; - Ok(TxBuilder { - wallet: self, - params, - coin_selection: DefaultCoinSelectionAlgorithm::default(), - }) + // Get wallet txouts. + let txouts = self + .list_indexed_txouts(params.canonical_params.clone()) + .map(|(_, txo)| (txo.outpoint, txo)) + .collect(); + + (assets, txouts) } - /// Sign a transaction with the provided signer containers. + /// Filters wallet `txos` by the spending criteria. /// - /// Signer containers are processed in the order provided. Signers inside each container are - /// processed according to their [`SignerOrdering`](crate::signer::SignerOrdering). + /// - `policy`: Closure indicating whether the output should be kept, used by some callers to + /// apply additional filters as in the case of RBF. + fn filter_spendable<'a, I, C, F>( + &'a self, + txos: I, + params: &'a PsbtParams, + policy: F, + ) -> impl Iterator> + 'a + where + I: IntoIterator> + 'a, + F: Fn(&FullTxOut) -> bool + 'a, + { + let current_height = params.maturity_height.unwrap_or(self.chain.tip().height()); + txos.into_iter().filter(move |txo| { + // Exclude outputs that are manually selected. + if params.set.contains(&txo.outpoint) { + return false; + } + // Filter outputs according to `policy` fn. + if !policy(txo) { + return false; + } + // Exclude locked UTXOs. + if self.is_outpoint_locked(txo.outpoint) { + return false; + } + // Exclude immature outputs. + if !txo.is_mature(current_height) { + return false; + } + // Exclude spent outputs. + if txo.spent_by.is_some() { + return false; + } + true + }) + } + + /// Maps the recipients of the `params` to a collection of target [`Output`]s. + fn target_outputs(&self, params: &PsbtParams) -> Vec { + params + .recipients + .iter() + .cloned() + .map( + |(script, value)| match self.tx_graph.index.index_of_spk(script.clone()) { + Some(&(keychain, index)) => { + let descriptor = self + .public_descriptor(keychain) + .at_derivation_index(index) + .expect("should be valid derivation index"); + Output::with_descriptor(descriptor, value) + } + None => Output::with_script(script, value), + }, + ) + .collect() + } + + /// Creates a PSBT with the given `params` and returns the updated [`Psbt`] and + /// [`Finalizer`]. /// - /// The [`SignOptions`] can be used to tweak the behavior of the software signers, and the way - /// the transaction is finalized at the end. Note that it can't be guaranteed that *every* - /// signer will follow the options, but the "software signers" (WIF keys and `xprv`) defined - /// in this library will. + /// This function uses the thread-local random number generator (RNG) to generate + /// randomness. To supply your own source of entropy see [`Wallet::create_psbt_with_rng`]. /// - /// Returns true if the PSBT was finalized, or false otherwise. + /// # Example /// - /// ## Example + /// ```rust,no_run + /// # use std::str::FromStr; + /// # use bitcoin::{Amount, Address, FeeRate, OutPoint}; + /// # use bdk_wallet::psbt::{PsbtParams, SelectionStrategy}; + /// # let mut wallet = bdk_wallet::doctest_wallet!(); + /// # let outpoint = OutPoint::null(); + /// # let address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5").unwrap().assume_checked(); + /// # let amount = Amount::ZERO; + /// let mut params = PsbtParams::default(); + /// params + /// .add_utxos(&[outpoint]) + /// .add_recipients([(address, amount)]) + /// .coin_selection(SelectionStrategy::SingleRandomDraw) + /// .fee_rate(FeeRate::BROADCAST_MIN); /// + /// let (psbt, finalizer) = wallet.create_psbt(params)?; + /// # Ok::<_, anyhow::Error>(()) /// ``` - /// # use bdk_wallet::*; - /// # use bdk_wallet::bitcoin::*; - /// # use bdk_wallet::bitcoin::{NetworkKind, secp256k1::Secp256k1}; - /// # use bdk_wallet::descriptor::IntoWalletDescriptor; - /// # use bdk_wallet::signer::SignersContainer; - /// # let mut wallet = doctest_wallet!(); - /// let signer_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)"; - /// let secp = Secp256k1::new(); - /// let (_, keymap) = signer_descriptor - /// .into_wallet_descriptor(&secp, NetworkKind::Test) - /// .unwrap(); - /// let external_signers = SignersContainer::build( - /// keymap, - /// wallet.public_descriptor(KeychainKind::External), - /// wallet.secp_ctx(), - /// ); /// - /// let to_address = wallet.next_unused_address(KeychainKind::External).address; - /// let mut psbt = { - /// let mut builder = wallet.build_tx(); - /// builder.drain_to(to_address.script_pubkey()).drain_wallet(); - /// builder.finish()? - /// }; + /// # Errors /// - /// let finalized = wallet.sign_with_signers( - /// &mut psbt, - /// &[&external_signers], - /// SignOptions::default(), - /// )?; - /// assert!(finalized); - /// # Ok::<(), anyhow::Error>(()) - /// ``` - pub fn sign_with_signers( - &self, - psbt: &mut Psbt, - signers: &[&SignersContainer], - sign_options: SignOptions, - ) -> Result { - // This adds all the PSBT metadata for the inputs, which will help us later figure out how - // to derive our keys. - self.update_psbt_with_descriptor(psbt) - .map_err(SignerError::MiniscriptPsbt)?; - - // If we aren't allowed to use `witness_utxo`, ensure that every input (except p2tr and - // finalized ones) has the `non_witness_utxo`. - if !sign_options.trust_witness_utxo - && psbt - .inputs - .iter() - .filter(|i| i.final_script_witness.is_none() && i.final_script_sig.is_none()) - .filter(|i| i.tap_internal_key.is_none() && i.tap_merkle_root.is_none()) - .any(|i| i.non_witness_utxo.is_none()) - { - return Err(SignerError::MissingNonWitnessUtxo); - } - - // If the user hasn't explicitly opted-in, refuse to sign the transaction unless every input - // is using `SIGHASH_ALL` or `SIGHASH_DEFAULT` for Taproot. - if !sign_options.allow_all_sighashes - && !psbt.inputs.iter().all(|i| { - i.sighash_type.is_none() - || i.sighash_type == Some(EcdsaSighashType::All.into()) - || i.sighash_type == Some(TapSighashType::All.into()) - || i.sighash_type == Some(TapSighashType::Default.into()) - }) - { - return Err(SignerError::NonStandardSighash); - } - - for signer in signers.iter().flat_map(|container| container.signers()) { - signer.sign_transaction(psbt, &sign_options, &self.secp)?; - } - - // Attempt to finalize. - if sign_options.try_finalize { - self.finalize_psbt(psbt, sign_options) - } else { - Ok(false) - } - } - - /// Returns the descriptor used to create addresses for a particular `keychain`. + /// A [`CreatePsbtError`] will be thrown if any of the following occurs /// - /// It's the "public" version of the wallet's descriptor, meaning a new descriptor that has - /// the same structure but with the all secret keys replaced by their corresponding public key. - /// This can be used to build a watch-only version of a wallet. - pub fn public_descriptor(&self, keychain: KeychainKind) -> &ExtendedDescriptor { - self.tx_graph - .index - .get_descriptor(self.map_keychain(keychain)) - .expect("keychain must exist") - } - - /// Finalize a PSBT, i.e., for each input determine if sufficient data is available to pass - /// validation and construct the respective `scriptSig` or `scriptWitness`. Please refer to - /// [BIP174](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki#Input_Finalizer), - /// and [BIP371](https://github.com/bitcoin/bips/blob/master/bip-0371.mediawiki) - /// for further information. + /// - A manually selected input is missing from the wallet, or could not be planned + /// - The input value is insufficient to fund the outputs + /// - Failure to complete coin selection + /// - Failure to create or update the PSBT. /// - /// Returns `true` if the PSBT could be finalized, and `false` otherwise. + /// # Change address /// - /// The [`SignOptions`] can be used to tweak the behavior of the finalizer. - pub fn finalize_psbt( - &self, - psbt: &mut Psbt, - sign_options: SignOptions, - ) -> Result { - let tx = &psbt.unsigned_tx; - let chain_tip = self.chain.tip().block_id(); - let prev_txids = tx - .input - .iter() - .map(|txin| txin.previous_output.txid) - .collect::>(); - let confirmation_heights = self - .tx_graph - .graph() - .list_canonical_txs(&self.chain, chain_tip, CanonicalizationParams::default()) - .filter(|canon_tx| prev_txids.contains(&canon_tx.tx_node.txid)) - // This is for a small performance gain. Although `.filter` filters out excess txs, it - // will still consume the internal `CanonicalIter` entirely. Having a `.take` here - // allows us to stop further unnecessary canonicalization. - .take(prev_txids.len()) - .map(|canon_tx| { - let txid = canon_tx.tx_node.txid; - match canon_tx.chain_position { - ChainPosition::Confirmed { anchor, .. } => (txid, anchor.block_id.height), - ChainPosition::Unconfirmed { .. } => (txid, u32::MAX), - } - }) - .collect::>(); - let current_height = sign_options - .assume_height - .unwrap_or_else(|| self.chain.tip().height()); - - Ok(self - .try_finalize_psbt_with( - psbt, - Some(current_height), - |_, input| { - confirmation_heights - .get(&input.previous_output.txid) - .copied() - }, - true, - )? - .is_finalized()) + /// When no [`ChangeScript`] is supplied via [`PsbtParams`], the wallet automatically selects + /// the next unused internal address and reveals it so that incoming change is tracked on + /// the next sync. The change address will not be marked used, so calling this function + /// again before syncing will use the same change address. If you intend to build + /// multiple transactions without syncing between them, either provide the change script in + /// the [`PsbtParams`], or do [`Wallet::mark_used`] after each call to prevent reuse. + /// + /// **You must persist the change set staged as a result of this call.** + /// See [`Wallet::take_staged`]. + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] + pub fn create_psbt( + &mut self, + params: PsbtParams, + ) -> Result<(Psbt, Finalizer), CreatePsbtError> { + self.create_psbt_with_rng(params, &mut rand::thread_rng()) } - /// Attempt to finalize each input of a PSBT and return per-input finalization results. + /// Creates a PSBT with the given `params` and random number generator (RNG). /// - /// Use this method when you need to inspect why a specific input could not be finalized. Call - /// [`FinalizePsbtOutcome::is_finalized`] on the returned value to check whether all inputs are - /// finalized after the call. + /// Return the updated [`Psbt`] and [`Finalizer`]. /// - /// Per-input finalization failures are reported as [`FinalizeInputOutcome`] values. This method - /// only returns `Err` when the PSBT is malformed, for example if its inputs are out of bounds. + /// ## Parameters: /// - /// Timelock satisfaction is evaluated from the PSBT transaction fields. This method does not - /// redact or clear output metadata. - pub fn try_finalize_psbt( - &self, - psbt: &mut Psbt, - ) -> Result { - self.try_finalize_psbt_with(psbt, None, |_, _| None, false) - } - - fn try_finalize_psbt_with( - &self, - psbt: &mut Psbt, - current_height: Option, - mut confirmation_height_for_input: F, - clear_output_derivations: bool, - ) -> Result - where - F: FnMut(usize, &bitcoin::TxIn) -> Option, - { - let tx = &psbt.unsigned_tx; - if psbt.inputs.len() < tx.input.len() { - return Err(IndexOutOfBoundsError::new( - psbt.inputs.len(), - psbt.inputs.len(), - )); + /// - `params`: [`PsbtParams`] + /// - `rng`: Source of entropy, may be used during coin selection and to sort inputs and outputs + /// by the [`TxOrdering`](crate::wallet::tx_builder::TxOrdering). + /// + /// See [`Wallet::create_psbt`] for notes on change address handling. + /// + /// **You must persist the change set staged as a result of this call.** + /// See [`Wallet::take_staged`]. + pub fn create_psbt_with_rng( + &mut self, + mut params: PsbtParams, + rng: &mut impl RngCore, + ) -> Result<(Psbt, Finalizer), CreatePsbtError> { + // Only permit no recipients if we're doing a sweep and an explicit change script is + // provided. + if params.recipients.is_empty() + && !(matches!(params.coin_selection, SelectionStrategy::All) + && params.change_script.is_some()) + { + return Err(CreatePsbtError::NoRecipients); } + let (change_info, change_script) = params + .change_script + .take() + .map(|change_script| (None, change_script)) + .unwrap_or_else(|| { + let (change_info, change_script) = self.peek_change_info(); + (Some(change_info), change_script) + }); - let mut outcomes = BTreeMap::new(); - - for (n, input) in tx.input.iter().enumerate() { - let psbt_input = &psbt - .inputs - .get(n) - .ok_or(IndexOutOfBoundsError::new(n, psbt.inputs.len()))?; - if psbt_input.final_script_sig.is_some() || psbt_input.final_script_witness.is_some() { - outcomes.insert(n, FinalizeInputOutcome::AlreadyFinalized); - continue; - } - - // - Try to derive the descriptor by looking at the txout. If it's in our database, we - // know exactly which `keychain` to use, and which derivation index it is. - // - If that fails, try to derive it by looking at the psbt input: the complete logic is - // in `src/descriptor/mod.rs`, but it will basically look at `bip32_derivation`, - // `redeem_script` and `witness_script` to determine the right derivation. - // - If that also fails, it will try it on the internal descriptor, if present. - let desc = psbt - .get_utxo_for(n) - .and_then(|txout| self.get_descriptor_for_txout(&txout)) - .or_else(|| { - self.tx_graph.index.keychains().find_map(|(_, desc)| { - desc.derive_from_psbt_input(psbt_input, psbt.get_utxo_for(n), &self.secp) - }) - }); + let (assets, txouts) = self.parse_params(¶ms); - match desc { - Some(desc) => { - let mut tmp_input = bitcoin::TxIn::default(); - let satisfy_result = if let Some(current_height) = current_height { - let confirmation_height = confirmation_height_for_input(n, input); - desc.satisfy( - &mut tmp_input, - ( - PsbtInputSatisfier::new(psbt, n), - After::new(Some(current_height), false), - Older::new(Some(current_height), confirmation_height, false), - ), - ) - } else { - desc.satisfy(&mut tmp_input, PsbtInputSatisfier::new(psbt, n)) - }; + let must_spend = self.build_must_spend_inputs(¶ms, &txouts, &assets)?; - match satisfy_result { - Ok(_) => { - let length = psbt.inputs.len(); - let psbt_input = psbt - .inputs - .get_mut(n) - .ok_or(IndexOutOfBoundsError::new(n, length))?; - let original = mem::take(psbt_input); - let final_script_sig = - (!tmp_input.script_sig.is_empty()).then_some(tmp_input.script_sig); - let final_script_witness = - (!tmp_input.witness.is_empty()).then_some(tmp_input.witness); + // Get input candidates + let mut may_spend: Vec = if params.manually_selected_only { + vec![] + } else { + self.filter_spendable(txouts.into_values(), ¶ms, |txo| { + (params.utxo_filter.0)(txo) + }) + .flat_map(|txo| self.plan_input(&txo, &assets)) + .collect() + }; - // BIP174 finalization clears input metadata except UTXOs, final - // scripts, and opaque fields the finalizer does not understand. - *psbt_input = bitcoin::psbt::Input { - non_witness_utxo: original.non_witness_utxo, - witness_utxo: original.witness_utxo, - final_script_sig, - final_script_witness, - proprietary: original.proprietary, - unknown: original.unknown, - ..Default::default() - }; - outcomes.insert(n, FinalizeInputOutcome::Finalized); - } - Err(err) => { - outcomes.insert(n, FinalizeInputOutcome::CouldNotSatisfy(err)); - } - } - } - None => { - outcomes.insert(n, FinalizeInputOutcome::MissingDescriptor); + // Apply fallback sequence to coin-selection candidates without a CSV requirement. + if let Some(seq) = params.fallback_sequence { + for input in &mut may_spend { + if input.sequence().is_none() { + input.set_sequence(seq).map_err(CreatePsbtError::Sequence)?; } } } - let finalized = FinalizePsbtOutcome::new(outcomes); - if clear_output_derivations && finalized.is_finalized() { - for output in &mut psbt.outputs { - output.bip32_derivation.clear(); - output.tap_key_origins.clear(); - } - } - - Ok(finalized) - } - - /// Return the secp256k1 context used for all signing operations. - pub fn secp_ctx(&self) -> &SecpCtx { - &self.secp - } - - /// The derivation index of this wallet. It will return `None` if it has not derived any - /// addresses. Otherwise, it will return the index of the highest address it has derived. - pub fn derivation_index(&self, keychain: KeychainKind) -> Option { - self.tx_graph.index.last_revealed_index(keychain) - } - - /// The index of the next address that you would get if you were to ask the wallet for a new - /// address. - pub fn next_derivation_index(&self, keychain: KeychainKind) -> u32 { - self.tx_graph - .index - .next_index(self.map_keychain(keychain)) - .expect("keychain must exist") - .0 - } - - fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option { - let &(keychain, child) = self - .tx_graph - .index - .index_of_spk(txout.script_pubkey.clone())?; - let descriptor = self.public_descriptor(keychain); - descriptor.at_derivation_index(child).ok() - } + let target_outputs = self.target_outputs(¶ms); - /// Given the options returns the list of utxos that must be used to form the - /// transaction and any further that may be used if needed. - fn filter_utxos( - &self, - params: &TxParams, - current_height: u32, - version: Version, - ) -> Vec { - if params.manually_selected_only { - vec![] - // Only process optional UTxOs if manually_selected_only is false. - } else { - let manually_selected_outpoints = params - .utxos - .iter() - .map(|wutxo| wutxo.utxo.outpoint()) - .collect::>(); + let input_candidates = InputCandidates::new(must_spend, may_spend); + if input_candidates.inputs().next().is_none() { + let target_amount: Amount = target_outputs.iter().map(|output| output.value).sum(); + let err = bdk_coin_select::InsufficientFunds { + missing: target_amount.to_sat(), + }; + return Err(CreatePsbtError::InsufficientFunds(err)); + } - self.tx_graph - .graph() - // Get all unspent UTxOs from wallet. - // NOTE: the UTxOs returned by the following method already belong to wallet as the - // call chain uses get_tx_node infallibly. - .filter_chain_unspents( - &self.chain, - self.chain.tip().block_id(), - CanonicalizationParams::default(), - self.tx_graph.index.outpoints().iter().cloned(), - ) - // Filter out locked outpoints. - .filter(|(_, txo)| !self.is_outpoint_locked(txo.outpoint)) - // Only create LocalOutput if UTxO is mature. - .filter_map(move |((k, i), full_txo)| { - full_txo - .is_mature(current_height) - .then(|| new_local_utxo(k, i, full_txo)) - }) - // Only add to optional UTXOs those that follows BIP-431 (TRUC) specification. - // see https://github.com/bitcoin/bips/blob/master/bip-0431.mediawiki#specification - .filter(|local_output| { - // If the output is confirmed, it can be spent by either non-TRUC/TRUC - // transactions. - if local_output.chain_position.is_confirmed() { - return true; - } + let mut selector = Selector::new( + &input_candidates, + SelectorParams::new(params.fee_rate, target_outputs, change_script), + ) + .map_err(CreatePsbtError::Selector)?; - // If building TRUC (V3), the unconfirmed outputs MUST be TRUC (V3). Otherwise, - // if building a non-TRUC, the unconfirmed outputs MUST be non-TRUC. - self.tx_graph() - .get_tx(local_output.outpoint.txid) - .is_some_and(|tx| (tx.version == Version(3)) == (version == Version(3))) - }) - // only process UTXOs not selected manually, they will be considered later in the - // chain - // NOTE: this avoid UTXOs in both required and optional list - .filter(|may_spend| !manually_selected_outpoints.contains(&may_spend.outpoint)) - // only add to optional UTxOs those which satisfy the change policy if we reuse - // change - .filter(|local_output| { - self.keychains().count() == 1 - || params.change_policy.is_satisfied_by(local_output) - }) - // Only add to optional UTxOs those marked as spendable. - .filter(|local_output| !params.unspendable.contains(&local_output.outpoint)) - // If bumping fees only add to optional UTxOs those confirmed. - .filter(|local_output| { - params.bumping_fee.is_none() || local_output.chain_position.is_confirmed() - }) - .map(|utxo| WeightedUtxo { - satisfaction_weight: self - .public_descriptor(utxo.keychain) - .max_weight_to_satisfy() - .unwrap(), - utxo: Utxo::Local(utxo), - }) - .collect() + let (psbt, finalizer) = self.create_psbt_from_selector(&mut selector, ¶ms, rng)?; + + // Reveal the auto-selected change address. + if let Some((keychain, index, spk)) = change_info { + if psbt + .unsigned_tx + .output + .iter() + .any(|txo| txo.script_pubkey == spk) + { + if let Some((_, index_changeset)) = + self.tx_graph.index.reveal_to_target(keychain, index) + { + self.stage.merge(index_changeset.into()); + } + } } + + Ok((psbt, finalizer)) } - fn complete_transaction( + /// Create the PSBT from [`Selector`] and `params`. + /// + /// Internal method for handling coin selection and building the + /// resulting PSBT. + fn create_psbt_from_selector( &self, - tx: Transaction, - selected: Vec, - params: TxParams, - ) -> Result { - let mut psbt = Psbt::from_unsigned_tx(tx)?; + selector: &mut Selector, + params: &PsbtParams, + rng: &mut impl RngCore, + ) -> Result<(Psbt, Finalizer), CreatePsbtError> { + // Select coins + match params.coin_selection { + SelectionStrategy::All => selector.select_all(), + SelectionStrategy::Custom { ref algorithm } => selector + .select_with_algorithm(|s| algorithm(s)) + .map_err(CreatePsbtError::Selector)?, + SelectionStrategy::LowestFee { + longterm_feerate, + max_rounds, + } => { + selector + .select_with_algorithm(selection_algorithm_lowest_fee_bnb( + longterm_feerate, + max_rounds, + )) + .map_err(CreatePsbtError::Bnb)?; + } + SelectionStrategy::SingleRandomDraw => { + // Implement a shuffle algorithm by associating every candidate with + // a random sort key. + selector.select_with_algorithm(|selector| -> Result<_, CreatePsbtError> { + let n = selector.inner().candidates().count(); + let keys: Vec = (0..n).map(|_| rng.next_u32()).collect(); + selector + .inner_mut() + .sort_candidates_by(|(a, _), (b, _)| keys[a].cmp(&keys[b])); + selector + .select_until_target_met() + .map_err(CreatePsbtError::InsufficientFunds) + })? + } + }; + let mut selection = selector.try_finalize().ok_or({ + let e = bdk_tx::CannotMeetTarget; + CreatePsbtError::Selector(bdk_tx::SelectorError::CannotMeetTarget(e)) + })?; + + // Change fell below the dust threshold and was dropped to fees, leaving + // the transaction with no outputs. + if selection.outputs().is_empty() { + return Err(CreatePsbtError::AllOutputsBelowDust); + } + + match ¶ms.ordering { + TxOrdering::Untouched => {} + TxOrdering::Shuffle => { + selection.shuffle_inputs(rng); + selection.shuffle_outputs(rng); + } + TxOrdering::Custom { + input_sort, + output_sort, + } => { + selection.sort_inputs_by(|a, b| input_sort(a, b)); + selection.sort_outputs_by(|a, b| output_sort(a, b)); + } + } + + let version = params.version.unwrap_or(transaction::Version::TWO); + let min_locktime = params.min_locktime.unwrap_or(absolute::LockTime::ZERO); + + // Create psbt + let mut psbt = selection + .create_psbt_with_rng( + bdk_tx::PsbtParams { + version, + min_locktime, + mandate_full_tx_for_segwit_v0: !params.only_witness_utxo, + anti_fee_sniping: params.anti_fee_sniping, + }, + rng, + ) + .map_err(CreatePsbtError::Psbt)?; + // Add global xpubs. if params.add_global_xpubs { - let all_xpubs = self + for xpub in self .keychains() .flat_map(|(_, desc)| desc.get_extended_keys()) - .collect::>(); - - for xpub in all_xpubs { + { let origin = match xpub.origin { Some(origin) => origin, None if xpub.xkey.depth == 0 => { (xpub.root_fingerprint(&self.secp), vec![].into()) } - _ => return Err(CreateTxError::MissingKeyOrigin(xpub.xkey.to_string())), + _ => return Err(CreatePsbtError::MissingKeyOrigin(xpub.xkey)), }; psbt.xpub.insert(xpub.xkey, origin); } } - let mut lookup_output = selected - .into_iter() - .map(|utxo| (utxo.outpoint(), utxo)) - .collect::>(); + let finalizer = selection.into_finalizer(); - // Add metadata for the inputs. - for (psbt_input, input) in psbt.inputs.iter_mut().zip(psbt.unsigned_tx.input.iter()) { - let utxo = match lookup_output.remove(&input.previous_output) { - Some(utxo) => utxo, - None => continue, - }; + Ok((psbt, finalizer)) + } - match utxo { - Utxo::Local(utxo) => { - *psbt_input = - match self.get_psbt_input(utxo, params.sighash, params.only_witness_utxo) { - Ok(psbt_input) => psbt_input, - Err(e) => match e { - CreateTxError::UnknownUtxo => psbt::Input { - sighash_type: params.sighash, - ..psbt::Input::default() - }, - _ => return Err(e), - }, - } - } - Utxo::Foreign { - outpoint, - psbt_input: foreign_psbt_input, - .. - } => { - let is_taproot = foreign_psbt_input - .witness_utxo - .as_ref() - .map(|txout| txout.script_pubkey.is_p2tr()) - .unwrap_or(false); - if !is_taproot - && !params.only_witness_utxo - && foreign_psbt_input.non_witness_utxo.is_none() - { - return Err(CreateTxError::MissingNonWitnessUtxo(outpoint)); - } - *psbt_input = *foreign_psbt_input; - } - } + /// Creates a Replace-By-Fee transaction (RBF) and returns the updated [`Psbt`] and + /// [`Finalizer`]. + /// + /// This function uses the thread-local random number generator (RNG) to generate + /// randomness. To supply your own source of entropy see [`Wallet::replace_by_fee_with_rng`]. + /// + /// # Errors + /// + /// A [`ReplaceByFeeError`] will be thrown if any of the following occurs + /// + /// - An original transaction is already confirmed + /// - An original transaction is missing from the wallet + /// - Failure to calculate the [fee](Wallet::calculate_fee) of an original transaction + /// - Failure to complete coin selection + /// - Failure to create or update the PSBT. + /// + /// # Change address + /// + /// When no [`ChangeScript`] is supplied via [`PsbtParams`], the wallet automatically selects + /// the next unused internal address and reveals it so that incoming change is tracked on + /// the next sync. The change address will not be marked used, so calling this function + /// again before syncing will use the same change address. If you intend to build + /// multiple transactions without syncing between them, either provide the change script in + /// the [`PsbtParams`], or do [`Wallet::mark_used`] after each call to prevent reuse. + /// + /// **You must persist the change set staged as a result of this call.** + /// See [`Wallet::take_staged`]. + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] + pub fn replace_by_fee( + &mut self, + params: PsbtParams, + ) -> Result<(Psbt, Finalizer), ReplaceByFeeError> { + self.replace_by_fee_with_rng(params, &mut rand::thread_rng()) + } + + /// Creates a Replace-By-Fee transaction (RBF) and returns the updated [`Psbt`] and + /// [`Finalizer`]. + /// + /// ## Parameters: + /// + /// - `params`: [`PsbtParams`] + /// - `rng`: Source of entropy, may be used during coin selection and to sort inputs and outputs + /// by the [`TxOrdering`](crate::wallet::tx_builder::TxOrdering). + /// + /// See [`Wallet::replace_by_fee`] for notes on change address handling. + /// + /// **You must persist the change set staged as a result of this call.** + /// See [`Wallet::take_staged`]. + pub fn replace_by_fee_with_rng( + &mut self, + mut params: PsbtParams, + rng: &mut impl RngCore, + ) -> Result<(Psbt, Finalizer), ReplaceByFeeError> { + if params.replace.is_empty() { + return Err(ReplaceByFeeError::NoOriginalTransactions); + } + // Only permit no recipients if we're doing a sweep and an explicit change script is + // provided. + if params.recipients.is_empty() + && !(matches!(params.coin_selection, SelectionStrategy::All) + && params.change_script.is_some()) + { + return Err(ReplaceByFeeError::CreatePsbt(CreatePsbtError::NoRecipients)); } + let (change_info, change_script) = params + .change_script + .take() + .map(|change_script| (None, change_script)) + .unwrap_or_else(|| { + let (change_info, change_script) = self.peek_change_info(); + (Some(change_info), change_script) + }); - self.update_psbt_with_descriptor(&mut psbt)?; + let (assets, txouts) = self.parse_params(¶ms); - Ok(psbt) - } + let PsbtParams { + replace: txids_to_replace, + .. + } = ¶ms; - /// Get the corresponding PSBT Input for a [`LocalOutput`]. - pub fn get_psbt_input( - &self, - utxo: LocalOutput, - sighash_type: Option, - only_witness_utxo: bool, - ) -> Result { - // Try to find the prev_script in our db to figure out if this is internal or external, - // and the derivation index. - let &(keychain, child) = self + // None of the txids-to-replace may already be confirmed + let chain_tip = self.chain.tip().block_id(); + let chain_positions: HashMap> = self .tx_graph - .index - .index_of_spk(utxo.txout.script_pubkey) - .ok_or(CreateTxError::UnknownUtxo)?; - - let mut psbt_input = psbt::Input { - sighash_type, - ..psbt::Input::default() - }; - - let desc = self.public_descriptor(keychain); - let derived_descriptor = desc - .at_derivation_index(child) - .expect("child can't be hardened"); - - psbt_input - .update_with_descriptor_unchecked(&derived_descriptor) - .map_err(MiniscriptPsbtError::Conversion)?; - - let prev_output = utxo.outpoint; - if let Some(prev_tx) = self.tx_graph.graph().get_tx(prev_output.txid) { - // We want to check that the prevout actually exists in the transaction before - // continuing. - let prevout = prev_tx.output.get(prev_output.vout as usize).ok_or( - MiniscriptPsbtError::UtxoUpdate(miniscript::psbt::UtxoUpdateError::UtxoCheck), - )?; - if desc.is_witness() || desc.is_taproot() { - psbt_input.witness_utxo = Some(prevout.clone()); - } - if !desc.is_taproot() && (!desc.is_witness() || !only_witness_utxo) { - psbt_input.non_witness_utxo = Some(prev_tx.as_ref().clone()); + .graph() + .list_canonical_txs(&self.chain, chain_tip, params.canonical_params.clone()) + .map(|canonical_tx| (canonical_tx.tx_node.txid, canonical_tx.chain_position)) + .collect(); + for &txid in txids_to_replace.iter() { + if chain_positions + .get(&txid) + .is_some_and(|chain_position| chain_position.is_confirmed()) + { + return Err(ReplaceByFeeError::TransactionConfirmed(txid)); } } - Ok(psbt_input) - } - fn update_psbt_with_descriptor(&self, psbt: &mut Psbt) -> Result<(), MiniscriptPsbtError> { - // We need to borrow `psbt` mutably within the loops, so we have to allocate a vec for all - // the input utxos and outputs. - let utxos = (0..psbt.inputs.len()) - .filter_map(|i| psbt.get_utxo_for(i).map(|utxo| (true, i, utxo))) - .chain( - psbt.unsigned_tx - .output + // For each txid being replaced, verify that at least one of its original inputs + // remains in the selected set. A replacement must conflict with every transaction it + // replaces — two transactions cannot spend the same UTXO. + for &txid in txids_to_replace.iter() { + if let Some(tx) = self.tx_graph.graph().get_tx(txid) { + if !tx + .input .iter() - .enumerate() - .map(|(i, out)| (false, i, out.clone())), - ) - .collect::>(); - - // Try to figure out the keychain and derivation for every input and output. - for (is_input, index, out) in utxos.into_iter() { - if let Some(&(keychain, child)) = self.tx_graph.index.index_of_spk(out.script_pubkey) { - let desc = self.public_descriptor(keychain); - let desc = desc - .at_derivation_index(child) - .expect("child can't be hardened"); - - if is_input { - psbt.update_input_with_descriptor(index, &desc) - .map_err(MiniscriptPsbtError::UtxoUpdate)?; - } else { - psbt.update_output_with_descriptor(index, &desc) - .map_err(MiniscriptPsbtError::OutputUpdate)?; + .any(|txin| params.set.contains(&txin.previous_output)) + { + return Err(ReplaceByFeeError::NoInputsFromOriginal(txid)); } } } - Ok(()) - } - - /// Return the checksum of the public descriptor associated to the `keychain`. - /// - /// Internally calls [`Self::public_descriptor`] to fetch the right descriptor. - pub fn descriptor_checksum(&self, keychain: KeychainKind) -> String { - self.public_descriptor(keychain) - .to_string() - .split_once('#') - .unwrap() - .1 - .to_string() - } + // Txs and their descendants to be replaced + // + // `direct_conflicts` are the transactions named in `params.replace`. Only these + // feed into `original_txs` for the RBF fee rate floor. + // + // `to_replace` also includes walked descendants so they are excluded from coin + // selection; their fees accumulate into `descendant_fee`. + let direct_conflicts: HashSet = txids_to_replace.iter().copied().collect(); - /// Applies an update to the wallet and stages the changes (but does not persist them). - /// - /// Usually you create an `update` by interacting with some blockchain data source and inserting - /// transactions related to your wallet into it. - /// - /// After applying updates you should persist the staged wallet changes. For an example of how - /// to persist staged wallet changes see [`Wallet::reveal_next_address`]. - pub fn apply_update(&mut self, update: impl Into) -> Result<(), CannotConnectError> { - let update = update.into(); - let mut changeset = match update.chain { - Some(chain_update) => ChangeSet::from(self.chain.apply_update(chain_update)?), - None => ChangeSet::default(), - }; + let descendants: HashSet = direct_conflicts + .iter() + .flat_map(|&txid| { + self.tx_graph + .graph() + .walk_descendants(txid, |_, txid| Some(txid)) + }) + .filter(|txid| !direct_conflicts.contains(txid)) + .collect(); - let index_changeset = self - .tx_graph - .index - .reveal_to_target_multi(&update.last_active_indices); - changeset.merge(index_changeset.into()); - changeset.merge(self.tx_graph.apply_update(update.tx_update).into()); - self.stage.merge(changeset); - Ok(()) - } + let to_replace: HashSet = direct_conflicts + .iter() + .chain(descendants.iter()) + .copied() + .collect(); - /// Applies an update to the wallet, stages the changes, and returns events. - /// - /// Usually you create an `update` by interacting with some blockchain data source and inserting - /// transactions related to your wallet into it. Staged changes are NOT persisted. - /// - /// After applying updates you should process the events in your app before persisting the - /// staged wallet changes. For an example of how to persist staged wallet changes see - /// [`Wallet::reveal_next_address`]. - /// - /// ```rust,no_run - /// # use bitcoin::*; - /// # use bdk_wallet::*; - /// use bdk_wallet::WalletEvent; - /// # let wallet_update = Update::default(); - /// # let mut wallet = doctest_wallet!(); - /// let events = wallet.apply_update_events(wallet_update)?; - /// // Handle wallet relevant events from this update. - /// events.iter().for_each(|event| { - /// match event { - /// // The chain tip changed. - /// WalletEvent::ChainTipChanged { old_tip, new_tip } => { - /// todo!() // handle event - /// } - /// // An unconfirmed tx is now confirmed in a block. - /// WalletEvent::TxConfirmed { - /// txid, - /// tx, - /// block_time, - /// old_block_time: None, - /// } => { - /// todo!() // handle event - /// } - /// // A confirmed tx is now confirmed in a new block (reorg). - /// WalletEvent::TxConfirmed { - /// txid, - /// tx, - /// block_time, - /// old_block_time: Some(old_block_time), - /// } => { - /// todo!() // handle event - /// } - /// // A new unconfirmed tx was seen in the mempool. - /// WalletEvent::TxUnconfirmed { - /// txid, - /// tx, - /// old_block_time: None, - /// } => { - /// todo!() // handle event - /// } - /// // A previously confirmed tx in now unconfirmed in the mempool (reorg). - /// WalletEvent::TxUnconfirmed { - /// txid, - /// tx, - /// old_block_time: Some(old_block_time), - /// } => { - /// todo!() // handle event - /// } - /// // An unconfirmed tx was replaced in the mempool (RBF or double spent input). - /// WalletEvent::TxReplaced { - /// txid, - /// tx, - /// conflicts, - /// } => { - /// todo!() // handle event - /// } - /// // An unconfirmed tx was dropped from the mempool (fee too low). - /// WalletEvent::TxDropped { txid, tx } => { - /// todo!() // handle event - /// } - /// _ => { - /// // unexpected event, do nothing - /// } - /// } - /// // take staged wallet changes - /// let staged = wallet.take_staged(); - /// // persist staged changes - /// }); - /// # Ok::<(), anyhow::Error>(()) - /// ``` - /// [`TxBuilder`]: crate::TxBuilder - pub fn apply_update_events( - &mut self, - update: impl Into, - ) -> Result, CannotConnectError> { - self.events_helper(|wallet| wallet.apply_update(update)) - } + let must_spend = self.build_must_spend_inputs(¶ms, &txouts, &assets)?; - /// Get a reference of the staged [`ChangeSet`] that is yet to be committed (if any). - pub fn staged(&self) -> Option<&ChangeSet> { - if self.stage.is_empty() { - None - } else { - Some(&self.stage) + // Validate that no manually-selected input spends an output of a transaction + // in `to_replace`. + for input in &must_spend { + let op = input.prev_outpoint(); + if to_replace.contains(&op.txid) { + return Err(ReplaceByFeeError::ConflictingInput(op)); + } } - } - /// Get a mutable reference of the staged [`ChangeSet`] that is yet to be committed (if any). - pub fn staged_mut(&mut self) -> Option<&mut ChangeSet> { - if self.stage.is_empty() { - None + // Get input candidates + let mut may_spend: Vec = if params.manually_selected_only { + vec![] } else { - Some(&mut self.stage) + self.filter_spendable(txouts.into_values(), ¶ms, |txo| { + // To be included for coin selection the UTXO + // - must not be contained in `to_replace` + // - must be confirmed per replacement policy Rule 2 (removed in Core v31) + // - must pass a user-defined filter + !to_replace.contains(&txo.outpoint.txid) + && txo.chain_position.is_confirmed() + && (params.utxo_filter.0)(txo) + }) + .flat_map(|txo| self.plan_input(&txo, &assets)) + .collect() + }; + + // Apply fallback sequence to coin-selection candidates without a CSV requirement. + if let Some(seq) = params.fallback_sequence { + for input in &mut may_spend { + if input.sequence().is_none() { + input.set_sequence(seq).map_err(CreatePsbtError::Sequence)?; + } + } } - } - /// Take the staged [`ChangeSet`] to be persisted now (if any). - pub fn take_staged(&mut self) -> Option { - self.stage.take() - } + let target_outputs = self.target_outputs(¶ms); - /// Get a reference to the inner [`TxGraph`]. - pub fn tx_graph(&self) -> &TxGraph { - self.tx_graph.graph() - } + let input_candidates = InputCandidates::new(must_spend, may_spend); + if input_candidates.inputs().next().is_none() { + let target_amount: Amount = target_outputs.iter().map(|output| output.value).sum(); + let err = bdk_coin_select::InsufficientFunds { + missing: target_amount.to_sat(), + }; + return Err(CreatePsbtError::InsufficientFunds(err))?; + } - /// Get a reference to the inner [`KeychainTxOutIndex`]. - pub fn spk_index(&self) -> &KeychainTxOutIndex { - &self.tx_graph.index - } + let original_txs: Vec = direct_conflicts + .iter() + .map(|&txid| -> Result<_, ReplaceByFeeError> { + let tx = self + .tx_graph + .graph() + .get_tx(txid) + .ok_or(ReplaceByFeeError::MissingTransaction(txid))?; + let fee = self + .calculate_fee(&tx) + .map_err(ReplaceByFeeError::PreviousFee)?; + Ok(OriginalTxStats { + weight: tx.weight(), + fee, + }) + }) + .collect::>()?; - /// Get a reference to the inner [`LocalChain`]. - pub fn local_chain(&self) -> &LocalChain { - &self.chain - } + // Sum fees from all descendants known to the tx graph. This assumes every + // descendant is currently in the mempool, which could slightly overestimate + // the fee floor if a descendant was evicted or never relayed. + let descendant_fee: Amount = descendants + .iter() + .filter_map(|&txid| { + let tx = self.tx_graph.graph().get_tx(txid)?; + self.calculate_fee(&tx).ok() + }) + .sum(); - /// List the locked outpoints. - pub fn list_locked_outpoints(&self) -> impl Iterator + '_ { - self.locked_outpoints.iter().copied() - } + let rbf_params = RbfParams { + original_txs, + descendant_fee, + incremental_relay_feerate: FeeRate::BROADCAST_MIN, + }; - /// List unspent outpoints that are currently locked. - pub fn list_locked_unspent(&self) -> impl Iterator + '_ { - self.list_unspent() - .filter(|output| self.is_outpoint_locked(output.outpoint)) - .map(|output| output.outpoint) - } + let mut selector = Selector::new( + &input_candidates, + SelectorParams { + replace: Some(rbf_params), + ..SelectorParams::new(params.fee_rate, target_outputs, change_script) + }, + ) + .map_err(CreatePsbtError::Selector)?; - /// Whether the `outpoint` is locked. See [`Wallet::lock_outpoint`] for more. - pub fn is_outpoint_locked(&self, outpoint: OutPoint) -> bool { - self.locked_outpoints.contains(&outpoint) - } + let (psbt, finalizer) = self + .create_psbt_from_selector(&mut selector, ¶ms, rng) + .map_err(ReplaceByFeeError::CreatePsbt)?; - /// Lock a wallet output identified by the given `outpoint`. - /// - /// A locked UTXO will not be selected as an input to fund a transaction. This is useful - /// for excluding or reserving candidate inputs during transaction creation. - /// - /// **You must persist the staged change for the lock status to be persistent**. To unlock a - /// previously locked outpoint, see [`Wallet::unlock_outpoint`]. - pub fn lock_outpoint(&mut self, outpoint: OutPoint) { - if self.locked_outpoints.insert(outpoint) { - let changeset = locked_outpoints::ChangeSet { - outpoints: [(outpoint, true)].into(), - }; - self.stage.merge(changeset.into()); + // Reveal the auto-selected change address + if let Some((keychain, index, spk)) = change_info { + if psbt + .unsigned_tx + .output + .iter() + .any(|txo| txo.script_pubkey == spk) + { + if let Some((_, index_changeset)) = + self.tx_graph.index.reveal_to_target(keychain, index) + { + self.stage.merge(index_changeset.into()); + } + } } - } - /// Unlock the wallet output of the specified `outpoint`. - /// - /// **You must persist the staged change for the lock status to be persistent**. - pub fn unlock_outpoint(&mut self, outpoint: OutPoint) { - if self.locked_outpoints.remove(&outpoint) { - let changeset = locked_outpoints::ChangeSet { - outpoints: [(outpoint, false)].into(), - }; - self.stage.merge(changeset.into()); - } + Ok((psbt, finalizer)) } - /// Introduces a `block` of `height` to the wallet, and tries to connect it to the - /// `prev_blockhash` of the block's header. - /// - /// This is a convenience method that is equivalent to calling [`apply_block_connected_to`] - /// with `prev_blockhash` and `height-1` as the `connected_to` parameter. + /// Builds the required inputs from the manually-selected spends in `params`. /// - /// [`apply_block_connected_to`]: Self::apply_block_connected_to - pub fn apply_block(&mut self, block: &Block, height: u32) -> Result<(), CannotConnectError> { - let connected_to = match height.checked_sub(1) { - Some(prev_height) => BlockId { - height: prev_height, - hash: block.header.prev_blockhash, - }, - None => BlockId { - height, - hash: block.block_hash(), - }, - }; - self.apply_block_connected_to(block, height, connected_to) - .map_err(|err| match err { - ApplyHeaderError::InconsistentBlocks => { - unreachable!("connected_to is derived from the block so must be consistent") + /// Wallet outpoints are planned into [`Input`]s in insertion order, then any per-input + /// sequence override or the fallback sequence is applied. Pre-built planned inputs are kept + /// in that same insertion order. + fn build_must_spend_inputs( + &self, + params: &PsbtParams, + txouts: &HashMap>, + assets: &Assets, + ) -> Result, CreatePsbtError> { + params + .must_spend + .iter() + .map(|item| match item { + MustSpend::Utxo(outpoint) => { + let txo = txouts + .get(outpoint) + .ok_or(CreatePsbtError::UnknownUtxo(*outpoint))?; + let mut input = self + .plan_input(txo, assets) + .ok_or(CreatePsbtError::Plan(*outpoint))?; + if let Some(&seq) = params.sequence_overrides.get(outpoint) { + input.set_sequence(seq).map_err(CreatePsbtError::Sequence)?; + } else if let Some(seq) = params.fallback_sequence { + if input.sequence().is_none() { + input.set_sequence(seq).map_err(CreatePsbtError::Sequence)?; + } + } + Ok(input) } - ApplyHeaderError::CannotConnect(err) => err, + MustSpend::Planned(input) => Ok(input.clone()), }) + .collect() } - /// Introduces a `block` of `height` to the wallet, and tries to connect it to the - /// `prev_blockhash` of the block's header and returns events. - /// - /// This is a convenience method that is equivalent to calling - /// [`apply_block_connected_to_events`] with `prev_blockhash` and `height-1` as the - /// `connected_to` parameter. - /// - /// See [`apply_update_events`] for more information on the returned [`WalletEvent`]s. - /// - /// [`apply_block_connected_to_events`]: Self::apply_block_connected_to_events - /// [`apply_update_events`]: Self::apply_update_events - pub fn apply_block_events( - &mut self, - block: &Block, - height: u32, - ) -> Result, CannotConnectError> { - self.events_helper(|wallet| wallet.apply_block(block, height)) - } + /// Plan the output with the available assets and return a new [`Input`] if possible. See also + /// [`Self::try_plan`]. + fn plan_input( + &self, + txo: &FullTxOut, + spend_assets: &Assets, + ) -> Option { + let op = txo.outpoint; + let txid = op.txid; + + // We want to afford the output with as many assets as we can. The plan + // will use only the ones needed to produce the minimum satisfaction. + let cur_height = self.latest_checkpoint().height(); + let abs_locktime = spend_assets + .absolute_timelock + .unwrap_or(absolute::LockTime::from_consensus(cur_height)); + + let rel_locktime = spend_assets.relative_timelock.unwrap_or_else(|| { + let age = match txo.chain_position.confirmation_height_upper_bound() { + Some(conf_height) => cur_height + .saturating_add(1) + .saturating_sub(conf_height) + .try_into() + .unwrap_or(u16::MAX), + None => 0, + }; + relative::LockTime::from_height(age) + }); + + let mut assets = Assets::new(); + assets.extend(spend_assets); + assets = assets.after(abs_locktime); + assets = assets.older(rel_locktime); - /// Applies relevant transactions from `block` of `height` to the wallet, and connects the - /// block to the internal chain. - /// - /// The `connected_to` parameter informs the wallet how this block connects to the internal - /// [`LocalChain`]. Relevant transactions are filtered from the `block` and inserted into the - /// internal [`TxGraph`]. - /// - /// **WARNING**: You must persist the changes resulting from one or more calls to this method - /// if you need the inserted block data to be reloaded after closing the wallet. - /// See [`Wallet::reveal_next_address`]. - pub fn apply_block_connected_to( - &mut self, - block: &Block, - height: u32, - connected_to: BlockId, - ) -> Result<(), ApplyHeaderError> { - let mut changeset = ChangeSet::default(); - changeset.merge( - self.chain - .apply_header_connected_to(&block.header, height, connected_to)? - .into(), - ); - changeset.merge(self.tx_graph.apply_block_relevant(block, height).into()); - self.stage.merge(changeset); - Ok(()) - } + let plan = self.try_plan(op, &assets)?; + let tx = self.tx_graph.graph().get_tx(txid)?; + let tx_status = status_from_position(txo.chain_position); - /// Applies relevant transactions from `block` of `height` to the wallet, connects the - /// block to the internal chain and returns events. - /// - /// See [`apply_block_connected_to`] for more information. - /// - /// See [`apply_update_events`] for more information on the returned [`WalletEvent`]s. - /// - /// [`apply_block_connected_to`]: Self::apply_block_connected_to - /// [`apply_update_events`]: Self::apply_update_events - pub fn apply_block_connected_to_events( - &mut self, - block: &Block, - height: u32, - connected_to: BlockId, - ) -> Result, ApplyHeaderError> { - self.events_helper(|wallet| wallet.apply_block_connected_to(block, height, connected_to)) + Input::from_prev_tx(plan, tx, op.vout as usize, tx_status).ok() } - /// Apply relevant unconfirmed transactions to the wallet. - /// - /// Transactions that are not relevant are filtered out. - /// - /// This method takes in an iterator of `(tx, last_seen)` where `last_seen` is the timestamp of - /// when the transaction was last seen in the mempool. This is used for conflict resolution - /// when there are conflicting unconfirmed transactions in the mempool. The transaction with the - /// later `last_seen` is prioritized. + /// Attempt to create a spending plan for the UTXO of the given `outpoint` + /// with the provided `assets`. /// - /// **WARNING**: You must persist the changes resulting from one or more calls to this method - /// if you need the applied unconfirmed transactions to be reloaded after closing the wallet. - /// See [`Wallet::reveal_next_address`]. - pub fn apply_unconfirmed_txs>>( - &mut self, - unconfirmed_txs: impl IntoIterator, - ) { - let indexed_graph_changeset = self - .tx_graph - .batch_insert_relevant_unconfirmed(unconfirmed_txs); - self.stage.merge(indexed_graph_changeset.into()); + /// Return `None` if `outpoint` doesn't correspond to an indexed txout, or + /// if the assets are not sufficient to create a plan. + fn try_plan(&self, outpoint: OutPoint, assets: &Assets) -> Option { + let indexer = &self.tx_graph.index; + let ((keychain, index), _) = indexer.txout(outpoint)?; + let def_desc = indexer + .get_descriptor(keychain)? + .at_derivation_index(index) + .expect("must be valid derivation index"); + def_desc.plan(assets).ok() } +} - /// Apply relevant unconfirmed transactions to the wallet and returns events. - /// - /// See [`apply_unconfirmed_txs`] for more information. - /// - /// See [`apply_update_events`] for more information on the returned [`WalletEvent`]s. - /// - /// [`apply_unconfirmed_txs`]: Self::apply_unconfirmed_txs - /// [`apply_update_events`]: Self::apply_update_events - pub fn apply_unconfirmed_txs_events>>( - &mut self, - unconfirmed_txs: impl IntoIterator, - ) -> Vec { - self.events_helper::<_, _, core::convert::Infallible>(|wallet| { - wallet.apply_unconfirmed_txs(unconfirmed_txs); - Ok(()) - }) - .expect("`apply_unconfirmed_txs` should not fail") +impl AsRef> for Wallet { + fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph { + self.tx_graph.graph() } +} - /// Apply evictions of the given transaction IDs with their associated timestamps. - /// - /// This function is used to mark specific unconfirmed transactions as evicted from the mempool. - /// Eviction means that these transactions are not considered canonical by default, and will - /// no longer be part of the wallet's [`transactions`] set. This can happen for example when - /// a transaction is dropped from the mempool due to low fees or conflicts with another - /// transaction. - /// - /// Only transactions that are currently unconfirmed and canonical are considered for eviction. - /// Transactions that are not relevant to the wallet are ignored. Note that an evicted - /// transaction can become canonical again if it is later observed on-chain or seen in the - /// mempool with a higher priority (e.g., due to a fee bump). - /// - /// ## Parameters - /// - /// `evicted_txs`: An iterator of `(Txid, u64)` tuples, where: - /// - `Txid`: The transaction ID of the transaction to be evicted. - /// - `u64`: The timestamp indicating when the transaction was evicted from the mempool. This - /// will usually correspond to the time of the latest chain sync. See docs for - /// [`start_sync_with_revealed_spks`]. - /// - /// ## Notes - /// - /// - Not all blockchain backends support automatic mempool eviction handling - this method may - /// be used in such cases. It can also be used to negate the effect of - /// [`apply_unconfirmed_txs`] for a particular transaction without the need for an additional - /// sync. - /// - The changes are staged in the wallet's internal state and must be persisted to ensure they - /// are retained across wallet restarts. Use [`Wallet::take_staged`] to retrieve the staged - /// changes and persist them to your database of choice. - /// - Evicted transactions are removed from the wallet's canonical transaction set, but the data - /// remains in the wallet's internal transaction graph for historical purposes. - /// - Ensure that the timestamps provided are accurate and monotonically increasing, as they - /// influence the wallet's canonicalization logic. - /// - /// [`transactions`]: Wallet::transactions - /// [`apply_unconfirmed_txs`]: Wallet::apply_unconfirmed_txs - /// [`start_sync_with_revealed_spks`]: Wallet::start_sync_with_revealed_spks - pub fn apply_evicted_txs(&mut self, evicted_txs: impl IntoIterator) { - let chain = &self.chain; - let canon_txids: Vec = self - .tx_graph - .graph() - .list_canonical_txs( - chain, - chain.tip().block_id(), - CanonicalizationParams::default(), - ) - .map(|c| c.tx_node.txid) - .collect(); - - let changeset = self.tx_graph.batch_insert_relevant_evicted_at( - evicted_txs - .into_iter() - .filter(|(txid, _)| canon_txids.contains(txid)), - ); +/// Generate a deterministic wallet name from the provided descriptors. +/// +/// The wallet name is the concatenation of the [checksum] of the external and (if provided) +/// internal public descriptors. If descriptors containing private keys are provided, the name +/// is computed from the corresponding public descriptors; the result is identical to calling +/// this function with the equivalent public (xpub) descriptors. +/// +/// # Errors +/// +/// If descriptor parsing fails or if checksum computation fails then a [`DescriptorError`] is +/// returned. +/// +/// [checksum]: crate::descriptor::checksum::calc_checksum +pub fn wallet_name_from_descriptor( + descriptor: T, + change_descriptor: Option, + network_kind: NetworkKind, + secp: &SecpCtx, +) -> Result +where + T: IntoWalletDescriptor, +{ + // Wallet name is defined by the checksums of the wallet's public descriptors. + let (descriptor, _keymap) = descriptor.into_wallet_descriptor(secp, network_kind)?; + let mut wallet_name = calc_checksum(&descriptor.to_string())?; - self.stage.merge(changeset.into()); + if let Some(change_descriptor) = change_descriptor { + let (change_descriptor, _change_keymap) = + change_descriptor.into_wallet_descriptor(secp, network_kind)?; + wallet_name.push_str(&calc_checksum(&change_descriptor.to_string())?); } - /// Apply evictions of the given transaction IDs with their associated timestamps and returns - /// events. - /// - /// See [`apply_evicted_txs`] for more information. - /// - /// See [`apply_update_events`] for more information on the returned [`WalletEvent`]s. - /// - /// [`apply_evicted_txs`]: Self::apply_evicted_txs - /// [`apply_update_events`]: Self::apply_update_events - pub fn apply_evicted_txs_events( - &mut self, - evicted_txs: impl IntoIterator, - ) -> Vec { - self.events_helper::<_, _, core::convert::Infallible>(|wallet| { - wallet.apply_evicted_txs(evicted_txs); - Ok(()) - }) - .expect("`apply_evicted_txs` should not fail") - } + Ok(wallet_name) +} - /// Generates wallet events by executing a wallet-mutating function and surfacing internal - /// state changes. - /// - /// It works by taking some wallet operation that modifies state, capturing "before" and "after" - /// snapshots of the wallet's chain tip and transactions and comparing them in order to - /// generate a list of [`WalletEvent`]s representing what changed. - /// - /// Common kinds of events include: - /// - /// - [`WalletEvent::ChainTipChanged`]: The blockchain tip changed - /// - [`WalletEvent::TxConfirmed`]: A transaction was confirmed in a block - /// - [`WalletEvent::TxUnconfirmed`]: A transaction was newly unconfirmed - /// - [`WalletEvent::TxReplaced`]: An unconfirmed transaction was replaced (e.g., via RBF) - /// - [`WalletEvent::TxDropped`]: An unconfirmed transaction was dropped from the mempool - /// - /// This is useful when you need to track specific changes to your wallet state, such - /// as updating a UI to reflect transaction status changes, triggering notifications when - /// transactions confirm, logging state changes for debugging or auditing, or responding to - /// chain reorganizations. - /// - /// # Example - /// - /// ```rust,no_run - /// # use bdk_chain::local_chain::CannotConnectError; - /// # use bdk_wallet::{Wallet, Update, WalletEvent}; - /// # let mut wallet: Wallet = todo!(); - /// // Apply an update and get events describing what changed - /// let update = Update::default(); - /// let func = |wallet: &mut Wallet| wallet.apply_update(update); - /// let events = wallet.events_helper(func)?; - /// # Ok::<(), anyhow::Error>(()) - /// ``` - /// - /// # Errors - /// - /// If `f` returns an error, then returns `E` of a type defined by the function - /// passed in. - pub fn events_helper(&mut self, f: F) -> Result, E> - where - F: FnOnce(&mut Self) -> Result, - E: Debug + Display, - { - // Snapshot of chain tip and transactions before - let chain_tip1 = self.chain.tip().block_id(); - let wallet_txs1 = self.map_transactions(); +fn new_local_utxo( + keychain: K, + derivation_index: u32, + full_txo: FullTxOut, +) -> LocalOutput { + LocalOutput { + outpoint: full_txo.outpoint, + txout: full_txo.txout, + is_spent: full_txo.spent_by.is_some(), + chain_position: full_txo.chain_position, + keychain, + derivation_index, + } +} - // Call `f` on self - f(self)?; +/// Build the indexed tx graph for a set of keychains. +/// +/// Infallible: `KeychainTxOutIndex::insert_descriptor` rejects a descriptor already assigned to +/// another keychain, or a keychain already assigned a descriptor. A [`KeyRing`](crate::KeyRing) +/// rules out both before a `Wallet` is ever constructed, and a loaded `ChangeSet` cannot hold a +/// duplicate either, since `Merge` refuses to rebind a keychain. +fn make_indexed_graph( + stage: &mut ChangeSet, + tx_graph_changeset: chain::tx_graph::ChangeSet, + indexer_changeset: chain::keychain_txout::ChangeSet, + descriptors: BTreeMap, + lookahead: u32, + use_spk_cache: bool, +) -> IndexedTxGraph> +where + K: Ord + Clone + core::fmt::Debug, +{ + let (indexed_graph, changeset) = IndexedTxGraph::from_changeset( + chain::indexed_tx_graph::ChangeSet { + tx_graph: tx_graph_changeset, + indexer: indexer_changeset, + }, + |idx_cs| -> Result, core::convert::Infallible> { + let mut idx = KeychainTxOutIndex::from_changeset(lookahead, use_spk_cache, idx_cs); - // Chain tip and transactions after - let chain_tip2 = self.chain.tip().block_id(); - let wallet_txs2 = self.map_transactions(); + for (keychain, descriptor) in descriptors { + let inserted = idx + .insert_descriptor(keychain, descriptor) + .expect("keychain and descriptor are unique: guaranteed by KeyRing/ChangeSet"); + assert!( + inserted, + "this must be the first time we are seeing this descriptor" + ); + } - Ok(wallet_events( - self, - chain_tip1, - chain_tip2, - wallet_txs1, - wallet_txs2, - )) - } + Ok(idx) + }, + ) + .expect("infallible"); + stage.tx_graph.merge(changeset.tx_graph); + stage.indexer.merge(changeset.indexer); + indexed_graph +} - /// Used internally to ensure that all methods requiring a [`KeychainKind`] will use a - /// keychain with an associated descriptor. For example in case the wallet was created - /// with only one keychain, passing [`KeychainKind::Internal`] here will instead return - /// [`KeychainKind::External`]. - fn map_keychain(&self, keychain: KeychainKind) -> KeychainKind { - if self.keychains().count() == 1 { - KeychainKind::External - } else { - keychain - } - } +/// Transforms a [`FeeRate`] to `f64` with unit as sat/vb. +#[macro_export] +#[doc(hidden)] +macro_rules! floating_rate { + ($rate:expr) => {{ + use $crate::bitcoin::constants::WITNESS_SCALE_FACTOR; + // sat_kwu / 250.0 -> sat_vb + $rate.to_sat_per_kwu() as f64 / ((1000 / WITNESS_SCALE_FACTOR) as f64) + }}; +} - /// Returns a map of canonical transactions keyed by txid. - /// - /// This is used internally to help generate [`WalletEvent`]s. - fn map_transactions( - &self, - ) -> BTreeMap, ChainPosition)> { - self.transactions() - .map(|wtx| { - ( - wtx.tx_node.txid, - (wtx.tx_node.tx.clone(), wtx.chain_position), - ) - }) - .collect() - } +#[macro_export] +#[doc(hidden)] +/// Macro for getting a [`Wallet`] for use in a doctest. +macro_rules! doctest_wallet { + () => {{ + use $crate::bitcoin::{transaction, absolute, Amount, BlockHash, Transaction, TxOut, Network, hashes::Hash}; + use $crate::chain::{ConfirmationBlockTime, BlockId, TxGraph, tx_graph}; + use $crate::{Update, KeychainKind, KeyRing, Wallet}; + use $crate::test_utils::*; + let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)"; + let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)"; + + let mut keyring = KeyRing::new(Network::Regtest, KeychainKind::External, descriptor) + .expect("valid descriptor"); + keyring.add_descriptor(KeychainKind::Internal, change_descriptor) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); + let address = wallet.peek_address(KeychainKind::External, 0).address; + let tx = Transaction { + version: transaction::Version::TWO, + lock_time: absolute::LockTime::ZERO, + input: vec![], + output: vec![TxOut { + value: Amount::from_sat(500_000), + script_pubkey: address.script_pubkey(), + }], + }; + let txid = tx.compute_txid(); + let block_id = BlockId { height: 500, hash: BlockHash::all_zeros() }; + insert_checkpoint(&mut wallet, block_id); + insert_checkpoint(&mut wallet, BlockId { height: 1_000, hash: BlockHash::all_zeros() }); + insert_tx(&mut wallet, tx); + let anchor = ConfirmationBlockTime { + confirmation_time: 50_000, + block_id, + }; + insert_anchor(&mut wallet, txid, anchor); + wallet + }} } -/// Methods to construct sync/full-scan requests for spk-based chain sources. -impl Wallet { - /// Create a partial [`SyncRequest`] for all revealed spks at `start_time`. +/// Methods that are specific to the standard two-keychain wallet. +/// +/// These rely on `KeychainKind`'s fixed external/internal split — transaction building picks +/// a change keychain, and PSBT construction maps outputs back to a known keychain. A wallet +/// generic over `K` has no canonical "change" keychain, so these stay here. +impl Wallet { + /// Build [`Wallet`] by loading from persistence or [`ChangeSet`]. /// - /// The `start_time` is used to record the time that a mempool transaction was last seen - /// (or evicted). See [`Wallet::start_sync_with_revealed_spks`] for more. - pub fn start_sync_with_revealed_spks_at( - &self, - start_time: u64, - ) -> SyncRequestBuilder<(KeychainKind, u32)> { - use bdk_chain::keychain_txout::SyncRequestBuilderExt; - SyncRequest::builder_at(start_time) - .chain_tip(self.chain.tip()) - .revealed_spks_from_indexer(&self.tx_graph.index, ..) - .expected_spk_txids(self.tx_graph.list_expected_spk_txids( - &self.chain, - self.chain.tip().block_id(), - .., - )) - } - - /// Create a partial [`SyncRequest`] for this wallet for all revealed spks. + /// Note that descriptor secret keys are not persisted. The wallet does not hold key + /// material: keep your own [`KeyMap`](miniscript::descriptor::KeyMap) and sign with + /// [`bitcoin::Psbt::sign`], or build a + /// [`SignersContainer`](crate::signer::SignersContainer) and pass it to + /// [`Wallet::sign_with_signers`]. You can check the wallet's descriptors are what you expect + /// with [`LoadParams::descriptor`]. /// - /// This is the first step when performing a spk-based wallet partial sync, the returned - /// [`SyncRequest`] collects all revealed script pubkeys from the wallet keychain needed to - /// start a blockchain sync with a spk based blockchain client. + /// # Synopsis /// - /// The time of the sync is the current system time and is used to record the - /// tx last-seen for mempool transactions. Or if an expected transaction is missing - /// or evicted, it is the time of the eviction. Note that timestamps may only increase - /// to be counted by the tx graph. To supply your own start time see - /// [`Wallet::start_sync_with_revealed_spks_at`]. - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - #[cfg(feature = "std")] - pub fn start_sync_with_revealed_spks(&self) -> SyncRequestBuilder<(KeychainKind, u32)> { - use bdk_chain::keychain_txout::SyncRequestBuilderExt; - SyncRequest::builder() - .chain_tip(self.chain.tip()) - .revealed_spks_from_indexer(&self.tx_graph.index, ..) - .expected_spk_txids(self.tx_graph.list_expected_spk_txids( - &self.chain, - self.chain.tip().block_id(), - .., - )) + /// ```rust,no_run + /// # use bdk_wallet::{Wallet, ChangeSet, KeychainKind}; + /// # use bitcoin::{BlockHash, Network, hashes::Hash}; + /// # fn main() -> anyhow::Result<()> { + /// # const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; + /// # const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; + /// # let changeset = ChangeSet::default(); + /// // Load a wallet from changeset (no persistence). + /// let wallet = Wallet::load() + /// .load_wallet_no_persist(changeset)? + /// .expect("must have data to load wallet"); + /// + /// // Load a wallet that is persisted to SQLite database. + /// # let temp_dir = tempfile::tempdir().expect("must create tempdir"); + /// # let file_path = temp_dir.path().join("store.db"); + /// # let genesis_hash = BlockHash::all_zeros(); + /// let mut conn = bdk_wallet::rusqlite::Connection::open(file_path)?; + /// let mut wallet = Wallet::load() + /// // check loaded descriptors match these values + /// .descriptor(KeychainKind::External, Some(EXTERNAL_DESC)) + /// .descriptor(KeychainKind::Internal, Some(INTERNAL_DESC)) + /// // ensure loaded wallet's genesis hash matches this value + /// .check_genesis_hash(genesis_hash) + /// // set a lookahead for our indexer + /// .lookahead(101) + /// .load_wallet(&mut conn)? + /// .expect("must have data to load wallet"); + /// # Ok(()) + /// # } + /// ``` + /// + /// A wallet generic over `K` has no inferable keychain type here; load one with + /// [`LoadParams::default`] and [`Wallet::load_with_params`] instead. + pub fn load() -> LoadParams { + LoadParams::new() } - /// Create a [`FullScanRequest] for this wallet. + /// Start building a transaction. /// - /// This is the first step when performing a spk-based wallet full scan, the returned - /// [`FullScanRequest] collects iterators for the wallet's keychain script pub keys needed to - /// start a blockchain full scan with a spk based blockchain client. + /// This returns a blank [`TxBuilder`] from which you can specify the parameters for the + /// transaction. /// - /// This operation is generally only used when importing or restoring a previously used wallet - /// in which the list of used scripts is not known. + /// ## Example /// - /// The time of the scan is the current system time and is used to record the tx last-seen for - /// mempool transactions. To supply your own start time see [`Wallet::start_full_scan_at`]. - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - #[cfg(feature = "std")] - pub fn start_full_scan(&self) -> FullScanRequestBuilder { - use bdk_chain::keychain_txout::FullScanRequestBuilderExt; - FullScanRequest::builder() - .chain_tip(self.chain.tip()) - .spks_from_indexer(&self.tx_graph.index) - } - - /// Create a [`FullScanRequest`] builder at `start_time`. - pub fn start_full_scan_at(&self, start_time: u64) -> FullScanRequestBuilder { - use bdk_chain::keychain_txout::FullScanRequestBuilderExt; - FullScanRequest::builder_at(start_time) - .chain_tip(self.chain.tip()) - .spks_from_indexer(&self.tx_graph.index) - } -} - -/// Maps a chain position to tx confirmation status, if `pos` is the confirmed -/// variant. -/// -/// - Returns None if the confirmation height or time is not a valid absolute [`Height`] or -/// [`Time`]. -/// -/// [`Height`]: bitcoin::absolute::Height -/// [`Time`]: bitcoin::absolute::Time -#[cfg(all(bdk_wallet_unstable, feature = "bdk-tx"))] -fn status_from_position(pos: ChainPosition) -> Option { - if let ChainPosition::Confirmed { anchor, .. } = pos { - let conf_height = anchor.confirmation_height_upper_bound(); - let height = absolute::Height::from_consensus(conf_height).ok()?; - // TODO: Currently BDK has no notion of MTP, we can use the confirmation block time for now. - let time = - absolute::Time::from_consensus(anchor.confirmation_time.try_into().ok()?).ok()?; - Some(ConfirmationStatus { - height, - prev_mtp: Some(time), - }) - } else { - None - } -} - -#[cfg(all(bdk_wallet_unstable, feature = "bdk-tx"))] -impl Wallet { - /// Return the "keys" assets, i.e. the ones we can trivially infer by scanning - /// the pubkeys of the wallet's descriptors. - fn assets(&self) -> Assets { - let mut pks = vec![]; - for (_, desc) in self.keychains() { - desc.for_each_key(|k| { - pks.extend(k.clone().into_single_keys()); - true - }); + /// ``` + /// # use std::str::FromStr; + /// # use bitcoin::*; + /// # use bdk_wallet::*; + /// # use bdk_wallet::ChangeSet; + /// # use bdk_wallet::error::CreateTxError; + /// # use bdk_wallet::descriptor::IntoWalletDescriptor; + /// # use bdk_wallet::signer::SignersContainer; + /// # use anyhow::Error; + /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; + /// # let mut wallet = doctest_wallet!(); + /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); + /// let psbt = { + /// let mut builder = wallet.build_tx(); + /// builder + /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)); + /// builder.finish()? + /// }; + /// + /// // sign and broadcast ... + /// # Ok::<(), anyhow::Error>(()) + /// ``` + /// + /// [`TxBuilder`]: crate::TxBuilder + pub fn build_tx(&mut self) -> TxBuilder<'_, DefaultCoinSelectionAlgorithm> { + TxBuilder { + wallet: self, + params: TxParams::default(), + coin_selection: DefaultCoinSelectionAlgorithm::default(), } - - Assets::new().add(pks) } - /// Peek at the next change address without revealing it, returning the auto-derived - /// change info `(keychain, index, spk)` alongside the [`ChangeScript`]. - /// - /// The next change address is the next unused address of the change keychain, or the - /// next-to-be-revealed address **without** mutating wallet state. Revelation is deferred - /// until after all error paths have been cleared by the caller. - fn peek_change_info(&self) -> ((KeychainKind, u32, ScriptBuf), ChangeScript) { - let change_keychain = self.map_keychain(KeychainKind::Internal); - let (index, spk) = self - .tx_graph - .index - .unused_keychain_spks(change_keychain) - .next() - .unwrap_or_else(|| { - let (next_index, _) = self - .tx_graph - .index - .next_index(change_keychain) - .expect("keychain must exist"); - let spk = self - .peek_address(change_keychain, next_index) - .script_pubkey(); - (next_index, spk) - }); - let descriptor = self - .public_descriptor(change_keychain) - .at_derivation_index(index) - .expect("should be valid derivation index"); - ( - (change_keychain, index, spk), - ChangeScript::from_descriptor(descriptor), - ) - } + pub(crate) fn create_tx( + &mut self, + coin_selection: Cs, + params: TxParams, + rng: &mut impl RngCore, + ) -> Result { + // The spending condition may be supplied by the caller via `TxBuilder::set_condition`. + // Otherwise we derive it from the descriptors themselves. Deriving needs no key material: + // signers only influence a policy's `contribution`/`satisfaction`, which `get_condition` + // ignores, so an empty container is sufficient. If a descriptor offers several ways to be + // satisfied, `get_condition` cannot choose between them and the caller must say which one + // it intends via `set_condition`. + let requirements = match params.condition { + Some(condition) => condition, + None => { + let keychains: BTreeMap<_, _> = self.tx_graph.index.keychains().collect(); + let no_signers = SignersContainer::default(); + let no_path = BTreeMap::new(); - /// Parses the common parameters used during PSBT creation and returns the spend assets - /// and a map of indexed tx outputs. - fn parse_params( - &self, - params: &PsbtParams, - ) -> (Assets, HashMap>) { - // Get spend assets. - let assets = match params.assets { - None => self.assets(), - Some(ref params_assets) => { - let mut assets = Assets::new(); - assets.extend(params_assets); - // Fill in the "keys" assets if none are provided. - if assets.keys.is_empty() { - assets.extend(&self.assets()); + let mut requirements = Condition::default(); + for (keychain, skip) in [ + ( + KeychainKind::External, + tx_builder::ChangeSpendPolicy::OnlyChange, + ), + ( + KeychainKind::Internal, + tx_builder::ChangeSpendPolicy::ChangeForbidden, + ), + ] { + if params.change_policy == skip { + continue; + } + let Some(descriptor) = keychains.get(&keychain) else { + continue; + }; + let Some(policy) = descriptor.extract_policy( + &no_signers, + BuildSatisfaction::None, + &self.secp, + )? + else { + continue; + }; + // A policy that cannot be resolved without an explicit path needs the caller + // to pick one. + let condition = policy + .get_condition(&no_path) + .map_err(|_| CreateTxError::SpendingPolicyRequired(keychain))?; + requirements = requirements.merge(&condition)?; } - assets + requirements } }; - // Get wallet txouts. - let txouts = self - .list_indexed_txouts(params.canonical_params.clone()) - .map(|(_, txo)| (txo.outpoint, txo)) - .collect(); + let version = match params.version { + Some(transaction::Version(0)) => return Err(CreateTxError::Version0), + Some(transaction::Version::ONE) if requirements.csv.is_some() => { + return Err(CreateTxError::Version1Csv); + } + Some(v) => v, + None => transaction::Version::TWO, + }; - (assets, txouts) - } + // We use a match here instead of a unwrap_or_else as it's way more readable :) + let current_height = match params.current_height { + // If they didn't tell us the current height, we assume it's the latest sync height. + None => { + let tip_height = self.chain.tip().height(); + absolute::LockTime::from_height(tip_height).expect("invalid height") + } + Some(h) => h, + }; - /// Filters wallet `txos` by the spending criteria. - /// - /// - `policy`: Closure indicating whether the output should be kept, used by some callers to - /// apply additional filters as in the case of RBF. - fn filter_spendable<'a, I, C, F>( - &'a self, - txos: I, - params: &'a PsbtParams, - policy: F, - ) -> impl Iterator> + 'a - where - I: IntoIterator> + 'a, - F: Fn(&FullTxOut) -> bool + 'a, - { - let current_height = params.maturity_height.unwrap_or(self.chain.tip().height()); - txos.into_iter().filter(move |txo| { - // Exclude outputs that are manually selected. - if params.set.contains(&txo.outpoint) { - return false; + let lock_time = match params.locktime { + // When no `nLockTime` is specified, we try to prevent fee sniping, if possible. + None => { + // Fee sniping can be partially prevented by setting the timelock + // to current_height. If we don't know the current_height, + // we default to 0. + let fee_sniping_height = current_height; + + // We choose the biggest between the required nlocktime and the fee sniping + // height. + match requirements.timelock { + // No requirement, just use the fee_sniping_height. + None => fee_sniping_height, + // There's a block-based requirement, but the value is lower than the + // fee_sniping_height. + Some(value @ absolute::LockTime::Blocks(_)) if value < fee_sniping_height => { + fee_sniping_height + } + // There's a time-based requirement or a block-based requirement greater + // than the fee_sniping_height use that value. + Some(value) => value, + } } - // Filter outputs according to `policy` fn. - if !policy(txo) { - return false; + // Specific nLockTime required and we have no constraints, so just set to that value. + Some(x) if requirements.timelock.is_none() => x, + // Specific nLockTime required and it's compatible with the constraints. + Some(x) + if requirements.timelock.unwrap().is_same_unit(x) + && x >= requirements.timelock.unwrap() => + { + x } - // Exclude locked UTXOs. - if self.is_outpoint_locked(txo.outpoint) { - return false; + // Invalid nLockTime required. + Some(x) => { + return Err(CreateTxError::LockTime { + requested: x, + required: requirements.timelock.unwrap(), + }); } - // Exclude immature outputs. - if !txo.is_mature(current_height) { - return false; + }; + + // nSequence value for inputs. + // When not explicitly specified, it defaults to 0xFFFFFFFD, meaning RBF signaling is + // enabled. + let n_sequence = match (params.sequence, requirements.csv) { + // Enable RBF by default. + (None, None) => Sequence::ENABLE_RBF_NO_LOCKTIME, + // None requested, use required. + (None, Some(csv)) => csv, + // Requested sequence is incompatible with requirements. + (Some(sequence), Some(csv)) if !check_nsequence_rbf(sequence, csv) => { + return Err(CreateTxError::RbfSequenceCsv { sequence, csv }); } - // Exclude spent outputs. - if txo.spent_by.is_some() { - return false; + // Use requested nSequence value. + (Some(sequence), _) => sequence, + }; + + let (fee_rate, mut fee_amount) = match params.fee_policy.unwrap_or_default() { + //FIXME: see https://github.com/bitcoindevkit/bdk/issues/256 + FeePolicy::FeeAmount(fee) => { + if let Some(previous_fee) = params.bumping_fee { + if fee < previous_fee.absolute { + return Err(CreateTxError::FeeTooLow { + required: previous_fee.absolute, + }); + } + } + (FeeRate::ZERO, fee) + } + FeePolicy::FeeRate(rate) => { + if let Some(previous_fee) = params.bumping_fee { + let required_feerate = FeeRate::from_sat_per_kwu( + previous_fee.rate.to_sat_per_kwu() + + FeeRate::BROADCAST_MIN.to_sat_per_kwu(), // +1 sat/vb + ); + if rate < required_feerate { + return Err(CreateTxError::FeeRateTooLow { + required: required_feerate, + }); + } + } + (rate, Amount::ZERO) + } + }; + + let mut tx = Transaction { + version, + lock_time, + input: vec![], + output: vec![], + }; + + if params.manually_selected_only && params.utxos.is_empty() { + return Err(CreateTxError::NoUtxosSelected); + } + + let mut outgoing = Amount::ZERO; + let recipients = params.recipients.iter().map(|(r, v)| (r, *v)); + + for (index, (script_pubkey, value)) in recipients.enumerate() { + if !params.allow_dust && value.is_dust(script_pubkey) && !script_pubkey.is_op_return() { + return Err(CreateTxError::OutputBelowDustLimit(index)); } - true - }) - } - /// Maps the recipients of the `params` to a collection of target [`Output`]s. - fn target_outputs(&self, params: &PsbtParams) -> Vec { - params - .recipients - .iter() - .cloned() - .map( - |(script, value)| match self.tx_graph.index.index_of_spk(script.clone()) { - Some(&(keychain, index)) => { - let descriptor = self - .public_descriptor(keychain) - .at_derivation_index(index) - .expect("should be valid derivation index"); - Output::with_descriptor(descriptor, value) - } - None => Output::with_script(script, value), - }, - ) - .collect() - } + let new_out = TxOut { + script_pubkey: script_pubkey.clone(), + value, + }; - /// Creates a PSBT with the given `params` and returns the updated [`Psbt`] and - /// [`Finalizer`]. - /// - /// This function uses the thread-local random number generator (RNG) to generate - /// randomness. To supply your own source of entropy see [`Wallet::create_psbt_with_rng`]. - /// - /// # Example - /// - /// ```rust,no_run - /// # use std::str::FromStr; - /// # use bitcoin::{Amount, Address, FeeRate, OutPoint}; - /// # use bdk_wallet::psbt::{PsbtParams, SelectionStrategy}; - /// # let mut wallet = bdk_wallet::doctest_wallet!(); - /// # let outpoint = OutPoint::null(); - /// # let address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5").unwrap().assume_checked(); - /// # let amount = Amount::ZERO; - /// let mut params = PsbtParams::default(); - /// params - /// .add_utxos(&[outpoint]) - /// .add_recipients([(address, amount)]) - /// .coin_selection(SelectionStrategy::SingleRandomDraw) - /// .fee_rate(FeeRate::BROADCAST_MIN); - /// - /// let (psbt, finalizer) = wallet.create_psbt(params)?; - /// # Ok::<_, anyhow::Error>(()) - /// ``` - /// - /// # Errors - /// - /// A [`CreatePsbtError`] will be thrown if any of the following occurs - /// - /// - A manually selected input is missing from the wallet, or could not be planned - /// - The input value is insufficient to fund the outputs - /// - Failure to complete coin selection - /// - Failure to create or update the PSBT. - /// - /// # Change address - /// - /// When no [`ChangeScript`] is supplied via [`PsbtParams`], the wallet automatically selects - /// the next unused internal address and reveals it so that incoming change is tracked on - /// the next sync. The change address will not be marked used, so calling this function - /// again before syncing will use the same change address. If you intend to build - /// multiple transactions without syncing between them, either provide the change script in - /// the [`PsbtParams`], or do [`Wallet::mark_used`] after each call to prevent reuse. - /// - /// **You must persist the change set staged as a result of this call.** - /// See [`Wallet::take_staged`]. - #[cfg(feature = "std")] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - pub fn create_psbt( - &mut self, - params: PsbtParams, - ) -> Result<(Psbt, Finalizer), CreatePsbtError> { - self.create_psbt_with_rng(params, &mut rand::thread_rng()) - } + tx.output.push(new_out); - /// Creates a PSBT with the given `params` and random number generator (RNG). - /// - /// Return the updated [`Psbt`] and [`Finalizer`]. - /// - /// ## Parameters: - /// - /// - `params`: [`PsbtParams`] - /// - `rng`: Source of entropy, may be used during coin selection and to sort inputs and outputs - /// by the [`TxOrdering`](crate::wallet::tx_builder::TxOrdering). - /// - /// See [`Wallet::create_psbt`] for notes on change address handling. - /// - /// **You must persist the change set staged as a result of this call.** - /// See [`Wallet::take_staged`]. - pub fn create_psbt_with_rng( - &mut self, - mut params: PsbtParams, - rng: &mut impl RngCore, - ) -> Result<(Psbt, Finalizer), CreatePsbtError> { - // Only permit no recipients if we're doing a sweep and an explicit change script is - // provided. - if params.recipients.is_empty() - && !(matches!(params.coin_selection, SelectionStrategy::All) - && params.change_script.is_some()) - { - return Err(CreatePsbtError::NoRecipients); + outgoing += value; } - let (change_info, change_script) = params - .change_script - .take() - .map(|change_script| (None, change_script)) - .unwrap_or_else(|| { - let (change_info, change_script) = self.peek_change_info(); - (Some(change_info), change_script) - }); - let (assets, txouts) = self.parse_params(¶ms); + fee_amount += fee_rate * tx.weight(); - let must_spend = self.build_must_spend_inputs(¶ms, &txouts, &assets)?; + let (required_utxos, optional_utxos) = { + // NOTE: manual selection overrides unspendable + let mut required: Vec = params.utxos.clone(); + let optional = self.filter_utxos(¶ms, current_height.to_consensus_u32(), version); - // Get input candidates - let mut may_spend: Vec = if params.manually_selected_only { - vec![] - } else { - self.filter_spendable(txouts.into_values(), ¶ms, |txo| { - (params.utxo_filter.0)(txo) - }) - .flat_map(|txo| self.plan_input(&txo, &assets)) - .collect() + // If `drain_wallet` is true, all UTxOs are required. + if params.drain_wallet { + required.extend(optional); + (required, vec![]) + } else { + (required, optional) + } }; - // Apply fallback sequence to coin-selection candidates without a CSV requirement. - if let Some(seq) = params.fallback_sequence { - for input in &mut may_spend { - if input.sequence().is_none() { - input.set_sequence(seq).map_err(CreatePsbtError::Sequence)?; + // Get drain script. + let mut drain_index = Option::<(KeychainKind, u32)>::None; + let drain_script = match params.drain_to { + Some(ref drain_recipient) => drain_recipient.clone(), + None => { + let change_keychain = self.map_keychain(KeychainKind::Internal); + let (index, spk) = self + .tx_graph + .index + .unused_keychain_spks(change_keychain) + .next() + .unwrap_or_else(|| { + let (next_index, _) = self + .tx_graph + .index + .next_index(change_keychain) + .expect("keychain must exist"); + let spk = self + .peek_address(change_keychain, next_index) + .script_pubkey(); + (next_index, spk) + }); + drain_index = Some((change_keychain, index)); + spk + } + }; + + let coin_selection = coin_selection + .coin_select( + required_utxos, + optional_utxos, + fee_rate, + outgoing + fee_amount, + &drain_script, + rng, + ) + .map_err(CreateTxError::CoinSelection)?; + + let excess = &coin_selection.excess; + tx.input = coin_selection + .selected + .iter() + .map(|u| bitcoin::TxIn { + previous_output: u.outpoint(), + script_sig: ScriptBuf::default(), + sequence: u.sequence().unwrap_or(n_sequence), + witness: Witness::new(), + }) + .collect(); + + if tx.output.is_empty() { + // Uh oh, our transaction has no outputs. + // We allow this when we have a `drain_to` address and either: + // - `drain_wallet` is enabled + // - there are UTXOs we must spend (this happens, for example, when + // sweeping specific UTXOs to a given address) + // Otherwise, we don't know who we should send the funds to, and how much + // we should send! + if params.drain_to.is_some() && (params.drain_wallet || !params.utxos.is_empty()) { + if let Excess::NoChange { + dust_threshold, + remaining_amount, + change_fee, + } = excess + { + return Err(CreateTxError::CoinSelection(InsufficientFunds { + needed: *dust_threshold, + available: remaining_amount + .checked_sub(*change_fee) + .unwrap_or_default(), + })); } + } else { + return Err(CreateTxError::NoRecipients); } } - let target_outputs = self.target_outputs(¶ms); - - let input_candidates = InputCandidates::new(must_spend, may_spend); - if input_candidates.inputs().next().is_none() { - let target_amount: Amount = target_outputs.iter().map(|output| output.value).sum(); - let err = bdk_coin_select::InsufficientFunds { - missing: target_amount.to_sat(), + // If there's change, create and add a change output. + if let Excess::Change { amount, .. } = excess { + // Create drain output. + let drain_output = TxOut { + value: *amount, + script_pubkey: drain_script, }; - return Err(CreatePsbtError::InsufficientFunds(err)); + + // TODO: We should pay attention when adding a new output: this might increase + // the length of the "number of vouts" parameter by 2 bytes, potentially making + // our feerate too low. + tx.output.push(drain_output); } - let mut selector = Selector::new( - &input_candidates, - SelectorParams::new(params.fee_rate, target_outputs, change_script), - ) - .map_err(CreatePsbtError::Selector)?; + // Sort inputs/outputs according to the chosen algorithm. + params.ordering.sort_tx_with_aux_rand(&mut tx, rng); - let (psbt, finalizer) = self.create_psbt_from_selector(&mut selector, ¶ms, rng)?; + let psbt = self.complete_transaction(tx, coin_selection.selected, params)?; - // Reveal the auto-selected change address. - if let Some((keychain, index, spk)) = change_info { - if psbt - .unsigned_tx - .output - .iter() - .any(|txo| txo.script_pubkey == spk) + // Recording changes to the change keychain. + if let (Excess::Change { .. }, Some((keychain, index))) = (excess, drain_index) { + if let Some((_, index_changeset)) = + self.tx_graph.index.reveal_to_target(keychain, index) { - if let Some((_, index_changeset)) = - self.tx_graph.index.reveal_to_target(keychain, index) - { - self.stage.merge(index_changeset.into()); - } + self.stage.merge(index_changeset.into()); + self.mark_used(keychain, index); } } - Ok((psbt, finalizer)) + Ok(psbt) } - /// Create the PSBT from [`Selector`] and `params`. + /// Bump the fee of a transaction previously created with this wallet. /// - /// Internal method for handling coin selection and building the - /// resulting PSBT. - fn create_psbt_from_selector( - &self, - selector: &mut Selector, - params: &PsbtParams, - rng: &mut impl RngCore, - ) -> Result<(Psbt, Finalizer), CreatePsbtError> { - // Select coins - match params.coin_selection { - SelectionStrategy::All => selector.select_all(), - SelectionStrategy::Custom { ref algorithm } => selector - .select_with_algorithm(|s| algorithm(s)) - .map_err(CreatePsbtError::Selector)?, - SelectionStrategy::LowestFee { - longterm_feerate, - max_rounds, - } => { - selector - .select_with_algorithm(selection_algorithm_lowest_fee_bnb( - longterm_feerate, - max_rounds, - )) - .map_err(CreatePsbtError::Bnb)?; - } - SelectionStrategy::SingleRandomDraw => { - // Implement a shuffle algorithm by associating every candidate with - // a random sort key. - selector.select_with_algorithm(|selector| -> Result<_, CreatePsbtError> { - let n = selector.inner().candidates().count(); - let keys: Vec = (0..n).map(|_| rng.next_u32()).collect(); - selector - .inner_mut() - .sort_candidates_by(|(a, _), (b, _)| keys[a].cmp(&keys[b])); - selector - .select_until_target_met() - .map_err(CreatePsbtError::InsufficientFunds) - })? - } - }; - let mut selection = selector.try_finalize().ok_or({ - let e = bdk_tx::CannotMeetTarget; - CreatePsbtError::Selector(bdk_tx::SelectorError::CannotMeetTarget(e)) - })?; + /// Returns an error if the transaction is already confirmed or doesn't explicitly signal + /// *replace by fee* (RBF). If the transaction can be fee bumped then it returns a [`TxBuilder`] + /// pre-populated with the inputs and outputs of the original transaction. + /// + /// ## Example + /// + /// ```no_run + /// # // TODO: remove norun -- bumping fee seems to need the tx in the wallet database first. + /// # use std::str::FromStr; + /// # use bitcoin::*; + /// # use bdk_wallet::*; + /// # use bdk_wallet::ChangeSet; + /// # use bdk_wallet::error::CreateTxError; + /// # use bdk_wallet::descriptor::IntoWalletDescriptor; + /// # use bdk_wallet::signer::SignersContainer; + /// # use anyhow::Error; + /// # let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)"; + /// # let mut wallet = doctest_wallet!(); + /// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked(); + /// let mut psbt = { + /// let mut builder = wallet.build_tx(); + /// builder + /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)); + /// builder.finish()? + /// }; + /// // Keys are caller-owned: build a signer container from the signing descriptor. + /// let (signing_desc, keymap) = + /// descriptor.into_wallet_descriptor(wallet.secp_ctx(), wallet.network().into())?; + /// let signers = SignersContainer::build(keymap, &signing_desc, wallet.secp_ctx()); + /// let _ = wallet.sign_with_signers(&mut psbt, &[&signers], SignOptions::default())?; + /// let tx = psbt.clone().extract_tx().expect("tx"); + /// // broadcast tx but it's taking too long to confirm so we want to bump the fee + /// let mut psbt = { + /// let mut builder = wallet.build_fee_bump(tx.compute_txid())?; + /// builder + /// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate")); + /// builder.finish()? + /// }; + /// + /// let _ = wallet.sign_with_signers(&mut psbt, &[&signers], SignOptions::default())?; + /// let fee_bumped_tx = psbt.extract_tx(); + /// // broadcast fee_bumped_tx to replace original + /// # Ok::<(), anyhow::Error>(()) + /// ``` + // TODO: support for merging multiple transactions while bumping the fees + pub fn build_fee_bump( + &mut self, + txid: Txid, + ) -> Result, BuildFeeBumpError> { + let tx_graph = self.tx_graph.graph(); + let txout_index = &self.tx_graph.index; + let chain_tip = self.chain.tip().block_id(); + let chain_positions: HashMap> = tx_graph + .list_canonical_txs(&self.chain, chain_tip, CanonicalizationParams::default()) + .map(|canon_tx| (canon_tx.tx_node.txid, canon_tx.chain_position)) + .collect(); - // Change fell below the dust threshold and was dropped to fees, leaving - // the transaction with no outputs. - if selection.outputs().is_empty() { - return Err(CreatePsbtError::AllOutputsBelowDust); + let mut tx = tx_graph + .get_tx(txid) + .ok_or(BuildFeeBumpError::TransactionNotFound(txid))? + .as_ref() + .clone(); + + if chain_positions + .get(&txid) + .ok_or(BuildFeeBumpError::TransactionNotFound(txid))? + .is_confirmed() + { + return Err(BuildFeeBumpError::TransactionConfirmed(txid)); } - match ¶ms.ordering { - TxOrdering::Untouched => {} - TxOrdering::Shuffle => { - selection.shuffle_inputs(rng); - selection.shuffle_outputs(rng); - } - TxOrdering::Custom { - input_sort, - output_sort, - } => { - selection.sort_inputs_by(|a, b| input_sort(a, b)); - selection.sort_outputs_by(|a, b| output_sort(a, b)); - } + if !tx + .input + .iter() + .any(|txin| txin.sequence.to_consensus_u32() <= 0xFFFFFFFD) + { + return Err(BuildFeeBumpError::IrreplaceableTransaction( + tx.compute_txid(), + )); } - let version = params.version.unwrap_or(transaction::Version::TWO); - let min_locktime = params.min_locktime.unwrap_or(absolute::LockTime::ZERO); + let fee = self + .calculate_fee(&tx) + .map_err(|_| BuildFeeBumpError::FeeRateUnavailable)?; + let fee_rate = fee / tx.weight(); - // Create psbt - let mut psbt = selection - .create_psbt_with_rng( - bdk_tx::PsbtParams { - version, - min_locktime, - mandate_full_tx_for_segwit_v0: !params.only_witness_utxo, - anti_fee_sniping: params.anti_fee_sniping, - }, - rng, - ) - .map_err(CreatePsbtError::Psbt)?; + // Remove the inputs from the tx and process them. + let utxos: Vec = tx + .input + .drain(..) + .map(|txin| -> Result<_, BuildFeeBumpError> { + let outpoint = txin.previous_output; + let prev_txout = tx_graph + .get_txout(outpoint) + .cloned() + .ok_or(BuildFeeBumpError::UnknownUtxo(outpoint))?; + match txout_index.index_of_spk(prev_txout.script_pubkey.clone()) { + Some(&(keychain, derivation_index)) => { + let txout = prev_txout; + let chain_position = chain_positions + .get(&outpoint.txid) + .cloned() + .ok_or(BuildFeeBumpError::TransactionNotFound(outpoint.txid))?; + Ok(WeightedUtxo { + satisfaction_weight: self + .public_descriptor(keychain) + .max_weight_to_satisfy() + .expect("descriptor should be satisfiable"), + utxo: Utxo::Local(LocalOutput { + outpoint, + txout, + keychain, + is_spent: true, + derivation_index, + chain_position, + }), + }) + } + None => Ok(WeightedUtxo { + satisfaction_weight: Weight::from_wu_usize( + serialize(&txin.script_sig).len() * 4 + serialize(&txin.witness).len(), + ), + utxo: Utxo::Foreign { + outpoint, + sequence: txin.sequence, + psbt_input: Box::new(psbt::Input { + witness_utxo: prev_txout + .script_pubkey + .witness_version() + .map(|_| prev_txout), + non_witness_utxo: tx_graph + .get_tx(outpoint.txid) + .map(|tx| tx.as_ref().clone()), + ..Default::default() + }), + }, + }), + } + }) + .collect::>()?; - // Add global xpubs. - if params.add_global_xpubs { - for xpub in self - .keychains() - .flat_map(|(_, desc)| desc.get_extended_keys()) - { - let origin = match xpub.origin { - Some(origin) => origin, - None if xpub.xkey.depth == 0 => { - (xpub.root_fingerprint(&self.secp), vec![].into()) + if tx.output.len() > 1 { + let mut change_index = None; + for (index, txout) in tx.output.iter().enumerate() { + let change_keychain = self.map_keychain(KeychainKind::Internal); + match txout_index.index_of_spk(txout.script_pubkey.clone()) { + Some((keychain, _)) if *keychain == change_keychain => { + change_index = Some(index) } - _ => return Err(CreatePsbtError::MissingKeyOrigin(xpub.xkey)), - }; + _ => {} + } + } - psbt.xpub.insert(xpub.xkey, origin); + if let Some(change_index) = change_index { + tx.output.remove(change_index); } } - let finalizer = selection.into_finalizer(); + let params = TxParams { + version: Some(tx.version), + recipients: tx + .output + .into_iter() + .map(|txout| (txout.script_pubkey, txout.value)) + .collect(), + utxos, + bumping_fee: Some(tx_builder::PreviousFee { + absolute: fee, + rate: fee_rate, + }), + ..Default::default() + }; - Ok((psbt, finalizer)) + Ok(TxBuilder { + wallet: self, + params, + coin_selection: DefaultCoinSelectionAlgorithm::default(), + }) } - /// Creates a Replace-By-Fee transaction (RBF) and returns the updated [`Psbt`] and - /// [`Finalizer`]. + /// Sign a transaction with the provided signer containers. /// - /// This function uses the thread-local random number generator (RNG) to generate - /// randomness. To supply your own source of entropy see [`Wallet::replace_by_fee_with_rng`]. + /// Signer containers are processed in the order provided. Signers inside each container are + /// processed according to their [`SignerOrdering`](crate::signer::SignerOrdering). /// - /// # Errors + /// The [`SignOptions`] can be used to tweak the behavior of the software signers, and the way + /// the transaction is finalized at the end. Note that it can't be guaranteed that *every* + /// signer will follow the options, but the "software signers" (WIF keys and `xprv`) defined + /// in this library will. /// - /// A [`ReplaceByFeeError`] will be thrown if any of the following occurs + /// Returns true if the PSBT was finalized, or false otherwise. /// - /// - An original transaction is already confirmed - /// - An original transaction is missing from the wallet - /// - Failure to calculate the [fee](Wallet::calculate_fee) of an original transaction - /// - Failure to complete coin selection - /// - Failure to create or update the PSBT. + /// ## Example /// - /// # Change address + /// ``` + /// # use bdk_wallet::*; + /// # use bdk_wallet::bitcoin::*; + /// # use bdk_wallet::bitcoin::{NetworkKind, secp256k1::Secp256k1}; + /// # use bdk_wallet::descriptor::IntoWalletDescriptor; + /// # use bdk_wallet::signer::SignersContainer; + /// # let mut wallet = doctest_wallet!(); + /// let signer_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)"; + /// let secp = Secp256k1::new(); + /// let (_, keymap) = signer_descriptor + /// .into_wallet_descriptor(&secp, NetworkKind::Test) + /// .unwrap(); + /// let external_signers = SignersContainer::build( + /// keymap, + /// wallet.public_descriptor(KeychainKind::External), + /// wallet.secp_ctx(), + /// ); /// - /// When no [`ChangeScript`] is supplied via [`PsbtParams`], the wallet automatically selects - /// the next unused internal address and reveals it so that incoming change is tracked on - /// the next sync. The change address will not be marked used, so calling this function - /// again before syncing will use the same change address. If you intend to build - /// multiple transactions without syncing between them, either provide the change script in - /// the [`PsbtParams`], or do [`Wallet::mark_used`] after each call to prevent reuse. + /// let to_address = wallet.next_unused_address(KeychainKind::External).address; + /// let mut psbt = { + /// let mut builder = wallet.build_tx(); + /// builder.drain_to(to_address.script_pubkey()).drain_wallet(); + /// builder.finish()? + /// }; /// - /// **You must persist the change set staged as a result of this call.** - /// See [`Wallet::take_staged`]. - #[cfg(feature = "std")] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - pub fn replace_by_fee( - &mut self, - params: PsbtParams, - ) -> Result<(Psbt, Finalizer), ReplaceByFeeError> { - self.replace_by_fee_with_rng(params, &mut rand::thread_rng()) - } + /// let finalized = wallet.sign_with_signers( + /// &mut psbt, + /// &[&external_signers], + /// SignOptions::default(), + /// )?; + /// assert!(finalized); + /// # Ok::<(), anyhow::Error>(()) + /// ``` + pub fn sign_with_signers( + &self, + psbt: &mut Psbt, + signers: &[&SignersContainer], + sign_options: SignOptions, + ) -> Result { + // This adds all the PSBT metadata for the inputs, which will help us later figure out how + // to derive our keys. + self.update_psbt_with_descriptor(psbt) + .map_err(SignerError::MiniscriptPsbt)?; - /// Creates a Replace-By-Fee transaction (RBF) and returns the updated [`Psbt`] and - /// [`Finalizer`]. - /// - /// ## Parameters: - /// - /// - `params`: [`PsbtParams`] - /// - `rng`: Source of entropy, may be used during coin selection and to sort inputs and outputs - /// by the [`TxOrdering`](crate::wallet::tx_builder::TxOrdering). - /// - /// See [`Wallet::replace_by_fee`] for notes on change address handling. - /// - /// **You must persist the change set staged as a result of this call.** - /// See [`Wallet::take_staged`]. - pub fn replace_by_fee_with_rng( - &mut self, - mut params: PsbtParams, - rng: &mut impl RngCore, - ) -> Result<(Psbt, Finalizer), ReplaceByFeeError> { - if params.replace.is_empty() { - return Err(ReplaceByFeeError::NoOriginalTransactions); - } - // Only permit no recipients if we're doing a sweep and an explicit change script is - // provided. - if params.recipients.is_empty() - && !(matches!(params.coin_selection, SelectionStrategy::All) - && params.change_script.is_some()) + // If we aren't allowed to use `witness_utxo`, ensure that every input (except p2tr and + // finalized ones) has the `non_witness_utxo`. + if !sign_options.trust_witness_utxo + && psbt + .inputs + .iter() + .filter(|i| i.final_script_witness.is_none() && i.final_script_sig.is_none()) + .filter(|i| i.tap_internal_key.is_none() && i.tap_merkle_root.is_none()) + .any(|i| i.non_witness_utxo.is_none()) { - return Err(ReplaceByFeeError::CreatePsbt(CreatePsbtError::NoRecipients)); - } - let (change_info, change_script) = params - .change_script - .take() - .map(|change_script| (None, change_script)) - .unwrap_or_else(|| { - let (change_info, change_script) = self.peek_change_info(); - (Some(change_info), change_script) - }); - - let (assets, txouts) = self.parse_params(¶ms); - - let PsbtParams { - replace: txids_to_replace, - .. - } = ¶ms; - - // None of the txids-to-replace may already be confirmed - let chain_tip = self.chain.tip().block_id(); - let chain_positions: HashMap> = self - .tx_graph - .graph() - .list_canonical_txs(&self.chain, chain_tip, params.canonical_params.clone()) - .map(|canonical_tx| (canonical_tx.tx_node.txid, canonical_tx.chain_position)) - .collect(); - for &txid in txids_to_replace.iter() { - if chain_positions - .get(&txid) - .is_some_and(|chain_position| chain_position.is_confirmed()) - { - return Err(ReplaceByFeeError::TransactionConfirmed(txid)); - } - } - - // For each txid being replaced, verify that at least one of its original inputs - // remains in the selected set. A replacement must conflict with every transaction it - // replaces — two transactions cannot spend the same UTXO. - for &txid in txids_to_replace.iter() { - if let Some(tx) = self.tx_graph.graph().get_tx(txid) { - if !tx - .input - .iter() - .any(|txin| params.set.contains(&txin.previous_output)) - { - return Err(ReplaceByFeeError::NoInputsFromOriginal(txid)); - } - } - } - - // Txs and their descendants to be replaced - // - // `direct_conflicts` are the transactions named in `params.replace`. Only these - // feed into `original_txs` for the RBF fee rate floor. - // - // `to_replace` also includes walked descendants so they are excluded from coin - // selection; their fees accumulate into `descendant_fee`. - let direct_conflicts: HashSet = txids_to_replace.iter().copied().collect(); - - let descendants: HashSet = direct_conflicts - .iter() - .flat_map(|&txid| { - self.tx_graph - .graph() - .walk_descendants(txid, |_, txid| Some(txid)) - }) - .filter(|txid| !direct_conflicts.contains(txid)) - .collect(); - - let to_replace: HashSet = direct_conflicts - .iter() - .chain(descendants.iter()) - .copied() - .collect(); - - let must_spend = self.build_must_spend_inputs(¶ms, &txouts, &assets)?; - - // Validate that no manually-selected input spends an output of a transaction - // in `to_replace`. - for input in &must_spend { - let op = input.prev_outpoint(); - if to_replace.contains(&op.txid) { - return Err(ReplaceByFeeError::ConflictingInput(op)); - } + return Err(SignerError::MissingNonWitnessUtxo); } - // Get input candidates - let mut may_spend: Vec = if params.manually_selected_only { - vec![] - } else { - self.filter_spendable(txouts.into_values(), ¶ms, |txo| { - // To be included for coin selection the UTXO - // - must not be contained in `to_replace` - // - must be confirmed per replacement policy Rule 2 (removed in Core v31) - // - must pass a user-defined filter - !to_replace.contains(&txo.outpoint.txid) - && txo.chain_position.is_confirmed() - && (params.utxo_filter.0)(txo) - }) - .flat_map(|txo| self.plan_input(&txo, &assets)) - .collect() - }; - - // Apply fallback sequence to coin-selection candidates without a CSV requirement. - if let Some(seq) = params.fallback_sequence { - for input in &mut may_spend { - if input.sequence().is_none() { - input.set_sequence(seq).map_err(CreatePsbtError::Sequence)?; - } - } + // If the user hasn't explicitly opted-in, refuse to sign the transaction unless every input + // is using `SIGHASH_ALL` or `SIGHASH_DEFAULT` for Taproot. + if !sign_options.allow_all_sighashes + && !psbt.inputs.iter().all(|i| { + i.sighash_type.is_none() + || i.sighash_type == Some(EcdsaSighashType::All.into()) + || i.sighash_type == Some(TapSighashType::All.into()) + || i.sighash_type == Some(TapSighashType::Default.into()) + }) + { + return Err(SignerError::NonStandardSighash); } - let target_outputs = self.target_outputs(¶ms); + for signer in signers.iter().flat_map(|container| container.signers()) { + signer.sign_transaction(psbt, &sign_options, &self.secp)?; + } - let input_candidates = InputCandidates::new(must_spend, may_spend); - if input_candidates.inputs().next().is_none() { - let target_amount: Amount = target_outputs.iter().map(|output| output.value).sum(); - let err = bdk_coin_select::InsufficientFunds { - missing: target_amount.to_sat(), - }; - return Err(CreatePsbtError::InsufficientFunds(err))?; + // Attempt to finalize. + if sign_options.try_finalize { + self.finalize_psbt(psbt, sign_options) + } else { + Ok(false) } + } - let original_txs: Vec = direct_conflicts - .iter() - .map(|&txid| -> Result<_, ReplaceByFeeError> { - let tx = self - .tx_graph - .graph() - .get_tx(txid) - .ok_or(ReplaceByFeeError::MissingTransaction(txid))?; - let fee = self - .calculate_fee(&tx) - .map_err(ReplaceByFeeError::PreviousFee)?; - Ok(OriginalTxStats { - weight: tx.weight(), - fee, + /// Given the options returns the list of utxos that must be used to form the + /// transaction and any further that may be used if needed. + fn filter_utxos( + &self, + params: &TxParams, + current_height: u32, + version: Version, + ) -> Vec { + if params.manually_selected_only { + vec![] + // Only process optional UTxOs if manually_selected_only is false. + } else { + let manually_selected_outpoints = params + .utxos + .iter() + .map(|wutxo| wutxo.utxo.outpoint()) + .collect::>(); + + self.tx_graph + .graph() + // Get all unspent UTxOs from wallet. + // NOTE: the UTxOs returned by the following method already belong to wallet as the + // call chain uses get_tx_node infallibly. + .filter_chain_unspents( + &self.chain, + self.chain.tip().block_id(), + CanonicalizationParams::default(), + self.tx_graph.index.outpoints().iter().cloned(), + ) + // Filter out locked outpoints. + .filter(|(_, txo)| !self.is_outpoint_locked(txo.outpoint)) + // Only create LocalOutput if UTxO is mature. + .filter_map(move |((k, i), full_txo)| { + full_txo + .is_mature(current_height) + .then(|| new_local_utxo(k, i, full_txo)) }) - }) - .collect::>()?; + // Only add to optional UTXOs those that follows BIP-431 (TRUC) specification. + // see https://github.com/bitcoin/bips/blob/master/bip-0431.mediawiki#specification + .filter(|local_output| { + // If the output is confirmed, it can be spent by either non-TRUC/TRUC + // transactions. + if local_output.chain_position.is_confirmed() { + return true; + } - // Sum fees from all descendants known to the tx graph. This assumes every - // descendant is currently in the mempool, which could slightly overestimate - // the fee floor if a descendant was evicted or never relayed. - let descendant_fee: Amount = descendants - .iter() - .filter_map(|&txid| { - let tx = self.tx_graph.graph().get_tx(txid)?; - self.calculate_fee(&tx).ok() - }) - .sum(); + // If building TRUC (V3), the unconfirmed outputs MUST be TRUC (V3). Otherwise, + // if building a non-TRUC, the unconfirmed outputs MUST be non-TRUC. + self.tx_graph() + .get_tx(local_output.outpoint.txid) + .is_some_and(|tx| (tx.version == Version(3)) == (version == Version(3))) + }) + // only process UTXOs not selected manually, they will be considered later in the + // chain + // NOTE: this avoid UTXOs in both required and optional list + .filter(|may_spend| !manually_selected_outpoints.contains(&may_spend.outpoint)) + // only add to optional UTxOs those which satisfy the change policy if we reuse + // change + .filter(|local_output| { + self.keychains().count() == 1 + || params.change_policy.is_satisfied_by(local_output) + }) + // Only add to optional UTxOs those marked as spendable. + .filter(|local_output| !params.unspendable.contains(&local_output.outpoint)) + // If bumping fees only add to optional UTxOs those confirmed. + .filter(|local_output| { + params.bumping_fee.is_none() || local_output.chain_position.is_confirmed() + }) + .map(|utxo| WeightedUtxo { + satisfaction_weight: self + .public_descriptor(utxo.keychain) + .max_weight_to_satisfy() + .unwrap(), + utxo: Utxo::Local(utxo), + }) + .collect() + } + } - let rbf_params = RbfParams { - original_txs, - descendant_fee, - incremental_relay_feerate: FeeRate::BROADCAST_MIN, - }; + fn complete_transaction( + &self, + tx: Transaction, + selected: Vec, + params: TxParams, + ) -> Result { + let mut psbt = Psbt::from_unsigned_tx(tx)?; - let mut selector = Selector::new( - &input_candidates, - SelectorParams { - replace: Some(rbf_params), - ..SelectorParams::new(params.fee_rate, target_outputs, change_script) - }, - ) - .map_err(CreatePsbtError::Selector)?; + if params.add_global_xpubs { + let all_xpubs = self + .keychains() + .flat_map(|(_, desc)| desc.get_extended_keys()) + .collect::>(); - let (psbt, finalizer) = self - .create_psbt_from_selector(&mut selector, ¶ms, rng) - .map_err(ReplaceByFeeError::CreatePsbt)?; + for xpub in all_xpubs { + let origin = match xpub.origin { + Some(origin) => origin, + None if xpub.xkey.depth == 0 => { + (xpub.root_fingerprint(&self.secp), vec![].into()) + } + _ => return Err(CreateTxError::MissingKeyOrigin(xpub.xkey.to_string())), + }; - // Reveal the auto-selected change address - if let Some((keychain, index, spk)) = change_info { - if psbt - .unsigned_tx - .output - .iter() - .any(|txo| txo.script_pubkey == spk) - { - if let Some((_, index_changeset)) = - self.tx_graph.index.reveal_to_target(keychain, index) - { - self.stage.merge(index_changeset.into()); - } + psbt.xpub.insert(xpub.xkey, origin); } } - Ok((psbt, finalizer)) - } + let mut lookup_output = selected + .into_iter() + .map(|utxo| (utxo.outpoint(), utxo)) + .collect::>(); - /// Builds the required inputs from the manually-selected spends in `params`. - /// - /// Wallet outpoints are planned into [`Input`]s in insertion order, then any per-input - /// sequence override or the fallback sequence is applied. Pre-built planned inputs are kept - /// in that same insertion order. - fn build_must_spend_inputs( - &self, - params: &PsbtParams, - txouts: &HashMap>, - assets: &Assets, - ) -> Result, CreatePsbtError> { - params - .must_spend - .iter() - .map(|item| match item { - MustSpend::Utxo(outpoint) => { - let txo = txouts - .get(outpoint) - .ok_or(CreatePsbtError::UnknownUtxo(*outpoint))?; - let mut input = self - .plan_input(txo, assets) - .ok_or(CreatePsbtError::Plan(*outpoint))?; - if let Some(&seq) = params.sequence_overrides.get(outpoint) { - input.set_sequence(seq).map_err(CreatePsbtError::Sequence)?; - } else if let Some(seq) = params.fallback_sequence { - if input.sequence().is_none() { - input.set_sequence(seq).map_err(CreatePsbtError::Sequence)?; + // Add metadata for the inputs. + for (psbt_input, input) in psbt.inputs.iter_mut().zip(psbt.unsigned_tx.input.iter()) { + let utxo = match lookup_output.remove(&input.previous_output) { + Some(utxo) => utxo, + None => continue, + }; + + match utxo { + Utxo::Local(utxo) => { + *psbt_input = + match self.get_psbt_input(utxo, params.sighash, params.only_witness_utxo) { + Ok(psbt_input) => psbt_input, + Err(e) => match e { + CreateTxError::UnknownUtxo => psbt::Input { + sighash_type: params.sighash, + ..psbt::Input::default() + }, + _ => return Err(e), + }, } + } + Utxo::Foreign { + outpoint, + psbt_input: foreign_psbt_input, + .. + } => { + let is_taproot = foreign_psbt_input + .witness_utxo + .as_ref() + .map(|txout| txout.script_pubkey.is_p2tr()) + .unwrap_or(false); + if !is_taproot + && !params.only_witness_utxo + && foreign_psbt_input.non_witness_utxo.is_none() + { + return Err(CreateTxError::MissingNonWitnessUtxo(outpoint)); } - Ok(input) + *psbt_input = *foreign_psbt_input; } - MustSpend::Planned(input) => Ok(input.clone()), - }) - .collect() + } + } + + self.update_psbt_with_descriptor(&mut psbt)?; + + Ok(psbt) } - /// Plan the output with the available assets and return a new [`Input`] if possible. See also - /// [`Self::try_plan`]. - fn plan_input( + /// Get the corresponding PSBT Input for a [`LocalOutput`]. + pub fn get_psbt_input( &self, - txo: &FullTxOut, - spend_assets: &Assets, - ) -> Option { - let op = txo.outpoint; - let txid = op.txid; - - // We want to afford the output with as many assets as we can. The plan - // will use only the ones needed to produce the minimum satisfaction. - let cur_height = self.latest_checkpoint().height(); - let abs_locktime = spend_assets - .absolute_timelock - .unwrap_or(absolute::LockTime::from_consensus(cur_height)); + utxo: LocalOutput, + sighash_type: Option, + only_witness_utxo: bool, + ) -> Result { + // Try to find the prev_script in our db to figure out if this is internal or external, + // and the derivation index. + let &(keychain, child) = self + .tx_graph + .index + .index_of_spk(utxo.txout.script_pubkey) + .ok_or(CreateTxError::UnknownUtxo)?; - let rel_locktime = spend_assets.relative_timelock.unwrap_or_else(|| { - let age = match txo.chain_position.confirmation_height_upper_bound() { - Some(conf_height) => cur_height - .saturating_add(1) - .saturating_sub(conf_height) - .try_into() - .unwrap_or(u16::MAX), - None => 0, - }; - relative::LockTime::from_height(age) - }); + let mut psbt_input = psbt::Input { + sighash_type, + ..psbt::Input::default() + }; - let mut assets = Assets::new(); - assets.extend(spend_assets); - assets = assets.after(abs_locktime); - assets = assets.older(rel_locktime); + let desc = self.public_descriptor(keychain); + let derived_descriptor = desc + .at_derivation_index(child) + .expect("child can't be hardened"); - let plan = self.try_plan(op, &assets)?; - let tx = self.tx_graph.graph().get_tx(txid)?; - let tx_status = status_from_position(txo.chain_position); + psbt_input + .update_with_descriptor_unchecked(&derived_descriptor) + .map_err(MiniscriptPsbtError::Conversion)?; - Input::from_prev_tx(plan, tx, op.vout as usize, tx_status).ok() + let prev_output = utxo.outpoint; + if let Some(prev_tx) = self.tx_graph.graph().get_tx(prev_output.txid) { + // We want to check that the prevout actually exists in the transaction before + // continuing. + let prevout = prev_tx.output.get(prev_output.vout as usize).ok_or( + MiniscriptPsbtError::UtxoUpdate(miniscript::psbt::UtxoUpdateError::UtxoCheck), + )?; + if desc.is_witness() || desc.is_taproot() { + psbt_input.witness_utxo = Some(prevout.clone()); + } + if !desc.is_taproot() && (!desc.is_witness() || !only_witness_utxo) { + psbt_input.non_witness_utxo = Some(prev_tx.as_ref().clone()); + } + } + Ok(psbt_input) } - /// Attempt to create a spending plan for the UTXO of the given `outpoint` - /// with the provided `assets`. - /// - /// Return `None` if `outpoint` doesn't correspond to an indexed txout, or - /// if the assets are not sufficient to create a plan. - fn try_plan(&self, outpoint: OutPoint, assets: &Assets) -> Option { - let indexer = &self.tx_graph.index; - let ((keychain, index), _) = indexer.txout(outpoint)?; - let def_desc = indexer - .get_descriptor(keychain)? - .at_derivation_index(index) - .expect("must be valid derivation index"); - def_desc.plan(assets).ok() - } -} + fn update_psbt_with_descriptor(&self, psbt: &mut Psbt) -> Result<(), MiniscriptPsbtError> { + // We need to borrow `psbt` mutably within the loops, so we have to allocate a vec for all + // the input utxos and outputs. + let utxos = (0..psbt.inputs.len()) + .filter_map(|i| psbt.get_utxo_for(i).map(|utxo| (true, i, utxo))) + .chain( + psbt.unsigned_tx + .output + .iter() + .enumerate() + .map(|(i, out)| (false, i, out.clone())), + ) + .collect::>(); -impl AsRef> for Wallet { - fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph { - self.tx_graph.graph() - } -} + // Try to figure out the keychain and derivation for every input and output. + for (is_input, index, out) in utxos.into_iter() { + if let Some(&(keychain, child)) = self.tx_graph.index.index_of_spk(out.script_pubkey) { + let desc = self.public_descriptor(keychain); + let desc = desc + .at_derivation_index(child) + .expect("child can't be hardened"); -/// Generate a deterministic wallet name from the provided descriptors. -/// -/// The wallet name is the concatenation of the [checksum] of the external and (if provided) -/// internal public descriptors. If descriptors containing private keys are provided, the name -/// is computed from the corresponding public descriptors; the result is identical to calling -/// this function with the equivalent public (xpub) descriptors. -/// -/// # Errors -/// -/// If descriptor parsing fails or if checksum computation fails then a [`DescriptorError`] is -/// returned. -/// -/// [checksum]: crate::descriptor::checksum::calc_checksum -pub fn wallet_name_from_descriptor( - descriptor: T, - change_descriptor: Option, - network_kind: NetworkKind, - secp: &SecpCtx, -) -> Result -where - T: IntoWalletDescriptor, -{ - // Wallet name is defined by the checksums of the wallet's public descriptors. - let (descriptor, _keymap) = descriptor.into_wallet_descriptor(secp, network_kind)?; - let mut wallet_name = calc_checksum(&descriptor.to_string())?; + if is_input { + psbt.update_input_with_descriptor(index, &desc) + .map_err(MiniscriptPsbtError::UtxoUpdate)?; + } else { + psbt.update_output_with_descriptor(index, &desc) + .map_err(MiniscriptPsbtError::OutputUpdate)?; + } + } + } - if let Some(change_descriptor) = change_descriptor { - let (change_descriptor, _change_keymap) = - change_descriptor.into_wallet_descriptor(secp, network_kind)?; - wallet_name.push_str(&calc_checksum(&change_descriptor.to_string())?); + Ok(()) } - Ok(wallet_name) -} - -fn new_local_utxo( - keychain: KeychainKind, - derivation_index: u32, - full_txo: FullTxOut, -) -> LocalOutput { - LocalOutput { - outpoint: full_txo.outpoint, - txout: full_txo.txout, - is_spent: full_txo.spent_by.is_some(), - chain_position: full_txo.chain_position, - keychain, - derivation_index, + /// Used internally to ensure that all methods requiring a [`KeychainKind`] will use a + /// keychain with an associated descriptor. For example in case the wallet was created + /// with only one keychain, passing [`KeychainKind::Internal`] here will instead return + /// [`KeychainKind::External`]. + fn map_keychain(&self, keychain: KeychainKind) -> KeychainKind { + if self.keychains().count() == 1 { + KeychainKind::External + } else { + keychain + } } } -fn make_indexed_graph( - stage: &mut ChangeSet, - tx_graph_changeset: chain::tx_graph::ChangeSet, - indexer_changeset: chain::keychain_txout::ChangeSet, - descriptor: ExtendedDescriptor, - change_descriptor: Option, - lookahead: u32, - use_spk_cache: bool, -) -> Result>, DescriptorError> -{ - let (indexed_graph, changeset) = IndexedTxGraph::from_changeset( - chain::indexed_tx_graph::ChangeSet { - tx_graph: tx_graph_changeset, - indexer: indexer_changeset, - }, - |idx_cs| -> Result, DescriptorError> { - let mut idx = KeychainTxOutIndex::from_changeset(lookahead, use_spk_cache, idx_cs); - - let descriptor_inserted = idx - .insert_descriptor(KeychainKind::External, descriptor) - .expect("already checked to be a unique, wildcard, non-multipath descriptor"); - assert!( - descriptor_inserted, - "this must be the first time we are seeing this descriptor" - ); - - let change_descriptor = match change_descriptor { - Some(change_descriptor) => change_descriptor, - None => return Ok(idx), - }; - - let change_descriptor_inserted = idx - .insert_descriptor(KeychainKind::Internal, change_descriptor) - .map_err(|e| { - use bdk_chain::indexer::keychain_txout::InsertDescriptorError; - match e { - InsertDescriptorError::DescriptorAlreadyAssigned { .. } => { - crate::descriptor::error::Error::ExternalAndInternalAreTheSame - } - InsertDescriptorError::KeychainAlreadyAssigned { .. } => { - unreachable!("this is the first time we're assigning internal") - } - } - })?; - assert!( - change_descriptor_inserted, - "this must be the first time we are seeing this descriptor" - ); - - Ok(idx) - }, - )?; - stage.tx_graph.merge(changeset.tx_graph); - stage.indexer.merge(changeset.indexer); - Ok(indexed_graph) -} - -/// Transforms a [`FeeRate`] to `f64` with unit as sat/vb. -#[macro_export] -#[doc(hidden)] -macro_rules! floating_rate { - ($rate:expr) => {{ - use $crate::bitcoin::constants::WITNESS_SCALE_FACTOR; - // sat_kwu / 250.0 -> sat_vb - $rate.to_sat_per_kwu() as f64 / ((1000 / WITNESS_SCALE_FACTOR) as f64) - }}; -} - -#[macro_export] -#[doc(hidden)] -/// Macro for getting a [`Wallet`] for use in a doctest. -macro_rules! doctest_wallet { - () => {{ - use $crate::bitcoin::{transaction, absolute, Amount, BlockHash, Transaction, TxOut, Network, hashes::Hash}; - use $crate::chain::{ConfirmationBlockTime, BlockId, TxGraph, tx_graph}; - use $crate::{Update, KeychainKind, Wallet}; - use $crate::test_utils::*; - let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)"; - let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)"; - - let mut wallet = Wallet::create(descriptor, change_descriptor) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); - let address = wallet.peek_address(KeychainKind::External, 0).address; - let tx = Transaction { - version: transaction::Version::TWO, - lock_time: absolute::LockTime::ZERO, - input: vec![], - output: vec![TxOut { - value: Amount::from_sat(500_000), - script_pubkey: address.script_pubkey(), - }], - }; - let txid = tx.compute_txid(); - let block_id = BlockId { height: 500, hash: BlockHash::all_zeros() }; - insert_checkpoint(&mut wallet, block_id); - insert_checkpoint(&mut wallet, BlockId { height: 1_000, hash: BlockHash::all_zeros() }); - insert_tx(&mut wallet, tx); - let anchor = ConfirmationBlockTime { - confirmation_time: 50_000, - block_id, - }; - insert_anchor(&mut wallet, txid, anchor); - wallet - }} -} - #[cfg_attr(coverage_nightly, coverage(off))] #[cfg(test)] mod test { use super::*; - use crate::miniscript::Error::Unexpected; + use crate::KeyRing; use crate::test_utils::get_test_tr_single_sig_xprv_and_change_desc; use crate::test_utils::insert_tx; @@ -3745,10 +3618,12 @@ mod test { let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_and_change_desc(); // Create new wallet. - let mut wallet = Wallet::create(external_desc, internal_desc) - .network(Network::Testnet) - .create_wallet_no_persist() - .unwrap(); + let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, external_desc) + .expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, internal_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); let two_output_tx = Transaction { input: vec![], @@ -3802,71 +3677,6 @@ mod test { assert_eq!(expected, received); } - #[test] - fn test_create_two_path_wallet() { - let two_path_descriptor = "wpkh([9a6a2580/84'/1'/0']tpubDDnGNapGEY6AZAdQbfRJgMg9fvz8pUBrLwvyvUqEgcUfgzM6zc2eVK4vY9x9L5FJWdX8WumXuLEDV5zDZnTfbn87vLe9XceCFwTu9so9Kks/<0;1>/*)"; - - // Test successful creation of a two-path wallet - let params = Wallet::create_from_two_path_descriptor(two_path_descriptor); - let wallet = params.network(Network::Testnet).create_wallet_no_persist(); - assert!(wallet.is_ok()); - - let wallet = wallet.unwrap(); - - // Verify that the wallet has both external and internal keychains - let keychains: Vec<_> = wallet.keychains().collect(); - assert_eq!(keychains.len(), 2); - - // Verify that the descriptors are different (receive vs change) - let external_desc = keychains - .iter() - .find(|(k, _)| *k == KeychainKind::External) - .unwrap() - .1; - let internal_desc = keychains - .iter() - .find(|(k, _)| *k == KeychainKind::Internal) - .unwrap() - .1; - assert_ne!(external_desc.to_string(), internal_desc.to_string()); - - // Verify that addresses can be generated - let external_addr = wallet.peek_address(KeychainKind::External, 0); - let internal_addr = wallet.peek_address(KeychainKind::Internal, 0); - assert_ne!(external_addr.address, internal_addr.address); - } - - #[test] - fn test_create_two_path_wallet_invalid_descriptor() { - // Test with invalid single-path descriptor - let single_path_descriptor = "wpkh([9a6a2580/84'/1'/0']tpubDDnGNapGEY6AZAdQbfRJgMg9fvz8pUBrLwvyvUqEgcUfgzM6zc2eVK4vY9x9L5FJWdX8WumXuLEDV5zDZnTfbn87vLe9XceCFwTu9so9Kks/0/*)"; - let params = Wallet::create_from_two_path_descriptor(single_path_descriptor); - let wallet = params.network(Network::Testnet).create_wallet_no_persist(); - assert!(matches!(wallet, Err(DescriptorError::MultiPath))); - - // Test with a private descriptor - // You get a Miniscript(Unexpected("Can't make an extended private key with multiple paths - // into a public key.")) error. - let private_multipath_descriptor = "wpkh(tprv8ZgxMBicQKsPdWAHbugK2tjtVtRjKGixYVZUdL7xLHMgXZS6BFbFi1UDb1CHT25Z5PU1F9j7wGxwUiRhqz9E3nZRztikGUV6HoRDYcqPhM4/84'/1'/0'/<0;1>/*)"; - let params = Wallet::create_from_two_path_descriptor(private_multipath_descriptor); - let wallet = params.network(Network::Testnet).create_wallet_no_persist(); - assert!(matches!( - wallet, - Err(DescriptorError::Miniscript(Unexpected(..))) - )); - - // Test with invalid 3-path multipath descriptor - let three_path_descriptor = "wpkh([9a6a2580/84'/1'/0']tpubDDnGNapGEY6AZAdQbfRJgMg9fvz8pUBrLwvyvUqEgcUfgzM6zc2eVK4vY9x9L5FJWdX8WumXuLEDV5zDZnTfbn87vLe9XceCFwTu9so9Kks/<0;1;2>/*)"; - let params = Wallet::create_from_two_path_descriptor(three_path_descriptor); - let wallet = params.network(Network::Testnet).create_wallet_no_persist(); - assert!(matches!(wallet, Err(DescriptorError::MultiPath))); - - // Test with completely invalid descriptor - let invalid_descriptor = "invalid_descriptor"; - let params = Wallet::create_from_two_path_descriptor(invalid_descriptor); - let wallet = params.network(Network::Testnet).create_wallet_no_persist(); - assert!(wallet.is_err()); - } #[test] fn test_wallet_name_from_descriptor_public_key_check() { let secp = SecpCtx::new(); diff --git a/src/wallet/params.rs b/src/wallet/params.rs index e2f1c9a7..9a4fcfac 100644 --- a/src/wallet/params.rs +++ b/src/wallet/params.rs @@ -1,3 +1,4 @@ +use crate::collections::BTreeMap; use alloc::boxed::Box; use bdk_chain::keychain_txout::DEFAULT_LOOKAHEAD; @@ -5,8 +6,7 @@ use bitcoin::{BlockHash, Network, NetworkKind}; use miniscript::descriptor::KeyMap; use crate::{ - AsyncWalletPersister, CreateWithPersistError, KeychainKind, LoadWithPersistError, Wallet, - WalletPersister, + AsyncWalletPersister, CreateWithPersistError, LoadWithPersistError, Wallet, WalletPersister, descriptor::{DescriptorError, ExtendedDescriptor, IntoWalletDescriptor}, utils::SecpCtx, }; @@ -58,86 +58,16 @@ where /// Parameters for [`Wallet::create`] or [`PersistedWallet::create`]. #[must_use] -pub struct CreateParams { - pub(crate) descriptor: DescriptorToExtract, - pub(crate) change_descriptor: Option, +pub struct CreateParams { + pub(crate) secp: SecpCtx, + pub(crate) descriptors: BTreeMap, pub(crate) network: Network, pub(crate) genesis_hash: Option, pub(crate) lookahead: u32, pub(crate) use_spk_cache: bool, } -impl CreateParams { - /// Construct parameters with provided `descriptor`. - /// - /// Default values: - /// * `change_descriptor` = `None` - /// * `network` = [`Network::Bitcoin`] - /// * `genesis_hash` = `None` - /// * `lookahead` = [`DEFAULT_LOOKAHEAD`] - /// - /// Use this method only when building a wallet with a single descriptor. See - /// also [`Wallet::create_single`]. - pub fn new_single(descriptor: D) -> Self { - Self { - descriptor: make_descriptor_to_extract(descriptor), - change_descriptor: None, - network: Network::Bitcoin, - genesis_hash: None, - lookahead: DEFAULT_LOOKAHEAD, - use_spk_cache: false, - } - } - - /// Construct parameters with provided `descriptor` and `change_descriptor`. - /// - /// Default values: - /// * `network` = [`Network::Bitcoin`] - /// * `genesis_hash` = `None` - /// * `lookahead` = [`DEFAULT_LOOKAHEAD`] - pub fn new( - descriptor: D, - change_descriptor: D, - ) -> Self { - Self { - descriptor: make_descriptor_to_extract(descriptor), - change_descriptor: Some(make_descriptor_to_extract(change_descriptor)), - network: Network::Bitcoin, - genesis_hash: None, - lookahead: DEFAULT_LOOKAHEAD, - use_spk_cache: false, - } - } - - /// Construct parameters with a two-path descriptor that will be parsed into receive and change - /// descriptors. - /// - /// This function parses a two-path descriptor (receive and change) and creates parameters - /// using the existing receive and change wallet creation logic. - /// - /// Default values: - /// * `network` = [`Network::Bitcoin`] - /// * `genesis_hash` = `None` - /// * `lookahead` = [`DEFAULT_LOOKAHEAD`] - pub fn new_two_path( - two_path_descriptor: D, - ) -> Self { - Self { - descriptor: make_two_path_descriptor_to_extract(two_path_descriptor.clone(), 0), - change_descriptor: Some(make_two_path_descriptor_to_extract(two_path_descriptor, 1)), - network: Network::Bitcoin, - genesis_hash: None, - lookahead: DEFAULT_LOOKAHEAD, - use_spk_cache: false, - } - } - - /// Set [`Self::network`]. - pub fn network(mut self, network: Network) -> Self { - self.network = network; - self - } - +impl CreateParams { /// Use a custom `genesis_hash`. pub fn genesis_hash(mut self, genesis_hash: BlockHash) -> Self { self.genesis_hash = Some(genesis_hash); @@ -168,9 +98,9 @@ impl CreateParams { pub fn create_wallet

( self, persister: &mut P, - ) -> Result, CreateWithPersistError> + ) -> Result, CreateWithPersistError> where - P: WalletPersister, + P: WalletPersister, { PersistedWallet::create(persister, self) } @@ -179,31 +109,34 @@ impl CreateParams { pub async fn create_wallet_async

( self, persister: &mut P, - ) -> Result, CreateWithPersistError> + ) -> Result, CreateWithPersistError> where - P: AsyncWalletPersister, + P: AsyncWalletPersister, { PersistedWallet::create_async(persister, self).await } - /// Create [`Wallet`] without persistence. - pub fn create_wallet_no_persist(self) -> Result { + /// Create a [`Wallet`] without persistence. + /// + /// Infallible: the [`KeyRing`](crate::KeyRing) these parameters came from has already + /// established that the descriptors are valid, match the network, and are uniquely paired + /// with their keychains. + pub fn create_wallet_no_persist(self) -> Wallet { Wallet::create_with_params(self) } } /// Parameters for [`Wallet::load`] or [`PersistedWallet::load`]. #[must_use] -pub struct LoadParams { +pub struct LoadParams { pub(crate) lookahead: u32, pub(crate) check_network: Option, pub(crate) check_genesis_hash: Option, - pub(crate) check_descriptor: Option>, - pub(crate) check_change_descriptor: Option>, + pub(crate) check_descriptors: BTreeMap>, pub(crate) use_spk_cache: bool, } -impl LoadParams { +impl LoadParams { /// Construct parameters with default values. /// /// Default values: `lookahead` = [`DEFAULT_LOOKAHEAD`] @@ -212,44 +145,18 @@ impl LoadParams { lookahead: DEFAULT_LOOKAHEAD, check_network: None, check_genesis_hash: None, - check_descriptor: None, - check_change_descriptor: None, + check_descriptors: BTreeMap::new(), use_spk_cache: false, } } /// Checks the `expected_descriptor` matches exactly what is loaded for `keychain`. - pub fn descriptor(mut self, keychain: KeychainKind, expected_descriptor: Option) -> Self + pub fn descriptor(mut self, keychain: K, expected_descriptor: Option) -> Self where D: IntoWalletDescriptor + Send + 'static, { let expected = expected_descriptor.map(|d| make_descriptor_to_extract(d)); - match keychain { - KeychainKind::External => self.check_descriptor = Some(expected), - KeychainKind::Internal => self.check_change_descriptor = Some(expected), - } - self - } - - /// Checks that the provided two-path descriptor matches exactly what is loaded for both the - /// external and internal keychains. - /// - /// # Note - /// - /// The provided descriptor may only contain extended public keys (`xpub`) with exactly 2 paths, - /// or an error will occur at load time. - pub fn two_path_descriptor(mut self, expected_descriptor: D) -> Self - where - D: IntoWalletDescriptor + Send + Clone + 'static, - { - let external: DescriptorToExtract = - make_two_path_descriptor_to_extract(expected_descriptor.clone(), 0); - let internal: DescriptorToExtract = - make_two_path_descriptor_to_extract(expected_descriptor, 1); - - self.check_descriptor = Some(Some(external)); - self.check_change_descriptor = Some(Some(internal)); - + self.check_descriptors.insert(keychain, expected); self } @@ -286,35 +193,66 @@ impl LoadParams { } /// Load [`PersistedWallet`] with the given [`WalletPersister`]. + #[allow(clippy::type_complexity)] pub fn load_wallet

( self, persister: &mut P, - ) -> Result>, LoadWithPersistError> + ) -> Result>, LoadWithPersistError> where - P: WalletPersister, + P: WalletPersister, { PersistedWallet::load(persister, self) } /// Load [`PersistedWallet`] with the given [`AsyncWalletPersister`]. + #[allow(clippy::type_complexity)] pub async fn load_wallet_async

( self, persister: &mut P, - ) -> Result>, LoadWithPersistError> + ) -> Result>, LoadWithPersistError> where - P: AsyncWalletPersister, + P: AsyncWalletPersister, { PersistedWallet::load_async(persister, self).await } /// Load [`Wallet`] without persistence. - pub fn load_wallet_no_persist(self, changeset: ChangeSet) -> Result, LoadError> { + pub fn load_wallet_no_persist( + self, + changeset: ChangeSet, + ) -> Result>, LoadError> { Wallet::load_with_params(changeset, self) } } -impl Default for LoadParams { +impl Default for LoadParams { fn default() -> Self { Self::new() } } + +impl LoadParams { + /// Checks that the provided two-path descriptor matches exactly what is loaded for both the + /// external and internal keychains. + /// + /// # Note + /// + /// The provided descriptor may only contain extended public keys (`xpub`) with exactly 2 paths, + /// or an error will occur at load time. + pub fn two_path_descriptor(mut self, expected_descriptor: D) -> Self + where + D: IntoWalletDescriptor + Send + Clone + 'static, + { + let external: DescriptorToExtract = + make_two_path_descriptor_to_extract(expected_descriptor.clone(), 0); + let internal: DescriptorToExtract = + make_two_path_descriptor_to_extract(expected_descriptor, 1); + + self.check_descriptors + .insert(KeychainKind::External, Some(external)); + self.check_descriptors + .insert(KeychainKind::Internal, Some(internal)); + + self + } +} diff --git a/src/wallet/persisted.rs b/src/wallet/persisted.rs index 0d323f7a..6c1dea6a 100644 --- a/src/wallet/persisted.rs +++ b/src/wallet/persisted.rs @@ -11,7 +11,7 @@ use chain::Merge; use crate::error::LoadError; use crate::{ - ChangeSet, CreateParams, LoadParams, Wallet, + ChangeSet, CreateParams, KeychainKind, LoadParams, Wallet, descriptor::{DescriptorError, calc_checksum}, }; @@ -23,7 +23,7 @@ use crate::{ /// that associated functions are hard to find (since they are not methods!). [`WalletPersister`] is /// used by [`PersistedWallet`] (a light wrapper around [`Wallet`]) which enforces some level of /// safety. Refer to [`PersistedWallet`] for more about the safety checks. -pub trait WalletPersister { +pub trait WalletPersister { /// Error type of the persister. type Error; @@ -46,14 +46,14 @@ pub trait WalletPersister { /// persister implementations may NOT require initialization at all (and not error). /// /// [`persist`]: WalletPersister::persist - fn initialize(persister: &mut Self) -> Result; + fn initialize(persister: &mut Self) -> Result, Self::Error>; /// Persist the given `changeset` to the `persister`. /// /// This method can fail if the `persister` is not [`initialize`]d. /// /// [`initialize`]: WalletPersister::initialize - fn persist(persister: &mut Self, changeset: &ChangeSet) -> Result<(), Self::Error>; + fn persist(persister: &mut Self, changeset: &ChangeSet) -> Result<(), Self::Error>; } #[cfg(feature = "std")] @@ -70,7 +70,7 @@ type FutureResult<'a, T, E> = Pin> + 'a>>; /// [`AsyncWalletPersister`] is used by [`PersistedWallet`] (a light wrapper around [`Wallet`]) /// which enforces some level of safety. Refer to [`PersistedWallet`] for more about the safety /// checks. -pub trait AsyncWalletPersister { +pub trait AsyncWalletPersister { /// Error type of the persister. type Error; @@ -93,7 +93,7 @@ pub trait AsyncWalletPersister { /// persister implementations may NOT require initialization at all (and not error). /// /// [`persist`]: AsyncWalletPersister::persist - fn initialize<'a>(persister: &'a mut Self) -> FutureResult<'a, ChangeSet, Self::Error> + fn initialize<'a>(persister: &'a mut Self) -> FutureResult<'a, ChangeSet, Self::Error> where Self: 'a; @@ -104,7 +104,7 @@ pub trait AsyncWalletPersister { /// [`initialize`]: AsyncWalletPersister::initialize fn persist<'a>( persister: &'a mut Self, - changeset: &'a ChangeSet, + changeset: &'a ChangeSet, ) -> FutureResult<'a, (), Self::Error> where Self: 'a; @@ -126,40 +126,40 @@ pub trait AsyncWalletPersister { /// not completely fool-proof as you can have multiple instances of the same `P` type that are /// connected to different databases. #[derive(Debug)] -pub struct PersistedWallet

{ - inner: Wallet, +pub struct PersistedWallet { + inner: Wallet, _marker: PhantomData, } -impl

Deref for PersistedWallet

{ - type Target = Wallet; +impl Deref for PersistedWallet { + type Target = Wallet; fn deref(&self) -> &Self::Target { &self.inner } } -impl

DerefMut for PersistedWallet

{ +impl DerefMut for PersistedWallet { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner } } /// Methods when `P` is a [`WalletPersister`]. -impl PersistedWallet

{ +impl> PersistedWallet { /// Create a new [`PersistedWallet`] with the given `persister` and `params`. pub fn create( persister: &mut P, - params: CreateParams, - ) -> Result> { - let existing = P::initialize(persister).map_err(CreateWithPersistError::Persist)?; + params: CreateParams, + ) -> Result> { + let existing: ChangeSet = + P::initialize(persister).map_err(CreateWithPersistError::Persist)?; if !existing.is_empty() { return Err(CreateWithPersistError::DataAlreadyExists(Box::new( existing, ))); } - let mut inner = - Wallet::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?; + let mut inner = Wallet::create_with_params(params); if let Some(changeset) = inner.take_staged() { P::persist(persister, &changeset).map_err(CreateWithPersistError::Persist)?; } @@ -172,8 +172,8 @@ impl PersistedWallet

{ /// Load a previously [`PersistedWallet`] from the given `persister` and `params`. pub fn load( persister: &mut P, - params: LoadParams, - ) -> Result, LoadWithPersistError> { + params: LoadParams, + ) -> Result, LoadWithPersistError> { let changeset = P::initialize(persister).map_err(LoadWithPersistError::Persist)?; Wallet::load_with_params(changeset, params) .map(|opt| { @@ -203,12 +203,12 @@ impl PersistedWallet

{ } /// Methods when `P` is an [`AsyncWalletPersister`]. -impl PersistedWallet

{ +impl> PersistedWallet { /// Create a new [`PersistedWallet`] with the given async `persister` and `params`. pub async fn create_async( persister: &mut P, - params: CreateParams, - ) -> Result> { + params: CreateParams, + ) -> Result> { let existing = P::initialize(persister) .await .map_err(CreateWithPersistError::Persist)?; @@ -217,8 +217,7 @@ impl PersistedWallet

{ existing, ))); } - let mut inner = - Wallet::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?; + let mut inner = Wallet::create_with_params(params); if let Some(changeset) = inner.take_staged() { P::persist(persister, &changeset) .await @@ -233,8 +232,8 @@ impl PersistedWallet

{ /// Load a previously [`PersistedWallet`] from the given async `persister` and `params`. pub async fn load_async( persister: &mut P, - params: LoadParams, - ) -> Result, LoadWithPersistError> { + params: LoadParams, + ) -> Result, LoadWithPersistError> { let changeset = P::initialize(persister) .await .map_err(LoadWithPersistError::Persist)?; @@ -266,24 +265,31 @@ impl PersistedWallet

{ } #[cfg(feature = "rusqlite")] -impl WalletPersister for bdk_chain::rusqlite::Transaction<'_> { +impl WalletPersister for bdk_chain::rusqlite::Transaction<'_> { type Error = bdk_chain::rusqlite::Error; - fn initialize(persister: &mut Self) -> Result { - ChangeSet::init_sqlite_tables(&*persister)?; - ChangeSet::from_sqlite(persister) + fn initialize(persister: &mut Self) -> Result, Self::Error> { + ChangeSet::::init_sqlite_tables(&*persister)?; + let mut changeset = ChangeSet::::from_sqlite(persister)?; + // Databases written by schema v0/v1 keep their descriptors in columns rather than in the + // per-keychain table; recover them so existing wallets still load. + ChangeSet::::read_legacy_descriptors(persister, &mut changeset)?; + Ok(changeset) } - fn persist(persister: &mut Self, changeset: &ChangeSet) -> Result<(), Self::Error> { + fn persist( + persister: &mut Self, + changeset: &ChangeSet, + ) -> Result<(), Self::Error> { changeset.persist_to_sqlite(persister) } } #[cfg(feature = "rusqlite")] -impl WalletPersister for bdk_chain::rusqlite::Connection { +impl WalletPersister for bdk_chain::rusqlite::Connection { type Error = bdk_chain::rusqlite::Error; - fn initialize(persister: &mut Self) -> Result { + fn initialize(persister: &mut Self) -> Result, Self::Error> { let mut db_tx = persister.transaction()?; let changeset = as WalletPersister>::initialize(&mut db_tx)?; @@ -291,7 +297,10 @@ impl WalletPersister for bdk_chain::rusqlite::Connection { Ok(changeset) } - fn persist(persister: &mut Self, changeset: &ChangeSet) -> Result<(), Self::Error> { + fn persist( + persister: &mut Self, + changeset: &ChangeSet, + ) -> Result<(), Self::Error> { let mut db_tx = persister.transaction()?; as WalletPersister>::persist(&mut db_tx, changeset)?; db_tx.commit() @@ -323,31 +332,34 @@ impl core::fmt::Display for FileStoreError { impl error::Error for FileStoreError {} #[cfg(feature = "file_store")] -impl WalletPersister for bdk_file_store::Store { +impl WalletPersister for bdk_file_store::Store { type Error = FileStoreError; - fn initialize(persister: &mut Self) -> Result { + fn initialize(persister: &mut Self) -> Result, Self::Error> { persister .dump() .map(Option::unwrap_or_default) .map_err(FileStoreError::Load) } - fn persist(persister: &mut Self, changeset: &ChangeSet) -> Result<(), Self::Error> { + fn persist( + persister: &mut Self, + changeset: &ChangeSet, + ) -> Result<(), Self::Error> { persister.append(changeset).map_err(FileStoreError::Write) } } /// Error type for [`PersistedWallet::load`]. #[derive(Debug, PartialEq)] -pub enum LoadWithPersistError { +pub enum LoadWithPersistError { /// Error from persistence. Persist(E), /// Occurs when the loaded changeset cannot construct [`Wallet`]. - InvalidChangeSet(LoadError), + InvalidChangeSet(LoadError), } -impl fmt::Display for LoadWithPersistError { +impl fmt::Display for LoadWithPersistError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Persist(err) => fmt::Display::fmt(err, f), @@ -356,20 +368,20 @@ impl fmt::Display for LoadWithPersistError { } } -impl error::Error for LoadWithPersistError {} +impl error::Error for LoadWithPersistError {} /// Error type for [`PersistedWallet::create`]. #[derive(Debug)] -pub enum CreateWithPersistError { +pub enum CreateWithPersistError { /// Error from persistence. Persist(E), /// Persister already has wallet data. - DataAlreadyExists(Box), + DataAlreadyExists(Box>), /// Occurs when the provided descriptor(s) cannot construct [`Wallet`]. Descriptor(DescriptorError), } -impl fmt::Display for CreateWithPersistError { +impl fmt::Display for CreateWithPersistError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Persist(err) => write!(f, "{err}"), @@ -387,27 +399,21 @@ impl fmt::Display for CreateWithPersistError { } } -impl error::Error for CreateWithPersistError {} +impl error::Error + for CreateWithPersistError +{ +} /// Helper function to display basic information about a [`ChangeSet`]. -fn changeset_info(f: &mut fmt::Formatter<'_>, changeset: &ChangeSet) -> fmt::Result { +fn changeset_info( + f: &mut fmt::Formatter<'_>, + changeset: &ChangeSet, +) -> fmt::Result { let network = changeset .network .as_ref() .map_or("None".to_string(), |n| n.to_string()); - let descriptor_checksum = changeset - .descriptor - .as_ref() - .and_then(|d| calc_checksum(&d.to_string()).ok()) - .unwrap_or_else(|| "None".to_string()); - - let change_descriptor_checksum = changeset - .change_descriptor - .as_ref() - .and_then(|d| calc_checksum(&d.to_string()).ok()) - .unwrap_or_else(|| "None".to_string()); - let tx_count = changeset.tx_graph.txs.len(); let anchor_count = changeset.tx_graph.anchors.len(); @@ -419,11 +425,11 @@ fn changeset_info(f: &mut fmt::Formatter<'_>, changeset: &ChangeSet) -> fmt::Res }; writeln!(f, " Network: {network}")?; - writeln!(f, " Descriptor Checksum: {descriptor_checksum}")?; - writeln!( - f, - " Change Descriptor Checksum: {change_descriptor_checksum}" - )?; + for (keychain, descriptor) in &changeset.descriptors { + let checksum = + calc_checksum(&descriptor.to_string()).unwrap_or_else(|_| "None".to_string()); + writeln!(f, " Descriptor Checksum ({keychain:?}): {checksum}")?; + } writeln!(f, " Transaction Count: {tx_count}")?; writeln!(f, " Anchor Count: {anchor_count}")?; writeln!(f, " Block Count: {block_count}")?; diff --git a/src/wallet/signer.rs b/src/wallet/signer.rs index 71b4554a..c1649ffc 100644 --- a/src/wallet/signer.rs +++ b/src/wallet/signer.rs @@ -73,9 +73,12 @@ //! //! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/0/*)"; //! let change_descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/1/*)"; -//! let wallet = Wallet::create(descriptor, change_descriptor) -//! .network(Network::Testnet) -//! .create_wallet_no_persist()?; +//! let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, descriptor) +//! .expect("valid descriptor"); +//! keyring +//! .add_descriptor(KeychainKind::Internal, change_descriptor) +//! .expect("valid change descriptor"); +//! let wallet = Wallet::create(keyring).create_wallet_no_persist(); //! let mut external_signers = SignersContainer::new(); //! external_signers.add_external( //! custom_signer.id(wallet.secp_ctx()), diff --git a/src/wallet/tx_builder.rs b/src/wallet/tx_builder.rs index 9ed110e1..f1fef547 100644 --- a/src/wallet/tx_builder.rs +++ b/src/wallet/tx_builder.rs @@ -112,7 +112,7 @@ use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo}; /// [`coin_selection`]: Self::coin_selection #[derive(Debug)] pub struct TxBuilder<'a, Cs> { - pub(crate) wallet: &'a mut Wallet, + pub(crate) wallet: &'a mut Wallet, pub(crate) params: TxParams, pub(crate) coin_selection: Cs, } @@ -222,9 +222,9 @@ impl<'a, Cs> TxBuilder<'a, Cs> { /// # "snj:and_v(v:pk(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),", /// # "after(630000))))", /// # ); - /// # let mut wallet = Wallet::create_single(descriptor) - /// # .network(Network::Regtest) - /// # .create_wallet_no_persist()?; + /// # let mut wallet = Wallet::create(KeyRing::new(Network::Regtest, KeychainKind::External, descriptor).expect("valid descriptors")) + /// # + /// # .create_wallet_no_persist(); /// let policy = wallet /// .public_descriptor(KeychainKind::External) /// .extract_policy( @@ -263,7 +263,7 @@ impl<'a, Cs> TxBuilder<'a, Cs> { /// If a UTXO is inserted multiple times, only the final insertion will take effect. pub fn add_utxos(&mut self, outpoints: &[OutPoint]) -> Result<&mut Self, AddUtxoError> { // Canonicalize once, instead of once for every call to `get_utxo`. - let unspent: HashMap = self + let unspent: HashMap> = self .wallet .list_unspent() .map(|output| (output.outpoint, output)) @@ -894,7 +894,7 @@ pub enum ChangeSpendPolicy { } impl ChangeSpendPolicy { - pub(crate) fn is_satisfied_by(&self, utxo: &LocalOutput) -> bool { + pub(crate) fn is_satisfied_by(&self, utxo: &LocalOutput) -> bool { match self { ChangeSpendPolicy::ChangeAllowed => true, ChangeSpendPolicy::OnlyChange => utxo.keychain == KeychainKind::Internal, @@ -906,6 +906,7 @@ impl ChangeSpendPolicy { #[cfg_attr(coverage_nightly, coverage(off))] #[cfg(test)] mod test { + use crate::KeyRing; const ORDERING_TEST_TX: &str = "0200000003c26f3eb7932f7acddc5ddd26602b77e7516079b03090a16e2c2f54\ 85d1fd600f0100000000ffffffffc26f3eb7932f7acddc5ddd26602b77e75160\ 79b03090a16e2c2f5485d1fd600f0000000000ffffffff571fb3e02278217852\ @@ -1067,7 +1068,7 @@ mod test { assert_ne!(tx_2, original_tx); } - fn get_test_utxos() -> Vec { + fn get_test_utxos() -> Vec> { use bitcoin::hashes::Hash; vec![ @@ -1148,10 +1149,15 @@ mod test { use bdk_chain::BlockId; use bitcoin::{BlockHash, Network, hashes::Hash}; - let mut wallet = Wallet::create_single(get_test_tr_single_sig()) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut wallet = Wallet::create( + KeyRing::new( + Network::Regtest, + KeychainKind::External, + get_test_tr_single_sig(), + ) + .expect("valid descriptors"), + ) + .create_wallet_no_persist(); let recipient = wallet.next_unused_address(KeychainKind::External).address; insert_checkpoint( @@ -1237,10 +1243,15 @@ mod test { use bdk_chain::BlockId; use bitcoin::{BlockHash, Network, hashes::Hash}; - let mut wallet = Wallet::create_single(get_test_tr_single_sig()) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut wallet = Wallet::create( + KeyRing::new( + Network::Regtest, + KeychainKind::External, + get_test_tr_single_sig(), + ) + .expect("valid descriptors"), + ) + .create_wallet_no_persist(); insert_checkpoint( &mut wallet, @@ -1310,10 +1321,16 @@ mod test { fn test_add_utxo_final_outpoint_retained() { // Create empty wallet let (desc, change_desc) = get_test_wpkh_and_change_desc(); - let mut wallet = Wallet::create(desc, change_desc) - .network(bdk_wallet::bitcoin::Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut keyring = KeyRing::new( + bdk_wallet::bitcoin::Network::Regtest, + KeychainKind::External, + desc, + ) + .expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); let outpoint_0 = receive_output( &mut wallet, diff --git a/tests/common.rs b/tests/common.rs index 4b16dfbd..c6a514a8 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -1,8 +1,8 @@ #![allow(unused)] -use bdk_wallet::Wallet; use bdk_wallet::descriptor::IntoWalletDescriptor; use bdk_wallet::signer::SignersContainer; +use bdk_wallet::{KeychainKind, Wallet}; use bitcoin::secp256k1::Secp256k1; use miniscript::{Descriptor, DescriptorPublicKey, descriptor::KeyMap}; @@ -11,7 +11,7 @@ use miniscript::{Descriptor, DescriptorPublicKey, descriptor::KeyMap}; /// The `Wallet` no longer holds key material, so tests that need to sign construct their own /// container from the descriptor that carries the secrets. pub fn signers_from_descriptor( - wallet: &Wallet, + wallet: &Wallet, descriptor: impl IntoWalletDescriptor, ) -> SignersContainer { let (descriptor, keymap) = descriptor @@ -21,7 +21,10 @@ pub fn signers_from_descriptor( } /// Extract just the [`KeyMap`] from a signing descriptor, for use with [`bitcoin::Psbt::sign`]. -pub fn keymap_from_descriptor(wallet: &Wallet, descriptor: impl IntoWalletDescriptor) -> KeyMap { +pub fn keymap_from_descriptor( + wallet: &Wallet, + descriptor: impl IntoWalletDescriptor, +) -> KeyMap { let (_, keymap) = descriptor .into_wallet_descriptor(wallet.secp_ctx(), wallet.network().into()) .expect("failed to parse signing descriptor"); diff --git a/tests/create_psbt.rs b/tests/create_psbt.rs index cd881d82..6bfc8a61 100644 --- a/tests/create_psbt.rs +++ b/tests/create_psbt.rs @@ -6,7 +6,7 @@ use bdk_tx::{ChangeScript, bdk_coin_select}; use bdk_wallet::bitcoin; use bdk_wallet::test_utils::*; use bdk_wallet::{ - KeychainKind, PsbtParams, SelectionStrategy, Wallet, error::CreatePsbtError, psbt, + KeyRing, KeychainKind, PsbtParams, SelectionStrategy, Wallet, error::CreatePsbtError, psbt, }; use bitcoin::{ Amount, FeeRate, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, absolute, @@ -18,10 +18,12 @@ use miniscript::plan::Assets; #[test] fn test_create_psbt() { let (desc, change_desc) = get_test_tr_single_sig_xprv_and_change_desc(); - let mut wallet = Wallet::create(desc, change_desc) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut keyring = + KeyRing::new(Network::Regtest, KeychainKind::External, desc).expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); let expected_xpub = match wallet.public_descriptor(KeychainKind::External) { miniscript::Descriptor::Tr(tr) => match tr.internal_key() { miniscript::DescriptorPublicKey::XPub(desc) => desc.xkey, @@ -106,10 +108,12 @@ fn test_create_psbt() { #[test] fn test_create_psbt_insufficient_funds_error() { let (desc, change_desc) = get_test_tr_single_sig_xprv_and_change_desc(); - let mut wallet = Wallet::create(desc, change_desc) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut keyring = + KeyRing::new(Network::Regtest, KeychainKind::External, desc).expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); let addr = wallet.reveal_next_address(KeychainKind::External); @@ -128,10 +132,12 @@ fn test_create_psbt_insufficient_funds_error() { #[test] fn test_create_psbt_maturity_height() { let (desc, change_desc) = get_test_tr_single_sig_xprv_and_change_desc(); - let mut wallet = Wallet::create(desc, change_desc) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut keyring = + KeyRing::new(Network::Regtest, KeychainKind::External, desc).expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); let receive_address = wallet.reveal_next_address(KeychainKind::External); let send_to_address = wallet.reveal_next_address(KeychainKind::External).address; @@ -191,10 +197,10 @@ fn test_create_psbt_cltv() { use absolute::LockTime; let desc = get_test_single_sig_cltv(); - let mut wallet = Wallet::create_single(desc) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut wallet = Wallet::create( + KeyRing::new(Network::Regtest, KeychainKind::External, desc).expect("valid descriptors"), + ) + .create_wallet_no_persist(); // Receive coins let anchor = ConfirmationBlockTime { @@ -268,10 +274,10 @@ fn test_create_psbt_cltv_timestamp() { let lock_time = LockTime::from_consensus(1734230218); let desc = get_test_single_sig_cltv_timestamp(); - let mut wallet = Wallet::create_single(desc) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut wallet = Wallet::create( + KeyRing::new(Network::Regtest, KeychainKind::External, desc).expect("valid descriptors"), + ) + .create_wallet_no_persist(); // Receive coins let op = receive_output(&mut wallet, Amount::ONE_BTC, ReceiveTo::Mempool(1)); @@ -324,10 +330,10 @@ fn test_create_psbt_csv() { use bitcoin::relative; let desc = get_test_single_sig_csv(); - let mut wallet = Wallet::create_single(desc) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut wallet = Wallet::create( + KeyRing::new(Network::Regtest, KeychainKind::External, desc).expect("valid descriptors"), + ) + .create_wallet_no_persist(); // Receive coins let anchor = ConfirmationBlockTime { @@ -408,10 +414,15 @@ fn test_create_psbt_fallback_sequence_applied_to_coin_selected_input() { #[test] fn test_create_psbt_fallback_sequence_skipped_for_csv_input() { use bitcoin::relative; - let mut wallet = Wallet::create_single(get_test_single_sig_csv()) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut wallet = Wallet::create( + KeyRing::new( + Network::Regtest, + KeychainKind::External, + get_test_single_sig_csv(), + ) + .expect("valid descriptors"), + ) + .create_wallet_no_persist(); let anchor = ConfirmationBlockTime { block_id: BlockId { height: 10_000, @@ -476,10 +487,15 @@ fn test_create_psbt_sequence_override_takes_precedence_over_fallback() { #[test] fn test_create_psbt_sequence_override_csv_conflict_returns_error() { use bitcoin::relative; - let mut wallet = Wallet::create_single(get_test_single_sig_csv()) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut wallet = Wallet::create( + KeyRing::new( + Network::Regtest, + KeychainKind::External, + get_test_single_sig_csv(), + ) + .expect("valid descriptors"), + ) + .create_wallet_no_persist(); let anchor = ConfirmationBlockTime { block_id: BlockId { height: 10_000, @@ -521,10 +537,12 @@ fn test_replace_by_fee_replaces_descendant_fees() { use KeychainKind::*; let (desc, change_desc) = get_test_wpkh_and_change_desc(); - let mut wallet = Wallet::create(desc, change_desc) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut keyring = + KeyRing::new(Network::Regtest, KeychainKind::External, desc).expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); let block_id = BlockId { height: 100, @@ -645,10 +663,12 @@ fn test_replace_by_fee_confirmed_tx_error() { use bdk_wallet::error::ReplaceByFeeError; let (desc, change_desc) = get_test_wpkh_and_change_desc(); - let mut wallet = Wallet::create(desc, change_desc) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut keyring = + KeyRing::new(Network::Regtest, KeychainKind::External, desc).expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); let block = BlockId { height: 100, @@ -708,10 +728,12 @@ fn test_replace_by_fee_no_inputs_from_original() { use bdk_wallet::error::ReplaceByFeeError; let (desc, change_desc) = get_test_wpkh_and_change_desc(); - let mut wallet = Wallet::create(desc, change_desc) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut keyring = + KeyRing::new(Network::Regtest, KeychainKind::External, desc).expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); let addr = wallet.reveal_next_address(External).address; @@ -762,10 +784,12 @@ fn test_replace_by_fee_no_original_transactions() { use bdk_wallet::error::ReplaceByFeeError; let (desc, change_desc) = get_test_wpkh_and_change_desc(); - let mut wallet = Wallet::create(desc, change_desc) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut keyring = + KeyRing::new(Network::Regtest, KeychainKind::External, desc).expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); // replace_txs with an empty iterator produces PsbtParams with an empty replace set. let params = PsbtParams::default().replace_txs(core::iter::empty::()); @@ -785,10 +809,12 @@ fn test_replace_by_fee_conflicting_input_descendant() { use bitcoin::{Sequence, psbt as btc_psbt}; let (desc, change_desc) = get_test_wpkh_and_change_desc(); - let mut wallet = Wallet::create(desc, change_desc) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut keyring = + KeyRing::new(Network::Regtest, KeychainKind::External, desc).expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); let addr = wallet.reveal_next_address(KeychainKind::External).address; @@ -872,10 +898,12 @@ fn test_replace_by_fee_conflicting_input_descendant() { #[test] fn test_create_psbt_utxo_filter() { let (desc, change_desc) = get_test_tr_single_sig_xprv_and_change_desc(); - let mut wallet = Wallet::create(desc, change_desc) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut keyring = + KeyRing::new(Network::Regtest, KeychainKind::External, desc).expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); let anchor = ConfirmationBlockTime { block_id: BlockId { @@ -969,10 +997,12 @@ fn test_create_psbt_no_recipients_error() { #[test] fn test_create_psbt_drain_wallet_change_below_dust_error() { let (desc, change_desc) = get_test_tr_single_sig_xprv_and_change_desc(); - let mut wallet = Wallet::create(desc, change_desc) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut keyring = + KeyRing::new(Network::Regtest, KeychainKind::External, desc).expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); let anchor = ConfirmationBlockTime { block_id: BlockId { @@ -1013,10 +1043,12 @@ fn test_replace_by_fee_drain_wallet_change_below_dust_error() { use bitcoin::transaction; let (desc, change_desc) = get_test_tr_single_sig_xprv_and_change_desc(); - let mut wallet = Wallet::create(desc, change_desc) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut keyring = + KeyRing::new(Network::Regtest, KeychainKind::External, desc).expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); let anchor = ConfirmationBlockTime { block_id: BlockId { @@ -1072,10 +1104,12 @@ fn test_replace_by_fee_drain_wallet_change_below_dust_error() { #[test] fn test_replace_tx_with_planned_input() { let (desc, change_desc) = get_test_wpkh_and_change_desc(); - let mut wallet = Wallet::create(desc, change_desc) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut keyring = + KeyRing::new(Network::Regtest, KeychainKind::External, desc).expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); let addr = wallet.reveal_next_address(KeychainKind::External).address; diff --git a/tests/persisted_wallet.rs b/tests/persisted_wallet.rs index c736404f..9d198040 100644 --- a/tests/persisted_wallet.rs +++ b/tests/persisted_wallet.rs @@ -12,7 +12,8 @@ use bdk_wallet::descriptor::IntoWalletDescriptor; use bdk_wallet::error::CreateTxError; use bdk_wallet::test_utils::*; use bdk_wallet::{ - ChangeSet, KeychainKind, LoadError, LoadMismatch, LoadWithPersistError, Wallet, WalletPersister, + ChangeSet, KeyRing, KeychainKind, LoadError, LoadMismatch, LoadWithPersistError, Wallet, + WalletPersister, }; use bitcoin::constants::ChainHash; use bitcoin::hashes::Hash; @@ -78,7 +79,7 @@ fn wallet_is_persisted() -> anyhow::Result<()> { assert_eq!(cache_cmp, expected_cmp, "{}", msg.as_ref()); } - fn staged_cache(wallet: &Wallet) -> SpkCacheChangeSet { + fn staged_cache(wallet: &Wallet) -> SpkCacheChangeSet { wallet.staged().map_or(SpkCacheChangeSet::default(), |cs| { cs.indexer.spk_cache.clone() }) @@ -92,7 +93,7 @@ fn wallet_is_persisted() -> anyhow::Result<()> { where CreateDb: Fn(&Path) -> anyhow::Result, OpenDb: Fn(&Path) -> anyhow::Result, - Db: WalletPersister, + Db: WalletPersister, Db::Error: core::error::Error + Send + Sync + 'static, { let temp_dir = tempfile::tempdir().expect("must create tempdir"); @@ -102,8 +103,12 @@ fn wallet_is_persisted() -> anyhow::Result<()> { // create new wallet let wallet_spk_index = { let mut db = create_db(&file_path)?; - let mut wallet = Wallet::create(external_desc, internal_desc) - .network(Network::Testnet) + let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, external_desc) + .expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, internal_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring) .use_spk_cache(true) .create_wallet(&mut db)?; @@ -238,7 +243,7 @@ fn wallet_load_checks() -> anyhow::Result<()> { where CreateDb: Fn(&Path) -> anyhow::Result, OpenDb: Fn(&Path) -> anyhow::Result, - Db: WalletPersister + std::fmt::Debug, + Db: WalletPersister + std::fmt::Debug, Db::Error: core::error::Error + Send + Sync + 'static, { let temp_dir = tempfile::tempdir().expect("must create tempdir"); @@ -247,9 +252,12 @@ fn wallet_load_checks() -> anyhow::Result<()> { let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_and_change_desc(); // create new wallet - let _ = Wallet::create(external_desc, internal_desc) - .network(network) - .create_wallet(&mut create_db(&file_path)?)?; + let mut keyring = + KeyRing::new(network, KeychainKind::External, external_desc).expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, internal_desc) + .expect("valid change descriptor"); + let _ = Wallet::create(keyring).create_wallet(&mut create_db(&file_path)?)?; assert_matches!( Wallet::load() @@ -296,8 +304,12 @@ fn wallet_load_checks() -> anyhow::Result<()> { run( "store.db", - |path| Ok(bdk_file_store::Store::::create(DB_MAGIC, path)?), - |path| Ok(bdk_file_store::Store::::load(DB_MAGIC, path)?.0), + |path| { + Ok(bdk_file_store::Store::>::create( + DB_MAGIC, path, + )?) + }, + |path| Ok(bdk_file_store::Store::>::load(DB_MAGIC, path)?.0), )?; run( "store.sqlite", @@ -316,10 +328,11 @@ fn wallet_should_persist_anchors_and_recover() { let mut db = rusqlite::Connection::open(db_path).unwrap(); let desc = get_test_tr_single_sig_xprv(); - let mut wallet = Wallet::create_single(desc) - .network(Network::Testnet) - .create_wallet(&mut db) - .unwrap(); + let mut wallet = Wallet::create( + KeyRing::new(Network::Testnet, KeychainKind::External, desc).expect("valid descriptors"), + ) + .create_wallet(&mut db) + .unwrap(); let small_output_tx = Transaction { input: vec![], output: vec![TxOut { @@ -374,10 +387,11 @@ fn single_descriptor_wallet_persist_and_recover() { let mut db = rusqlite::Connection::open(db_path).unwrap(); let desc = get_test_tr_single_sig_xprv(); - let mut wallet = Wallet::create_single(desc) - .network(Network::Testnet) - .create_wallet(&mut db) - .unwrap(); + let mut wallet = Wallet::create( + KeyRing::new(Network::Testnet, KeychainKind::External, desc).expect("valid descriptors"), + ) + .create_wallet(&mut db) + .unwrap(); let _ = wallet.reveal_addresses_to(KeychainKind::External, 2); assert!(wallet.persist(&mut db).unwrap()); @@ -414,16 +428,34 @@ fn two_path_descriptor_wallet_persist_and_recover() { let db_path = temp_dir.path().join("wallet.db"); let mut db = rusqlite::Connection::open(db_path).unwrap(); - let two_path_descriptor = get_test_two_path_wpkh(); - let mut wallet = Wallet::create_from_two_path_descriptor(two_path_descriptor) - .network(Network::Testnet4) - .create_wallet(&mut db) - .unwrap(); + // Split the two-path descriptor into the single-path halves the wallet actually holds. + let secp = Secp256k1::new(); + let (multipath, _keymap) = get_test_two_path_wpkh() + .into_wallet_descriptor(&secp, Network::Testnet4.into()) + .expect("valid descriptor"); + let paths = multipath + .into_single_descriptors() + .expect("descriptor splits into single paths"); + assert_eq!(paths.len(), 2); + let (external_desc, internal_desc) = (paths[0].clone(), paths[1].clone()); + + let mut keyring = KeyRing::new( + Network::Testnet4, + KeychainKind::External, + external_desc.clone(), + ) + .expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, internal_desc.clone()) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet(&mut db).unwrap(); + let _ = wallet.reveal_addresses_to(KeychainKind::External, 2); assert!(wallet.persist(&mut db).unwrap()); let loaded = Wallet::load() - .two_path_descriptor(two_path_descriptor) + .descriptor(KeychainKind::External, Some(external_desc)) + .descriptor(KeychainKind::Internal, Some(internal_desc)) .check_network(Network::Testnet4) .load_wallet(&mut db) .unwrap() @@ -490,9 +522,12 @@ fn test_lock_outpoint_persist() -> anyhow::Result<()> { let mut conn = rusqlite::Connection::open_in_memory()?; let (desc, change_desc) = get_test_tr_single_sig_xprv_and_change_desc(); - let mut wallet = Wallet::create(desc, change_desc) - .network(Network::Signet) - .create_wallet(&mut conn)?; + let mut keyring = + KeyRing::new(Network::Signet, KeychainKind::External, desc).expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet(&mut conn)?; // Receive coins. let mut outpoints = vec![]; diff --git a/tests/wallet.rs b/tests/wallet.rs index 40eb4352..539b0258 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -7,14 +7,14 @@ use bdk_wallet::KeychainKind; use bdk_wallet::coin_selection; use bdk_wallet::coin_selection::InsufficientFunds; use bdk_wallet::descriptor::policy::BuildSatisfaction; -use bdk_wallet::descriptor::{DescriptorError, ExtractPolicy, IntoWalletDescriptor, calc_checksum}; +use bdk_wallet::descriptor::{ExtractPolicy, IntoWalletDescriptor, calc_checksum}; use bdk_wallet::error::CreateTxError; use bdk_wallet::psbt::PsbtUtils; use bdk_wallet::signer::{SignOptions, SignerError, SignersContainer}; use bdk_wallet::test_utils::*; use bdk_wallet::{ - AddressInfo, Balance, FinalizeInputOutcome, IndexOutOfBoundsError, PersistedWallet, Update, - Wallet, WalletTx, + AddressInfo, Balance, FinalizeInputOutcome, IndexOutOfBoundsError, KeyRing, PersistedWallet, + Update, Wallet, WalletTx, keyring::KeyRingError, }; use bitcoin::constants::COINBASE_MATURITY; use bitcoin::hashes::Hash; @@ -35,22 +35,22 @@ use common::signers_from_descriptor; fn test_error_external_and_internal_are_the_same() { // identical descriptors should fail to create wallet let desc = get_test_wpkh(); - let err = Wallet::create(desc, desc) - .network(Network::Testnet) - .create_wallet_no_persist(); + let mut keyring = + KeyRing::new(Network::Testnet, KeychainKind::External, desc).expect("valid descriptor"); + let err = keyring.add_descriptor(KeychainKind::Internal, desc); assert!( - matches!(&err, Err(DescriptorError::ExternalAndInternalAreTheSame)), + matches!(&err, Err(KeyRingError::DescriptorAlreadyAssigned(..))), "expected same descriptors error, got {err:?}", ); // public + private of same descriptor should fail to create wallet let desc = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)"; let change_desc = "wpkh([3c31d632/84'/1'/0']tpubDCYwFkks2cg78N7eoYbBatsFEGje8vW8arSKW4rLwD1AU1s9KJMDRHE32JkvYERuiFjArrsH7qpWSpJATed5ShZbG9KsskA5Rmi6NSYgYN2/0/*)"; - let err = Wallet::create(desc, change_desc) - .network(Network::Testnet) - .create_wallet_no_persist(); + let mut keyring = + KeyRing::new(Network::Testnet, KeychainKind::External, desc).expect("valid descriptor"); + let err = keyring.add_descriptor(KeychainKind::Internal, change_desc); assert!( - matches!(err, Err(DescriptorError::ExternalAndInternalAreTheSame)), + matches!(err, Err(KeyRingError::DescriptorAlreadyAssigned(..))), "expected same descriptors error, got {err:?}", ); } @@ -1074,10 +1074,12 @@ fn test_create_tx_policy_path_required() { #[test] fn test_create_tx_policy_path_no_csv() { let (descriptor, change_descriptor) = get_test_wpkh_and_change_desc(); - let mut wallet = Wallet::create(descriptor, change_descriptor) - .network(Network::Regtest) - .create_wallet_no_persist() - .expect("wallet"); + let mut keyring = KeyRing::new(Network::Regtest, KeychainKind::External, descriptor) + .expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_descriptor) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); let tx = Transaction { version: transaction::Version::non_standard(0), @@ -1282,18 +1284,26 @@ fn test_create_tx_increment_change_index() { // create wallet let (params, change_keychain) = match test.change_descriptor { Some(change_desc) => ( - Wallet::create(test.descriptor, change_desc), + Wallet::create({ + let mut keyring = + KeyRing::new(Network::Regtest, KeychainKind::External, test.descriptor) + .expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_desc) + .expect("valid change descriptor"); + keyring + }), KeychainKind::Internal, ), None => ( - Wallet::create_single(test.descriptor), + Wallet::create( + KeyRing::new(Network::Regtest, KeychainKind::External, test.descriptor) + .expect("valid descriptors"), + ), KeychainKind::External, ), }; - let mut wallet = params - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut wallet = params.create_wallet_no_persist(); // fund wallet receive_output(&mut wallet, amount, ReceiveTo::Mempool(0)); // create tx @@ -2097,10 +2107,12 @@ fn test_sign_nonstandard_sighash() { fn test_unused_address() { let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; let change_descriptor = get_test_wpkh(); - let mut wallet = Wallet::create(descriptor, change_descriptor) - .network(Network::Testnet) - .create_wallet_no_persist() - .expect("wallet"); + let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, descriptor) + .expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_descriptor) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); // `list_unused_addresses` should be empty if we haven't revealed any assert!( @@ -2130,10 +2142,12 @@ fn test_unused_address() { fn test_next_unused_address() { let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; let change_descriptor = get_test_wpkh(); - let mut wallet = Wallet::create(descriptor, change_descriptor) - .network(Network::Testnet) - .create_wallet_no_persist() - .expect("wallet"); + let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, descriptor) + .expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_descriptor) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); assert_eq!(wallet.derivation_index(KeychainKind::External), None); assert_eq!( @@ -2180,10 +2194,12 @@ fn test_next_unused_address() { fn test_peek_address_at_index() { let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; let change_descriptor = get_test_wpkh(); - let mut wallet = Wallet::create(descriptor, change_descriptor) - .network(Network::Testnet) - .create_wallet_no_persist() - .expect("wallet"); + let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, descriptor) + .expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_descriptor) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); assert_eq!( wallet.peek_address(KeychainKind::External, 1).to_string(), @@ -2219,10 +2235,12 @@ fn test_peek_address_at_index() { #[test] fn test_peek_address_at_index_not_derivable() { let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)"; - let wallet = Wallet::create(descriptor, get_test_wpkh()) - .network(Network::Testnet) - .create_wallet_no_persist() - .unwrap(); + let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, descriptor) + .expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, get_test_wpkh()) + .expect("valid change descriptor"); + let wallet = Wallet::create(keyring).create_wallet_no_persist(); assert_eq!( wallet.peek_address(KeychainKind::External, 1).to_string(), @@ -2243,10 +2261,12 @@ fn test_peek_address_at_index_not_derivable() { #[test] fn test_returns_index_and_address() { let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)"; - let mut wallet = Wallet::create(descriptor, get_test_wpkh()) - .network(Network::Testnet) - .create_wallet_no_persist() - .unwrap(); + let mut keyring = KeyRing::new(Network::Testnet, KeychainKind::External, descriptor) + .expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, get_test_wpkh()) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); // new index 0 assert_eq!( @@ -2312,13 +2332,16 @@ fn test_sending_to_bip350_bech32m_address() { fn test_get_address() { use bdk_wallet::descriptor::template::Bip84; let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); - let wallet = Wallet::create( + let mut keyring = KeyRing::new( + Network::Regtest, + KeychainKind::External, Bip84(key, KeychainKind::External), - Bip84(key, KeychainKind::Internal), ) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + .expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, Bip84(key, KeychainKind::Internal)) + .expect("valid change descriptor"); + let wallet = Wallet::create(keyring).create_wallet_no_persist(); assert_eq!( wallet.peek_address(KeychainKind::External, 0), @@ -2346,10 +2369,12 @@ fn test_get_address() { #[test] fn test_reveal_addresses() { let (desc, change_desc) = get_test_tr_single_sig_xprv_and_change_desc(); - let mut wallet = Wallet::create(desc, change_desc) - .network(Network::Signet) - .create_wallet_no_persist() - .unwrap(); + let mut keyring = + KeyRing::new(Network::Signet, KeychainKind::External, desc).expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); let keychain = KeychainKind::External; let last_revealed_addr = wallet.reveal_addresses_to(keychain, 9).last().unwrap(); @@ -2370,13 +2395,16 @@ fn test_get_address_no_reuse() { use std::collections::HashSet; let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap(); - let mut wallet = Wallet::create( + let mut keyring = KeyRing::new( + Network::Regtest, + KeychainKind::External, Bip84(key, KeychainKind::External), - Bip84(key, KeychainKind::Internal), ) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + .expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, Bip84(key, KeychainKind::Internal)) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); let mut used_set = HashSet::new(); @@ -2612,7 +2640,7 @@ fn test_taproot_sign_using_non_witness_utxo() { ); } -fn test_spend_from_wallet(mut wallet: Wallet, descriptor: impl IntoWalletDescriptor) { +fn test_spend_from_wallet(mut wallet: Wallet, descriptor: impl IntoWalletDescriptor) { let signers = signers_from_descriptor(&wallet, descriptor); let addr = wallet.next_unused_address(KeychainKind::External); @@ -2822,10 +2850,16 @@ fn test_taproot_sign_derive_index_from_psbt() { let mut psbt = builder.finish().unwrap(); // re-create the wallet with an empty db - let wallet_empty = Wallet::create(get_test_tr_single_sig_xprv(), get_test_tr_single_sig()) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut keyring = KeyRing::new( + Network::Regtest, + KeychainKind::External, + get_test_tr_single_sig_xprv(), + ) + .expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, get_test_tr_single_sig()) + .expect("valid change descriptor"); + let wallet_empty = Wallet::create(keyring).create_wallet_no_persist(); // signing with an empty db means that we will only look at the psbt to infer the // derivation index @@ -2932,10 +2966,12 @@ fn test_taproot_sign_non_default_sighash() { #[test] fn test_spend_coinbase() { let (desc, change_desc) = get_test_wpkh_and_change_desc(); - let mut wallet = Wallet::create(desc, change_desc) - .network(Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut keyring = + KeyRing::new(Network::Regtest, KeychainKind::External, desc).expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); let confirmation_height = 5; let confirmation_block_id = BlockId { @@ -3259,17 +3295,22 @@ fn test_keychains_with_overlapping_spks() { #[test] fn test_thread_safety() { fn thread_safe() {} - thread_safe::(); // compiles only if true - thread_safe::>(); + thread_safe::>(); // compiles only if true + thread_safe::>(); } #[test] fn single_descriptor_wallet_can_create_tx_and_receive_change() { // create single descriptor wallet and fund it - let mut wallet = Wallet::create_single(get_test_tr_single_sig_xprv()) - .network(Network::Testnet) - .create_wallet_no_persist() - .unwrap(); + let mut wallet = Wallet::create( + KeyRing::new( + Network::Testnet, + KeychainKind::External, + get_test_tr_single_sig_xprv(), + ) + .expect("valid descriptors"), + ) + .create_wallet_no_persist(); assert_eq!(wallet.keychains().count(), 1); let amount = Amount::from_sat(5_000); receive_output(&mut wallet, amount * 2, ReceiveTo::Mempool(2)); @@ -3466,10 +3507,12 @@ fn test_tx_ordering_untouched_preserves_insertion_ordering() { fn test_tx_ordering_untouched_preserves_insertion_ordering_bnb_success() { // Create empty wallet let (desc, change_desc) = get_test_wpkh_and_change_desc(); - let mut wallet = Wallet::create(desc, change_desc) - .network(bdk_wallet::bitcoin::Network::Regtest) - .create_wallet_no_persist() - .unwrap(); + let mut keyring = + KeyRing::new(Network::Regtest, KeychainKind::External, desc).expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); // Set up UTXOs with specific values so BnB can find an exact match (avoiding change). // - outpoint_0 (required): 35,000 sat - not enough alone @@ -3514,10 +3557,12 @@ fn test_tx_ordering_untouched_preserves_insertion_ordering_bnb_success() { #[test] fn test_create_and_spend_from_truc_tx() -> anyhow::Result<()> { let (descriptor, change_descriptor) = get_test_wpkh_and_change_desc(); - let mut wallet = Wallet::create(descriptor, change_descriptor) - .network(Network::Regtest) - .create_wallet_no_persist() - .expect("should create wallet successfully!"); + let mut keyring = KeyRing::new(Network::Regtest, KeychainKind::External, descriptor) + .expect("valid descriptor"); + keyring + .add_descriptor(KeychainKind::Internal, change_descriptor) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); // establish a chain tip so confirmed funds can be anchored to a block in the active chain. let block = BlockId { @@ -3568,10 +3613,15 @@ fn test_create_and_spend_from_truc_tx() -> anyhow::Result<()> { insert_tx(&mut wallet, tx_a); let balance = wallet.balance(); + assert_eq!( + balance.trusted_pending, + Amount::ZERO, + "nothing is trusted before it is mined" + ); assert_eq!( balance.untrusted_pending, - Amount::from_sat(125_000), - "wallet balance SHOULD have 125K unconfirmed (TRUC) UTXO after txA!" + Amount::from_sat(249_859), + "wallet balance SHOULD have the 125K unconfirmed (TRUC) UTXO after txA, plus change!" ); // create txB (non-TRUC) @@ -3606,8 +3656,8 @@ fn test_create_and_spend_from_truc_tx() -> anyhow::Result<()> { let balance = wallet.balance(); assert_eq!( balance.untrusted_pending, - Amount::from_sat(250_000), - "wallet balance SHOULD have 250K unconfirmed, both non-TRUC (txB) and TRUC (txA) UTXOs after txB!" + Amount::from_sat(499_718), + "wallet balance SHOULD have both non-TRUC (txB) and TRUC (txA) UTXOs after txB, plus change!" ); // create txC (TRUC) @@ -3640,8 +3690,8 @@ fn test_create_and_spend_from_truc_tx() -> anyhow::Result<()> { let balance = wallet.balance(); assert_eq!( balance.untrusted_pending, - Amount::from_sat(325_000), - "wallet balance SHOULD have 325K unconfirmed UTXOs after txC!" + Amount::from_sat(499_509), + "wallet balance SHOULD have all unconfirmed UTXOs after txC, including change!" ); // create txD (non-TRUC) @@ -3662,3 +3712,57 @@ fn test_create_and_spend_from_truc_tx() -> anyhow::Result<()> { Ok(()) } + +/// A keychain type that is not [`KeychainKind`], with a keychain that has no conventional +/// counterpart. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum SyncKeychain { + Receive, + Change, + Cold, +} + +/// Sync and full-scan requests can be built from a wallet generic over `K`. +#[test] +fn test_sync_requests_are_generic_over_keychain() { + let (desc, change_desc) = get_test_tr_single_sig_xprv_and_change_desc(); + let cold_desc = get_test_tr_single_sig_xprv(); + + let mut keyring = + KeyRing::new(Network::Regtest, SyncKeychain::Receive, desc).expect("valid descriptor"); + keyring + .add_descriptor(SyncKeychain::Change, change_desc) + .expect("valid change descriptor"); + keyring + .add_descriptor(SyncKeychain::Cold, cold_desc) + .expect("valid cold descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); + + // A full scan covers every keychain the wallet holds, including the one with no + // `KeychainKind` equivalent. + let mut request = wallet.start_full_scan().build(); + let mut keychains = request.keychains(); + keychains.sort(); + assert_eq!( + keychains, + vec![ + SyncKeychain::Receive, + SyncKeychain::Change, + SyncKeychain::Cold + ] + ); + assert!( + request.iter_spks(SyncKeychain::Cold).next().is_some(), + "full scan must produce spks for a custom keychain" + ); + + // A sync covers exactly the revealed spks, across all keychains. + let _ = wallet.reveal_addresses_to(SyncKeychain::Receive, 2); // indices 0..=2 + let _ = wallet.reveal_next_address(SyncKeychain::Cold); // index 0 + let request = wallet.start_sync_with_revealed_spks_at(0).build(); + assert_eq!( + request.progress().total_spks(), + 4, + "sync must cover the 3 revealed receive spks and the 1 revealed cold spk" + ); +} From 6fafa928c5a12e800d12ff1182ba513130feca5f Mon Sep 17 00:00:00 2001 From: thunderbiscuit Date: Tue, 4 Aug 2026 15:13:03 -0400 Subject: [PATCH 3/7] fix(keyring) relax KeyRingError bound from Display to Debug --- src/keyring/error.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/keyring/error.rs b/src/keyring/error.rs index af8a7015..21a09f4c 100644 --- a/src/keyring/error.rs +++ b/src/keyring/error.rs @@ -29,12 +29,15 @@ impl From for KeyRingError { } } -impl fmt::Display for KeyRingError { +impl fmt::Display for KeyRingError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Descriptor(e) => e.fmt(f), Self::KeychainAlreadyAssigned(keychain) => { - write!(f, "keychain {keychain} is already assigned to a descriptor") + write!( + f, + "keychain {keychain:?} is already assigned to a descriptor" + ) } Self::DescriptorAlreadyAssigned(descriptor) => { write!( @@ -46,4 +49,4 @@ impl fmt::Display for KeyRingError { } } -impl core::error::Error for KeyRingError {} +impl core::error::Error for KeyRingError {} From ceacfc4b6a5cfc32844f9557cf6a763201a673fc Mon Sep 17 00:00:00 2001 From: thunderbiscuit Date: Tue, 4 Aug 2026 15:18:57 -0400 Subject: [PATCH 4/7] feat(persist)! make the persisters generic over the keychain type --- src/wallet/changeset.rs | 150 ++++++++++++++++++++++++++++++++++------ src/wallet/persisted.rs | 60 ++++++++-------- 2 files changed, 158 insertions(+), 52 deletions(-) diff --git a/src/wallet/changeset.rs b/src/wallet/changeset.rs index 8372292c..b442f4a1 100644 --- a/src/wallet/changeset.rs +++ b/src/wallet/changeset.rs @@ -401,30 +401,27 @@ where self.indexer.persist_to_sqlite(db_tx)?; Ok(()) } -} -#[cfg(feature = "rusqlite")] -impl ChangeSet { /// Recover descriptors written by schema v0 or v1. /// /// Those versions stored the wallet's two descriptors as `descriptor` and `change_descriptor` - /// columns on a single row, rather than one row per keychain. This reads them and maps them - /// onto [`External`](crate::KeychainKind::External) and - /// [`Internal`](crate::KeychainKind::Internal). + /// columns on a single row, rather than one row per keychain in + /// [`WALLET_KEYCHAIN_TABLE_NAME`](Self::WALLET_KEYCHAIN_TABLE_NAME). /// - /// Only meaningful for wallets keyed by [`KeychainKind`](crate::KeychainKind): interpreting - /// the two legacy columns *requires* knowing they mean external and change. A wallet using a - /// custom keychain type has no v0/v1 database to recover, since those schemas predate custom - /// keychains entirely. + /// The legacy columns carry no keychain identifier of their own — the schema encoded it + /// positionally. They are recovered here by asking `K` to parse the same strings + /// [`KeychainKind`](crate::KeychainKind) serialises to, `"external"` and `"internal"`. A + /// keychain type that does not recognise them cannot have written a v0/v1 database in the + /// first place, so it is left untouched. /// - /// Descriptors already present are left alone, so this never overwrites what schema v2 holds. + /// Existing keychains are never overwritten: anything already read from the v2 table wins. pub fn read_legacy_descriptors( db_tx: &chain::rusqlite::Transaction, changeset: &mut Self, ) -> chain::rusqlite::Result<()> { - use crate::KeychainKind; use chain::Impl; use chain::rusqlite::OptionalExtension; + use chain::rusqlite::types::ValueRef; let mut statement = db_tx.prepare(&format!( "SELECT descriptor, change_descriptor FROM {}", @@ -445,17 +442,16 @@ impl ChangeSet { return Ok(()); }; - if let Some(Impl(descriptor)) = descriptor { - changeset - .descriptors - .entry(KeychainKind::External) - .or_insert(descriptor); - } - if let Some(Impl(change_descriptor)) = change_descriptor { - changeset - .descriptors - .entry(KeychainKind::Internal) - .or_insert(change_descriptor); + for (column, keychain_name) in [(descriptor, "external"), (change_descriptor, "internal")] { + let Some(Impl(descriptor)) = column else { + continue; + }; + // `K` not recognising the legacy name means this database was never written by a + // wallet using this keychain type. Nothing to migrate. + let Ok(keychain) = K::column_result(ValueRef::Text(keychain_name.as_bytes())) else { + continue; + }; + changeset.descriptors.entry(keychain).or_insert(descriptor); } Ok(()) @@ -705,4 +701,112 @@ mod test { "schema v2 is authoritative; a legacy column must not overwrite it" ); } + + /// A keychain type that has nothing to do with `external`/`internal`. + #[cfg(feature = "rusqlite")] + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] + enum Account { + Savings, + Spending, + } + + #[cfg(feature = "rusqlite")] + impl chain::rusqlite::ToSql for Account { + fn to_sql(&self) -> chain::rusqlite::Result> { + Ok(match self { + Account::Savings => "savings".into(), + Account::Spending => "spending".into(), + }) + } + } + + #[cfg(feature = "rusqlite")] + impl chain::rusqlite::types::FromSql for Account { + fn column_result( + value: chain::rusqlite::types::ValueRef<'_>, + ) -> chain::rusqlite::types::FromSqlResult { + match value.as_str()? { + "savings" => Ok(Account::Savings), + "spending" => Ok(Account::Spending), + other => Err(chain::rusqlite::types::FromSqlError::Other( + alloc::boxed::Box::new(crate::types::UnknownKeychain( + alloc::string::String::from(other), + )), + )), + } + } + } + + #[cfg(feature = "rusqlite")] + #[test] + fn descriptors_round_trip_through_sqlite_for_a_custom_keychain() { + use bitcoin::Network; + use chain::rusqlite::Connection; + + const SAVINGS: &str = "wpkh([41f2aed0/84h/1h/0h]tpubDDFSdQWw75hk1ewbwnNpPp5DvXFRKt68ioPoyJDY752cNHKkFxPWqkqCyCf4hxrEfpuxh46QisehL3m8Bi6MsAv394QVLopwbtfvryFQNUH/0/*)"; + const SPENDING: &str = "wpkh([41f2aed0/84h/1h/0h]tpubDDFSdQWw75hk1ewbwnNpPp5DvXFRKt68ioPoyJDY752cNHKkFxPWqkqCyCf4hxrEfpuxh46QisehL3m8Bi6MsAv394QVLopwbtfvryFQNUH/1/*)"; + + let mut changeset = ChangeSet:: { + network: Some(Network::Testnet), + ..Default::default() + }; + changeset + .descriptors + .insert(Account::Savings, SAVINGS.parse().unwrap()); + changeset + .descriptors + .insert(Account::Spending, SPENDING.parse().unwrap()); + + let mut conn = Connection::open_in_memory().unwrap(); + let db_tx = conn.transaction().unwrap(); + ChangeSet::::init_sqlite_tables(&db_tx).unwrap(); + changeset.persist_to_sqlite(&db_tx).unwrap(); + let read_back = ChangeSet::::from_sqlite(&db_tx).unwrap(); + db_tx.commit().unwrap(); + + assert_eq!( + read_back.descriptors, changeset.descriptors, + "a custom keychain type must survive the sqlite round-trip" + ); + assert_eq!(read_back.network, Some(Network::Testnet)); + } + + #[cfg(feature = "rusqlite")] + #[test] + fn legacy_read_is_a_no_op_for_a_keychain_type_that_never_wrote_one() { + use bitcoin::Network; + use chain::rusqlite::{Connection, named_params}; + + const EXTERNAL: &str = "wpkh([41f2aed0/84h/1h/0h]tpubDDFSdQWw75hk1ewbwnNpPp5DvXFRKt68ioPoyJDY752cNHKkFxPWqkqCyCf4hxrEfpuxh46QisehL3m8Bi6MsAv394QVLopwbtfvryFQNUH/0/*)"; + + let mut conn = Connection::open_in_memory().unwrap(); + let db_tx = conn.transaction().unwrap(); + ChangeSet::::init_sqlite_tables(&db_tx).unwrap(); + + // A v0/v1-shaped row, whose descriptors are keyed positionally as external/internal. + let external: Descriptor = EXTERNAL.parse().unwrap(); + db_tx + .execute( + &format!( + "INSERT INTO {}(id, descriptor, network) VALUES(:id, :descriptor, :network)", + ChangeSet::::WALLET_TABLE_NAME + ), + named_params! { + ":id": 0, + ":descriptor": chain::Impl(external), + ":network": chain::Impl(Network::Testnet), + }, + ) + .unwrap(); + + // `Account` cannot represent "external", so there is nothing to recover — and crucially + // this must not be an error, since such a database was never written by this wallet. + let mut changeset = ChangeSet::::default(); + ChangeSet::::read_legacy_descriptors(&db_tx, &mut changeset).unwrap(); + + assert!( + changeset.descriptors.is_empty(), + "legacy descriptors must not be forced onto an unrelated keychain type" + ); + } } diff --git a/src/wallet/persisted.rs b/src/wallet/persisted.rs index 6c1dea6a..7195b1b6 100644 --- a/src/wallet/persisted.rs +++ b/src/wallet/persisted.rs @@ -11,7 +11,7 @@ use chain::Merge; use crate::error::LoadError; use crate::{ - ChangeSet, CreateParams, KeychainKind, LoadParams, Wallet, + ChangeSet, CreateParams, LoadParams, Wallet, descriptor::{DescriptorError, calc_checksum}, }; @@ -265,44 +265,46 @@ impl> PersistedWal } #[cfg(feature = "rusqlite")] -impl WalletPersister for bdk_chain::rusqlite::Transaction<'_> { +impl WalletPersister for bdk_chain::rusqlite::Transaction<'_> +where + K: Ord + Clone + bdk_chain::rusqlite::ToSql + bdk_chain::rusqlite::types::FromSql, +{ type Error = bdk_chain::rusqlite::Error; - fn initialize(persister: &mut Self) -> Result, Self::Error> { - ChangeSet::::init_sqlite_tables(&*persister)?; - let mut changeset = ChangeSet::::from_sqlite(persister)?; + fn initialize(persister: &mut Self) -> Result, Self::Error> { + ChangeSet::::init_sqlite_tables(&*persister)?; + let mut changeset = ChangeSet::::from_sqlite(persister)?; // Databases written by schema v0/v1 keep their descriptors in columns rather than in the // per-keychain table; recover them so existing wallets still load. - ChangeSet::::read_legacy_descriptors(persister, &mut changeset)?; + ChangeSet::::read_legacy_descriptors(persister, &mut changeset)?; Ok(changeset) } - fn persist( - persister: &mut Self, - changeset: &ChangeSet, - ) -> Result<(), Self::Error> { + fn persist(persister: &mut Self, changeset: &ChangeSet) -> Result<(), Self::Error> { changeset.persist_to_sqlite(persister) } } #[cfg(feature = "rusqlite")] -impl WalletPersister for bdk_chain::rusqlite::Connection { +impl WalletPersister for bdk_chain::rusqlite::Connection +where + K: Ord + Clone + bdk_chain::rusqlite::ToSql + bdk_chain::rusqlite::types::FromSql, +{ type Error = bdk_chain::rusqlite::Error; - fn initialize(persister: &mut Self) -> Result, Self::Error> { + fn initialize(persister: &mut Self) -> Result, Self::Error> { let mut db_tx = persister.transaction()?; let changeset = - as WalletPersister>::initialize(&mut db_tx)?; + as WalletPersister>::initialize(&mut db_tx)?; db_tx.commit()?; Ok(changeset) } - fn persist( - persister: &mut Self, - changeset: &ChangeSet, - ) -> Result<(), Self::Error> { + fn persist(persister: &mut Self, changeset: &ChangeSet) -> Result<(), Self::Error> { let mut db_tx = persister.transaction()?; - as WalletPersister>::persist(&mut db_tx, changeset)?; + as WalletPersister>::persist( + &mut db_tx, changeset, + )?; db_tx.commit() } } @@ -310,15 +312,15 @@ impl WalletPersister for bdk_chain::rusqlite::Connection { /// Error for [`bdk_file_store`]'s implementation of [`WalletPersister`]. #[cfg(feature = "file_store")] #[derive(Debug)] -pub enum FileStoreError { +pub enum FileStoreError { /// Error when loading from the store. - Load(bdk_file_store::StoreErrorWithDump), + Load(bdk_file_store::StoreErrorWithDump>), /// Error when writing to the store. Write(std::io::Error), } #[cfg(feature = "file_store")] -impl core::fmt::Display for FileStoreError { +impl core::fmt::Display for FileStoreError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use core::fmt::Display; match self { @@ -329,23 +331,23 @@ impl core::fmt::Display for FileStoreError { } #[cfg(feature = "file_store")] -impl error::Error for FileStoreError {} +impl error::Error for FileStoreError {} #[cfg(feature = "file_store")] -impl WalletPersister for bdk_file_store::Store { - type Error = FileStoreError; +impl WalletPersister for bdk_file_store::Store> +where + K: Ord + Clone + serde::Serialize + serde::de::DeserializeOwned, +{ + type Error = FileStoreError; - fn initialize(persister: &mut Self) -> Result, Self::Error> { + fn initialize(persister: &mut Self) -> Result, Self::Error> { persister .dump() .map(Option::unwrap_or_default) .map_err(FileStoreError::Load) } - fn persist( - persister: &mut Self, - changeset: &ChangeSet, - ) -> Result<(), Self::Error> { + fn persist(persister: &mut Self, changeset: &ChangeSet) -> Result<(), Self::Error> { persister.append(changeset).map_err(FileStoreError::Write) } } From 5207231e0632bf4c42d6dce7d76516137c7c4526 Mon Sep 17 00:00:00 2001 From: thunderbiscuit Date: Tue, 4 Aug 2026 15:23:00 -0400 Subject: [PATCH 5/7] docs(examples): add multi-keychain examples --- Cargo.toml | 12 ++ examples/multi_keychain/README.md | 58 ++++++++ examples/multi_keychain/address_generation.rs | 62 +++++++++ examples/multi_keychain/custom_keychain.rs | 119 ++++++++++++++++ examples/multi_keychain/persistence.rs | 130 ++++++++++++++++++ 5 files changed, 381 insertions(+) create mode 100644 examples/multi_keychain/README.md create mode 100644 examples/multi_keychain/address_generation.rs create mode 100644 examples/multi_keychain/custom_keychain.rs create mode 100644 examples/multi_keychain/persistence.rs diff --git a/Cargo.toml b/Cargo.toml index 11c5ad61..4059a71e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,3 +88,15 @@ required-features = ["bdk-tx"] name = "replace_by_fee" required-features = ["bdk-tx"] +[[example]] +name = "multi_keychain_custom_keychain" +path = "examples/multi_keychain/custom_keychain.rs" + +[[example]] +name = "multi_keychain_address_generation" +path = "examples/multi_keychain/address_generation.rs" + +[[example]] +name = "multi_keychain_persistence" +path = "examples/multi_keychain/persistence.rs" +required-features = ["rusqlite"] diff --git a/examples/multi_keychain/README.md b/examples/multi_keychain/README.md new file mode 100644 index 00000000..f681d7f9 --- /dev/null +++ b/examples/multi_keychain/README.md @@ -0,0 +1,58 @@ +# Multi-keychain examples + +A `Wallet` tracks any number of keychains, each identified by a value of type `K`. The default +is `KeychainKind`, which gives the familiar two-keychain wallet; supplying your own `K` gives you +as many keychains as you need. + +The wallet requires `K: Ord + Clone + Debug` and nothing more, so `K` can carry whatever metadata +your application wants to attach to a keychain. `Ord` is required because keychains are held in a +`BTreeMap`, which also means it decides the order they iterate in. + +| Example | Shows | +| --- | --- | +| [`custom_keychain.rs`](./custom_keychain.rs) | A keychain identifier that carries application metadata, and using it to choose which keychain to reveal from | +| [`address_generation.rs`](./address_generation.rs) | Per-keychain derivation indices, `reveal_next_address` vs `next_unused_address`, reading back the last revealed index | +| [`persistence.rs`](./persistence.rs) | Persisting a multi-keychain wallet to sqlite and loading it back, including load-time descriptor checks | + +Run them with: + +```shell +cargo run --example multi_keychain_custom_keychain +cargo run --example multi_keychain_address_generation +cargo run --example multi_keychain_persistence --features rusqlite +``` + +## Building a wallet + +Descriptors go into a `KeyRing`, which validates each one against the network and rejects duplicate +keychains and duplicate descriptors as they are added: + +```rust,ignore +let mut keyring = KeyRing::new(Network::Signet, Keychain::A, DESC_A)?; +keyring.add_descriptor(Keychain::B, DESC_B)?; + +let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); +``` + +Because the `KeyRing` has already done that checking, building the wallet from it cannot fail — +`create_wallet_no_persist` returns a `Wallet` rather than a `Result`. Descriptor errors surface +at `KeyRing::new` and `KeyRing::add_descriptor`, where the descriptor was actually supplied. + +The conventional two-keychain wallet is the same two calls with `KeychainKind`: + +```rust,ignore +let mut keyring = KeyRing::new(Network::Signet, KeychainKind::External, external_desc)?; +keyring.add_descriptor(KeychainKind::Internal, internal_desc)?; +``` + +## Current limitations + +One thing a multi-keychain wallet cannot do yet: + +- **No transaction building.** `TxBuilder` lives on `impl Wallet`, because building a + transaction has to pick a change keychain and a wallet generic over `K` has no canonical one. + Relatedly, `balance()` counts nothing as trusted until it is mined. + +To persist a custom keychain type, implement `rusqlite`'s `ToSql` and `FromSql` for it (see +[`persistence.rs`](./persistence.rs)); for the file store, implement `serde::Serialize` and +`serde::Deserialize`. diff --git a/examples/multi_keychain/address_generation.rs b/examples/multi_keychain/address_generation.rs new file mode 100644 index 00000000..0b80dbce --- /dev/null +++ b/examples/multi_keychain/address_generation.rs @@ -0,0 +1,62 @@ +// Adapted from bitcoindevkit/bdk_wallet#318 (examples/multi_keychain/address_generation.rs). +// +// Address generation across several keychains: +// - revealing addresses from any keychain +// - each keychain keeping its own derivation index +// - `next_unused_address` vs `reveal_next_address` +// - reading back the last revealed index per keychain + +use bdk_wallet::bitcoin::Network; +use bdk_wallet::{KeyRing, Wallet}; + +const DESC_A: &str = "tr([5bc5d243/86'/1'/0']tpubDC72NVP1RK5qwy2QdEfWphDsUBAfBu7oiV6jEFooHP8tGQGFVUeFxhgZxuk1j6EQRJ1YsS3th2RyDgReRqCL4zqp4jtuV2z7gbiqDH2iyUS/0/*)"; +const DESC_B: &str = "wpkh([5bc5d243/84'/1'/0']tpubDCA4DcMLVSDifbfUxyJaVVAx57ztsVjke6DRYF95jFFgJqvzA9oENovVd7n34NNURmZxFNRB1VLGyDEqxvaZNXie3ZroEGFbeTS2xLYuaN1/0/*)"; +const DESC_C: &str = "pkh([5bc5d243/44'/1'/0']tpubDDNQtvd8Sg1mXtSGtxRWEcgg7PbPwUSAyAmBonDSL3HLuutthe54Yih4XDYcywVdcduwqaQonpbTAGjjSh5kcLeCj5MTjYooa9ve2Npx6ho/0/*)"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum Keychain { + A, + B, + C, +} + +fn main() -> Result<(), Box> { + let mut keyring = KeyRing::new(Network::Signet, Keychain::A, DESC_A)?; + keyring.add_descriptor(Keychain::B, DESC_B)?; + keyring.add_descriptor(Keychain::C, DESC_C)?; + + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); + + println!("Created a wallet with 3 keychains (A, B, C)\n"); + + // Each keychain keeps its own derivation index, exactly as the external and internal + // keychains do on a standard two-keychain wallet. + println!("1. Revealing addresses from each keychain"); + for (keychain, count) in [(Keychain::A, 5), (Keychain::B, 3), (Keychain::C, 2)] { + println!(" Keychain {keychain:?}:"); + for _ in 0..count { + let addr = wallet.reveal_next_address(keychain); + println!(" - index {}: {}", addr.index, addr.address); + } + } + + // `next_unused_address` hands back the lowest revealed address that has not been used yet, + // rather than revealing a new one. With no transactions in this wallet, nothing has been used, + // so it returns index 0 instead of advancing past what we revealed above. + println!("\n2. next_unused_address does not advance the index"); + for keychain in [Keychain::A, Keychain::B, Keychain::C] { + let addr = wallet.next_unused_address(keychain); + println!(" Keychain {keychain:?}: index {}", addr.index); + } + + println!("\n3. Last revealed index per keychain"); + for (keychain, _descriptor) in wallet.keychains() { + // `derivation_index` is `None` until something has actually been revealed. + println!( + " Keychain {keychain:?}: {:?}", + wallet.derivation_index(keychain) + ); + } + + Ok(()) +} diff --git a/examples/multi_keychain/custom_keychain.rs b/examples/multi_keychain/custom_keychain.rs new file mode 100644 index 00000000..1eae93e8 --- /dev/null +++ b/examples/multi_keychain/custom_keychain.rs @@ -0,0 +1,119 @@ +// Adapted from bitcoindevkit/bdk_wallet#318 (examples/multi_keychain/wallet.rs). +// +// A `Wallet` tracks a map of keychain identifiers (`K`) to descriptors. `K` can be something +// simple like `KeychainKind`, but it can also be a type of your own that carries whatever metadata +// your application needs. The wallet only ever requires `K: Ord + Clone + Debug`, so the rest of +// the type is yours. +// +// Here Johnny keeps several keychains in one wallet, and the identifier records what each one is +// for — which is what lets the application decide which keychain to reveal an address from. + +use std::cmp::Ordering; + +use bdk_wallet::bitcoin::Network; +use bdk_wallet::miniscript::descriptor::DescriptorType; +use bdk_wallet::{KeyRing, Wallet}; + +const DESC_1: &str = "tr([5bc5d243/86'/1'/0']tpubDC72NVP1RK5qwy2QdEfWphDsUBAfBu7oiV6jEFooHP8tGQGFVUeFxhgZxuk1j6EQRJ1YsS3th2RyDgReRqCL4zqp4jtuV2z7gbiqDH2iyUS/0/*)#xh44xwsp"; +const DESC_2: &str = "wpkh([5bc5d243/84'/1'/0']tpubDCA4DcMLVSDifbfUxyJaVVAx57ztsVjke6DRYF95jFFgJqvzA9oENovVd7n34NNURmZxFNRB1VLGyDEqxvaZNXie3ZroEGFbeTS2xLYuaN1/0/*)#q8afsa3z"; +const DESC_3: &str = "pkh([5bc5d243/44'/1'/0']tpubDDNQtvd8Sg1mXtSGtxRWEcgg7PbPwUSAyAmBonDSL3HLuutthe54Yih4XDYcywVdcduwqaQonpbTAGjjSh5kcLeCj5MTjYooa9ve2Npx6ho/1/*)#g73kgtdn"; +const DESC_4: &str = "sh(wpkh([5bc5d243/49'/1'/0']tpubDDd6eupua2nhRp2egUAgYGjkxHeh5jPrBDaKLExeySwRvUb1hU7s8osoeACRhXs2w1UGZSMmEpZ1FkjYJ2Pxvfsy7w6XRqYYW7Vw89Unrzr/0/*))#svvvc6el"; + +fn main() -> Result<(), Box> { + let everyday = KeychainId { + number: 1, + nickname: "Johnny's favorite keychain", + script_type: DescriptorType::Tr, + day_type: DayType::WeekDay, + }; + let party = KeychainId { + number: 2, + nickname: "Johnny's party keychain", + script_type: DescriptorType::Wpkh, + day_type: DayType::WeekEnd, + }; + let legacy = KeychainId { + number: 3, + nickname: "Johnny's old P2PKH keychain", + script_type: DescriptorType::Pkh, + day_type: DayType::AnyDay, + }; + let donations = KeychainId { + number: 4, + nickname: "Johnny's project donations keychain", + script_type: DescriptorType::ShWpkh, + day_type: DayType::AnyDay, + }; + + // A `KeyRing` is built one keychain at a time. Every descriptor is validated against the + // network as it goes in, and duplicate keychains or duplicate descriptors are rejected here — + // which is why building the wallet from it afterwards cannot fail. + let mut keyring = KeyRing::new(Network::Signet, everyday, DESC_1)?; + keyring.add_descriptor(party, DESC_2)?; + keyring.add_descriptor(legacy, DESC_3)?; + keyring.add_descriptor(donations, DESC_4)?; + + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); + + println!( + "This wallet tracks {} keychains:", + wallet.keychains().count() + ); + for (keychain, _descriptor) in wallet.keychains() { + println!( + " {}. {:?}, used {:?} ({})", + keychain.number, keychain.script_type, keychain.day_type, keychain.nickname + ); + } + + // Because the identifier carries the metadata, picking a keychain is ordinary application + // logic rather than something the wallet has to model. + println!(); + let party_address = wallet.reveal_next_address(party); + println!("Party address: {}", party_address.address); + + let donation_address = wallet.reveal_next_address(donations); + println!("Donation address: {}", donation_address.address); + + Ok(()) +} + +/// Johnny's keychain identifier. +/// +/// Only `number` participates in equality and ordering: it is the identity of the keychain, and the +/// rest is metadata hanging off it. Note that ordering also decides the order keychains come back +/// in from [`Wallet::keychains`], since they are held in a `BTreeMap`. +#[derive(Debug, Clone, Copy)] +struct KeychainId { + number: u32, + nickname: &'static str, + script_type: DescriptorType, + day_type: DayType, +} + +impl PartialEq for KeychainId { + fn eq(&self, other: &Self) -> bool { + self.number == other.number + } +} + +impl Eq for KeychainId {} + +impl PartialOrd for KeychainId { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for KeychainId { + fn cmp(&self, other: &Self) -> Ordering { + self.number.cmp(&other.number) + } +} + +#[derive(Debug, Clone, Copy)] +enum DayType { + AnyDay, + WeekDay, + WeekEnd, +} diff --git a/examples/multi_keychain/persistence.rs b/examples/multi_keychain/persistence.rs new file mode 100644 index 00000000..05eeb41b --- /dev/null +++ b/examples/multi_keychain/persistence.rs @@ -0,0 +1,130 @@ +// Adapted from bitcoindevkit/bdk_wallet#318 (examples/multi_keychain/persistence.rs). +// +// Persisting and reloading a wallet that tracks several keychains: +// 1. create a wallet with three keychains and persist it to sqlite +// 2. load it back +// 3. load it again with expectations, and watch the check fail when one does not match +// +// A custom keychain type has to be storable, so it implements `ToSql` and `FromSql`. Those two +// impls are the only extra work a custom `K` needs in order to persist. + +use bdk_wallet::bitcoin::Network; +use bdk_wallet::chain::rusqlite; +use bdk_wallet::chain::rusqlite::types::{FromSql, FromSqlResult, ToSql, ToSqlOutput, ValueRef}; +use bdk_wallet::{KeyRing, LoadParams, Wallet}; + +const SAVINGS_DESC: &str = "tr([5bc5d243/86'/1'/0']tpubDC72NVP1RK5qwy2QdEfWphDsUBAfBu7oiV6jEFooHP8tGQGFVUeFxhgZxuk1j6EQRJ1YsS3th2RyDgReRqCL4zqp4jtuV2z7gbiqDH2iyUS/0/*)"; +const SPENDING_DESC: &str = "wpkh([5bc5d243/84'/1'/0']tpubDCA4DcMLVSDifbfUxyJaVVAx57ztsVjke6DRYF95jFFgJqvzA9oENovVd7n34NNURmZxFNRB1VLGyDEqxvaZNXie3ZroEGFbeTS2xLYuaN1/0/*)"; +const DONATIONS_DESC: &str = "pkh([5bc5d243/44'/1'/0']tpubDDNQtvd8Sg1mXtSGtxRWEcgg7PbPwUSAyAmBonDSL3HLuutthe54Yih4XDYcywVdcduwqaQonpbTAGjjSh5kcLeCj5MTjYooa9ve2Npx6ho/1/*)"; + +// Deliberately not the descriptor we stored, to show the load-time check rejecting a mismatch. +const DONATIONS_WRONG_DESC: &str = "pkh([5bc5d243/44'/1'/0']tpubDDNQtvd8Sg1mXtSGtxRWEcgg7PbPwUSAyAmBonDSL3HLuutthe54Yih4XDYcywVdcduwqaQonpbTAGjjSh5kcLeCj5MTjYooa9ve2Npx6ho/2/*)"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum Account { + Savings, + Spending, + Donations, +} + +// How the keychain is written to, and read back from, the database. The stored string is what +// identifies the keychain across restarts, so it must stay stable for the life of the wallet. +impl ToSql for Account { + fn to_sql(&self) -> rusqlite::Result> { + Ok(match self { + Account::Savings => "savings".into(), + Account::Spending => "spending".into(), + Account::Donations => "donations".into(), + }) + } +} + +impl FromSql for Account { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + match value.as_str()? { + "savings" => Ok(Account::Savings), + "spending" => Ok(Account::Spending), + "donations" => Ok(Account::Donations), + other => Err(rusqlite::types::FromSqlError::Other(Box::new( + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("unknown account keychain: {other}"), + ), + ))), + } + } +} + +fn main() -> Result<(), Box> { + // A temporary database, so the example is repeatable. + let db_dir = tempfile::tempdir()?; + let db_path = db_dir.path().join("multi_keychain_persistence.sqlite3"); + + // 1. Create and persist + println!("1. Creating a wallet with Savings, Spending and Donations keychains"); + { + let mut conn = rusqlite::Connection::open(&db_path)?; + + let mut keyring = KeyRing::new(Network::Regtest, Account::Savings, SAVINGS_DESC)?; + keyring.add_descriptor(Account::Spending, SPENDING_DESC)?; + keyring.add_descriptor(Account::Donations, DONATIONS_DESC)?; + + let mut wallet = Wallet::create(keyring).create_wallet(&mut conn)?; + + for account in [Account::Savings, Account::Spending, Account::Donations] { + let addr = wallet.reveal_next_address(account); + println!(" {:?} address {}: {}", account, addr.index, addr.address); + } + + wallet.persist(&mut conn)?; + println!(" Persisted.\n"); + } + + // 2. Load it back + println!("2. Loading the wallet back from the database"); + { + let mut conn = rusqlite::Connection::open(&db_path)?; + + let params = LoadParams::::new().check_network(Network::Regtest); + match params.load_wallet(&mut conn)? { + Some(wallet) => { + println!(" Recovered {} keychains:", wallet.keychains().count()); + for (account, descriptor) in wallet.keychains() { + // Revealed indices survive the round-trip along with the descriptors. + println!( + " - {:?} (last revealed index {:?}): {}", + account, + wallet.derivation_index(account), + descriptor + ); + } + } + None => println!(" No wallet found."), + } + println!(); + } + + // 3. Load with expectations, one of which is wrong on purpose + println!("3. Loading with expectations, where the Donations descriptor is intentionally wrong"); + { + let mut conn = rusqlite::Connection::open(&db_path)?; + + let params = LoadParams::::new() + .check_network(Network::Regtest) + .check_genesis_hash( + bdk_wallet::bitcoin::constants::genesis_block(Network::Regtest).block_hash(), + ) + // Each of these asserts "this keychain must be loaded, and must be this descriptor". + .descriptor(Account::Savings, Some(SAVINGS_DESC)) + .descriptor(Account::Spending, Some(SPENDING_DESC)) + .descriptor(Account::Donations, Some(DONATIONS_WRONG_DESC)); + + match params.load_wallet(&mut conn) { + Ok(Some(_wallet)) => println!(" Loaded (unexpected — the check should have failed)"), + Ok(None) => println!(" No wallet found."), + Err(e) => println!(" Rejected, as expected:\n {e}"), + } + } + + Ok(()) +} From 76a0a9de85744a6dd42476ec2ac0e78c8fa13b99 Mon Sep 17 00:00:00 2001 From: thunderbiscuit Date: Wed, 5 Aug 2026 11:26:14 -0400 Subject: [PATCH 6/7] feat(wallet)remove LoadParams::two_path_descriptor --- src/wallet/params.rs | 52 -------------------------------------------- 1 file changed, 52 deletions(-) diff --git a/src/wallet/params.rs b/src/wallet/params.rs index 9a4fcfac..071fafd4 100644 --- a/src/wallet/params.rs +++ b/src/wallet/params.rs @@ -13,32 +13,6 @@ use crate::{ use super::{ChangeSet, LoadError, PersistedWallet}; -fn make_two_path_descriptor_to_extract( - two_path_descriptor: D, - index: usize, -) -> DescriptorToExtract -where - D: IntoWalletDescriptor + Send + 'static, -{ - Box::new(move |secp, network| { - let (desc, keymap) = two_path_descriptor.into_wallet_descriptor(secp, network)?; - - if !desc.is_multipath() { - return Err(DescriptorError::MultiPath); - } - - let descriptors = desc - .into_single_descriptors() - .map_err(DescriptorError::Miniscript)?; - - if descriptors.len() != 2 { - return Err(DescriptorError::MultiPath); - } - - Ok((descriptors[index].clone(), keymap)) - }) -} - /// This atrocity is to avoid having type parameters on [`CreateParams`] and [`LoadParams`]. /// /// The better option would be to do `Box`, but we cannot due to Rust's @@ -230,29 +204,3 @@ impl Default for LoadParams { Self::new() } } - -impl LoadParams { - /// Checks that the provided two-path descriptor matches exactly what is loaded for both the - /// external and internal keychains. - /// - /// # Note - /// - /// The provided descriptor may only contain extended public keys (`xpub`) with exactly 2 paths, - /// or an error will occur at load time. - pub fn two_path_descriptor(mut self, expected_descriptor: D) -> Self - where - D: IntoWalletDescriptor + Send + Clone + 'static, - { - let external: DescriptorToExtract = - make_two_path_descriptor_to_extract(expected_descriptor.clone(), 0); - let internal: DescriptorToExtract = - make_two_path_descriptor_to_extract(expected_descriptor, 1); - - self.check_descriptors - .insert(KeychainKind::External, Some(external)); - self.check_descriptors - .insert(KeychainKind::Internal, Some(internal)); - - self - } -} From eaa09460ced92c2bce02b57d4e8328a014abae5e Mon Sep 17 00:00:00 2001 From: thunderbiscuit Date: Wed, 5 Aug 2026 12:56:53 -0400 Subject: [PATCH 7/7] feat(wallet)! users must supply change keychain explicitly --- examples/multi_keychain/README.md | 28 +++++- examples/psbt.rs | 1 + examples/replace_by_fee.rs | 1 + src/psbt/params.rs | 51 +++++++--- src/wallet/error.rs | 17 ++++ src/wallet/mod.rs | 97 +++++++++++------- tests/create_psbt.rs | 161 ++++++++++++++++++++++++++++++ 7 files changed, 303 insertions(+), 53 deletions(-) diff --git a/examples/multi_keychain/README.md b/examples/multi_keychain/README.md index f681d7f9..6060b465 100644 --- a/examples/multi_keychain/README.md +++ b/examples/multi_keychain/README.md @@ -45,13 +45,31 @@ let mut keyring = KeyRing::new(Network::Signet, KeychainKind::External, external keyring.add_descriptor(KeychainKind::Internal, internal_desc)?; ``` -## Current limitations +## Building transactions + +`Wallet::create_psbt` and `Wallet::replace_by_fee` work on any `Wallet`. Because a wallet +generic over `K` has no canonical change keychain, you name one explicitly: + +```rust,ignore +let mut params = PsbtParams::default(); +params + .add_recipients([(recipient_spk, Amount::from_sat(10_000))]) + .change_keychain(Keychain::Change); -One thing a multi-keychain wallet cannot do yet: +let (psbt, finalizer) = wallet.create_psbt(params)?; +``` + +Set `change_keychain` or `change_script` — without one, PSBT creation fails with +`CreatePsbtError::NoChangeSource`, and naming a keychain the wallet does not hold fails with +`CreatePsbtError::UnknownChangeKeychain`. Change derived from `change_keychain` is revealed and +staged, so it stays tracked; you must persist the resulting changeset. + +## Current limitations -- **No transaction building.** `TxBuilder` lives on `impl Wallet`, because building a - transaction has to pick a change keychain and a wallet generic over `K` has no canonical one. - Relatedly, `balance()` counts nothing as trusted until it is mined. +- **No `TxBuilder`.** The older `TxBuilder` API still lives on `impl Wallet`, since it + has no way to be told which keychain change belongs to. Use `create_psbt` instead. +- **Nothing is trusted before it is mined.** `balance()` cannot know which of your keychains hold + self-owned change, so all unconfirmed output counts as untrusted-pending. To persist a custom keychain type, implement `rusqlite`'s `ToSql` and `FromSql` for it (see [`persistence.rs`](./persistence.rs)); for the file store, implement `serde::Serialize` and diff --git a/examples/psbt.rs b/examples/psbt.rs index 3adf123c..86d88406 100644 --- a/examples/psbt.rs +++ b/examples/psbt.rs @@ -49,6 +49,7 @@ fn main() -> anyhow::Result<()> { // Build params. let mut params = PsbtParams::default(); + params.change_keychain(Internal); let addr = Address::from_str(SEND_TO)?.require_network(NETWORK)?; let feerate = feerate_unchecked(FEERATE); params diff --git a/examples/replace_by_fee.rs b/examples/replace_by_fee.rs index 33a79b70..927928c9 100644 --- a/examples/replace_by_fee.rs +++ b/examples/replace_by_fee.rs @@ -49,6 +49,7 @@ fn main() -> anyhow::Result<()> { // Create tx1: sweep all funds to our own address at a low feerate let mut params = PsbtParams::new(); + params.change_keychain(KeychainKind::Internal); params .change_script(ChangeScript::from_descriptor(derived_descriptor.clone())) .fee_rate(FeeRate::from_sat_per_vb(2).expect("valid feerate")) diff --git a/src/psbt/params.rs b/src/psbt/params.rs index cba8b7e5..4c452802 100644 --- a/src/psbt/params.rs +++ b/src/psbt/params.rs @@ -26,7 +26,7 @@ pub struct ReplaceTx; /// Parameters to create a PSBT. // TODO: Can we derive `Clone` for this? #[derive(Debug)] -pub struct PsbtParams { +pub struct PsbtParams { /// Set of selected UTXO outpoints, `HashSet` ensures uniqueness pub(crate) set: HashSet, /// Ordered list of manually-selected spends. @@ -34,7 +34,14 @@ pub struct PsbtParams { /// List of recipient script/amount pairs. pub(crate) recipients: Vec<(ScriptBuf, Amount)>, /// Optional script or descriptor designated for change. + /// + /// Takes precedence over [`change_keychain`](Self::change_keychain). pub(crate) change_script: Option, + /// Keychain the change output is derived from. + /// + /// The wallet reveals the keychain's next unused address and stages the resulting changeset, + /// so change stays tracked. Ignored when [`change_script`](Self::change_script) is set. + pub(crate) change_keychain: Option, /// Optional assets for creating a spend plan. pub(crate) assets: Option, /// Target fee rate. @@ -77,7 +84,7 @@ pub struct PsbtParams { pub(crate) marker: core::marker::PhantomData, } -impl Default for PsbtParams { +impl Default for PsbtParams { fn default() -> Self { Self { set: Default::default(), @@ -85,6 +92,7 @@ impl Default for PsbtParams { assets: Default::default(), recipients: Default::default(), change_script: Default::default(), + change_keychain: Default::default(), fee_rate: FeeRate::BROADCAST_MIN, coin_selection: Default::default(), canonical_params: Default::default(), @@ -105,7 +113,7 @@ impl Default for PsbtParams { } } -impl PsbtParams { +impl PsbtParams { /// Create a new [`PsbtParams`]. pub fn new() -> Self { Self::default() @@ -156,6 +164,7 @@ impl PsbtParams { /// /// ```rust,no_run /// use bdk_tx::Input; + /// # use bdk_wallet::KeychainKind; /// # use bdk_wallet::psbt::PsbtParams; /// # use bitcoin::{psbt, OutPoint, Sequence, TxOut}; /// # let outpoint = OutPoint::null(); @@ -164,7 +173,7 @@ impl PsbtParams { /// # let satisfaction_weight = 0; /// # let tx_status = None; /// # let is_coinbase = false; - /// let mut params = PsbtParams::default(); + /// let mut params = PsbtParams::<_, KeychainKind>::default(); /// let input = Input::from_psbt_input( /// outpoint, /// sequence, @@ -211,7 +220,7 @@ impl PsbtParams { /// [`add_planned_input`]: PsbtParams::add_planned_input /// [`Input`]: bdk_tx::Input /// [`NoOriginalTransactions`]: crate::error::ReplaceByFeeError::NoOriginalTransactions - pub fn replace_txs(self, txs: impl IntoIterator) -> PsbtParams + pub fn replace_txs(self, txs: impl IntoIterator) -> PsbtParams where T: Into>, { @@ -221,13 +230,14 @@ impl PsbtParams { } /// Transition this [`PsbtParams`] to the [`ReplaceTx`] state. - fn into_replace_params(self) -> PsbtParams { + fn into_replace_params(self) -> PsbtParams { PsbtParams { set: self.set, must_spend: self.must_spend, assets: self.assets, recipients: self.recipients, change_script: self.change_script, + change_keychain: self.change_keychain, fee_rate: self.fee_rate, coin_selection: self.coin_selection, canonical_params: self.canonical_params, @@ -248,7 +258,7 @@ impl PsbtParams { } } -impl PsbtParams { +impl PsbtParams { /// Get the currently selected spends. pub fn utxos(&self) -> &HashSet { &self.set @@ -380,6 +390,21 @@ impl PsbtParams { self } + /// Set the keychain that the change output is derived from. + /// + /// The wallet takes the keychain's next unused address, reveals it, and stages the resulting + /// changeset so that incoming change is tracked on the next sync. See + /// [`Wallet::create_psbt`](crate::Wallet::create_psbt) for notes on change address reuse. + /// + /// Every PSBT needs a change destination: set this or + /// [`change_script`](Self::change_script), or creating the PSBT fails with + /// [`NoChangeSource`](crate::wallet::error::CreatePsbtError::NoChangeSource). When both are + /// set, `change_script` wins. + pub fn change_keychain(&mut self, keychain: K) -> &mut Self { + self.change_keychain = Some(keychain); + self + } + /// Filter [`FullTxOut`]s by the provided closure. /// /// This option can be used to mark specific outputs unspendable or apply custom UTXO @@ -574,7 +599,7 @@ impl fmt::Debug for UtxoFilter { } } -impl PsbtParams { +impl PsbtParams { /// Replace spends of the provided `txs`. This will internally set the list of UTXOs /// to be spent. fn replace(&mut self, txs: impl IntoIterator) @@ -672,6 +697,7 @@ impl AssetsExt for Assets { #[cfg(test)] mod test { use super::*; + use crate::KeychainKind; use crate::test_utils::new_tx; use bitcoin::hashes::Hash; @@ -697,7 +723,7 @@ mod test { let txid1 = tx.compute_txid(); // Replace tx - let mut params = PsbtParams::default().replace_txs([tx]); + let mut params = PsbtParams::::default().replace_txs([tx]); params.add_recipients([(ScriptBuf::new_op_return([0xb1, 0x0c]), Amount::ZERO)]); let feerate = FeeRate::from_sat_per_vb(8).unwrap(); params.fee_rate(feerate); @@ -763,14 +789,15 @@ mod test { let expect_spends: HashSet = [tx_a.input[0].previous_output, tx_c.input[0].previous_output].into(); - let params = PsbtParams::new().replace_txs([tx_a, tx_b, tx_c, tx_d]); + let params = + PsbtParams::::new().replace_txs([tx_a, tx_b, tx_c, tx_d]); assert_eq!(params.set, expect_spends); assert_eq!(params.replace, [txid_a, txid_c].into()); } #[test] fn test_selected_outpoints_are_unique() { - let mut params = PsbtParams::default(); + let mut params = PsbtParams::::default(); let op = OutPoint::null(); // Try adding the same outpoint repeatedly. @@ -861,7 +888,7 @@ mod test { ) .unwrap(); - let mut params = PsbtParams::default(); + let mut params = PsbtParams::::default(); params .add_planned_input(conflicted_input) .add_planned_input(safe_input); diff --git a/src/wallet/error.rs b/src/wallet/error.rs index b29711bc..cb00bccb 100644 --- a/src/wallet/error.rs +++ b/src/wallet/error.rs @@ -384,6 +384,16 @@ pub enum CreatePsbtError { /// [`SelectionStrategy::All`]: crate::SelectionStrategy::All /// [`change_script`]: crate::PsbtParams::change_script NoRecipients, + /// No change destination was configured. Every PSBT needs one: set either + /// [`change_keychain`] or [`change_script`]. + /// + /// [`change_keychain`]: crate::PsbtParams::change_keychain + /// [`change_script`]: crate::PsbtParams::change_script + NoChangeSource, + /// The keychain given to [`change_keychain`] is not held by this wallet. + /// + /// [`change_keychain`]: crate::PsbtParams::change_keychain + UnknownChangeKeychain, /// After coin selection, all outputs fell below the dust threshold and were /// dropped to fees. AllOutputsBelowDust, @@ -413,6 +423,13 @@ impl fmt::Display for CreatePsbtError { Self::Bnb(e) => write!(f, "{e}"), Self::InsufficientFunds(e) => write!(f, "{e}"), Self::NoRecipients => write!(f, "no output destinations were configured"), + Self::NoChangeSource => write!( + f, + "no change destination configured: set a change keychain or a change script" + ), + Self::UnknownChangeKeychain => { + write!(f, "the change keychain is not held by this wallet") + } Self::AllOutputsBelowDust => write!(f, "all outputs are below the dust threshold",), Self::MissingKeyOrigin(e) => write!(f, "missing key origin: {e}"), Self::Plan(op) => write!(f, "failed to create a plan for txout with outpoint {op}"), diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 96b56b2e..6d1bf51e 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1771,7 +1771,10 @@ fn status_from_position(pos: ChainPosition) -> Option Wallet +where + K: Ord + Clone + core::fmt::Debug, +{ /// Return the "keys" assets, i.e. the ones we can trivially infer by scanning /// the pubkeys of the wallet's descriptors. fn assets(&self) -> Assets { @@ -1786,45 +1789,55 @@ impl Wallet { Assets::new().add(pks) } - /// Peek at the next change address without revealing it, returning the auto-derived - /// change info `(keychain, index, spk)` alongside the [`ChangeScript`]. + /// Peek at the next change address without revealing it, returning the change info + /// `(keychain, index, spk)` alongside the [`ChangeScript`]. /// - /// The next change address is the next unused address of the change keychain, or the + /// The next change address is the next unused address of `change_keychain`, or the /// next-to-be-revealed address **without** mutating wallet state. Revelation is deferred /// until after all error paths have been cleared by the caller. - fn peek_change_info(&self) -> ((KeychainKind, u32, ScriptBuf), ChangeScript) { - let change_keychain = self.map_keychain(KeychainKind::Internal); + fn peek_change_info( + &self, + change_keychain: K, + ) -> Result<((K, u32, ScriptBuf), ChangeScript), CreatePsbtError> { + if self + .tx_graph + .index + .get_descriptor(change_keychain.clone()) + .is_none() + { + return Err(CreatePsbtError::UnknownChangeKeychain); + } let (index, spk) = self .tx_graph .index - .unused_keychain_spks(change_keychain) + .unused_keychain_spks(change_keychain.clone()) .next() .unwrap_or_else(|| { let (next_index, _) = self .tx_graph .index - .next_index(change_keychain) + .next_index(change_keychain.clone()) .expect("keychain must exist"); let spk = self - .peek_address(change_keychain, next_index) + .peek_address(change_keychain.clone(), next_index) .script_pubkey(); (next_index, spk) }); let descriptor = self - .public_descriptor(change_keychain) + .public_descriptor(change_keychain.clone()) .at_derivation_index(index) .expect("should be valid derivation index"); - ( + Ok(( (change_keychain, index, spk), ChangeScript::from_descriptor(descriptor), - ) + )) } /// Parses the common parameters used during PSBT creation and returns the spend assets /// and a map of indexed tx outputs. fn parse_params( &self, - params: &PsbtParams, + params: &PsbtParams, ) -> (Assets, HashMap>) { // Get spend assets. let assets = match params.assets { @@ -1856,7 +1869,7 @@ impl Wallet { fn filter_spendable<'a, I, C, F>( &'a self, txos: I, - params: &'a PsbtParams, + params: &'a PsbtParams, policy: F, ) -> impl Iterator> + 'a where @@ -1890,14 +1903,15 @@ impl Wallet { } /// Maps the recipients of the `params` to a collection of target [`Output`]s. - fn target_outputs(&self, params: &PsbtParams) -> Vec { + fn target_outputs(&self, params: &PsbtParams) -> Vec { params .recipients .iter() .cloned() .map( |(script, value)| match self.tx_graph.index.index_of_spk(script.clone()) { - Some(&(keychain, index)) => { + Some((keychain, index)) => { + let (keychain, index) = (keychain.clone(), *index); let descriptor = self .public_descriptor(keychain) .at_derivation_index(index) @@ -1961,7 +1975,7 @@ impl Wallet { #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn create_psbt( &mut self, - params: PsbtParams, + params: PsbtParams, ) -> Result<(Psbt, Finalizer), CreatePsbtError> { self.create_psbt_with_rng(params, &mut rand::thread_rng()) } @@ -1982,7 +1996,7 @@ impl Wallet { /// See [`Wallet::take_staged`]. pub fn create_psbt_with_rng( &mut self, - mut params: PsbtParams, + mut params: PsbtParams, rng: &mut impl RngCore, ) -> Result<(Psbt, Finalizer), CreatePsbtError> { // Only permit no recipients if we're doing a sweep and an explicit change script is @@ -1993,14 +2007,17 @@ impl Wallet { { return Err(CreatePsbtError::NoRecipients); } - let (change_info, change_script) = params - .change_script - .take() - .map(|change_script| (None, change_script)) - .unwrap_or_else(|| { - let (change_info, change_script) = self.peek_change_info(); + let (change_info, change_script) = match params.change_script.take() { + Some(change_script) => (None, change_script), + None => { + let change_keychain = params + .change_keychain + .clone() + .ok_or(CreatePsbtError::NoChangeSource)?; + let (change_info, change_script) = self.peek_change_info(change_keychain)?; (Some(change_info), change_script) - }); + } + }; let (assets, txouts) = self.parse_params(¶ms); @@ -2071,7 +2088,7 @@ impl Wallet { fn create_psbt_from_selector( &self, selector: &mut Selector, - params: &PsbtParams, + params: &PsbtParams, rng: &mut impl RngCore, ) -> Result<(Psbt, Finalizer), CreatePsbtError> { // Select coins @@ -2202,7 +2219,7 @@ impl Wallet { #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn replace_by_fee( &mut self, - params: PsbtParams, + params: PsbtParams, ) -> Result<(Psbt, Finalizer), ReplaceByFeeError> { self.replace_by_fee_with_rng(params, &mut rand::thread_rng()) } @@ -2222,7 +2239,7 @@ impl Wallet { /// See [`Wallet::take_staged`]. pub fn replace_by_fee_with_rng( &mut self, - mut params: PsbtParams, + mut params: PsbtParams, rng: &mut impl RngCore, ) -> Result<(Psbt, Finalizer), ReplaceByFeeError> { if params.replace.is_empty() { @@ -2236,14 +2253,22 @@ impl Wallet { { return Err(ReplaceByFeeError::CreatePsbt(CreatePsbtError::NoRecipients)); } - let (change_info, change_script) = params - .change_script - .take() - .map(|change_script| (None, change_script)) - .unwrap_or_else(|| { - let (change_info, change_script) = self.peek_change_info(); + let (change_info, change_script) = match params.change_script.take() { + Some(change_script) => (None, change_script), + None => { + let change_keychain = + params + .change_keychain + .clone() + .ok_or(ReplaceByFeeError::CreatePsbt( + CreatePsbtError::NoChangeSource, + ))?; + let (change_info, change_script) = self + .peek_change_info(change_keychain) + .map_err(ReplaceByFeeError::CreatePsbt)?; (Some(change_info), change_script) - }); + } + }; let (assets, txouts) = self.parse_params(¶ms); @@ -2431,7 +2456,7 @@ impl Wallet { /// in that same insertion order. fn build_must_spend_inputs( &self, - params: &PsbtParams, + params: &PsbtParams, txouts: &HashMap>, assets: &Assets, ) -> Result, CreatePsbtError> { diff --git a/tests/create_psbt.rs b/tests/create_psbt.rs index 6bfc8a61..778e8016 100644 --- a/tests/create_psbt.rs +++ b/tests/create_psbt.rs @@ -50,6 +50,7 @@ fn test_create_psbt() { let addr = wallet.reveal_next_address(KeychainKind::External); let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::Internal); let feerate = FeeRate::from_sat_per_vb(4).unwrap(); let selection_strategy = psbt::SelectionStrategy::LowestFee { longterm_feerate: FeeRate::from_sat_per_vb(2).unwrap(), @@ -118,6 +119,7 @@ fn test_create_psbt_insufficient_funds_error() { let addr = wallet.reveal_next_address(KeychainKind::External); let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::Internal); params.add_recipients([(addr.script_pubkey(), Amount::from_sat(10_000))]); let result = wallet.create_psbt(params); @@ -161,6 +163,7 @@ fn test_create_psbt_maturity_height() { // The output is still immature at height = 99. let mut p = PsbtParams::default(); + p.change_keychain(KeychainKind::Internal); p.add_recipients([(send_to_address.clone(), Amount::from_sat(58_000))]) .maturity_height(bitcoin::absolute::Height::from_consensus(99).unwrap()); @@ -170,6 +173,7 @@ fn test_create_psbt_maturity_height() { // We can use the params to coerce the coinbase maturity. let mut p = PsbtParams::default(); + p.change_keychain(KeychainKind::Internal); p.add_recipients([(send_to_address.clone(), Amount::from_sat(58_000))]) .maturity_height(bitcoin::absolute::Height::from_consensus(100).unwrap()); @@ -185,6 +189,7 @@ fn test_create_psbt_maturity_height() { }; insert_checkpoint(&mut wallet, block_100); let mut p = PsbtParams::default(); + p.change_keychain(KeychainKind::Internal); p.add_recipients([(send_to_address.clone(), Amount::from_sat(58_000))]); let _ = wallet @@ -218,6 +223,7 @@ fn test_create_psbt_cltv() { // No assets fail { let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::External); params .add_utxos(&[op]) .add_recipients([(addr.script_pubkey(), Amount::from_btc(0.42).unwrap())]); @@ -231,6 +237,7 @@ fn test_create_psbt_cltv() { // Add assets ok { let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::External); params .add_utxos(&[op]) .add_assets(Assets::new().after(LockTime::from_consensus(100_000))) @@ -248,6 +255,7 @@ fn test_create_psbt_cltv() { insert_checkpoint(&mut wallet, block_id); let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::External); params .add_utxos(&[op]) .add_recipients([(addr.script_pubkey(), Amount::from_btc(0.42).unwrap())]); @@ -258,6 +266,7 @@ fn test_create_psbt_cltv() { // Locktime greater than required { let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::External); params .add_utxos(&[op]) .locktime(LockTime::from_consensus(200_000)) @@ -287,6 +296,7 @@ fn test_create_psbt_cltv_timestamp() { // No assets fail { let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::External); params .add_utxos(&[op]) .add_recipients([(addr.script_pubkey(), Amount::from_btc(0.42).unwrap())]); @@ -300,6 +310,7 @@ fn test_create_psbt_cltv_timestamp() { // Add assets ok { let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::External); params .add_utxos(&[op]) .add_assets(Assets::new().after(lock_time)) @@ -313,6 +324,7 @@ fn test_create_psbt_cltv_timestamp() { let new_lock_time = 1772167108; assert!(new_lock_time > lock_time.to_consensus_u32()); let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::External); params .add_utxos(&[op]) .add_assets(Assets::new().after(lock_time)) @@ -351,6 +363,7 @@ fn test_create_psbt_csv() { // No assets fail { let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::External); params .add_utxos(&[op]) .add_recipients([(addr.script_pubkey(), Amount::from_btc(0.42).unwrap())]); @@ -364,6 +377,7 @@ fn test_create_psbt_csv() { // Add assets ok { let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::External); let rel_locktime = relative::LockTime::from_consensus(6).unwrap(); params .add_utxos(&[op]) @@ -384,6 +398,7 @@ fn test_create_psbt_csv() { }; insert_checkpoint(&mut wallet, anchor.block_id); let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::External); params .add_utxos(&[op]) .add_recipients([(addr.script_pubkey(), Amount::from_btc(0.42).unwrap())]); @@ -399,6 +414,7 @@ fn test_create_psbt_fallback_sequence_applied_to_coin_selected_input() { let (mut wallet, _) = get_funded_wallet_wpkh(); let addr = wallet.next_unused_address(KeychainKind::External); let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::Internal); params .add_recipients([(addr.script_pubkey(), Amount::from_sat(25_000))]) .fallback_sequence(Sequence::ENABLE_RBF_NO_LOCKTIME); @@ -440,6 +456,7 @@ fn test_create_psbt_fallback_sequence_skipped_for_csv_input() { let addr = wallet.next_unused_address(KeychainKind::External); let rel_locktime = relative::LockTime::from_consensus(6).unwrap(); let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::External); params .add_utxos(&[op]) .add_assets(Assets::new().older(rel_locktime)) @@ -457,6 +474,7 @@ fn test_create_psbt_sequence_override_manually_selected_input() { let utxo = OutPoint::new(txid, 0); let addr = wallet.next_unused_address(KeychainKind::External); let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::Internal); params .add_recipients([(addr.script_pubkey(), Amount::from_sat(25_000))]) .add_utxos(&[utxo]) @@ -473,6 +491,7 @@ fn test_create_psbt_sequence_override_takes_precedence_over_fallback() { let utxo = OutPoint::new(txid, 0); let addr = wallet.next_unused_address(KeychainKind::External); let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::Internal); params .add_recipients([(addr.script_pubkey(), Amount::from_sat(25_000))]) .add_utxos(&[utxo]) @@ -513,6 +532,7 @@ fn test_create_psbt_sequence_override_csv_conflict_returns_error() { let addr = wallet.next_unused_address(KeychainKind::External); let rel_locktime = relative::LockTime::from_consensus(6).unwrap(); let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::External); params .add_utxos(&[op]) .add_assets(Assets::new().older(rel_locktime)) @@ -639,6 +659,7 @@ fn test_replace_by_fee_replaces_descendant_fees() { // Build replacement A'. The wallet walks A's descendants (B and C) so their // fees are included in the minimum required replacement fee. let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::Internal); params.add_recipients([(external, Amount::from_sat(100_000))]); params.fee_rate(FeeRate::from_sat_per_vb(4).unwrap()); let params = params.replace_txs([tx_a]); @@ -693,6 +714,7 @@ fn test_replace_by_fee_confirmed_tx_error() { ScriptBuf::from_hex("5120e8f5c4dc2f5d6a7595e7b108cb063da9c7550312da1e22875d78b9db62b59cd5") .unwrap(); let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::Internal); params .add_utxos(&[funding_op]) .add_recipients([(recip.clone(), Amount::from_sat(100_000))]); @@ -709,6 +731,7 @@ fn test_replace_by_fee_confirmed_tx_error() { // Attempting to replace the now-confirmed tx should return TransactionConfirmed. let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::Internal); params.add_recipients([(recip, Amount::from_sat(10_000))]); params.fee_rate(FeeRate::from_sat_per_vb(10).unwrap()); let params = params.replace_txs([unconfirmed_tx]); @@ -757,6 +780,7 @@ fn test_replace_by_fee_no_inputs_from_original() { ScriptBuf::from_hex("5120e8f5c4dc2f5d6a7595e7b108cb063da9c7550312da1e22875d78b9db62b59cd5") .unwrap(); let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::Internal); params .add_utxos(&[funding_op]) .add_recipients([(recip.clone(), Amount::from_sat(100_000))]); @@ -766,6 +790,7 @@ fn test_replace_by_fee_no_inputs_from_original() { // Build replacement params with a recipient but remove the original inputs. let mut params = PsbtParams::default().replace_txs([unconfirmed_tx]); + params.change_keychain(KeychainKind::Internal); params .remove_utxo(&funding_op) .add_recipients([(recip, Amount::from_sat(50_000))]); @@ -839,6 +864,7 @@ fn test_replace_by_fee_conflicting_input_descendant() { // tx_parent: the transaction we will eventually replace. let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::Internal); params .add_utxos(&[funding_op]) .add_recipients([(recip.clone(), Amount::from_sat(100_000))]); @@ -884,6 +910,7 @@ fn test_replace_by_fee_conflicting_input_descendant() { // Build replacement for tx_parent, adding the grandchild planned input. let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::Internal); params.add_planned_input(grandchild_input); params.add_recipients([(recip, Amount::from_sat(50_000))]); let params = params.replace_txs([tx_parent]); @@ -925,6 +952,7 @@ fn test_create_psbt_utxo_filter() { assert_eq!(wallet.balance().total().to_sat(), 2100); let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::Internal); params.fee_rate(FeeRate::ZERO); // Avoid selection of dust utxos params.utxo_filter(|txo| { @@ -979,6 +1007,7 @@ fn test_create_psbt_no_recipients_error() { // drain_wallet with an explicit change_script and no recipients should succeed (sweep to // change). let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::Internal); let change_descriptor = wallet .public_descriptor(KeychainKind::Internal) .at_derivation_index(0) @@ -1023,6 +1052,7 @@ fn test_create_psbt_drain_wallet_change_below_dust_error() { .at_derivation_index(0) .unwrap(); let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::Internal); params .coin_selection(SelectionStrategy::All) .change_script(ChangeScript::from_descriptor(change_descriptor)); @@ -1088,6 +1118,7 @@ fn test_replace_by_fee_drain_wallet_change_below_dust_error() { .at_derivation_index(0) .unwrap(); let mut params = PsbtParams::default().replace_txs([original_tx]); + params.change_keychain(KeychainKind::Internal); params .coin_selection(SelectionStrategy::All) .change_script(ChangeScript::from_descriptor(change_descriptor)); @@ -1154,6 +1185,7 @@ fn test_replace_tx_with_planned_input() { .unwrap(); let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::Internal); params .add_utxos(&[funding_op]) .add_planned_input(planned_input.clone()) @@ -1164,6 +1196,7 @@ fn test_replace_tx_with_planned_input() { // Add the planned input *before* calling replace_txs. The replace() method // should respect pre-registered planned inputs in the unique set. let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::Internal); params .add_planned_input(planned_input.clone()) .add_recipients([(recip, Amount::from_sat(99_000))]); @@ -1222,6 +1255,7 @@ fn test_add_planned_psbt_input() -> anyhow::Result<()> { // Build tx: 2-in / 2-out let mut params = PsbtParams::default(); + params.change_keychain(KeychainKind::Internal); params.add_utxos(&[op1]); params.add_planned_input(input); params.add_recipients([(send_to, Amount::from_sat(20_000))]); @@ -1245,3 +1279,130 @@ fn test_add_planned_psbt_input() -> anyhow::Result<()> { Ok(()) } + +/// A wallet keychain type that is not `KeychainKind`, with more than two keychains and no +/// conventional "internal" one. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum Keychain { + Receive, + Change, + Cold, +} + +/// `create_psbt` works on a wallet generic over `K`, given an explicit change keychain. +#[test] +fn test_create_psbt_custom_keychain() { + use bdk_chain::CheckPoint; + use bdk_wallet::Update; + use bitcoin::BlockHash; + use std::sync::Arc; + + let (desc, change_desc) = get_test_tr_single_sig_xprv_and_change_desc(); + let cold_desc = get_test_wpkh(); + + let mut keyring = + KeyRing::new(Network::Regtest, Keychain::Receive, desc).expect("valid descriptor"); + keyring + .add_descriptor(Keychain::Change, change_desc) + .expect("valid change descriptor"); + keyring + .add_descriptor(Keychain::Cold, cold_desc) + .expect("valid cold descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); + + // Fund the wallet with a confirmed output on the `Receive` keychain. + let addr = wallet.reveal_next_address(Keychain::Receive).address; + let tx = Transaction { + version: bitcoin::transaction::Version::ONE, + lock_time: absolute::LockTime::ZERO, + input: vec![], + output: vec![TxOut { + value: Amount::ONE_BTC, + script_pubkey: addr.script_pubkey(), + }], + }; + let txid = tx.compute_txid(); + let genesis = BlockId { + height: 0, + hash: BlockHash::from_byte_array(Network::Regtest.chain_hash().to_bytes()), + }; + let tip = BlockId { + height: 100, + hash: Hash::hash(b"100"), + }; + let anchor = ConfirmationBlockTime { + block_id: tip, + confirmation_time: 1234567000, + }; + let mut update = Update { + chain: CheckPoint::from_block_ids([genesis, tip]).ok(), + ..Default::default() + }; + update.tx_update.txs = vec![Arc::new(tx)]; + update.tx_update.anchors = [(anchor, txid)].into(); + wallet.apply_update(update).expect("update must apply"); + assert_eq!(wallet.balance().total(), Amount::ONE_BTC); + + // Without a change source, PSBT creation fails. + let mut params = PsbtParams::default(); + params.add_recipients([( + ScriptBuf::new_op_return([0xb1, 0x0c]), + Amount::from_sat(10_000), + )]); + let err = wallet.create_psbt(params).unwrap_err(); + assert!( + matches!(err, CreatePsbtError::NoChangeSource), + "expected NoChangeSource, got {err:?}" + ); + + // With an explicit change keychain, it succeeds and change lands on that keychain. + let mut params = PsbtParams::default(); + params + .add_recipients([( + ScriptBuf::new_op_return([0xb1, 0x0c]), + Amount::from_sat(10_000), + )]) + .change_keychain(Keychain::Change); + let (psbt, _finalizer) = wallet.create_psbt(params).expect("psbt must be created"); + + let change_spk = wallet + .peek_address(Keychain::Change, 0) + .address + .script_pubkey(); + assert!( + psbt.unsigned_tx + .output + .iter() + .any(|txo| txo.script_pubkey == change_spk), + "change output must be derived from the Change keychain" + ); + // The change address was revealed and staged. + assert_eq!(wallet.derivation_index(Keychain::Change), Some(0)); +} + +/// Naming a change keychain the wallet does not hold is an error, not a silent fallback. +#[test] +fn test_create_psbt_unknown_change_keychain() { + let (desc, change_desc) = get_test_tr_single_sig_xprv_and_change_desc(); + + // Wallet holds `Receive` and `Change`, but not `Cold`. + let mut keyring = + KeyRing::new(Network::Regtest, Keychain::Receive, desc).expect("valid descriptor"); + keyring + .add_descriptor(Keychain::Change, change_desc) + .expect("valid change descriptor"); + let mut wallet = Wallet::create(keyring).create_wallet_no_persist(); + + let mut params = PsbtParams::default(); + params + .add_recipients([( + ScriptBuf::new_op_return([0xb1, 0x0c]), + Amount::from_sat(10_000), + )]) + .change_keychain(Keychain::Cold); + let err = wallet.create_psbt(params).unwrap_err(); + assert!( + matches!(err, CreatePsbtError::UnknownChangeKeychain), + "expected UnknownChangeKeychain, got {err:?}" + ); +}