From 67ee04024efa788a202bbf9458542038e93516a2 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Mon, 30 Jun 2025 17:17:50 +0100 Subject: [PATCH 1/4] docs(wallet): add `sync` to `bdk_wallet` examples - add partial syncing to electrum, esplora_async and esplora_blocking examples - add applying evicted txns to wallet in electrum, esplora_async and esplora_blocking examples --- examples/example_wallet_electrum/src/main.rs | 78 ++++++++++++++++- .../example_wallet_esplora_async/src/main.rs | 83 +++++++++++++++++-- .../src/main.rs | 74 ++++++++++++++++- 3 files changed, 225 insertions(+), 10 deletions(-) diff --git a/examples/example_wallet_electrum/src/main.rs b/examples/example_wallet_electrum/src/main.rs index f27d1f03..4fde628e 100644 --- a/examples/example_wallet_electrum/src/main.rs +++ b/examples/example_wallet_electrum/src/main.rs @@ -1,3 +1,4 @@ +use bdk_wallet::bitcoin::Txid; use bdk_wallet::file_store::Store; use bdk_wallet::Wallet; use std::io::Write; @@ -44,7 +45,7 @@ fn main() -> Result<(), anyhow::Error> { let balance = wallet.balance(); println!("Wallet balance before syncing: {}", balance.total()); - print!("Syncing..."); + println!("=== Performing Full Sync ==="); let client = BdkElectrumClient::new(electrum_client::Client::new(ELECTRUM_URL)?); // Populate the electrum client's transaction cache so it doesn't redownload transaction we @@ -71,7 +72,12 @@ fn main() -> Result<(), anyhow::Error> { wallet.persist(&mut db)?; let balance = wallet.balance(); - println!("Wallet balance after syncing: {}", balance.total()); + println!("Wallet balance after full sync: {}", balance.total()); + println!( + "Wallet has {} transactions and {} utxos after full sync", + wallet.transactions().count(), + wallet.list_unspent().count() + ); if balance.total() < SEND_AMOUNT { println!("Please send at least {SEND_AMOUNT} to the receiving address"); @@ -89,5 +95,73 @@ fn main() -> Result<(), anyhow::Error> { client.transaction_broadcast(&tx)?; println!("Tx broadcasted! Txid: {}", tx.compute_txid()); + let unconfirmed_txids: HashSet = wallet + .transactions() + .filter(|tx| tx.chain_position.is_unconfirmed()) + .map(|tx| tx.tx_node.txid) + .collect(); + + client.populate_tx_cache(wallet.tx_graph().full_txs().map(|tx_node| tx_node.tx)); + + println!("\n=== Performing Partial Sync ===\n"); + print!("SCANNING: "); + let mut last_printed = 0; + let sync_request = wallet + .start_sync_with_revealed_spks() + .inspect(move |_, sync_progress| { + let progress_percent = + (100 * sync_progress.consumed()) as f32 / sync_progress.total() as f32; + let progress_percent = progress_percent.round() as u32; + if progress_percent.is_multiple_of(5) && progress_percent > last_printed { + print!("{}% ", progress_percent); + std::io::stdout().flush().expect("must flush"); + last_printed = progress_percent; + } + }); + client.populate_tx_cache(wallet.tx_graph().full_txs().map(|tx_node| tx_node.tx)); + let sync_update = client.sync(sync_request, BATCH_SIZE, false)?; + println!(); + + let mut evicted_txs = Vec::new(); + for txid in unconfirmed_txids { + let tx_node = wallet + .tx_graph() + .full_txs() + .find(|full_tx| full_tx.txid == txid); + let wallet_tx = wallet.get_tx(txid); + + let is_evicted = match wallet_tx { + Some(wallet_tx) => { + !wallet_tx.chain_position.is_unconfirmed() + && !wallet_tx.chain_position.is_confirmed() + } + None => true, + }; + + if is_evicted { + if let Some(full_tx) = tx_node { + evicted_txs.push((full_tx.txid, full_tx.last_seen.unwrap_or(0))); + } else { + evicted_txs.push((txid, 0)); + } + } + } + + if !evicted_txs.is_empty() { + wallet.apply_evicted_txs(evicted_txs.clone()); + println!("Applied {} evicted transactions", evicted_txs.len()); + } + + wallet.apply_update(sync_update)?; + wallet.persist(&mut db)?; + + let balance_after_sync = wallet.balance(); + println!("Wallet balance after sync: {}", balance_after_sync.total()); + println!( + "Wallet has {} transactions and {} utxos after partial sync", + wallet.transactions().count(), + wallet.list_unspent().count() + ); + Ok(()) } diff --git a/examples/example_wallet_esplora_async/src/main.rs b/examples/example_wallet_esplora_async/src/main.rs index a0c61369..0477eb9e 100644 --- a/examples/example_wallet_esplora_async/src/main.rs +++ b/examples/example_wallet_esplora_async/src/main.rs @@ -1,12 +1,14 @@ -use std::{collections::BTreeSet, io::Write}; - use anyhow::Ok; use bdk_esplora::{esplora_client, EsploraAsyncExt}; use bdk_wallet::{ - bitcoin::{Amount, Network}, + bitcoin::{Amount, Network, Txid}, rusqlite::Connection, KeychainKind, SignOptions, Wallet, }; +use std::{ + collections::{BTreeSet, HashSet}, + io::Write, +}; const SEND_AMOUNT: Amount = Amount::from_sat(5000); const STOP_GAP: usize = 5; @@ -42,7 +44,7 @@ async fn main() -> Result<(), anyhow::Error> { let balance = wallet.balance(); println!("Wallet balance before syncing: {}", balance.total()); - print!("Syncing..."); + println!("=== Performing Full Sync ==="); let client = esplora_client::Builder::new(ESPLORA_URL).build_async()?; let request = wallet.start_full_scan().inspect({ @@ -66,7 +68,12 @@ async fn main() -> Result<(), anyhow::Error> { println!(); let balance = wallet.balance(); - println!("Wallet balance after syncing: {}", balance.total()); + println!("Wallet balance after full sync: {}", balance.total()); + println!( + "Wallet has {} transactions and {} utxos after full sync", + wallet.transactions().count(), + wallet.list_unspent().count() + ); if balance.total() < SEND_AMOUNT { println!("Please send at least {SEND_AMOUNT} to the receiving address"); @@ -84,5 +91,71 @@ async fn main() -> Result<(), anyhow::Error> { client.broadcast(&tx).await?; println!("Tx broadcasted! Txid: {}", tx.compute_txid()); + let unconfirmed_txids: HashSet = wallet + .transactions() + .filter(|tx| tx.chain_position.is_unconfirmed()) + .map(|tx| tx.tx_node.txid) + .collect(); + + println!("\n=== Performing Partial Sync ===\n"); + print!("SCANNING: "); + let mut printed = 0; + let sync_request = wallet + .start_sync_with_revealed_spks() + .inspect(move |_, sync_progress| { + let progress_percent = + (100 * sync_progress.consumed()) as f32 / sync_progress.total() as f32; + let progress_percent = progress_percent.round() as u32; + if progress_percent.is_multiple_of(5) && progress_percent > printed { + print!("{}% ", progress_percent); + std::io::stdout().flush().expect("must flush"); + printed = progress_percent; + } + }); + let sync_update = client.sync(sync_request, PARALLEL_REQUESTS).await?; + println!(); + + let mut evicted_txs = Vec::new(); + for txid in unconfirmed_txids { + let tx_node = wallet + .tx_graph() + .full_txs() + .find(|full_tx| full_tx.txid == txid); + let wallet_tx = wallet.get_tx(txid); + + let is_evicted = match wallet_tx { + Some(wallet_tx) => { + !wallet_tx.chain_position.is_unconfirmed() + && !wallet_tx.chain_position.is_confirmed() + } + None => true, + }; + + if is_evicted { + if let Some(full_tx) = tx_node { + evicted_txs.push((full_tx.txid, full_tx.last_seen.unwrap_or(0))); + } else { + evicted_txs.push((txid, 0)); + } + } + } + + if !evicted_txs.is_empty() { + let evicted_count = evicted_txs.len(); + wallet.apply_evicted_txs(evicted_txs); + println!("Applied {evicted_count} evicted transactions"); + } + + wallet.apply_update(sync_update)?; + wallet.persist(&mut conn)?; + + let balance_after_sync = wallet.balance(); + println!("Wallet balance after sync: {}", balance_after_sync.total()); + println!( + "Wallet has {} transactions and {} utxos after partial sync", + wallet.transactions().count(), + wallet.list_unspent().count() + ); + Ok(()) } diff --git a/examples/example_wallet_esplora_blocking/src/main.rs b/examples/example_wallet_esplora_blocking/src/main.rs index de1512ee..b964de87 100644 --- a/examples/example_wallet_esplora_blocking/src/main.rs +++ b/examples/example_wallet_esplora_blocking/src/main.rs @@ -1,11 +1,13 @@ -use std::{collections::BTreeSet, io::Write}; - use bdk_esplora::{esplora_client, EsploraExt}; use bdk_wallet::{ - bitcoin::{Amount, Network}, + bitcoin::{Amount, Network, Txid}, file_store::Store, KeychainKind, SignOptions, Wallet, }; +use std::{ + collections::{BTreeSet, HashSet}, + io::Write, +}; const DB_MAGIC: &str = "bdk_wallet_esplora_example"; const DB_PATH: &str = "bdk-example-esplora-blocking.db"; @@ -84,5 +86,71 @@ fn main() -> Result<(), anyhow::Error> { client.broadcast(&tx)?; println!("Tx broadcasted! Txid: {}", tx.compute_txid()); + let unconfirmed_txids: HashSet = wallet + .transactions() + .filter(|tx| tx.chain_position.is_unconfirmed()) + .map(|tx| tx.tx_node.txid) + .collect(); + + println!("\n=== Performing Partial Sync ===\n"); + print!("SCANNING: "); + let mut printed = 0; + let sync_request = wallet + .start_sync_with_revealed_spks() + .inspect(move |_, sync_progress| { + let progress_percent = + (100 * sync_progress.consumed()) as f32 / sync_progress.total() as f32; + let progress_percent = progress_percent.round() as u32; + if progress_percent.is_multiple_of(5) && progress_percent > printed { + print!("{progress_percent}% "); + std::io::stdout().flush().expect("must flush"); + printed = progress_percent; + } + }); + let sync_update = client.sync(sync_request, PARALLEL_REQUESTS)?; + println!(); + + let mut evicted_txs = Vec::new(); + for txid in unconfirmed_txids { + let tx_node = wallet + .tx_graph() + .full_txs() + .find(|full_tx| full_tx.txid == txid); + let wallet_tx = wallet.get_tx(txid); + + let is_evicted = match wallet_tx { + Some(wallet_tx) => { + !wallet_tx.chain_position.is_unconfirmed() + && !wallet_tx.chain_position.is_confirmed() + } + None => true, + }; + + if is_evicted { + if let Some(full_tx) = tx_node { + evicted_txs.push((full_tx.txid, full_tx.last_seen.unwrap_or(0))); + } else { + evicted_txs.push((txid, 0)); + } + } + } + + if !evicted_txs.is_empty() { + let evicted_count = evicted_txs.len(); + wallet.apply_evicted_txs(evicted_txs); + println!("Applied {evicted_count} evicted transactions"); + } + + wallet.apply_update(sync_update)?; + wallet.persist(&mut db)?; + + let balance_after_sync = wallet.balance(); + println!("Wallet balance after sync: {}", balance_after_sync.total()); + println!( + "Wallet has {} transactions and {} utxos after partial sync", + wallet.transactions().count(), + wallet.list_unspent().count() + ); + Ok(()) } From fabd2d9c39fd98f66841e92dc78fb746ba034b3f Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Fri, 4 Jul 2025 12:05:16 +0100 Subject: [PATCH 2/4] docs(wallet): fix clippy warnings --- examples/example_wallet_electrum/src/main.rs | 2 +- examples/example_wallet_esplora_async/src/main.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/example_wallet_electrum/src/main.rs b/examples/example_wallet_electrum/src/main.rs index 4fde628e..2479b111 100644 --- a/examples/example_wallet_electrum/src/main.rs +++ b/examples/example_wallet_electrum/src/main.rs @@ -113,7 +113,7 @@ fn main() -> Result<(), anyhow::Error> { (100 * sync_progress.consumed()) as f32 / sync_progress.total() as f32; let progress_percent = progress_percent.round() as u32; if progress_percent.is_multiple_of(5) && progress_percent > last_printed { - print!("{}% ", progress_percent); + print!("{progress_percent}% "); std::io::stdout().flush().expect("must flush"); last_printed = progress_percent; } diff --git a/examples/example_wallet_esplora_async/src/main.rs b/examples/example_wallet_esplora_async/src/main.rs index 0477eb9e..4191ea0d 100644 --- a/examples/example_wallet_esplora_async/src/main.rs +++ b/examples/example_wallet_esplora_async/src/main.rs @@ -107,7 +107,7 @@ async fn main() -> Result<(), anyhow::Error> { (100 * sync_progress.consumed()) as f32 / sync_progress.total() as f32; let progress_percent = progress_percent.round() as u32; if progress_percent.is_multiple_of(5) && progress_percent > printed { - print!("{}% ", progress_percent); + print!("{progress_percent}% "); std::io::stdout().flush().expect("must flush"); printed = progress_percent; } From 5d2d25fe1a5cd83ab6edb941f4e946bb30320d6a Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Tue, 29 Jul 2025 11:09:14 +0100 Subject: [PATCH 3/4] docs(wallet): add bumping tx to examples --- examples/example_wallet_electrum/src/main.rs | 84 +++++++------ .../example_wallet_esplora_async/src/main.rs | 111 +++++++++++------- .../src/main.rs | 99 +++++++++------- 3 files changed, 175 insertions(+), 119 deletions(-) diff --git a/examples/example_wallet_electrum/src/main.rs b/examples/example_wallet_electrum/src/main.rs index 2479b111..d623e2b3 100644 --- a/examples/example_wallet_electrum/src/main.rs +++ b/examples/example_wallet_electrum/src/main.rs @@ -1,5 +1,6 @@ -use bdk_wallet::bitcoin::Txid; +use bdk_wallet::bitcoin::FeeRate; use bdk_wallet::file_store::Store; +use bdk_wallet::psbt::PsbtUtils; use bdk_wallet::Wallet; use std::io::Write; @@ -45,7 +46,7 @@ fn main() -> Result<(), anyhow::Error> { let balance = wallet.balance(); println!("Wallet balance before syncing: {}", balance.total()); - println!("=== Performing Full Sync ==="); + println!("Performing Full Sync..."); let client = BdkElectrumClient::new(electrum_client::Client::new(ELECTRUM_URL)?); // Populate the electrum client's transaction cache so it doesn't redownload transaction we @@ -59,7 +60,9 @@ fn main() -> Result<(), anyhow::Error> { if once.insert(k) { print!("\nScanning keychain [{k:?}]"); } - print!(" {spk_i:<3}"); + if spk_i.is_multiple_of(5) { + print!(" {spk_i:<3}"); + } stdout.flush().expect("must flush"); } }); @@ -83,27 +86,20 @@ fn main() -> Result<(), anyhow::Error> { println!("Please send at least {SEND_AMOUNT} to the receiving address"); std::process::exit(0); } - let mut tx_builder = wallet.build_tx(); tx_builder.add_recipient(address.script_pubkey(), SEND_AMOUNT); let mut psbt = tx_builder.finish()?; let finalized = wallet.sign(&mut psbt, SignOptions::default())?; assert!(finalized); - + let original_fee = psbt.fee_amount().unwrap(); + let tx_feerate = psbt.fee_rate().unwrap(); let tx = psbt.extract_tx()?; client.transaction_broadcast(&tx)?; - println!("Tx broadcasted! Txid: {}", tx.compute_txid()); - - let unconfirmed_txids: HashSet = wallet - .transactions() - .filter(|tx| tx.chain_position.is_unconfirmed()) - .map(|tx| tx.tx_node.txid) - .collect(); + let txid = tx.compute_txid(); + println!("Tx broadcasted! Txid: {txid}"); - client.populate_tx_cache(wallet.tx_graph().full_txs().map(|tx_node| tx_node.tx)); - - println!("\n=== Performing Partial Sync ===\n"); + println!("Partial Sync..."); print!("SCANNING: "); let mut last_printed = 0; let sync_request = wallet @@ -121,30 +117,44 @@ fn main() -> Result<(), anyhow::Error> { client.populate_tx_cache(wallet.tx_graph().full_txs().map(|tx_node| tx_node.tx)); let sync_update = client.sync(sync_request, BATCH_SIZE, false)?; println!(); + wallet.apply_update(sync_update)?; + wallet.persist(&mut db)?; + + // bump fee tx + let feerate = FeeRate::from_sat_per_kwu(tx_feerate.to_sat_per_kwu() + 250); + let mut builder = wallet.build_fee_bump(txid).expect("failed to bump tx"); + builder.fee_rate(feerate); + let mut bumped_psbt = builder.finish().unwrap(); + let finalize_btx = wallet.sign(&mut bumped_psbt, SignOptions::default())?; + assert!(finalize_btx); + let new_fee = bumped_psbt.fee_amount().unwrap(); + let bumped_tx = bumped_psbt.extract_tx()?; + assert_eq!( + bumped_tx + .output + .iter() + .find(|txout| txout.script_pubkey == address.script_pubkey()) + .unwrap() + .value, + SEND_AMOUNT, + "Recipient output should remain unchanged" + ); + assert!( + new_fee > original_fee, + "New fee ({}) should be higher than original ({})", + new_fee, + original_fee + ); + client.transaction_broadcast(&bumped_tx)?; + println!("Broadcasted bumped tx. Txid: {}", bumped_tx.compute_txid()); + + print!("Syncing after bumped tx broadcast..."); + let sync_request = wallet.start_sync_with_revealed_spks().inspect(|_, _| {}); + let sync_update = client.sync(sync_request, BATCH_SIZE, false)?; let mut evicted_txs = Vec::new(); - for txid in unconfirmed_txids { - let tx_node = wallet - .tx_graph() - .full_txs() - .find(|full_tx| full_tx.txid == txid); - let wallet_tx = wallet.get_tx(txid); - - let is_evicted = match wallet_tx { - Some(wallet_tx) => { - !wallet_tx.chain_position.is_unconfirmed() - && !wallet_tx.chain_position.is_confirmed() - } - None => true, - }; - - if is_evicted { - if let Some(full_tx) = tx_node { - evicted_txs.push((full_tx.txid, full_tx.last_seen.unwrap_or(0))); - } else { - evicted_txs.push((txid, 0)); - } - } + for (txid, last_seen) in &sync_update.tx_update.evicted_ats { + evicted_txs.push((*txid, *last_seen)); } if !evicted_txs.is_empty() { diff --git a/examples/example_wallet_esplora_async/src/main.rs b/examples/example_wallet_esplora_async/src/main.rs index 4191ea0d..7d3265de 100644 --- a/examples/example_wallet_esplora_async/src/main.rs +++ b/examples/example_wallet_esplora_async/src/main.rs @@ -1,14 +1,12 @@ use anyhow::Ok; use bdk_esplora::{esplora_client, EsploraAsyncExt}; use bdk_wallet::{ - bitcoin::{Amount, Network, Txid}, + bitcoin::{Amount, FeeRate, Network}, + psbt::PsbtUtils, rusqlite::Connection, KeychainKind, SignOptions, Wallet, }; -use std::{ - collections::{BTreeSet, HashSet}, - io::Write, -}; +use std::{collections::BTreeSet, io::Write}; const SEND_AMOUNT: Amount = Amount::from_sat(5000); const STOP_GAP: usize = 5; @@ -23,7 +21,6 @@ const ESPLORA_URL: &str = "http://signet.bitcoindevkit.net"; #[tokio::main] async fn main() -> Result<(), anyhow::Error> { let mut conn = Connection::open(DB_PATH)?; - let wallet_opt = Wallet::load() .descriptor(KeychainKind::External, Some(EXTERNAL_DESC)) .descriptor(KeychainKind::Internal, Some(INTERNAL_DESC)) @@ -39,12 +36,12 @@ async fn main() -> Result<(), anyhow::Error> { let address = wallet.next_unused_address(KeychainKind::External); wallet.persist(&mut conn)?; - println!("Next unused address: ({}) {}", address.index, address); + println!("Next unused address: ({}) {address}", address.index); let balance = wallet.balance(); println!("Wallet balance before syncing: {}", balance.total()); - println!("=== Performing Full Sync ==="); + println!("Full Sync..."); let client = esplora_client::Builder::new(ESPLORA_URL).build_async()?; let request = wallet.start_full_scan().inspect({ @@ -54,7 +51,9 @@ async fn main() -> Result<(), anyhow::Error> { if once.insert(keychain) { print!("\nScanning keychain [{keychain:?}]"); } - print!(" {spk_i:<3}"); + if spk_i.is_multiple_of(5) { + print!(" {spk_i:<3}"); + } stdout.flush().expect("must flush") } }); @@ -79,27 +78,22 @@ async fn main() -> Result<(), anyhow::Error> { println!("Please send at least {SEND_AMOUNT} to the receiving address"); std::process::exit(0); } - let mut tx_builder = wallet.build_tx(); tx_builder.add_recipient(address.script_pubkey(), SEND_AMOUNT); let mut psbt = tx_builder.finish()?; let finalized = wallet.sign(&mut psbt, SignOptions::default())?; assert!(finalized); - + let original_fee = psbt.fee_amount().unwrap(); + let tx_feerate = psbt.fee_rate().unwrap(); let tx = psbt.extract_tx()?; client.broadcast(&tx).await?; - println!("Tx broadcasted! Txid: {}", tx.compute_txid()); - - let unconfirmed_txids: HashSet = wallet - .transactions() - .filter(|tx| tx.chain_position.is_unconfirmed()) - .map(|tx| tx.tx_node.txid) - .collect(); + let txid = tx.compute_txid(); + println!("Tx broadcasted! Txid: {txid}"); - println!("\n=== Performing Partial Sync ===\n"); + println!("Partial Sync..."); print!("SCANNING: "); - let mut printed = 0; + let mut printed: u32 = 0; let sync_request = wallet .start_sync_with_revealed_spks() .inspect(move |_, sync_progress| { @@ -114,30 +108,63 @@ async fn main() -> Result<(), anyhow::Error> { }); let sync_update = client.sync(sync_request, PARALLEL_REQUESTS).await?; println!(); + wallet.apply_update(sync_update)?; + wallet.persist(&mut conn)?; - let mut evicted_txs = Vec::new(); - for txid in unconfirmed_txids { - let tx_node = wallet - .tx_graph() - .full_txs() - .find(|full_tx| full_tx.txid == txid); - let wallet_tx = wallet.get_tx(txid); - - let is_evicted = match wallet_tx { - Some(wallet_tx) => { - !wallet_tx.chain_position.is_unconfirmed() - && !wallet_tx.chain_position.is_confirmed() - } - None => true, - }; - - if is_evicted { - if let Some(full_tx) = tx_node { - evicted_txs.push((full_tx.txid, full_tx.last_seen.unwrap_or(0))); - } else { - evicted_txs.push((txid, 0)); + let feerate = FeeRate::from_sat_per_kwu(tx_feerate.to_sat_per_kwu() + 250); + let mut builder = wallet.build_fee_bump(txid).expect("failed to bump tx"); + builder.fee_rate(feerate); + let mut bumped_psbt = builder.finish().unwrap(); + let finalize_btx = wallet.sign(&mut bumped_psbt, SignOptions::default())?; + assert!(finalize_btx); + let new_fee = bumped_psbt.fee_amount().unwrap(); + let bumped_tx = bumped_psbt.extract_tx()?; + assert_eq!( + bumped_tx + .output + .iter() + .find(|txout| txout.script_pubkey == address.script_pubkey()) + .unwrap() + .value, + SEND_AMOUNT, + "Outputs should be the same" + ); + assert!( + new_fee > original_fee, + "New fee ({new_fee}) should be higher than original ({original_fee})", + ); + client.broadcast(&bumped_tx).await?; + println!("Broadcasted bumped tx. Txid: {}", bumped_tx.compute_txid()); + + println!("syncing after broadcasting bumped tx..."); + print!("SCANNING: "); + let sync_request = wallet + .start_sync_with_revealed_spks() + .inspect(move |_, sync_progress| { + let progress_percent = + (100 * sync_progress.consumed()) as f32 / sync_progress.total() as f32; + let progress_percent = progress_percent.round() as u32; + if progress_percent.is_multiple_of(10) && progress_percent > printed { + print!("{progress_percent}% "); + std::io::stdout().flush().expect("must flush"); + printed = progress_percent; } - } + }); + let sync_update = client.sync(sync_request, PARALLEL_REQUESTS).await?; + println!(); + + let mut evicted_txs = Vec::new(); + + let last_seen = wallet + .tx_graph() + .full_txs() + .find(|full_tx| full_tx.txid == txid) + .map_or(0, |full_tx| full_tx.last_seen.unwrap_or(0)); + if !evicted_txs + .iter() + .any(|(evicted_txid, _)| evicted_txid == &txid) + { + evicted_txs.push((txid, last_seen)); } if !evicted_txs.is_empty() { diff --git a/examples/example_wallet_esplora_blocking/src/main.rs b/examples/example_wallet_esplora_blocking/src/main.rs index b964de87..276feb91 100644 --- a/examples/example_wallet_esplora_blocking/src/main.rs +++ b/examples/example_wallet_esplora_blocking/src/main.rs @@ -1,13 +1,11 @@ use bdk_esplora::{esplora_client, EsploraExt}; use bdk_wallet::{ - bitcoin::{Amount, Network, Txid}, + bitcoin::{Amount, FeeRate, Network}, file_store::Store, + psbt::PsbtUtils, KeychainKind, SignOptions, Wallet, }; -use std::{ - collections::{BTreeSet, HashSet}, - io::Write, -}; +use std::{collections::BTreeSet, io::Write}; const DB_MAGIC: &str = "bdk_wallet_esplora_example"; const DB_PATH: &str = "bdk-example-esplora-blocking.db"; @@ -46,7 +44,7 @@ fn main() -> Result<(), anyhow::Error> { let balance = wallet.balance(); println!("Wallet balance before syncing: {}", balance.total()); - print!("Syncing..."); + println!("Full Sync..."); let client = esplora_client::Builder::new(ESPLORA_URL).build_blocking(); let request = wallet.start_full_scan().inspect({ @@ -56,13 +54,14 @@ fn main() -> Result<(), anyhow::Error> { if once.insert(keychain) { print!("\nScanning keychain [{keychain:?}] "); } - print!(" {spk_i:<3}"); + if spk_i.is_multiple_of(5) { + print!(" {spk_i:<3}"); + } stdout.flush().expect("must flush") } }); let update = client.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)?; - wallet.apply_update(update)?; wallet.persist(&mut db)?; println!(); @@ -81,18 +80,14 @@ fn main() -> Result<(), anyhow::Error> { let mut psbt = tx_builder.finish()?; let finalized = wallet.sign(&mut psbt, SignOptions::default())?; assert!(finalized); - + let original_fee = psbt.fee_amount().unwrap(); + let tx_feerate = psbt.fee_rate().unwrap(); let tx = psbt.extract_tx()?; client.broadcast(&tx)?; - println!("Tx broadcasted! Txid: {}", tx.compute_txid()); - - let unconfirmed_txids: HashSet = wallet - .transactions() - .filter(|tx| tx.chain_position.is_unconfirmed()) - .map(|tx| tx.tx_node.txid) - .collect(); + let txid = tx.compute_txid(); + println!("Tx broadcasted! Txid: {txid}"); - println!("\n=== Performing Partial Sync ===\n"); + println!("Partial Sync..."); print!("SCANNING: "); let mut printed = 0; let sync_request = wallet @@ -108,31 +103,55 @@ fn main() -> Result<(), anyhow::Error> { } }); let sync_update = client.sync(sync_request, PARALLEL_REQUESTS)?; + + wallet.apply_update(sync_update)?; + wallet.persist(&mut db)?; + println!(); + + let feerate = FeeRate::from_sat_per_kwu(tx_feerate.to_sat_per_kwu() + 200); + let mut builder = wallet.build_fee_bump(txid).unwrap(); + builder.fee_rate(feerate); + let mut new_psbt = builder.finish().unwrap(); + let finalize_tx = wallet.sign(&mut new_psbt, SignOptions::default())?; + assert!(finalize_tx); + let new_fee = new_psbt.fee_amount().unwrap(); + let bumped_tx = new_psbt.extract_tx()?; + assert_eq!( + bumped_tx + .output + .iter() + .find(|txout| txout.script_pubkey == address.script_pubkey()) + .unwrap() + .value, + SEND_AMOUNT, + "Outputs should remain the same" + ); + assert!( + new_fee > original_fee, + "Replacement tx fee ({new_fee}) should be higher than original ({original_fee})", + ); + client.broadcast(&bumped_tx)?; + println!( + "Broadcast replacement transaction. Txid: {}", + bumped_tx.compute_txid() + ); + + println!("Syncing after bumped tx..."); + let sync_request = wallet.start_sync_with_revealed_spks().inspect(|_, _| {}); + let sync_update = client.sync(sync_request, PARALLEL_REQUESTS)?; println!(); let mut evicted_txs = Vec::new(); - for txid in unconfirmed_txids { - let tx_node = wallet - .tx_graph() - .full_txs() - .find(|full_tx| full_tx.txid == txid); - let wallet_tx = wallet.get_tx(txid); - - let is_evicted = match wallet_tx { - Some(wallet_tx) => { - !wallet_tx.chain_position.is_unconfirmed() - && !wallet_tx.chain_position.is_confirmed() - } - None => true, - }; - - if is_evicted { - if let Some(full_tx) = tx_node { - evicted_txs.push((full_tx.txid, full_tx.last_seen.unwrap_or(0))); - } else { - evicted_txs.push((txid, 0)); - } - } + let last_seen = wallet + .tx_graph() + .full_txs() + .find(|full_tx| full_tx.txid == txid) + .map_or(0, |full_tx| full_tx.last_seen.unwrap_or(0)); + if !evicted_txs + .iter() + .any(|(evicted_txid, _)| evicted_txid == &txid) + { + evicted_txs.push((txid, last_seen)); } if !evicted_txs.is_empty() { @@ -147,7 +166,7 @@ fn main() -> Result<(), anyhow::Error> { let balance_after_sync = wallet.balance(); println!("Wallet balance after sync: {}", balance_after_sync.total()); println!( - "Wallet has {} transactions and {} utxos after partial sync", + "Wallet has {} transactions and {} utxos", wallet.transactions().count(), wallet.list_unspent().count() ); From 11bfc2f649432ba795ce61a16f85ece7c0aab490 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Wed, 30 Jul 2025 15:22:30 -0500 Subject: [PATCH 4/4] docs(wallet): for electrum, esplora examples use sqlite+testnet4, and mempool.space For electrum, esplora examples print mempool.space tx URLs and add delay before RBF. For rpc example also add justfile to help testing with regtest bitcoind. --- examples/example_wallet_electrum/Cargo.toml | 2 +- examples/example_wallet_electrum/src/main.rs | 51 +++++----- .../example_wallet_esplora_async/src/main.rs | 54 +++++------ .../Cargo.toml | 2 +- .../src/main.rs | 45 ++++----- examples/example_wallet_rpc/.gitignore | 1 + examples/example_wallet_rpc/Cargo.toml | 2 +- examples/example_wallet_rpc/README.md | 62 ++++++------- examples/example_wallet_rpc/justfile | 92 +++++++++++++++++++ examples/example_wallet_rpc/src/main.rs | 18 ++-- 10 files changed, 213 insertions(+), 116 deletions(-) create mode 100644 examples/example_wallet_rpc/.gitignore create mode 100644 examples/example_wallet_rpc/justfile diff --git a/examples/example_wallet_electrum/Cargo.toml b/examples/example_wallet_electrum/Cargo.toml index 3b20cf60..477fa461 100644 --- a/examples/example_wallet_electrum/Cargo.toml +++ b/examples/example_wallet_electrum/Cargo.toml @@ -4,6 +4,6 @@ version = "0.2.0" edition = "2021" [dependencies] -bdk_wallet = { path = "../../wallet", features = ["file_store"] } +bdk_wallet = { path = "../../wallet", features = ["rusqlite"] } bdk_electrum = { version = "0.23.0" } anyhow = "1" diff --git a/examples/example_wallet_electrum/src/main.rs b/examples/example_wallet_electrum/src/main.rs index d623e2b3..f431873d 100644 --- a/examples/example_wallet_electrum/src/main.rs +++ b/examples/example_wallet_electrum/src/main.rs @@ -1,31 +1,29 @@ -use bdk_wallet::bitcoin::FeeRate; -use bdk_wallet::file_store::Store; -use bdk_wallet::psbt::PsbtUtils; -use bdk_wallet::Wallet; -use std::io::Write; - use bdk_electrum::electrum_client; use bdk_electrum::BdkElectrumClient; use bdk_wallet::bitcoin::Amount; +use bdk_wallet::bitcoin::FeeRate; use bdk_wallet::bitcoin::Network; use bdk_wallet::chain::collections::HashSet; +use bdk_wallet::psbt::PsbtUtils; +use bdk_wallet::rusqlite::Connection; +use bdk_wallet::Wallet; use bdk_wallet::{KeychainKind, SignOptions}; +use std::io::Write; +use std::thread::sleep; +use std::time::Duration; -const DB_MAGIC: &str = "bdk_wallet_electrum_example"; const SEND_AMOUNT: Amount = Amount::from_sat(5000); const STOP_GAP: usize = 50; const BATCH_SIZE: usize = 5; -const NETWORK: Network = Network::Testnet; +const DB_PATH: &str = "bdk-example-electrum.sqlite"; +const NETWORK: Network = Network::Testnet4; const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; -const ELECTRUM_URL: &str = "ssl://electrum.blockstream.info:60002"; +const ELECTRUM_URL: &str = "ssl://mempool.space:40002"; fn main() -> Result<(), anyhow::Error> { - let db_path = "bdk-electrum-example.db"; - - let (mut db, _) = Store::::load_or_create(DB_MAGIC.as_bytes(), db_path)?; - + let mut db = Connection::open(DB_PATH)?; let wallet_opt = Wallet::load() .descriptor(KeychainKind::External, Some(EXTERNAL_DESC)) .descriptor(KeychainKind::Internal, Some(INTERNAL_DESC)) @@ -86,8 +84,11 @@ fn main() -> Result<(), anyhow::Error> { println!("Please send at least {SEND_AMOUNT} to the receiving address"); std::process::exit(0); } + + let target_fee_rate = FeeRate::from_sat_per_vb(1).unwrap(); let mut tx_builder = wallet.build_tx(); tx_builder.add_recipient(address.script_pubkey(), SEND_AMOUNT); + tx_builder.fee_rate(target_fee_rate); let mut psbt = tx_builder.finish()?; let finalized = wallet.sign(&mut psbt, SignOptions::default())?; @@ -97,7 +98,7 @@ fn main() -> Result<(), anyhow::Error> { let tx = psbt.extract_tx()?; client.transaction_broadcast(&tx)?; let txid = tx.compute_txid(); - println!("Tx broadcasted! Txid: {txid}"); + println!("Tx broadcasted! Txid: https://mempool.space/testnet4/tx/{txid}"); println!("Partial Sync..."); print!("SCANNING: "); @@ -120,8 +121,8 @@ fn main() -> Result<(), anyhow::Error> { wallet.apply_update(sync_update)?; wallet.persist(&mut db)?; - // bump fee tx - let feerate = FeeRate::from_sat_per_kwu(tx_feerate.to_sat_per_kwu() + 250); + // bump fee rate for tx by at least 1 sat per vbyte + let feerate = FeeRate::from_sat_per_vb(tx_feerate.to_sat_per_vb_ceil() + 1).unwrap(); let mut builder = wallet.build_fee_bump(txid).expect("failed to bump tx"); builder.fee_rate(feerate); let mut bumped_psbt = builder.finish().unwrap(); @@ -141,14 +142,18 @@ fn main() -> Result<(), anyhow::Error> { ); assert!( new_fee > original_fee, - "New fee ({}) should be higher than original ({})", - new_fee, - original_fee + "New fee ({new_fee}) should be higher than original ({original_fee})" ); + + // wait for first transaction to make it into the mempool and be indexed on mempool.space + sleep(Duration::from_secs(10)); client.transaction_broadcast(&bumped_tx)?; - println!("Broadcasted bumped tx. Txid: {}", bumped_tx.compute_txid()); + println!( + "Broadcasted bumped tx. Txid: https://mempool.space/testnet4/tx/{}", + bumped_tx.compute_txid() + ); - print!("Syncing after bumped tx broadcast..."); + println!("Syncing after bumped tx broadcast..."); let sync_request = wallet.start_sync_with_revealed_spks().inspect(|_, _| {}); let sync_update = client.sync(sync_request, BATCH_SIZE, false)?; @@ -157,12 +162,10 @@ fn main() -> Result<(), anyhow::Error> { evicted_txs.push((*txid, *last_seen)); } + wallet.apply_update(sync_update)?; if !evicted_txs.is_empty() { - wallet.apply_evicted_txs(evicted_txs.clone()); println!("Applied {} evicted transactions", evicted_txs.len()); } - - wallet.apply_update(sync_update)?; wallet.persist(&mut db)?; let balance_after_sync = wallet.balance(); diff --git a/examples/example_wallet_esplora_async/src/main.rs b/examples/example_wallet_esplora_async/src/main.rs index 7d3265de..a852c3e2 100644 --- a/examples/example_wallet_esplora_async/src/main.rs +++ b/examples/example_wallet_esplora_async/src/main.rs @@ -7,35 +7,36 @@ use bdk_wallet::{ KeychainKind, SignOptions, Wallet, }; use std::{collections::BTreeSet, io::Write}; +use tokio::time::{sleep, Duration}; const SEND_AMOUNT: Amount = Amount::from_sat(5000); const STOP_GAP: usize = 5; const PARALLEL_REQUESTS: usize = 5; const DB_PATH: &str = "bdk-example-esplora-async.sqlite"; -const NETWORK: Network = Network::Signet; +const NETWORK: Network = Network::Testnet4; const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; -const ESPLORA_URL: &str = "http://signet.bitcoindevkit.net"; +const ESPLORA_URL: &str = "https://mempool.space/testnet4/api"; #[tokio::main] async fn main() -> Result<(), anyhow::Error> { - let mut conn = Connection::open(DB_PATH)?; + let mut db = Connection::open(DB_PATH)?; let wallet_opt = Wallet::load() .descriptor(KeychainKind::External, Some(EXTERNAL_DESC)) .descriptor(KeychainKind::Internal, Some(INTERNAL_DESC)) .extract_keys() .check_network(NETWORK) - .load_wallet(&mut conn)?; + .load_wallet(&mut db)?; let mut wallet = match wallet_opt { Some(wallet) => wallet, None => Wallet::create(EXTERNAL_DESC, INTERNAL_DESC) .network(NETWORK) - .create_wallet(&mut conn)?, + .create_wallet(&mut db)?, }; let address = wallet.next_unused_address(KeychainKind::External); - wallet.persist(&mut conn)?; + wallet.persist(&mut db)?; println!("Next unused address: ({}) {address}", address.index); let balance = wallet.balance(); @@ -63,7 +64,7 @@ async fn main() -> Result<(), anyhow::Error> { .await?; wallet.apply_update(update)?; - wallet.persist(&mut conn)?; + wallet.persist(&mut db)?; println!(); let balance = wallet.balance(); @@ -78,8 +79,11 @@ async fn main() -> Result<(), anyhow::Error> { println!("Please send at least {SEND_AMOUNT} to the receiving address"); std::process::exit(0); } + + let target_fee_rate = FeeRate::from_sat_per_vb(1).unwrap(); let mut tx_builder = wallet.build_tx(); tx_builder.add_recipient(address.script_pubkey(), SEND_AMOUNT); + tx_builder.fee_rate(target_fee_rate); let mut psbt = tx_builder.finish()?; let finalized = wallet.sign(&mut psbt, SignOptions::default())?; @@ -89,7 +93,7 @@ async fn main() -> Result<(), anyhow::Error> { let tx = psbt.extract_tx()?; client.broadcast(&tx).await?; let txid = tx.compute_txid(); - println!("Tx broadcasted! Txid: {txid}"); + println!("Tx broadcasted! Txid: https://mempool.space/testnet4/tx/{txid}"); println!("Partial Sync..."); print!("SCANNING: "); @@ -109,9 +113,10 @@ async fn main() -> Result<(), anyhow::Error> { let sync_update = client.sync(sync_request, PARALLEL_REQUESTS).await?; println!(); wallet.apply_update(sync_update)?; - wallet.persist(&mut conn)?; + wallet.persist(&mut db)?; - let feerate = FeeRate::from_sat_per_kwu(tx_feerate.to_sat_per_kwu() + 250); + // bump fee rate for tx by at least 1 sat per vbyte + let feerate = FeeRate::from_sat_per_vb(tx_feerate.to_sat_per_vb_ceil() + 1).unwrap(); let mut builder = wallet.build_fee_bump(txid).expect("failed to bump tx"); builder.fee_rate(feerate); let mut bumped_psbt = builder.finish().unwrap(); @@ -133,8 +138,14 @@ async fn main() -> Result<(), anyhow::Error> { new_fee > original_fee, "New fee ({new_fee}) should be higher than original ({original_fee})", ); + + // wait for first transaction to make it into the mempool and be indexed on mempool.space + sleep(Duration::from_secs(10)).await; client.broadcast(&bumped_tx).await?; - println!("Broadcasted bumped tx. Txid: {}", bumped_tx.compute_txid()); + println!( + "Broadcasted bumped tx. Txid: https://mempool.space/testnet4/tx/{}", + bumped_tx.compute_txid() + ); println!("syncing after broadcasting bumped tx..."); print!("SCANNING: "); @@ -155,26 +166,17 @@ async fn main() -> Result<(), anyhow::Error> { let mut evicted_txs = Vec::new(); - let last_seen = wallet - .tx_graph() - .full_txs() - .find(|full_tx| full_tx.txid == txid) - .map_or(0, |full_tx| full_tx.last_seen.unwrap_or(0)); - if !evicted_txs - .iter() - .any(|(evicted_txid, _)| evicted_txid == &txid) - { - evicted_txs.push((txid, last_seen)); + for (txid, last_seen) in &sync_update.tx_update.evicted_ats { + evicted_txs.push((*txid, *last_seen)); } + wallet.apply_update(sync_update)?; + if !evicted_txs.is_empty() { - let evicted_count = evicted_txs.len(); - wallet.apply_evicted_txs(evicted_txs); - println!("Applied {evicted_count} evicted transactions"); + println!("Applied {} evicted transactions", evicted_txs.len()); } - wallet.apply_update(sync_update)?; - wallet.persist(&mut conn)?; + wallet.persist(&mut db)?; let balance_after_sync = wallet.balance(); println!("Wallet balance after sync: {}", balance_after_sync.total()); diff --git a/examples/example_wallet_esplora_blocking/Cargo.toml b/examples/example_wallet_esplora_blocking/Cargo.toml index e0139ef5..db85e0f0 100644 --- a/examples/example_wallet_esplora_blocking/Cargo.toml +++ b/examples/example_wallet_esplora_blocking/Cargo.toml @@ -7,6 +7,6 @@ publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bdk_wallet = { path = "../../wallet", features = ["file_store"] } +bdk_wallet = { path = "../../wallet", features = ["rusqlite"] } bdk_esplora = { version = "0.22.0", features = ["blocking"] } anyhow = "1" diff --git a/examples/example_wallet_esplora_blocking/src/main.rs b/examples/example_wallet_esplora_blocking/src/main.rs index 276feb91..45e9201e 100644 --- a/examples/example_wallet_esplora_blocking/src/main.rs +++ b/examples/example_wallet_esplora_blocking/src/main.rs @@ -1,26 +1,26 @@ use bdk_esplora::{esplora_client, EsploraExt}; +use bdk_wallet::rusqlite::Connection; use bdk_wallet::{ bitcoin::{Amount, FeeRate, Network}, - file_store::Store, psbt::PsbtUtils, KeychainKind, SignOptions, Wallet, }; +use std::thread::sleep; +use std::time::Duration; use std::{collections::BTreeSet, io::Write}; -const DB_MAGIC: &str = "bdk_wallet_esplora_example"; -const DB_PATH: &str = "bdk-example-esplora-blocking.db"; const SEND_AMOUNT: Amount = Amount::from_sat(5000); const STOP_GAP: usize = 5; const PARALLEL_REQUESTS: usize = 5; -const NETWORK: Network = Network::Signet; +const DB_PATH: &str = "bdk-example-esplora-blocking.sqlite"; +const NETWORK: Network = Network::Testnet4; const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)"; const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)"; -const ESPLORA_URL: &str = "http://signet.bitcoindevkit.net"; +const ESPLORA_URL: &str = "https://mempool.space/testnet4/api"; fn main() -> Result<(), anyhow::Error> { - let (mut db, _) = Store::::load_or_create(DB_MAGIC.as_bytes(), DB_PATH)?; - + let mut db = Connection::open(DB_PATH)?; let wallet_opt = Wallet::load() .descriptor(KeychainKind::External, Some(EXTERNAL_DESC)) .descriptor(KeychainKind::Internal, Some(INTERNAL_DESC)) @@ -74,8 +74,10 @@ fn main() -> Result<(), anyhow::Error> { std::process::exit(0); } + let target_fee_rate = FeeRate::from_sat_per_vb(1).unwrap(); let mut tx_builder = wallet.build_tx(); tx_builder.add_recipient(address.script_pubkey(), SEND_AMOUNT); + tx_builder.fee_rate(target_fee_rate); let mut psbt = tx_builder.finish()?; let finalized = wallet.sign(&mut psbt, SignOptions::default())?; @@ -85,7 +87,7 @@ fn main() -> Result<(), anyhow::Error> { let tx = psbt.extract_tx()?; client.broadcast(&tx)?; let txid = tx.compute_txid(); - println!("Tx broadcasted! Txid: {txid}"); + println!("Tx broadcasted! Txid: https://mempool.space/testnet4/tx/{txid}"); println!("Partial Sync..."); print!("SCANNING: "); @@ -108,7 +110,8 @@ fn main() -> Result<(), anyhow::Error> { wallet.persist(&mut db)?; println!(); - let feerate = FeeRate::from_sat_per_kwu(tx_feerate.to_sat_per_kwu() + 200); + // bump fee rate for tx by at least 1 sat per vbyte + let feerate = FeeRate::from_sat_per_vb(tx_feerate.to_sat_per_vb_ceil() + 1).unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap(); builder.fee_rate(feerate); let mut new_psbt = builder.finish().unwrap(); @@ -130,9 +133,12 @@ fn main() -> Result<(), anyhow::Error> { new_fee > original_fee, "Replacement tx fee ({new_fee}) should be higher than original ({original_fee})", ); + + // wait for first transaction to make it into the mempool and be indexed on mempool.space + sleep(Duration::from_secs(10)); client.broadcast(&bumped_tx)?; println!( - "Broadcast replacement transaction. Txid: {}", + "Broadcast replacement transaction. Txid: https://mempool.space/testnet4/tx/{}", bumped_tx.compute_txid() ); @@ -142,25 +148,14 @@ fn main() -> Result<(), anyhow::Error> { println!(); let mut evicted_txs = Vec::new(); - let last_seen = wallet - .tx_graph() - .full_txs() - .find(|full_tx| full_tx.txid == txid) - .map_or(0, |full_tx| full_tx.last_seen.unwrap_or(0)); - if !evicted_txs - .iter() - .any(|(evicted_txid, _)| evicted_txid == &txid) - { - evicted_txs.push((txid, last_seen)); + for (txid, last_seen) in &sync_update.tx_update.evicted_ats { + evicted_txs.push((*txid, *last_seen)); } + wallet.apply_update(sync_update)?; if !evicted_txs.is_empty() { - let evicted_count = evicted_txs.len(); - wallet.apply_evicted_txs(evicted_txs); - println!("Applied {evicted_count} evicted transactions"); + println!("Applied {} evicted transactions", evicted_txs.len()); } - - wallet.apply_update(sync_update)?; wallet.persist(&mut db)?; let balance_after_sync = wallet.balance(); diff --git a/examples/example_wallet_rpc/.gitignore b/examples/example_wallet_rpc/.gitignore new file mode 100644 index 00000000..61d5a15b --- /dev/null +++ b/examples/example_wallet_rpc/.gitignore @@ -0,0 +1 @@ +test_data/ \ No newline at end of file diff --git a/examples/example_wallet_rpc/Cargo.toml b/examples/example_wallet_rpc/Cargo.toml index 4efa187b..354f8f15 100644 --- a/examples/example_wallet_rpc/Cargo.toml +++ b/examples/example_wallet_rpc/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bdk_wallet = { path = "../../wallet", features = ["file_store"] } +bdk_wallet = { path = "../../wallet", features = ["rusqlite"] } bdk_bitcoind_rpc = { version = "0.20.0" } anyhow = "1" diff --git a/examples/example_wallet_rpc/README.md b/examples/example_wallet_rpc/README.md index 9edb7ef2..a6dca181 100644 --- a/examples/example_wallet_rpc/README.md +++ b/examples/example_wallet_rpc/README.md @@ -1,34 +1,34 @@ # Wallet RPC Example -``` -$ cargo run --bin example_wallet_rpc -- --help - -Bitcoind RPC example using `bdk_wallet::Wallet` - -Usage: example_wallet_rpc [OPTIONS] [CHANGE_DESCRIPTOR] - -Arguments: - Wallet descriptor [env: DESCRIPTOR=] - [CHANGE_DESCRIPTOR] Wallet change descriptor [env: CHANGE_DESCRIPTOR=] - -Options: - --start-height Earliest block height to start sync from [env: START_HEIGHT=] [default: 0] - - --network Bitcoin network to connect to [env: BITCOIN_NETWORK=] [default: regtest] - - --db-path Where to store wallet data [env: BDK_DB_PATH=] [default: .bdk_wallet_rpc_example.db] - - --url RPC URL [env: RPC_URL=] [default: 127.0.0.1:18443] - - --rpc-cookie RPC auth cookie file [env: RPC_COOKIE=] - - --rpc-user RPC auth username [env: RPC_USER=] - - --rpc-pass RPC auth password [env: RPC_PASS=] - - -h, --help Print help - - -V, --version Print version - -``` +1. Install bitcoind +2. Start bitcoind in regtest mode. + ``` + just start + ``` +3. Create test bitcoind wallet and generate regtest blocks. + ``` + just create + just generate 110 $(just address) + ``` +4. Run the example and note the wallet's address and balance. + ``` + just run + ``` +5. Send regtest coins to the wallet address. + ``` + just send 10 + just generate 6 $(just address) + ``` +6. Re-run example and note the new balance. + ``` + just run + ``` +7. Stop the regtest bitcoind. + ``` + just stop + ``` +8. Cleanup test data (optional). + ``` + just clean + ``` diff --git a/examples/example_wallet_rpc/justfile b/examples/example_wallet_rpc/justfile new file mode 100644 index 00000000..ffcb4525 --- /dev/null +++ b/examples/example_wallet_rpc/justfile @@ -0,0 +1,92 @@ +set quiet := true +default_wallet := 'test' +default_private := 'false' +default_testdata := 'test_data' + +# list of recipes +default: + just --list + +# start regtest bitcoind in default test data directory +start: + mkdir -p {{default_testdata}}/bitcoind; bitcoind -datadir={{default_testdata}}/bitcoind -regtest -server -fallbackfee=0.0002 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0 -blockfilterindex=1 -peerblockfilters=1 -daemon + @echo "Regtest bitcoind process started with PID: $(cat {{default_testdata}}/bitcoind/regtest/bitcoind.pid)" + +# stop regtest bitcoind +stop: + bitcoin-cli -datadir={{default_testdata}}/bitcoind -regtest stop + +# delete all bdk_wallet and bitcoind test data +clean: + rm -rf {{default_testdata}} + +# run the example +run: + cargo run -- "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)" "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)" + +# create a regtest wallet +[group('regtest')] +create wallet=default_wallet: + bitcoin-cli -datadir={{default_testdata}}/bitcoind -regtest createwallet {{wallet}} + +# list regtest wallets +[group('regtest')] +list wallet=default_wallet: + bitcoin-cli -datadir={{default_testdata}}/bitcoind -regtest listwallets + +# load regtest wallet +[group('regtest')] +load wallet=default_wallet: + bitcoin-cli -datadir={{default_testdata}}/bitcoind -regtest loadwallet {{wallet}} + +# unload regtest wallet +[group('regtest')] +unload wallet=default_wallet: + bitcoin-cli -datadir={{default_testdata}}/bitcoind -regtest unloadwallet {{wallet}} + +# view debug log for regtest wallet address +[group('regtest')] +debug: + less +G {{default_testdata}}/bitcoind/regtest/debug.log + +# get regtest wallet address +[group('regtest')] +address wallet=default_wallet: + bitcoin-cli -datadir={{default_testdata}}/bitcoind -regtest -rpcwallet={{wallet}} getnewaddress + +# generate n new blocks to given address +[group('regtest')] +generate n address: + bitcoin-cli -datadir={{default_testdata}}/bitcoind -regtest generatetoaddress {{n}} {{address}} + +# get regtest wallet balance +[group('regtest')] +balance wallet=default_wallet: + bitcoin-cli -datadir={{default_testdata}}/bitcoind -regtest -rpcwallet={{wallet}} getbalance + +# list wallet descriptors info, private = (true | false) +[group('regtest')] +descriptors private wallet=default_wallet: + bitcoin-cli -datadir={{default_testdata}}/bitcoind -regtest -rpcwallet={{wallet}} listdescriptors {{private}} | jq '[.descriptors[].desc]' | grep \"wpkh + +# send n btc to address from wallet +[group('regtest')] +send n address wallet=default_wallet: + bitcoin-cli -named -datadir={{default_testdata}}/bitcoind -regtest -rpcwallet={{wallet}} sendtoaddress address={{address}} amount={{n}} + +# manually created bitcoin-cli RPC command, see just rpc help +[group('regtest')] +rpc command wallet=default_wallet: + bitcoin-cli -named -datadir={{default_testdata}}/bitcoind -regtest -rpcwallet={{wallet}} {{command}} + +[group('regtest')] +sign psbt wallet=default_wallet: + bitcoin-cli -named -datadir={{default_testdata}}/bitcoind -regtest -rpcwallet={{wallet}} walletprocesspsbt psbt={{psbt}} + +[group('regtest')] +finalize psbt wallet=default_wallet: + bitcoin-cli -named -datadir={{default_testdata}}/bitcoind -regtest -rpcwallet={{wallet}} finalizepsbt psbt={{psbt}} extract=true + +[group('regtest')] +broadcast tx wallet=default_wallet: + bitcoin-cli -named -datadir={{default_testdata}}/bitcoind -regtest -rpcwallet={{wallet}} sendrawtransaction {{tx}} diff --git a/examples/example_wallet_rpc/src/main.rs b/examples/example_wallet_rpc/src/main.rs index dd65bb48..840e167b 100644 --- a/examples/example_wallet_rpc/src/main.rs +++ b/examples/example_wallet_rpc/src/main.rs @@ -2,9 +2,9 @@ use bdk_bitcoind_rpc::{ bitcoincore_rpc::{Auth, Client, RpcApi}, Emitter, MempoolEvent, }; +use bdk_wallet::rusqlite::Connection; use bdk_wallet::{ bitcoin::{Block, Network}, - file_store::Store, KeychainKind, Wallet, }; use clap::{self, Parser}; @@ -15,8 +15,6 @@ use std::{ time::Instant, }; -const DB_MAGIC: &str = "bdk-rpc-wallet-example"; - /// Bitcoind RPC example using `bdk_wallet::Wallet`. /// /// This syncs the chain block-by-block and prints the current balance, transaction count and UTXO @@ -41,7 +39,7 @@ pub struct Args { #[clap( env = "BDK_DB_PATH", long, - default_value = ".bdk_wallet_rpc_example.db" + default_value = "test_data/bdk-example-rpc.sqlite" )] pub db_path: PathBuf, @@ -49,7 +47,11 @@ pub struct Args { #[clap(env = "RPC_URL", long, default_value = "127.0.0.1:18443")] pub url: String, /// RPC auth cookie file - #[clap(env = "RPC_COOKIE", long)] + #[clap( + env = "RPC_COOKIE", + long, + default_value = "test_data/bitcoind/regtest/.cookie" + )] pub rpc_cookie: Option, /// RPC auth username #[clap(env = "RPC_USER", long)] @@ -91,8 +93,7 @@ fn main() -> anyhow::Result<()> { ); let start_load_wallet = Instant::now(); - let (mut db, _) = - Store::::load_or_create(DB_MAGIC.as_bytes(), args.db_path)?; + let mut db = Connection::open(args.db_path)?; let wallet_opt = Wallet::load() .descriptor(KeychainKind::External, Some(args.descriptor.clone())) .descriptor(KeychainKind::Internal, args.change_descriptor.clone()) @@ -115,6 +116,9 @@ fn main() -> anyhow::Result<()> { start_load_wallet.elapsed().as_secs_f32() ); + let address = wallet.reveal_next_address(KeychainKind::External).address; + println!("Wallet address: {address}"); + let balance = wallet.balance(); println!("Wallet balance before syncing: {}", balance.total());