Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions ci/pin-msrv.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,10 @@ set -euo pipefail
cargo update -p once_cell --precise "1.20.3"
cargo update -p syn --precise "2.0.106"
cargo update -p quote --precise "1.0.41"
cargo update -p serde_json --precise "1.0.145"
cargo update -p anyhow --precise "1.0.100"
cargo update -p tempfile --precise "3.25.0"
cargo update -p proc-macro2 --precise "1.0.103"
cargo update -p ryu --precise "1.0.20"
cargo update -p itoa --precise "1.0.15"
cargo update -p unicode-ident --precise "1.0.22"
16 changes: 10 additions & 6 deletions wallet/src/wallet/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,26 +82,30 @@ 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: &mut Wallet,
wallet: &Wallet,
chain_tip1: BlockId,
chain_tip2: BlockId,
wallet_txs1: BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>,
wallet_txs2: BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>,
) -> Vec<WalletEvent> {
let mut events: Vec<WalletEvent> = Vec::new();

// find chain tip change
if chain_tip1 != chain_tip2 {
events.push(WalletEvent::ChainTipChanged {
old_tip: chain_tip1,
new_tip: chain_tip2,
});
}

wallet_txs2.iter().for_each(|(txid2, (tx2, cp2))| {
if let Some((tx1, cp1)) = wallet_txs1.get(txid2) {
assert_eq!(tx1.compute_txid(), *txid2);
match (cp1, cp2) {
// find transaction canonical status changes
wallet_txs2.iter().for_each(|(txid2, (tx2, pos2))| {
if let Some((tx1, pos1)) = wallet_txs1.get(txid2) {
debug_assert_eq!(tx1.compute_txid(), *txid2);
match (pos1, pos2) {
(Unconfirmed { .. }, Confirmed { anchor, .. }) => {
events.push(WalletEvent::TxConfirmed {
txid: *txid2,
Expand Down Expand Up @@ -139,7 +143,7 @@ pub(crate) fn wallet_events(
}
}
} else {
match cp2 {
match pos2 {
Confirmed { anchor, .. } => {
events.push(WalletEvent::TxConfirmed {
txid: *txid2,
Expand Down
230 changes: 125 additions & 105 deletions wallet/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2448,40 +2448,7 @@ impl Wallet {
&mut self,
update: impl Into<Update>,
) -> Result<Vec<WalletEvent>, 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::<BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>>();

// 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::<BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>>();

Ok(wallet_events(
self,
chain_tip1,
chain_tip2,
wallet_txs1,
wallet_txs2,
))
self.events_helper(|wallet| wallet.apply_update(update))
}

/// Get a reference of the staged [`ChangeSet`] that is yet to be committed (if any).
Expand Down Expand Up @@ -2550,7 +2517,7 @@ impl Wallet {
}

/// Introduces a `block` of `height` to the wallet, and tries to connect it to the
/// `prev_blockhash` of the block's header.
/// `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
Expand All @@ -2565,39 +2532,7 @@ impl Wallet {
block: &Block,
height: u32,
) -> Result<Vec<WalletEvent>, 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::<BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>>();

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::<BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>>();

Ok(wallet_events(
self,
chain_tip1,
chain_tip2,
wallet_txs1,
wallet_txs2,
))
self.events_helper(|wallet| wallet.apply_block(block, height))
}

/// Applies relevant transactions from `block` of `height` to the wallet, and connects the
Expand Down Expand Up @@ -2631,8 +2566,8 @@ impl Wallet {
Ok(())
}

/// Applies relevant transactions from `block` of `height` to the wallet, and connects the
/// block to the internal chain.
/// 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.
///
Expand All @@ -2646,39 +2581,7 @@ impl Wallet {
height: u32,
connected_to: BlockId,
) -> Result<Vec<WalletEvent>, 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::<BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>>();

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::<BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>>();

Ok(wallet_events(
self,
chain_tip1,
chain_tip2,
wallet_txs1,
wallet_txs2,
))
self.events_helper(|wallet| wallet.apply_block_connected_to(block, height, connected_to))
}

/// Apply relevant unconfirmed transactions to the wallet.
Expand All @@ -2687,8 +2590,8 @@ impl Wallet {
///
/// 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 is conflicting unconfirmed transactions. The transaction with the later
/// `last_seen` is prioritized.
/// 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.
Expand All @@ -2703,6 +2606,25 @@ impl Wallet {
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<T: Into<Arc<Transaction>>>(
&mut self,
unconfirmed_txs: impl IntoIterator<Item = (T, u64)>,
) -> Vec<WalletEvent> {
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.
Expand Down Expand Up @@ -2763,6 +2685,88 @@ impl Wallet {
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<Item = (Txid, u64)>,
) -> Vec<WalletEvent> {
self.events_helper::<_, _, core::convert::Infallible>(|wallet| {
wallet.apply_evicted_txs(evicted_txs);
Ok(())
})
.expect("`apply_evicted_txs` should not fail")
}

/// 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, event::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<F, T, E>(&mut self, f: F) -> Result<Vec<WalletEvent>, E>
where
F: FnOnce(&mut Self) -> Result<T, E>,
E: fmt::Debug + fmt::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,
))
}

/// 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
Expand All @@ -2774,6 +2778,22 @@ impl Wallet {
keychain
}
}

/// Returns a map of canonical transactions keyed by txid.
///
/// This is used internally to help generate [`WalletEvent`]s.
fn map_transactions(
&self,
) -> BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)> {
self.transactions()
.map(|wtx| {
(
wtx.tx_node.txid,
(wtx.tx_node.tx.clone(), wtx.chain_position),
)
})
.collect()
}
}

/// Methods to construct sync/full-scan requests for spk-based chain sources.
Expand Down
Loading
Loading