]> Untitled Git - bdk-cli/commitdiff
feat(WalletEvents): Add wallet events to clients
authorVihiga Tyonum <withtvpeter@gmail.com>
Sat, 4 Jul 2026 10:38:46 +0000 (11:38 +0100)
committerVihiga Tyonum <withtvpeter@gmail.com>
Sat, 4 Jul 2026 10:38:46 +0000 (11:38 +0100)
- Update bdk_wallet to v2.4.0 and other deps
- Add wallet events to RPC, electrum, esplora and
kyoto clients

Cargo.lock
Cargo.toml
src/client.rs
src/handlers/online.rs
src/utils/common.rs

index 93134ad3ca5214c0d0cdaedc8671c1e85bba2ba3..407c962f9277930832090219eccba544eb800ec5 100644 (file)
@@ -235,9 +235,9 @@ dependencies = [
 
 [[package]]
 name = "bdk_bitcoind_rpc"
-version = "0.21.0"
+version = "0.22.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a52a57cc5e07e207711a5064def0bd04b18e183b9f2247f41561cb12dd393be"
+checksum = "99a9a7a93a0c377d1858263122afe0565e6b38800fe39b2025f217e01f146294"
 dependencies = [
  "bdk_core",
  "bitcoin",
@@ -259,9 +259,9 @@ dependencies = [
 
 [[package]]
 name = "bdk_core"
-version = "0.6.2"
+version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0dbbe4aad0c898bfeb5253c222be3ea3dccfb380a07e72c87e3e4ed6664a6753"
+checksum = "cb3028782f6bf14a6df987244333d34e6b272b5a40a53e4879ec2dfd82275a3a"
 dependencies = [
  "bitcoin",
  "hashbrown 0.14.5",
@@ -270,9 +270,9 @@ dependencies = [
 
 [[package]]
 name = "bdk_electrum"
-version = "0.23.2"
+version = "0.24.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b59a3f7fbe678874fa34354097644a171276e02a49934c13b3d61c54610ddf39"
+checksum = "00a9846105bf6e751adbb6946b000ff919ce24a15a941cbfe68485549b552a9c"
 dependencies = [
  "bdk_core",
  "electrum-client",
@@ -280,9 +280,9 @@ dependencies = [
 
 [[package]]
 name = "bdk_esplora"
-version = "0.22.1"
+version = "0.22.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c9f5961444b5f51b9c3937e729a212363d0e4cde6390ded6e01e16292078df4"
+checksum = "83986307ea92997c3d051e8c306af8115a05add601e22acb7c1903008e6b614e"
 dependencies = [
  "async-trait",
  "bdk_core",
@@ -323,9 +323,9 @@ dependencies = [
 
 [[package]]
 name = "bdk_wallet"
-version = "2.1.0"
+version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d30b5dba770184863b5d966ccbc6a11d12c145450be3b6a4435308297e6a12dc"
+checksum = "c99821af39c7df004bd411ece2ef22d9fba5a631b4489f3d9a0ac0d19637e2d0"
 dependencies = [
  "bdk_chain",
  "bip39",
@@ -958,9 +958,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
 
 [[package]]
 name = "electrum-client"
-version = "0.24.1"
+version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5059f13888a90486e7268bbce59b175f5f76b1c55e5b9c568ceaa42d2b8507c"
+checksum = "1970c5d7bd9de6d4041cbfc3e46faa3e85fe7efcea2b0eb06750d3ae1ef577b7"
 dependencies = [
  "bitcoin",
  "byteorder",
@@ -1014,9 +1014,9 @@ dependencies = [
 
 [[package]]
 name = "esplora-client"
-version = "0.12.1"
+version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0af349d96a5d9ad77ba59f1437aa6f348b03c5865d4f7d6e7a662d60aedce39"
+checksum = "f19e3ea99dbfbef0c1ec26d83e69de0c579f6aa6aaac4f44597805fcc27e97af"
 dependencies = [
  "bitcoin",
  "hex-conservative 0.2.2",
@@ -1024,6 +1024,7 @@ dependencies = [
  "minreq",
  "reqwest 0.12.28",
  "serde",
+ "serde_json",
  "tokio",
 ]
 
index 8f76de0dae98392c28b1f3cc8e4e4295265a718e..36ff48497bfb2b6bab87717a92c3cf29f7bc27f6 100644 (file)
@@ -12,7 +12,7 @@ readme = "README.md"
 license = "MIT"
 
 [dependencies]
-bdk_wallet = { version = "2.1.0", features = ["rusqlite", "keys-bip39", "compiler", "std"] }
+bdk_wallet = { version = "2.4.0", features = ["rusqlite", "keys-bip39", "compiler", "std"] }
 clap = { version = "4.6", features = ["derive","env"] }
 clap_complete = "4.6"
 dirs = {  version = "6.0.0" }
@@ -28,9 +28,9 @@ toml = "1.1.0"
 serde= {version = "1.0", features = ["derive"]}
 
 # Optional dependencies
-bdk_bitcoind_rpc = { version = "0.21.0", features = ["std"], optional = true }
-bdk_electrum = { version = "0.23.2", optional = true }
-bdk_esplora = { version = "0.22.1", features = ["async-https", "tokio"], optional = true }
+bdk_bitcoind_rpc = { version = "0.22.0", features = ["std"], optional = true }
+bdk_electrum = { version = "0.24.0", optional = true }
+bdk_esplora = { version = "0.22.2", features = ["async-https", "tokio"], optional = true }
 bdk_kyoto = { version = "0.15.4", optional = true }
 bdk_redb = { version = "0.1.1", optional = true }
 bdk_sp = { version = "0.1.0", optional = true, git = "https://github.com/bitcoindevkit/bdk-sp", tag = "v0.1.0" }
index ad51391e7c93cdff0f29712f78af6a9900361c5c..483183c01b2c2b3c23f4b0af97dcb8c63db8a778 100644 (file)
@@ -308,9 +308,10 @@ pub async fn sync_kyoto_client(
 
     let update = handle.update_subscriber.lock().await.update().await?;
     tracing::info!("Received update: applying to wallet");
-    wallet
-        .apply_update(update)
+    let events = wallet
+        .apply_update_events(update)
         .map_err(|e| Error::Generic(format!("Failed to apply update: {e}")))?;
+    crate::utils::print_wallet_events(&events);
 
     tracing::info!(
         "Chain tip: {}, Transactions: {}, Balance: {}",
index f4fc06277a259e7cb088eaa10902e164c2931149..2416e9df5d6f154f54c0ae1889185fc985c59cd2 100644 (file)
@@ -29,6 +29,7 @@ use {
     crate::payjoin::{PayjoinManager, ohttp::RelayManager},
     crate::utils::is_final,
     crate::utils::output::FormatOutput,
+    crate::utils::print_wallet_events,
     crate::utils::types::{StatusResult, TransactionResult},
     bdk_wallet::bitcoin::{
         Psbt, Transaction, Txid, base64::Engine, base64::prelude::BASE64_STANDARD,
@@ -112,7 +113,8 @@ impl AsyncAppCommand<AppContext<OnlineOperations<'_>>> for FullScanCommand {
             Electrum { client, batch_size } => {
                 client.populate_tx_cache(wallet.tx_graph().full_txs().map(|tx_node| tx_node.tx));
                 let update = client.full_scan(request, self.stop_gap, *batch_size, false)?;
-                wallet.apply_update(update)?;
+                let events = wallet.apply_update_events(update)?;
+                print_wallet_events(&events);
             }
 
             #[cfg(feature = "esplora")]
@@ -124,7 +126,8 @@ impl AsyncAppCommand<AppContext<OnlineOperations<'_>>> for FullScanCommand {
                     .full_scan(request, self.stop_gap, *parallel_requests)
                     .await
                     .map_err(|e| *e)?;
-                wallet.apply_update(update)?;
+                let events = wallet.apply_update_events(update)?;
+                print_wallet_events(&events);
             }
             #[cfg(feature = "rpc")]
             RpcClient { client } => {
@@ -142,27 +145,35 @@ impl AsyncAppCommand<AppContext<OnlineOperations<'_>>> for FullScanCommand {
                     NO_EXPECTED_MEMPOOL_TXS,
                 );
 
-                while let Some(block_event) = emitter.next_block()? {
-                    if block_event.block_height() % 10_000 == 0 {
-                        let percent_done = f64::from(block_event.block_height())
-                            / f64::from(blockchain_info.headers as u32)
-                            * 100f64;
-                        println!(
-                            "Applying block at height: {}, {:.2}% done.",
+                let block_events = wallet.events_helper(|w| {
+                    while let Some(block_event) = emitter.next_block()? {
+                        if block_event.block_height() % 10_000 == 0 {
+                            let percent_done = f64::from(block_event.block_height())
+                                / f64::from(blockchain_info.headers as u32)
+                                * 100f64;
+                            println!(
+                                "Applying block at height: {}, {:.2}% done.",
+                                block_event.block_height(),
+                                percent_done
+                            );
+                        }
+
+                        w.apply_block_connected_to(
+                            &block_event.block,
                             block_event.block_height(),
-                            percent_done
-                        );
+                            block_event.connected_to(),
+                        )?;
                     }
-
-                    wallet.apply_block_connected_to(
-                        &block_event.block,
-                        block_event.block_height(),
-                        block_event.connected_to(),
-                    )?;
-                }
+                    Ok::<_, Error>(())
+                })?;
+                print_wallet_events(&block_events);
 
                 let mempool_txs = emitter.mempool()?;
-                wallet.apply_unconfirmed_txs(mempool_txs.update);
+                let mempool_events = wallet.apply_unconfirmed_txs_events(mempool_txs.update);
+                print_wallet_events(&mempool_events);
+
+                let evicted_events = wallet.apply_evicted_txs_events(mempool_txs.evicted);
+                print_wallet_events(&evicted_events);
             }
 
             #[cfg(feature = "cbf")]
@@ -210,9 +221,8 @@ impl AsyncAppCommand<AppContext<OnlineOperations<'_>>> for SyncCommand {
                 client.populate_tx_cache(wallet.tx_graph().full_txs().map(|tx_node| tx_node.tx));
 
                 let update = client.sync(request, *batch_size, false)?;
-                wallet
-                    .apply_update(update)
-                    .map_err(|e| Error::Generic(e.to_string()))?;
+                let events = wallet.apply_update_events(update)?;
+                print_wallet_events(&events);
             }
             #[cfg(feature = "esplora")]
             Esplora {
@@ -223,9 +233,8 @@ impl AsyncAppCommand<AppContext<OnlineOperations<'_>>> for SyncCommand {
                     .sync(request, *parallel_requests)
                     .await
                     .map_err(|e| *e)?;
-                wallet
-                    .apply_update(update)
-                    .map_err(|e| Error::Generic(e.to_string()))?;
+                let events = wallet.apply_update_events(update)?;
+                print_wallet_events(&events);
             }
             #[cfg(feature = "rpc")]
             RpcClient { client } => {
@@ -247,27 +256,35 @@ impl AsyncAppCommand<AppContext<OnlineOperations<'_>>> for SyncCommand {
                         .filter(|tx| tx.chain_position.is_unconfirmed()),
                 );
 
-                while let Some(block_event) = emitter.next_block()? {
-                    if block_event.block_height() % 10_000 == 0 {
-                        let percent_done = f64::from(block_event.block_height())
-                            / f64::from(blockchain_info.headers as u32)
-                            * 100f64;
-                        println!(
-                            "Applying block at height: {}, {:.2}% done.",
+                let block_events = wallet.events_helper(|w| {
+                    while let Some(block_event) = emitter.next_block()? {
+                        if block_event.block_height() % 10_000 == 0 {
+                            let percent_done = f64::from(block_event.block_height())
+                                / f64::from(blockchain_info.headers as u32)
+                                * 100f64;
+                            println!(
+                                "Applying block at height: {}, {:.2}% done.",
+                                block_event.block_height(),
+                                percent_done
+                            );
+                        }
+
+                        w.apply_block_connected_to(
+                            &block_event.block,
                             block_event.block_height(),
-                            percent_done
-                        );
+                            block_event.connected_to(),
+                        )?;
                     }
-
-                    wallet.apply_block_connected_to(
-                        &block_event.block,
-                        block_event.block_height(),
-                        block_event.connected_to(),
-                    )?;
-                }
+                    Ok::<_, Error>(())
+                })?;
+                print_wallet_events(&block_events);
 
                 let mempool_txs = emitter.mempool()?;
-                wallet.apply_unconfirmed_txs(mempool_txs.update);
+                let mempool_events = wallet.apply_unconfirmed_txs_events(mempool_txs.update);
+                print_wallet_events(&mempool_events);
+
+                let evicted_events = wallet.apply_evicted_txs_events(mempool_txs.evicted);
+                print_wallet_events(&evicted_events);
             }
             #[cfg(feature = "cbf")]
             KyotoClient { client } => sync_kyoto_client(wallet, client)
index 21be3ad487450fe2b352f66c4488086f26fd0c10..4a2fad5ccffe57cf8a28ec82e0dbe551a1514dcb 100644 (file)
@@ -11,7 +11,7 @@ use bdk_sp::encoding::SilentPaymentCode;
     feature = "cbf",
     feature = "rpc"
 ))]
-use bdk_wallet::bitcoin::Psbt;
+use bdk_wallet::{bitcoin::Psbt, event::WalletEvent};
 use bdk_wallet::bitcoin::{Address, Network, OutPoint, ScriptBuf};
 
 use crate::commands::OfflineWalletSubCommand;
@@ -245,3 +245,64 @@ pub fn command_requires_db(command: &OfflineWalletSubCommand) -> bool {
         OfflineWalletSubCommand::CreateSpTx(_) => false,
     }
 }
+
+/// Print a human-readable summary of the wallet events produced by a sync or scan.
+///
+/// Emitted to stderr so it does not pollute the JSON result on stdout.
+#[cfg(any(
+    feature = "electrum",
+    feature = "esplora",
+    feature = "rpc",
+    feature = "cbf"
+))]
+pub fn print_wallet_events(events: &[WalletEvent]) {
+    for event in events {
+        match event {
+            WalletEvent::ChainTipChanged { old_tip, new_tip } => {
+                eprintln!(
+                    "Chain tip advanced from height {} to {}",
+                    old_tip.height, new_tip.height
+                );
+            }
+            WalletEvent::TxConfirmed {
+                txid,
+                block_time,
+                old_block_time,
+                ..
+            } => match old_block_time {
+                Some(old) => eprintln!(
+                    "Transaction {txid} re-confirmed at height {} (was height {}, likely a reorg)",
+                    block_time.block_id.height, old.block_id.height
+                ),
+                None => eprintln!(
+                    "Transaction {txid} confirmed at height {}",
+                    block_time.block_id.height
+                ),
+            },
+            WalletEvent::TxUnconfirmed {
+                txid,
+                old_block_time,
+                ..
+            } => match old_block_time {
+                Some(old) => eprintln!(
+                    "Transaction {txid} became unconfirmed (was confirmed at height {}, likely a reorg)",
+                    old.block_id.height
+                ),
+                None => eprintln!("Transaction {txid} seen in mempool"),
+            },
+            WalletEvent::TxReplaced {
+                txid, conflicts, ..
+            } => {
+                let ids: Vec<String> = conflicts.iter().map(|(_, c)| c.to_string()).collect();
+                eprintln!(
+                    "Transaction {txid} was replaced (conflicts with: {})",
+                    ids.join(", ")
+                );
+            }
+            WalletEvent::TxDropped { txid, .. } => {
+                eprintln!("Transaction {txid} dropped from the mempool");
+            }
+            _ => {}
+        }
+    }
+}