From c21b0ff19d49828bae2c51e1fa7201469e22232d Mon Sep 17 00:00:00 2001 From: valued mammal Date: Sat, 27 Dec 2025 10:58:30 -0500 Subject: [PATCH 1/2] deps: Update `bdk_chain` to latest (b4c8c6a1) Update dependencies: chain core bitcoind_rpc -> 0.22.0 electrum esplora file_store -> 0.22.0 Changes: - Update usage of `LocalChain` and `CheckPoint` which are now generic. - Change structure of `WalletTx` (alias for `CanonicalTx`). - Introduce `Wallet::canonical_view`. - Update `Wallet::balance` with re-defined trust predicate. - Updated docs for `apply_update` to include an Errors section and a note about the canonical view. - Changed `insert_txout`, `apply_evicted_txs` to internally use `apply_update` as a way to centralize the update logic when it comes to adding transaction data to the wallet. - `apply_unconfirmed_txs` now calls `self.update_canonical_view` internally. --- Cargo.toml | 29 ++- examples/bitcoind_rpc.rs | 3 +- src/test_utils.rs | 10 +- src/wallet/changeset.rs | 2 +- src/wallet/error.rs | 10 +- src/wallet/export.rs | 2 +- src/wallet/mod.rs | 362 ++++++++++++++++++-------------------- src/wallet/tx_builder.rs | 2 +- tests/add_foreign_utxo.rs | 6 +- tests/build_fee_bump.rs | 11 +- tests/persisted_wallet.rs | 5 +- tests/wallet.rs | 51 ++---- tests/wallet_event.rs | 22 ++- 13 files changed, 253 insertions(+), 262 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9b42de687..3f4f25403 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ serde = { version = "1", features = ["derive"] } # Optional dependencies anyhow = { version = "1.0.98", optional = true } -bdk_file_store = { version = "0.21.1", optional = true } +bdk_file_store = { version = "0.22.0", optional = true } bip39 = { version = "2.0", optional = true } tempfile = { version = "3.20.0", optional = true } @@ -46,7 +46,7 @@ test-utils = ["std", "anyhow", "tempfile"] [dev-dependencies] anyhow = "1" assert_matches = "1.5.0" -bdk_bitcoind_rpc = { version = "0.21.0" } +bdk_bitcoind_rpc = { version = "0.22.0" } bdk_electrum = { version = "0.23.1" } bdk_esplora = { version = "0.22.1", features = ["async-https", "blocking-https", "tokio"] } bdk_wallet = { path = ".", features = ["rusqlite", "file_store", "test-utils"] } @@ -77,3 +77,28 @@ name = "esplora_blocking" [[example]] name = "bitcoind_rpc" + + +[patch.crates-io.bdk_chain] +git = "https://github.com/bitcoindevkit/bdk" +rev = "b4c8c6a110de14c1b04d2a525cd8fb3e4d67cec6" + +[patch.crates-io.bdk_core] +git = "https://github.com/bitcoindevkit/bdk" +rev = "b4c8c6a110de14c1b04d2a525cd8fb3e4d67cec6" + +[patch.crates-io.bdk_bitcoind_rpc] +git = "https://github.com/bitcoindevkit/bdk" +rev = "b4c8c6a110de14c1b04d2a525cd8fb3e4d67cec6" + +[patch.crates-io.bdk_electrum] +git = "https://github.com/bitcoindevkit/bdk" +rev = "b4c8c6a110de14c1b04d2a525cd8fb3e4d67cec6" + +[patch.crates-io.bdk_esplora] +git = "https://github.com/bitcoindevkit/bdk" +rev = "b4c8c6a110de14c1b04d2a525cd8fb3e4d67cec6" + +[patch.crates-io.bdk_file_store] +git = "https://github.com/bitcoindevkit/bdk" +rev = "b4c8c6a110de14c1b04d2a525cd8fb3e4d67cec6" diff --git a/examples/bitcoind_rpc.rs b/examples/bitcoind_rpc.rs index f0bbd7290..733f8950a 100644 --- a/examples/bitcoind_rpc.rs +++ b/examples/bitcoind_rpc.rs @@ -144,7 +144,8 @@ fn main() -> anyhow::Result<()> { args.start_height, wallet .transactions() - .filter(|tx| tx.chain_position.is_unconfirmed()), + .filter(|tx| tx.pos.is_unconfirmed()) + .map(|tx| tx.tx), ); spawn(move || -> Result<(), anyhow::Error> { while let Some(emission) = emitter.next_block()? { diff --git a/src/test_utils.rs b/src/test_utils.rs index ff288ac8d..f8c391934 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -124,7 +124,13 @@ pub fn new_wallet_and_funding_update( block_id: b3, confirmation_time: 200, }; - update.chain = CheckPoint::from_block_ids([b0, b1, b2, b3]).ok(); + update.chain = CheckPoint::from_blocks([ + (b0.height, b0.hash), + (b1.height, b1.hash), + (b2.height, b2.hash), + (b3.height, b3.hash), + ]) + .ok(); update.tx_update.anchors = [(a2, tx0.compute_txid()), (a3, tx1.compute_txid())].into(); update.tx_update.txs = [Arc::new(tx0), Arc::new(tx1)].into(); @@ -305,7 +311,7 @@ pub fn receive_output_to_address( /// a different one at the same height, then all later blocks are evicted as well. pub fn insert_checkpoint(wallet: &mut Wallet, block: BlockId) { let mut cp = wallet.latest_checkpoint(); - cp = cp.insert(block); + cp = cp.insert(block.height, block.hash); wallet .apply_update(Update { chain: Some(cp), diff --git a/src/wallet/changeset.rs b/src/wallet/changeset.rs index 0945d7a13..86815a5fd 100644 --- a/src/wallet/changeset.rs +++ b/src/wallet/changeset.rs @@ -97,7 +97,7 @@ type IndexedTxGraphChangeSet = /// [merged]: bdk_chain::Merge /// [`network`]: Self::network /// [`PersistedWallet`]: crate::PersistedWallet -/// [SQLite]: bdk_chain::rusqlite_impl +/// [SQLite]: /// [`Update`]: crate::Update /// [`WalletPersister`]: crate::WalletPersister /// [`Wallet::staged`]: crate::Wallet::staged diff --git a/src/wallet/error.rs b/src/wallet/error.rs index d88b64b4a..9a7cd6612 100644 --- a/src/wallet/error.rs +++ b/src/wallet/error.rs @@ -13,8 +13,7 @@ use crate::descriptor::policy::PolicyError; use crate::descriptor::{DescriptorError, ExtendedDescriptor}; -use crate::wallet::coin_selection; -use crate::{descriptor, KeychainKind, LoadWithPersistError}; +use crate::{coin_selection, descriptor, KeychainKind, LoadWithPersistError}; use alloc::{ boxed::Box, string::{String, ToString}, @@ -22,8 +21,8 @@ use alloc::{ use bitcoin::{absolute, psbt, Amount, BlockHash, Network, OutPoint, Sequence, Txid}; use core::fmt; -/// The error type when loading a [`Wallet`] from a [`ChangeSet`]. -#[derive(Debug, PartialEq)] +/// The error type when loading a [`Wallet`](crate::Wallet) from a [`ChangeSet`](crate::ChangeSet). +#[derive(Debug)] pub enum LoadError { /// There was a problem with the passed-in descriptor(s). Descriptor(crate::descriptor::DescriptorError), @@ -54,7 +53,8 @@ impl fmt::Display for LoadError { #[cfg(feature = "std")] impl std::error::Error for LoadError {} -/// Represents a mismatch with what is loaded and what is expected from [`LoadParams`]. +/// Represents a mismatch with what is loaded and what is expected from +/// [`LoadParams`](crate::LoadParams). #[derive(Debug, PartialEq)] pub enum LoadMismatch { /// Network does not match. diff --git a/src/wallet/export.rs b/src/wallet/export.rs index 58137abee..ef61bb756 100644 --- a/src/wallet/export.rs +++ b/src/wallet/export.rs @@ -212,7 +212,7 @@ impl FullyNodedExport { let blockheight = if include_blockheight { wallet.transactions().next().map_or(0, |canonical_tx| { canonical_tx - .chain_position + .pos .confirmation_height_upper_bound() .unwrap_or(0) }) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 886dea5d2..d74b1b61d 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -29,9 +29,9 @@ use bdk_chain::{ FullScanRequest, FullScanRequestBuilder, FullScanResponse, SyncRequest, SyncRequestBuilder, SyncResponse, }, - tx_graph::{CalculateFeeError, CanonicalTx, TxGraph, TxUpdate}, - BlockId, CanonicalizationParams, ChainPosition, ConfirmationBlockTime, DescriptorExt, - FullTxOut, Indexed, IndexedTxGraph, Indexer, Merge, + tx_graph::{CalculateFeeError, TxGraph, TxUpdate}, + BlockId, CanonicalTx, CanonicalizationParams, ChainPosition, ConfirmationBlockTime, + DescriptorExt, FullTxOut, Indexed, IndexedTxGraph, Indexer, Merge, }; use bitcoin::{ absolute, @@ -40,8 +40,8 @@ use bitcoin::{ psbt, secp256k1::Secp256k1, sighash::{EcdsaSighashType, TapSighashType}, - transaction, Address, Amount, Block, FeeRate, Network, NetworkKind, OutPoint, Psbt, ScriptBuf, - Sequence, SignedAmount, Transaction, TxOut, Txid, Weight, Witness, + transaction, Address, Amount, Block, BlockHash, FeeRate, Network, NetworkKind, OutPoint, Psbt, + ScriptBuf, Sequence, SignedAmount, Transaction, TxOut, Txid, Weight, Witness, }; use miniscript::{ descriptor::KeyMap, @@ -62,7 +62,7 @@ pub mod signer; pub mod tx_builder; pub(crate) mod utils; -use crate::collections::{BTreeMap, HashMap, HashSet}; +use crate::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use crate::descriptor::{ check_wallet_descriptor, error::Error as DescriptorError, policy::BuildSatisfaction, DerivedDescriptor, DescriptorMeta, ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, @@ -88,6 +88,9 @@ pub use persisted::*; pub use utils::IsDust; pub use utils::TxDetails; +/// A consistent view of canonical transactions. +pub type CanonicalView = bdk_chain::CanonicalView; + /// A Bitcoin wallet /// /// The `Wallet` acts as a way of coherently interfacing with output descriptors and related @@ -115,6 +118,7 @@ pub struct Wallet { network: Network, secp: SecpCtx, locked_outpoints: HashSet, + canonical_view: CanonicalView, } /// An update to [`Wallet`]. @@ -180,7 +184,7 @@ impl fmt::Display for AddressInfo { } /// A `CanonicalTx` managed by a `Wallet`. -pub type WalletTx<'a> = CanonicalTx<'a, Arc, ConfirmationBlockTime>; +pub type WalletTx = CanonicalTx; impl Wallet { /// Build a new single descriptor [`Wallet`]. @@ -316,7 +320,7 @@ impl Wallet { let genesis_hash = params .genesis_hash .unwrap_or(genesis_block(network).block_hash()); - let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash); + let (chain, chain_changeset) = LocalChain::from_genesis(genesis_hash); let (descriptor, mut descriptor_keymap) = (params.descriptor)(&secp, network_kind)?; check_wallet_descriptor(&descriptor)?; @@ -363,6 +367,12 @@ impl Wallet { params.use_spk_cache, )?; + let canonical_view = tx_graph.canonical_view( + &chain, + chain.tip().block_id(), + CanonicalizationParams::default(), + ); + Ok(Wallet { signers, change_signers, @@ -372,6 +382,7 @@ impl Wallet { stage, secp, locked_outpoints, + canonical_view, }) } @@ -570,6 +581,12 @@ impl Wallet { ) .map_err(LoadError::Descriptor)?; + let canonical_view = tx_graph.canonical_view( + &chain, + chain.tip().block_id(), + CanonicalizationParams::default(), + ); + Ok(Some(Wallet { signers, change_signers, @@ -579,6 +596,7 @@ impl Wallet { network, secp, locked_outpoints, + canonical_view, })) } @@ -776,14 +794,8 @@ impl Wallet { /// Return the list of unspent outputs of this wallet pub fn list_unspent(&self) -> impl Iterator + '_ { - self.tx_graph - .graph() - .filter_chain_unspents( - &self.chain, - self.chain.tip().block_id(), - CanonicalizationParams::default(), - self.tx_graph.index.outpoints().iter().cloned(), - ) + self.canonical_view + .filter_unspent_outpoints(self.tx_graph.index.outpoints().iter().cloned()) .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo)) } @@ -792,13 +804,13 @@ impl Wallet { /// If the transaction with txid [`Txid`] cannot be found in the wallet's transactions, `None` /// is returned. pub fn tx_details(&self, txid: Txid) -> Option { - let tx: WalletTx = self.transactions().find(|c| c.tx_node.txid == txid)?; + let tx: WalletTx = self.transactions().find(|c| c.txid == txid)?; - let (sent, received) = self.sent_and_received(&tx.tx_node.tx); - let fee: Option = self.calculate_fee(&tx.tx_node.tx).ok(); - let fee_rate: Option = self.calculate_fee_rate(&tx.tx_node.tx).ok(); - let balance_delta: SignedAmount = self.tx_graph.index.net_value(&tx.tx_node.tx, ..); - let chain_position = tx.chain_position; + let (sent, received) = self.sent_and_received(&tx.tx); + let fee: Option = self.calculate_fee(&tx.tx).ok(); + let fee_rate: Option = self.calculate_fee_rate(&tx.tx).ok(); + let balance_delta: SignedAmount = self.tx_graph.index.net_value(&tx.tx, ..); + let chain_position = tx.pos; let tx_details: TxDetails = TxDetails { txid, @@ -808,29 +820,45 @@ impl Wallet { fee_rate, balance_delta, chain_position, - tx: tx.tx_node.tx, + tx: tx.tx, }; Some(tx_details) } + /// Get a reference to the latest [`CanonicalView`] of transactions. + pub fn canonical_view(&self) -> &CanonicalView { + &self.canonical_view + } + + /// Obtain a new [`CanonicalView`] modified by the `params`. + /// + /// Note, the result of this call is not stored anywhere. + pub fn canonical_view_with_params(&self, params: CanonicalizationParams) -> CanonicalView { + self.tx_graph + .canonical_view(&self.chain, self.chain.tip().block_id(), params) + } + + /// Update the wallet's [`CanonicalView`] of transactions. + fn update_canonical_view(&mut self) { + self.canonical_view = self.tx_graph.canonical_view( + &self.chain, + self.chain.tip().block_id(), + CanonicalizationParams::default(), + ) + } + /// 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 + '_ { - self.tx_graph - .graph() - .filter_chain_txouts( - &self.chain, - self.chain.tip().block_id(), - CanonicalizationParams::default(), - self.tx_graph.index.outpoints().iter().cloned(), - ) - .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo)) + self.canonical_view + .filter_outpoints(self.tx_graph.index.outpoints().iter().cloned()) + .map(|((k, i), txo)| new_local_utxo(k, i, txo)) } /// Get all the checkpoints the wallet is currently storing indexed by height. - pub fn checkpoints(&self) -> CheckPointIter { + pub fn checkpoints(&self) -> CheckPointIter { self.chain.iter_checkpoints() } @@ -869,19 +897,13 @@ impl Wallet { } /// Returns the utxo owned by this wallet corresponding to `outpoint` if it exists in the - /// wallet's database. + /// wallet's transaction graph. pub fn get_utxo(&self, op: OutPoint) -> Option { let ((keychain, index), _) = self.tx_graph.index.txout(op)?; - self.tx_graph - .graph() - .filter_chain_unspents( - &self.chain, - self.chain.tip().block_id(), - CanonicalizationParams::default(), - core::iter::once(((), op)), - ) - .map(|(_, full_txo)| new_local_utxo(keychain, index, full_txo)) + self.canonical_view + .filter_unspent_outpoints([((), op)]) .next() + .map(|(_, txo)| new_local_utxo(keychain, index, txo)) } /// Inserts a [`TxOut`] at [`OutPoint`] into the wallet's transaction graph. @@ -902,8 +924,13 @@ impl Wallet { /// [`list_unspent`]: Self::list_unspent /// [`list_output`]: Self::list_output pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) { - let additions = self.tx_graph.insert_txout(outpoint, txout); - self.stage.merge(additions.into()); + let mut tx_update = TxUpdate::default(); + tx_update.txouts = [(outpoint, txout)].into(); + self.apply_update(Update { + tx_update, + ..Default::default() + }) + .expect("Applying a `TxUpdate` cannot fail") } /// Calculates the fee of a given transaction. Returns [`Amount::ZERO`] if `tx` is a coinbase @@ -921,7 +948,7 @@ impl Wallet { /// # use bdk_wallet::Wallet; /// # let mut wallet: Wallet = todo!(); /// # let txid:Txid = todo!(); - /// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + /// let tx = wallet.get_tx(txid).expect("transaction").tx; /// let fee = wallet.calculate_fee(&tx).expect("fee"); /// ``` /// @@ -952,7 +979,7 @@ impl Wallet { /// # use bdk_wallet::Wallet; /// # let mut wallet: Wallet = todo!(); /// # let txid:Txid = todo!(); - /// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + /// let tx = wallet.get_tx(txid).expect("transaction").tx; /// let fee_rate = wallet.calculate_fee_rate(&tx).expect("fee rate"); /// ``` /// @@ -982,7 +1009,7 @@ impl Wallet { /// # use bdk_wallet::Wallet; /// # let mut wallet: Wallet = todo!(); /// # let txid:Txid = todo!(); - /// let tx = wallet.get_tx(txid).expect("tx exists").tx_node.tx; + /// let tx = wallet.get_tx(txid).expect("tx exists").tx; /// let (sent, received) = wallet.sent_and_received(&tx); /// ``` /// @@ -1016,19 +1043,11 @@ impl Wallet { /// /// let wallet_tx = wallet.get_tx(my_txid).expect("panic if tx does not exist"); /// - /// // get reference to full transaction - /// println!("my tx: {:#?}", wallet_tx.tx_node.tx); + /// // Get reference to full transaction + /// println!("my tx: {:#?}", wallet_tx.tx); /// - /// // list all transaction anchors - /// for anchor in wallet_tx.tx_node.anchors { - /// println!( - /// "tx is anchored by block of hash {}", - /// anchor.anchor_block().hash - /// ); - /// } - /// - /// // get confirmation status of transaction - /// match wallet_tx.chain_position { + /// // Get confirmation status of transaction + /// match wallet_tx.pos { /// ChainPosition::Confirmed { /// anchor, /// transitively: None, @@ -1051,15 +1070,10 @@ impl Wallet { /// ``` /// /// [`Anchor`]: bdk_chain::Anchor - pub fn get_tx(&self, txid: Txid) -> Option> { - let graph = self.tx_graph.graph(); - graph - .list_canonical_txs( - &self.chain, - self.chain.tip().block_id(), - CanonicalizationParams::default(), - ) - .find(|tx| tx.tx_node.txid == txid) + pub fn get_tx(&self, txid: Txid) -> Option { + self.canonical_view + .txs() + .find(|canon_tx| canon_tx.txid == txid) } /// Iterate over relevant and canonical transactions in the wallet. @@ -1072,17 +1086,11 @@ impl Wallet { /// [`TxGraph::full_txs`]. /// /// To iterate over all canonical transactions, including those that are irrelevant, use - /// [`TxGraph::list_canonical_txs`]. - pub fn transactions(&self) -> impl Iterator> + '_ { - let tx_graph = self.tx_graph.graph(); - let tx_index = &self.tx_graph.index; - tx_graph - .list_canonical_txs( - &self.chain, - self.chain.tip().block_id(), - CanonicalizationParams::default(), - ) - .filter(|c_tx| tx_index.is_tx_relevant(&c_tx.tx_node.tx)) + /// [`CanonicalView::txs`]. + pub fn transactions(&self) -> impl Iterator + '_ { + self.canonical_view + .txs() + .filter(|canon_tx| self.tx_graph.index.is_tx_relevant(&canon_tx.tx)) } /// Array of relevant and canonical transactions in the wallet sorted with a comparator @@ -1095,13 +1103,12 @@ impl Wallet { /// /// ```rust,no_run /// # use bdk_wallet::{LoadParams, Wallet, WalletTx}; - /// # let mut wallet:Wallet = todo!(); + /// # 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)); + /// let sorted_txs: Vec = wallet.transactions_sort_by(|tx1, tx2| tx2.pos.cmp(&tx1.pos)); /// # Ok::<(), anyhow::Error>(()) /// ``` - pub fn transactions_sort_by(&self, compare: F) -> Vec> + pub fn transactions_sort_by(&self, compare: F) -> Vec where F: FnMut(&WalletTx, &WalletTx) -> Ordering, { @@ -1113,15 +1120,29 @@ impl Wallet { /// 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.canonical_view.balance( self.tx_graph.index.outpoints().iter().cloned(), - |&(k, _), _| k == KeychainKind::Internal, + |_, txo| self.is_tx_trusted(txo.outpoint.txid), + /* min_confirmations: */ 1, ) } + /// Whether the transaction of `txid` is trusted by this wallet. + /// + /// A tx is considered "trusted" if all of the inputs are controlled by this wallet, + /// i.e. the input corresponds to an outpoint that is indexed under a tracked keychain. + fn is_tx_trusted(&self, txid: Txid) -> bool { + let Some(tx) = self.tx_graph.graph().get_tx(txid) else { + return false; + }; + if tx.input.is_empty() { + return false; + } + tx.input + .iter() + .all(|txin| self.tx_graph.index.txout(txin.previous_output).is_some()) + } + /// Add an external signer /// /// See [the `signer` module](signer) for an example. @@ -1594,10 +1615,10 @@ impl Wallet { ) -> 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)) + let chain_positions: HashMap> = self + .canonical_view + .txs() + .map(|canon_tx| (canon_tx.txid, canon_tx.pos)) .collect(); let mut tx = tx_graph @@ -1841,24 +1862,22 @@ impl Wallet { 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)) + .canonical_view + .txs() + .filter(|canon_tx| prev_txids.contains(&canon_tx.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 { + let txid = canon_tx.txid; + match canon_tx.pos { ChainPosition::Confirmed { anchor, .. } => (txid, anchor.block_id.height), ChainPosition::Unconfirmed { .. } => (txid, u32::MAX), } @@ -2001,17 +2020,9 @@ impl Wallet { .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(), - ) + self.canonical_view + // Get all unspent UTXOs from wallet. + .filter_unspent_outpoints(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. @@ -2224,8 +2235,15 @@ impl Wallet { /// 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`]. + /// After applying updates the [`canonical_view`](Self::canonical_view) of transactions is + /// updated to reflect changes to the transaction graph. + /// + /// You should persist the staged wallet changes. For an example of how to persist staged + /// wallet changes see [`Wallet::reveal_next_address`]. + /// + /// # Errors + /// + /// If the [`Update::chain`] update fails, a [`CannotConnectError`] will occur. pub fn apply_update(&mut self, update: impl Into) -> Result<(), CannotConnectError> { let update = update.into(); let mut changeset = match update.chain { @@ -2240,6 +2258,7 @@ impl Wallet { changeset.merge(index_changeset.into()); changeset.merge(self.tx_graph.apply_update(update.tx_update).into()); self.stage.merge(changeset); + self.update_canonical_view(); Ok(()) } @@ -2329,30 +2348,14 @@ impl Wallet { ) -> Result, CannotConnectError> { // snapshot of chain tip and transactions before update let chain_tip1 = self.chain.tip().block_id(); - let wallet_txs1 = self - .transactions() - .map(|wtx| { - ( - wtx.tx_node.txid, - (wtx.tx_node.tx.clone(), wtx.chain_position), - ) - }) - .collect::, ChainPosition)>>(); + let wallet_txs1 = self.map_transactions(); // apply update self.apply_update(update)?; // chain tip and transactions after update let chain_tip2 = self.chain.tip().block_id(); - let wallet_txs2 = self - .transactions() - .map(|wtx| { - ( - wtx.tx_node.txid, - (wtx.tx_node.tx.clone(), wtx.chain_position), - ) - }) - .collect::, ChainPosition)>>(); + let wallet_txs2 = self.map_transactions(); Ok(wallet_events( self, @@ -2491,29 +2494,13 @@ impl Wallet { ) -> Result, CannotConnectError> { // snapshot of chain tip and transactions before update let chain_tip1 = self.chain.tip().block_id(); - let wallet_txs1 = self - .transactions() - .map(|wtx| { - ( - wtx.tx_node.txid, - (wtx.tx_node.tx.clone(), wtx.chain_position), - ) - }) - .collect::, ChainPosition)>>(); + let wallet_txs1 = self.map_transactions(); self.apply_block(block, height)?; // chain tip and transactions after update let chain_tip2 = self.chain.tip().block_id(); - let wallet_txs2 = self - .transactions() - .map(|wtx| { - ( - wtx.tx_node.txid, - (wtx.tx_node.tx.clone(), wtx.chain_position), - ) - }) - .collect::, ChainPosition)>>(); + let wallet_txs2 = self.map_transactions(); Ok(wallet_events( self, @@ -2548,6 +2535,7 @@ impl Wallet { ); changeset.merge(self.tx_graph.apply_block_relevant(block, height).into()); self.stage.merge(changeset); + self.update_canonical_view(); Ok(()) } @@ -2568,29 +2556,13 @@ impl Wallet { ) -> Result, ApplyHeaderError> { // snapshot of chain tip and transactions before update let chain_tip1 = self.chain.tip().block_id(); - let wallet_txs1 = self - .transactions() - .map(|wtx| { - ( - wtx.tx_node.txid, - (wtx.tx_node.tx.clone(), wtx.chain_position), - ) - }) - .collect::, ChainPosition)>>(); + let wallet_txs1 = self.map_transactions(); self.apply_block_connected_to(block, height, connected_to)?; // chain tip and transactions after update let chain_tip2 = self.chain.tip().block_id(); - let wallet_txs2 = self - .transactions() - .map(|wtx| { - ( - wtx.tx_node.txid, - (wtx.tx_node.tx.clone(), wtx.chain_position), - ) - }) - .collect::, ChainPosition)>>(); + let wallet_txs2 = self.map_transactions(); Ok(wallet_events( self, @@ -2621,6 +2593,7 @@ impl Wallet { .tx_graph .batch_insert_relevant_unconfirmed(unconfirmed_txs); self.stage.merge(indexed_graph_changeset.into()); + self.update_canonical_view(); } /// Apply evictions of the given transaction IDs with their associated timestamps. @@ -2662,25 +2635,28 @@ impl Wallet { /// [`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) + let canon_txids: BTreeSet = self.canonical_view.txs().map(|c| c.txid).collect(); + let mut tx_update = TxUpdate::default(); + tx_update.evicted_ats = evicted_txs + .into_iter() + .filter(|(txid, _)| canon_txids.contains(txid)) .collect(); + self.apply_update(Update { + tx_update, + ..Default::default() + }) + .expect("Applying a TxUpdate should not fail"); + } - 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()); + /// 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(|canon_tx| (canon_tx.txid, (canon_tx.tx, canon_tx.pos))) + .collect() } /// Used internally to ensure that all methods requiring a [`KeychainKind`] will use a @@ -2710,11 +2686,10 @@ impl Wallet { 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(), - .., - )) + .expected_spk_txids( + self.canonical_view + .list_expected_spk_txids(&self.tx_graph.index, ..), + ) } /// Create a partial [`SyncRequest`] for this wallet for all revealed spks. @@ -2735,11 +2710,10 @@ impl Wallet { 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(), - .., - )) + .expected_spk_txids( + self.canonical_view + .list_expected_spk_txids(&self.tx_graph.index, ..), + ) } /// Create a [`FullScanRequest] for this wallet. diff --git a/src/wallet/tx_builder.rs b/src/wallet/tx_builder.rs index 9ad794f85..51396ff9a 100644 --- a/src/wallet/tx_builder.rs +++ b/src/wallet/tx_builder.rs @@ -1345,7 +1345,7 @@ mod test { assert_ne!(txid1, txid2); let utxo1 = wallet1.list_unspent().next().unwrap(); - let tx1 = wallet1.get_tx(txid1).unwrap().tx_node.tx.clone(); + let tx1 = wallet1.get_tx(txid1).unwrap().tx.clone(); let satisfaction_weight = wallet1 .public_descriptor(KeychainKind::External) diff --git a/tests/add_foreign_utxo.rs b/tests/add_foreign_utxo.rs index 1dd0a8c9d..a7feb6ba3 100644 --- a/tests/add_foreign_utxo.rs +++ b/tests/add_foreign_utxo.rs @@ -139,8 +139,8 @@ fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() { get_funded_wallet_single("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); let utxo2 = wallet2.list_unspent().next().unwrap(); - let tx1 = wallet1.get_tx(txid1).unwrap().tx_node.tx.clone(); - let tx2 = wallet2.get_tx(txid2).unwrap().tx_node.tx.clone(); + let tx1 = wallet1.get_tx(txid1).unwrap().tx.clone(); + let tx2 = wallet2.get_tx(txid2).unwrap().tx.clone(); let satisfaction_weight = wallet2 .public_descriptor(KeychainKind::External) @@ -230,7 +230,7 @@ fn test_add_foreign_utxo_only_witness_utxo() { let mut builder = wallet1.build_tx(); builder.add_recipient(addr.script_pubkey(), Amount::from_sat(60_000)); - let tx2 = wallet2.get_tx(txid2).unwrap().tx_node.tx; + let tx2 = wallet2.get_tx(txid2).unwrap().tx; let psbt_input = psbt::Input { non_witness_utxo: Some(tx2.as_ref().clone()), ..Default::default() diff --git a/tests/build_fee_bump.rs b/tests/build_fee_bump.rs index 4a243dfad..4e700f66a 100644 --- a/tests/build_fee_bump.rs +++ b/tests/build_fee_bump.rs @@ -358,8 +358,7 @@ fn test_bump_fee_remove_output_manually_selected_only() { }], }; - let position: ChainPosition = - wallet.transactions().last().unwrap().chain_position; + let position: ChainPosition = wallet.transactions().last().unwrap().pos; insert_tx(&mut wallet, init_tx.clone()); match position { ChainPosition::Confirmed { anchor, .. } => { @@ -410,8 +409,7 @@ fn test_bump_fee_add_input() { }], }; let txid = init_tx.compute_txid(); - let pos: ChainPosition = - wallet.transactions().last().unwrap().chain_position; + let pos: ChainPosition = wallet.transactions().last().unwrap().pos; insert_tx(&mut wallet, init_tx); match pos { ChainPosition::Confirmed { anchor, .. } => insert_anchor(&mut wallet, txid, anchor), @@ -846,8 +844,7 @@ fn test_legacy_bump_fee_add_input() { }], }; let txid = init_tx.compute_txid(); - let pos: ChainPosition = - wallet.transactions().last().unwrap().chain_position; + let pos: ChainPosition = wallet.transactions().last().unwrap().pos; insert_tx(&mut wallet, init_tx); match pos { ChainPosition::Confirmed { anchor, .. } => insert_anchor(&mut wallet, txid, anchor), @@ -977,7 +974,7 @@ fn test_bump_fee_pay_to_anchor_foreign_utxo() { let tx = psbt.unsigned_tx.clone(); assert!(tx.input.iter().any(|txin| txin.previous_output == outpoint)); let txid1 = tx.compute_txid(); - wallet.apply_unconfirmed_txs([(tx, 123456)]); + insert_tx(&mut wallet, tx); // Now build fee bump. let mut tx_builder = wallet diff --git a/tests/persisted_wallet.rs b/tests/persisted_wallet.rs index 0b9076bcb..529dde207 100644 --- a/tests/persisted_wallet.rs +++ b/tests/persisted_wallet.rs @@ -367,10 +367,7 @@ fn wallet_should_persist_anchors_and_recover() { if let ChainPosition::Confirmed { anchor: obtained_anchor, .. - } = wallet - .get_tx(txid) - .expect("should retrieve stored tx") - .chain_position + } = wallet.get_tx(txid).expect("should retrieve stored tx").pos { assert_eq!(obtained_anchor, expected_anchor) } else { diff --git a/tests/wallet.rs b/tests/wallet.rs index c779c0a4e..dc6260e9f 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use std::sync::Arc; use assert_matches::assert_matches; -use bdk_chain::{BlockId, CanonicalizationParams, ConfirmationBlockTime}; +use bdk_chain::{BlockId, ConfirmationBlockTime}; use bdk_wallet::coin_selection; use bdk_wallet::descriptor::{calc_checksum, DescriptorError}; use bdk_wallet::error::CreateTxError; @@ -84,11 +84,11 @@ fn test_get_funded_wallet_sent_and_received() { let mut tx_amounts: Vec<(Txid, (Amount, Amount))> = wallet .transactions() - .map(|ct| (ct.tx_node.txid, wallet.sent_and_received(&ct.tx_node))) + .map(|ct| (ct.txid, wallet.sent_and_received(&ct.tx))) .collect(); tx_amounts.sort_by(|a1, a2| a1.0.cmp(&a2.0)); - let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + let tx = wallet.get_tx(txid).expect("transaction").tx; let (sent, received) = wallet.sent_and_received(&tx); // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 @@ -102,7 +102,7 @@ fn test_get_funded_wallet_sent_and_received() { fn test_get_funded_wallet_tx_fees() { let (wallet, txid) = get_funded_wallet_wpkh(); - let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + let tx = wallet.get_tx(txid).expect("transaction").tx; let tx_fee = wallet.calculate_fee(&tx).expect("transaction fee"); // The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000 @@ -115,7 +115,7 @@ fn test_get_funded_wallet_tx_fees() { fn test_get_funded_wallet_tx_fee_rate() { let (wallet, txid) = get_funded_wallet_wpkh(); - let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + let tx = wallet.get_tx(txid).expect("transaction").tx; let tx_fee_rate = wallet .calculate_fee_rate(&tx) .expect("transaction fee rate"); @@ -135,7 +135,7 @@ fn test_get_funded_wallet_tx_fee_rate() { fn test_legacy_get_funded_wallet_tx_fee_rate() { let (wallet, txid) = get_funded_wallet_single(get_test_pkh()); - let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx; + let tx = wallet.get_tx(txid).expect("transaction").tx; let tx_fee_rate = wallet .calculate_fee_rate(&tx) .expect("transaction fee rate"); @@ -2139,8 +2139,7 @@ fn test_taproot_sign_using_non_witness_utxo() { let mut psbt = builder.finish().unwrap(); psbt.inputs[0].witness_utxo = None; - psbt.inputs[0].non_witness_utxo = - Some(wallet.get_tx(prev_txid).unwrap().tx_node.as_ref().clone()); + psbt.inputs[0].non_witness_utxo = wallet.get_tx(prev_txid).map(|c| c.tx.as_ref().clone()); assert!( psbt.inputs[0].non_witness_utxo.is_some(), "Previous tx should be present in the database" @@ -2878,11 +2877,10 @@ fn test_transactions_sort_by() { receive_output(&mut wallet, Amount::from_sat(25_000), ReceiveTo::Mempool(0)); // sort by chain position, unconfirmed then confirmed by descending block height - let sorted_txs: Vec = - wallet.transactions_sort_by(|t1, t2| t2.chain_position.cmp(&t1.chain_position)); + let sorted_txs: Vec = wallet.transactions_sort_by(|t1, t2| t2.pos.cmp(&t1.pos)); let conf_heights: Vec> = sorted_txs .iter() - .map(|tx| tx.chain_position.confirmation_height_upper_bound()) + .map(|tx| tx.pos.confirmation_height_upper_bound()) .collect(); assert_eq!([None, Some(2000), Some(1000)], conf_heights.as_slice()); } @@ -2898,15 +2896,7 @@ fn test_wallet_transactions_relevant() { let (mut test_wallet, _txid) = get_funded_wallet_wpkh(); let relevant_tx_count_before = test_wallet.transactions().count(); let full_tx_count_before = test_wallet.tx_graph().full_txs().count(); - let chain_tip = test_wallet.local_chain().tip().block_id(); - let canonical_tx_count_before = test_wallet - .tx_graph() - .list_canonical_txs( - test_wallet.local_chain(), - chain_tip, - CanonicalizationParams::default(), - ) - .count(); + let canonical_tx_count_before = test_wallet.canonical_view().txs().count(); // add not relevant transaction to test wallet let (other_external_desc, other_internal_desc) = get_test_tr_single_sig_xprv_and_change_desc(); @@ -2920,27 +2910,16 @@ fn test_wallet_transactions_relevant() { // verify transaction from other wallet was added but is not in relevant transactions list. let relevant_tx_count_after = test_wallet.transactions().count(); let full_tx_count_after = test_wallet.tx_graph().full_txs().count(); - let canonical_tx_count_after = test_wallet - .tx_graph() - .list_canonical_txs( - test_wallet.local_chain(), - chain_tip, - CanonicalizationParams::default(), - ) - .count(); + let canonical_tx_count_after = test_wallet.canonical_view().txs().count(); assert_eq!(relevant_tx_count_before, relevant_tx_count_after); assert!(!test_wallet .transactions() - .any(|wallet_tx| wallet_tx.tx_node.txid == other_txid)); + .any(|wallet_tx| wallet_tx.txid == other_txid)); assert!(test_wallet - .tx_graph() - .list_canonical_txs( - test_wallet.local_chain(), - chain_tip, - CanonicalizationParams::default() - ) - .any(|wallet_tx| wallet_tx.tx_node.txid == other_txid)); + .canonical_view() + .txs() + .any(|wallet_tx| wallet_tx.txid == other_txid)); assert!(full_tx_count_before < full_tx_count_after); assert!(canonical_tx_count_before < canonical_tx_count_after); } diff --git a/tests/wallet_event.rs b/tests/wallet_event.rs index 3dfa688ed..f41974244 100644 --- a/tests/wallet_event.rs +++ b/tests/wallet_event.rs @@ -45,7 +45,7 @@ fn test_tx_unconfirmed_event() { hash: BlockHash::from_slice(&[1; 32]).unwrap(), }; let mut cp = wallet.latest_checkpoint(); - cp = cp.insert(reorg_block); + cp = cp.insert(reorg_block.height, reorg_block.hash); let reorg_update = Update { chain: Some(cp), ..Default::default() @@ -157,7 +157,11 @@ fn test_tx_confirmed_event() { block_id: new_block, confirmation_time: 300, }; - update.chain = CheckPoint::from_block_ids([parent_block, new_block]).ok(); + update.chain = CheckPoint::from_blocks([ + (parent_block.height, parent_block.hash), + (new_block.height, new_block.hash), + ]) + .ok(); update.tx_update.anchors = [(new_anchor, new_txid)].into(); let orig_tip = wallet.local_chain().tip().block_id(); @@ -213,7 +217,11 @@ fn test_tx_confirmed_new_block_event() { block_id: new_block, confirmation_time: 300, }; - update.chain = CheckPoint::from_block_ids([parent_block, new_block]).ok(); + update.chain = CheckPoint::from_blocks([ + (parent_block.height, parent_block.hash), + (new_block.height, new_block.hash), + ]) + .ok(); update.tx_update.anchors = [(new_anchor, new_txid)].into(); let orig_tip = wallet.local_chain().tip().block_id(); @@ -239,7 +247,11 @@ fn test_tx_confirmed_new_block_event() { block_id: reorg_block, confirmation_time: 310, }; - update.chain = CheckPoint::from_block_ids([parent_block, reorg_block]).ok(); + update.chain = CheckPoint::from_blocks([ + (parent_block.height, parent_block.hash), + (reorg_block.height, reorg_block.hash), + ]) + .ok(); update.tx_update.anchors = [(reorg_anchor, new_txid)].into(); let events = wallet.apply_update_events(update).unwrap(); @@ -438,7 +450,7 @@ fn test_apply_block_tx_confirmed_new_block_event() { .map(|tx| (**tx).clone()) .collect(), ); - let events = wallet.apply_block_events(&block1, 1).unwrap(); + let events = dbg!(wallet.apply_block_events(&block1, 1).unwrap()); assert_eq!(events.len(), 2); // apply spending block From 7c6a87e68d28b5b44b04b21f6d8457321ed1c06b Mon Sep 17 00:00:00 2001 From: valued mammal Date: Thu, 12 Feb 2026 10:34:44 -0500 Subject: [PATCH 2/2] deps: Update `miniscript` to 13.0.0 --- .github/workflows/cont_integration.yml | 8 +-- Cargo.toml | 20 +++++--- src/descriptor/checksum.rs | 28 ++++++++--- src/descriptor/dsl.rs | 36 ++++++++------ src/descriptor/error.rs | 34 ++++++++++++- src/descriptor/mod.rs | 67 ++++++++++++------------- src/descriptor/policy.rs | 5 +- src/keys/mod.rs | 11 ++-- src/wallet/error.rs | 6 +-- src/wallet/export.rs | 16 +++--- src/wallet/mod.rs | 69 +++++++++++--------------- src/wallet/persisted.rs | 2 +- src/wallet/signer.rs | 12 +++-- 13 files changed, 186 insertions(+), 128 deletions(-) diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index f3ff0c88a..5b715d38f 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -21,7 +21,7 @@ jobs: - ubuntu-latest - ubuntu-24.04-arm features: - - --no-default-features --features miniscript/no-std,bdk_chain/hashbrown + - --no-default-features --features bdk_chain/hashbrown - --all-features steps: - name: Checkout @@ -51,7 +51,7 @@ jobs: - ubuntu-latest - ubuntu-24.04-arm features: - - --no-default-features --features miniscript/no-std,bdk_chain/hashbrown + - --no-default-features --features bdk_chain/hashbrown - --all-features steps: - name: Checkout @@ -84,7 +84,7 @@ jobs: cache: true - name: Check no-std # TODO "--target thumbv6m-none-eabi" should work but currently does not - run: cargo check --no-default-features --features miniscript/no-std,bdk_chain/hashbrown + run: cargo check --no-default-features --features bdk_chain/hashbrown check-wasm: name: Check WASM @@ -110,7 +110,7 @@ jobs: - name: Check WASM run: | rustup target add wasm32-unknown-unknown - cargo check --target wasm32-unknown-unknown --no-default-features --features miniscript/no-std,bdk_chain/hashbrown + cargo check --target wasm32-unknown-unknown --no-default-features --features bdk_chain/hashbrown fmt: name: Rust fmt diff --git a/Cargo.toml b/Cargo.toml index 3f4f25403..98a6f1e7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } [dependencies] bdk_chain = { version = "0.23.1", features = ["miniscript", "serde"], default-features = false } bitcoin = { version = "0.32.7", features = ["serde", "base64"], default-features = false } -miniscript = { version = "12.3.1", features = ["serde"], default-features = false } +miniscript = { version = "13.0.0", features = ["serde"], default-features = false } rand_core = { version = "0.6.0" } serde_json = { version = "1" } serde = { version = "1", features = ["derive"] } @@ -81,24 +81,30 @@ name = "bitcoind_rpc" [patch.crates-io.bdk_chain] git = "https://github.com/bitcoindevkit/bdk" -rev = "b4c8c6a110de14c1b04d2a525cd8fb3e4d67cec6" +branch = "master" +# rev = "b45ecb9bd41c80a03921dcf42a9845b19774502e" [patch.crates-io.bdk_core] git = "https://github.com/bitcoindevkit/bdk" -rev = "b4c8c6a110de14c1b04d2a525cd8fb3e4d67cec6" +branch = "master" +# rev = "b45ecb9bd41c80a03921dcf42a9845b19774502e" [patch.crates-io.bdk_bitcoind_rpc] git = "https://github.com/bitcoindevkit/bdk" -rev = "b4c8c6a110de14c1b04d2a525cd8fb3e4d67cec6" +branch = "master" +# rev = "b45ecb9bd41c80a03921dcf42a9845b19774502e" [patch.crates-io.bdk_electrum] git = "https://github.com/bitcoindevkit/bdk" -rev = "b4c8c6a110de14c1b04d2a525cd8fb3e4d67cec6" +branch = "master" +# rev = "b45ecb9bd41c80a03921dcf42a9845b19774502e" [patch.crates-io.bdk_esplora] git = "https://github.com/bitcoindevkit/bdk" -rev = "b4c8c6a110de14c1b04d2a525cd8fb3e4d67cec6" +branch = "master" +# rev = "b45ecb9bd41c80a03921dcf42a9845b19774502e" [patch.crates-io.bdk_file_store] git = "https://github.com/bitcoindevkit/bdk" -rev = "b4c8c6a110de14c1b04d2a525cd8fb3e4d67cec6" +branch = "master" +# rev = "b45ecb9bd41c80a03921dcf42a9845b19774502e" diff --git a/src/descriptor/checksum.rs b/src/descriptor/checksum.rs index 24937d7d2..c0660a962 100644 --- a/src/descriptor/checksum.rs +++ b/src/descriptor/checksum.rs @@ -17,20 +17,23 @@ use crate::descriptor::DescriptorError; use alloc::string::String; -use miniscript::descriptor::checksum::desc_checksum; +use miniscript::descriptor::checksum::Engine; /// Compute the checksum of a descriptor, excludes any existing checksum in the descriptor string /// from the calculation pub fn calc_checksum(desc: &str) -> Result { - if let Some(split) = desc.split_once('#') { - let og_checksum = split.1; - let checksum = desc_checksum(split.0)?; - if og_checksum != checksum { + if let Some((descriptor, original_checksum)) = desc.split_once('#') { + let mut engine = Engine::new(); + engine.input(descriptor)?; + let checksum = engine.checksum(); + if original_checksum != checksum { return Err(DescriptorError::InvalidDescriptorChecksum); } Ok(checksum) } else { - Ok(desc_checksum(desc)?) + let mut engine = Engine::new(); + engine.input(desc)?; + Ok(engine.checksum()) } } @@ -39,6 +42,7 @@ pub fn calc_checksum(desc: &str) -> Result { mod test { use super::*; use crate::descriptor::calc_checksum; + use alloc::string::ToString; use assert_matches::assert_matches; // test calc_checksum() function; it should return the same value as Bitcoin Core @@ -81,7 +85,17 @@ mod test { assert_matches!( calc_checksum(&invalid_desc), - Err(DescriptorError::Miniscript(miniscript::Error::BadDescriptor(e))) if e == format!("Invalid character in checksum: '{sparkle_heart}'") + Err(DescriptorError::Miniscript( + miniscript::Error::Parse( + miniscript::ParseError::Tree( + miniscript::ParseTreeError::Checksum( + miniscript::descriptor::checksum::Error::InvalidCharacter { ch, pos }) + ) + ) + ) + ) + if ch.to_string() == sparkle_heart + && pos == 85 ); } } diff --git a/src/descriptor/dsl.rs b/src/descriptor/dsl.rs index eca67ecf5..b0293cf2d 100644 --- a/src/descriptor/dsl.rs +++ b/src/descriptor/dsl.rs @@ -30,7 +30,8 @@ macro_rules! impl_top_level_sh { use $crate::miniscript::$ctx; let build_desc = |k, pks| { - Ok((Descriptor::::$inner_struct($inner_struct::$sortedmulti_constructor(k, pks)?), PhantomData::<$ctx>)) + let thresh = miniscript::Threshold::new(k, pks)?; + Ok((Descriptor::::$inner_struct($inner_struct::$sortedmulti_constructor(thresh)?), PhantomData::<$ctx>)) }; $crate::impl_sortedmulti!(build_desc, sortedmulti $( $inner )*) @@ -42,7 +43,8 @@ macro_rules! impl_top_level_sh { use $crate::miniscript::$ctx; let build_desc = |k, pks| { - Ok((Descriptor::::$inner_struct($inner_struct::$sortedmulti_constructor(k, pks)?), PhantomData::<$ctx>)) + let thresh = miniscript::Threshold::new(k, pks)?; + Ok((Descriptor::::$inner_struct($inner_struct::$sortedmulti_constructor(thresh)?), PhantomData::<$ctx>)) }; $crate::impl_sortedmulti!(build_desc, sortedmulti_vec $( $inner )*) @@ -294,7 +296,7 @@ macro_rules! parse_tap_tree { .and_then(|tree_a| Ok((tree_a, $tree_b?))) .and_then(|((a_tree, mut a_keymap, a_network_kinds), (b_tree, b_keymap, b_network_kinds))| { a_keymap.extend(b_keymap.into_iter()); - Ok((TapTree::combine(a_tree, b_tree), a_keymap, $crate::keys::intersect_network_kinds(&a_network_kinds, &b_network_kinds))) + Ok((TapTree::combine(a_tree, b_tree)?, a_keymap, $crate::keys::intersect_network_kinds(&a_network_kinds, &b_network_kinds))) }) }}; @@ -331,11 +333,10 @@ macro_rules! parse_tap_tree { // Single leaf ( $op:ident ( $( $minisc:tt )* ) ) => {{ - use $crate::alloc::sync::Arc; use $crate::miniscript::descriptor::TapTree; $crate::fragment!( $op ( $( $minisc )* ) ) - .map(|(a_minisc, a_keymap, a_network_kinds)| (TapTree::Leaf(Arc::new(a_minisc)), a_keymap, a_network_kinds)) + .map(|(a_minisc, a_keymap, a_network_kinds)| (TapTree::leaf(a_minisc), a_keymap, a_network_kinds)) }}; } @@ -721,10 +722,14 @@ macro_rules! fragment { $crate::keys::make_pkh($key, &secp) }); ( after ( $value:expr ) ) => ({ - $crate::impl_leaf_opcode_value!(After, $crate::miniscript::AbsLockTime::from_consensus($value).expect("valid `AbsLockTime`")) + $crate::miniscript::AbsLockTime::from_consensus($value) + .map_err($crate::descriptor::DescriptorError::from) + .and_then(|abs_lt| $crate::impl_leaf_opcode_value!(After, abs_lt)) }); ( older ( $value:expr ) ) => ({ - $crate::impl_leaf_opcode_value!(Older, $crate::miniscript::RelLockTime::from_consensus($value).expect("valid `RelLockTime`")) // TODO!! + $crate::miniscript::RelLockTime::from_consensus($value) + .map_err($crate::descriptor::DescriptorError::from) + .and_then(|rel_lt| $crate::impl_leaf_opcode_value!(Older, rel_lt)) }); ( sha256 ( $hash:expr ) ) => ({ $crate::impl_leaf_opcode_value!(Sha256, $hash) @@ -775,9 +780,12 @@ macro_rules! fragment { (keys_acc, net_acc) }); - let thresh = $crate::miniscript::Threshold::new($thresh, items).expect("valid threshold and pks collection"); - $crate::impl_leaf_opcode_value!(Thresh, thresh) - .map(|(minisc, _, _)| (minisc, key_maps, valid_network_kinds)) + $crate::miniscript::Threshold::new($thresh, items) + .map_err($crate::descriptor::DescriptorError::from) + .and_then(|thresh| { + $crate::impl_leaf_opcode_value!(Thresh, thresh) + .map(|(minisc, _, _)| (minisc, key_maps, valid_network_kinds)) + }) }); ( thresh ( $thresh:expr, $( $inner:tt )* ) ) => ({ let items = $crate::fragment_internal!( @v $( $inner )* ); @@ -789,8 +797,8 @@ macro_rules! fragment { let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); let fun = |k, pks| { - let thresh = $crate::miniscript::Threshold::new(k, pks).expect("valid threshold and pks collection"); - $crate::miniscript::Terminal::Multi(thresh) + let thresh = $crate::miniscript::Threshold::new(k, pks)?; + Ok($crate::miniscript::Terminal::Multi(thresh)) }; $crate::keys::make_multi($thresh, fun, $keys, &secp) @@ -803,8 +811,8 @@ macro_rules! fragment { let secp = $crate::bitcoin::secp256k1::Secp256k1::new(); let fun = |k, pks| { - let thresh = $crate::miniscript::Threshold::new(k, pks).expect("valid threshold and pks collection"); - $crate::miniscript::Terminal::MultiA(thresh) + let thresh = $crate::miniscript::Threshold::new(k, pks)?; + Ok($crate::miniscript::Terminal::MultiA(thresh)) }; $crate::keys::make_multi($thresh, fun, $keys, &secp) diff --git a/src/descriptor/error.rs b/src/descriptor/error.rs index 836bab128..51b82c5ec 100644 --- a/src/descriptor/error.rs +++ b/src/descriptor/error.rs @@ -13,7 +13,7 @@ use core::fmt; /// Errors related to the parsing and usage of descriptors -#[derive(Debug, PartialEq)] +#[derive(Debug)] pub enum Error { /// Invalid HD Key path, such as having a wildcard but a length != 1 InvalidHdKeyPath, @@ -126,3 +126,35 @@ impl From for Error { Error::Policy(err) } } + +impl From for Error { + fn from(e: miniscript::descriptor::checksum::Error) -> Self { + Self::Miniscript(miniscript::Error::Parse(miniscript::ParseError::Tree( + miniscript::ParseTreeError::Checksum(e), + ))) + } +} + +impl From for Error { + fn from(e: miniscript::descriptor::TapTreeDepthError) -> Self { + Self::Miniscript(miniscript::Error::TapTreeDepthError(e)) + } +} + +impl From for Error { + fn from(e: miniscript::ThresholdError) -> Self { + Self::Miniscript(miniscript::Error::Threshold(e)) + } +} + +impl From for Error { + fn from(e: miniscript::AbsLockTimeError) -> Self { + Self::Miniscript(miniscript::Error::AbsoluteLockTime(e)) + } +} + +impl From for Error { + fn from(e: miniscript::RelLockTimeError) -> Self { + Self::Miniscript(miniscript::Error::RelativeLockTime(e)) + } +} diff --git a/src/descriptor/mod.rs b/src/descriptor/mod.rs index 60cadf7de..89ca292b1 100644 --- a/src/descriptor/mod.rs +++ b/src/descriptor/mod.rs @@ -32,7 +32,7 @@ use miniscript::descriptor::{ pub use miniscript::{ Descriptor, DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0, }; -use miniscript::{ForEachKey, MiniscriptKey, TranslatePk}; +use miniscript::{ForEachKey, MiniscriptKey}; use crate::descriptor::policy::BuildSatisfaction; @@ -147,8 +147,11 @@ impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) { network_kind: NetworkKind, } - impl miniscript::Translator for Translator<'_, '_> { - fn pk(&mut self, pk: &DescriptorPublicKey) -> Result { + impl miniscript::Translator for Translator<'_, '_> { + type TargetPk = String; + type Error = DescriptorError; + + fn pk(&mut self, pk: &DescriptorPublicKey) -> Result { let secp = &self.secp; let (_, _, network_kinds) = if self.descriptor.is_taproot() { @@ -223,9 +226,10 @@ impl IntoWalletDescriptor for DescriptorTemplateOut { network_kind: NetworkKind, } - impl miniscript::Translator - for Translator - { + impl miniscript::Translator for Translator { + type TargetPk = DescriptorPublicKey; + type Error = DescriptorError; + fn pk( &mut self, pk: &DescriptorPublicKey, @@ -269,23 +273,22 @@ impl IntoWalletDescriptor for DescriptorTemplateOut { Err(TranslateErr::OuterError(e)) => return Err(e.into()), }; // ...and in the key map. - let fixed_keymap = keymap - .into_iter() - .map(|(mut k, mut v)| { - match (&mut k, &mut v) { - (DescriptorPublicKey::XPub(xpub), DescriptorSecretKey::XPrv(xprv)) => { - xpub.xkey.network = network_kind; - xprv.xkey.network = network_kind; - } - (_, DescriptorSecretKey::Single(key)) => { - key.key.network = network_kind; - } - _ => {} + let inner = keymap.into_iter().map(|(mut k, mut v)| { + match (&mut k, &mut v) { + (DescriptorPublicKey::XPub(xpub), DescriptorSecretKey::XPrv(xprv)) => { + xpub.xkey.network = network_kind; + xprv.xkey.network = network_kind; + } + (_, DescriptorSecretKey::Single(key)) => { + key.key.network = network_kind; } + _ => {} + } - (k, v) - }) - .collect(); + (k, v) + }); + let mut fixed_keymap = KeyMap::new(); + fixed_keymap.extend(inner); Ok((translated, fixed_keymap)) } @@ -314,11 +317,9 @@ pub(crate) fn check_wallet_descriptor( } if descriptor.is_multipath() { - return Err(DescriptorError::Miniscript( - miniscript::Error::BadDescriptor( - "`check_wallet_descriptor` must not contain multipath keys".to_string(), - ), - )); + return Err(DescriptorError::Miniscript(miniscript::Error::Unexpected( + "`check_wallet_descriptor` must not contain multipath keys".to_string(), + ))); } // Run miniscript's sanity check, which will look for duplicated keys and other potential @@ -899,9 +900,9 @@ mod test { assert_matches!( result, - Err(DescriptorError::Miniscript( - miniscript::Error::BadDescriptor(_) - )) + Err(DescriptorError::Miniscript(miniscript::Error::Unexpected( + .. + ))) ); // Repeated pubkeys. @@ -969,13 +970,9 @@ mod test { assert!(descriptor.is_multipath()); - // Miniscript can't make an extended private key with multiple paths into a public key. - // ref: + // `miniscript` should make an extended private key with multiple paths into a public key. let descriptor_str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/<0;1>/*)"; - assert!(matches!( - Descriptor::parse_descriptor(&secp, descriptor_str), - Err(miniscript::Error::Unexpected(..)), - )); + Descriptor::parse_descriptor(&secp, descriptor_str).expect("should parse multi xkey"); let _ = descriptor_str .into_wallet_descriptor(&secp, NetworkKind::Main) .unwrap_err(); diff --git a/src/descriptor/policy.rs b/src/descriptor/policy.rs index 7066b86f0..4b996ab34 100644 --- a/src/descriptor/policy.rs +++ b/src/descriptor/policy.rs @@ -1159,8 +1159,9 @@ impl ExtractPolicy for Descriptor { let mut items = vec![key_spend_sig]; items.append( &mut tr - .iter_scripts() - .filter_map(|(_, ms)| { + .leaves() + .filter_map(|item| { + let ms = item.miniscript(); ms.extract_policy(signers, build_sat, secp).transpose() }) .collect::, _>>()?, diff --git a/src/keys/mod.rs b/src/keys/mod.rs index 300d42755..2f80b383c 100644 --- a/src/keys/mod.rs +++ b/src/keys/mod.rs @@ -117,7 +117,7 @@ impl DescriptorKey { let public = secret .to_public(secp) .map_err(|e| miniscript::Error::Unexpected(e.to_string()))?; - key_map.insert(public.clone(), secret); + key_map.extend([(public.clone(), secret)]); Ok((public, key_map, valid_network_kinds)) } @@ -865,7 +865,10 @@ pub fn make_pkh, Ctx: ScriptContext>( pub fn make_multi< Pk: IntoDescriptorKey, Ctx: ScriptContext, - V: Fn(usize, Vec) -> Terminal, + V: Fn( + usize, + Vec, + ) -> Result, DescriptorError>, >( thresh: usize, variant: V, @@ -880,7 +883,7 @@ pub fn make_multi< DescriptorError, > { let (pks, key_map, valid_network_kinds) = expand_multi_keys(pks, secp)?; - let minisc = Miniscript::from_ast(variant(thresh, pks))?; + let minisc = Miniscript::from_ast(variant(thresh, pks)?)?; minisc.check_miniscript()?; @@ -995,7 +998,7 @@ impl IntoDescriptorKey for PrivateKey { } /// Errors thrown while working with [`keys`](crate::keys). -#[derive(Debug, PartialEq)] +#[derive(Debug)] pub enum KeyError { /// The key cannot exist in the given script context InvalidScriptContext, diff --git a/src/wallet/error.rs b/src/wallet/error.rs index 9a7cd6612..6c0e49c12 100644 --- a/src/wallet/error.rs +++ b/src/wallet/error.rs @@ -130,8 +130,8 @@ impl From for LoadError { /// Errors returned by miniscript when updating inconsistent PSBTs #[derive(Debug, Clone)] pub enum MiniscriptPsbtError { - /// Descriptor key conversion error - Conversion(miniscript::descriptor::ConversionError), + /// Non-definite key error + NonDefiniteKey(miniscript::descriptor::NonDefiniteKeyError), /// Return error type for PsbtExt::update_input_with_descriptor UtxoUpdate(miniscript::psbt::UtxoUpdateError), /// Return error type for PsbtExt::update_output_with_descriptor @@ -141,7 +141,7 @@ pub enum MiniscriptPsbtError { impl fmt::Display for MiniscriptPsbtError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Conversion(err) => write!(f, "Conversion error: {err}"), + Self::NonDefiniteKey(err) => write!(f, "{err}"), Self::UtxoUpdate(err) => write!(f, "UTXO update error: {err}"), Self::OutputUpdate(err) => write!(f, "Output update error: {err}"), } diff --git a/src/wallet/export.rs b/src/wallet/export.rs index ef61bb756..519704ff3 100644 --- a/src/wallet/export.rs +++ b/src/wallet/export.rs @@ -682,24 +682,28 @@ impl CaravanExport { let k = self.quorum.required_signers as usize; // Use proper Descriptor construction methods + let external_threshold = + miniscript::Threshold::new(k, external_keys).map_err(|_| "invalid `Threshold`")?; let external_desc: Descriptor = match self.address_type { - CaravanAddressType::P2SH => Descriptor::new_sh_sortedmulti(k, external_keys) + CaravanAddressType::P2SH => Descriptor::new_sh_sortedmulti(external_threshold) .map_err(|_| "Failed to create P2SH sortedmulti descriptor")?, - CaravanAddressType::P2WSH => Descriptor::new_wsh_sortedmulti(k, external_keys) + CaravanAddressType::P2WSH => Descriptor::new_wsh_sortedmulti(external_threshold) .map_err(|_| "Failed to create P2WSH sortedmulti descriptor")?, CaravanAddressType::P2SHWrappedP2WSH => { - Descriptor::new_sh_wsh_sortedmulti(k, external_keys) + Descriptor::new_sh_wsh_sortedmulti(external_threshold) .map_err(|_| "Failed to create P2SH-P2WSH sortedmulti descriptor")? } }; + let internal_threshold = + miniscript::Threshold::new(k, internal_keys).map_err(|_| "invalid `Threshold`")?; let internal_desc: Descriptor = match self.address_type { - CaravanAddressType::P2SH => Descriptor::new_sh_sortedmulti(k, internal_keys) + CaravanAddressType::P2SH => Descriptor::new_sh_sortedmulti(internal_threshold) .map_err(|_| "Failed to create P2SH sortedmulti descriptor")?, - CaravanAddressType::P2WSH => Descriptor::new_wsh_sortedmulti(k, internal_keys) + CaravanAddressType::P2WSH => Descriptor::new_wsh_sortedmulti(internal_threshold) .map_err(|_| "Failed to create P2WSH sortedmulti descriptor")?, CaravanAddressType::P2SHWrappedP2WSH => { - Descriptor::new_sh_wsh_sortedmulti(k, internal_keys) + Descriptor::new_sh_wsh_sortedmulti(internal_threshold) .map_err(|_| "Failed to create P2SH-P2WSH sortedmulti descriptor")? } }; diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index d74b1b61d..ab70b31df 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -2164,7 +2164,7 @@ impl Wallet { psbt_input .update_with_descriptor_unchecked(&derived_descriptor) - .map_err(MiniscriptPsbtError::Conversion)?; + .map_err(MiniscriptPsbtError::NonDefiniteKey)?; let prev_output = utxo.outpoint; if let Some(prev_tx) = self.tx_graph.graph().get_tx(prev_output.txid) { @@ -2907,7 +2907,6 @@ macro_rules! doctest_wallet { #[cfg(test)] mod test { use super::*; - use crate::miniscript::Error::Unexpected; use crate::test_utils::get_test_tr_single_sig_xprv_and_change_desc; use crate::test_utils::insert_tx; @@ -2969,35 +2968,38 @@ mod test { #[test] fn test_create_two_path_wallet() { let two_path_descriptor = "wpkh([9a6a2580/84'/1'/0']tpubDDnGNapGEY6AZAdQbfRJgMg9fvz8pUBrLwvyvUqEgcUfgzM6zc2eVK4vY9x9L5FJWdX8WumXuLEDV5zDZnTfbn87vLe9XceCFwTu9so9Kks/<0;1>/*)"; + let private_multipath_descriptor = "wpkh(tprv8ZgxMBicQKsPdWAHbugK2tjtVtRjKGixYVZUdL7xLHMgXZS6BFbFi1UDb1CHT25Z5PU1F9j7wGxwUiRhqz9E3nZRztikGUV6HoRDYcqPhM4/84'/1'/0'/<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); + for test_descriptor in [two_path_descriptor, private_multipath_descriptor] { + // Test successful creation of a two-path wallet + let params = Wallet::create_from_two_path_descriptor(test_descriptor); + let wallet = params + .network(Network::Testnet) + .create_wallet_no_persist() + .expect("failed to create Wallet"); - // 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 the wallet has both external and internal keychains + let keychains: Vec<_> = wallet.keychains().collect(); + assert_eq!(keychains.len(), 2); - // 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); + // 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] @@ -3008,17 +3010,6 @@ mod test { 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); diff --git a/src/wallet/persisted.rs b/src/wallet/persisted.rs index 003dca13c..11c147151 100644 --- a/src/wallet/persisted.rs +++ b/src/wallet/persisted.rs @@ -340,7 +340,7 @@ impl WalletPersister for bdk_file_store::Store { } /// Error type for [`PersistedWallet::load`]. -#[derive(Debug, PartialEq)] +#[derive(Debug)] pub enum LoadWithPersistError { /// Error from persistence. Persist(E), diff --git a/src/wallet/signer.rs b/src/wallet/signer.rs index 54548b3d1..36a1d22a7 100644 --- a/src/wallet/signer.rs +++ b/src/wallet/signer.rs @@ -649,11 +649,13 @@ pub struct SignersContainer(BTreeMap KeyMap { - self.0 - .values() - .filter_map(|signer| signer.descriptor_secret_key()) - .filter_map(|secret| secret.to_public(secp).ok().map(|public| (public, secret))) - .collect() + let mut keymap = KeyMap::new(); + keymap.extend(self.0.values().filter_map(|signer| { + let desc_sk = signer.descriptor_secret_key()?; + let desc_pk = desc_sk.to_public(secp).ok()?; + Some((desc_pk, desc_sk)) + })); + keymap } /// Build a new signer container from a [`KeyMap`]