Skip to content

docs(wallet): add sync operation to bdk_wallet examples #274

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 6, 2025
Merged
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
2 changes: 1 addition & 1 deletion examples/example_wallet_electrum/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
119 changes: 103 additions & 16 deletions examples/example_wallet_electrum/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
use bdk_wallet::file_store::Store;
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::<bdk_wallet::ChangeSet>::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))
Expand All @@ -44,7 +44,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
Expand All @@ -58,7 +58,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");
}
});
Expand All @@ -71,23 +73,108 @@ 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");
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())?;
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 txid = tx.compute_txid();
println!("Tx broadcasted! Txid: https://mempool.space/testnet4/tx/{txid}");

println!("Partial Sync...");
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!();
wallet.apply_update(sync_update)?;
wallet.persist(&mut db)?;

// 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();
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 ({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: https://mempool.space/testnet4/tx/{}",
bumped_tx.compute_txid()
);

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)?;

let mut evicted_txs = Vec::new();
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() {
println!("Applied {} evicted transactions", evicted_txs.len());
}
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(())
}
136 changes: 119 additions & 17 deletions examples/example_wallet_esplora_async/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,48 +1,48 @@
use std::{collections::BTreeSet, io::Write};

use anyhow::Ok;
use bdk_esplora::{esplora_client, EsploraAsyncExt};
use bdk_wallet::{
bitcoin::{Amount, Network},
bitcoin::{Amount, FeeRate, Network},
psbt::PsbtUtils,
rusqlite::Connection,
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)?;
println!("Next unused address: ({}) {}", address.index, address);
wallet.persist(&mut db)?;
println!("Next unused address: ({}) {address}", address.index);

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_async()?;

let request = wallet.start_full_scan().inspect({
Expand All @@ -52,7 +52,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")
}
});
Expand All @@ -62,27 +64,127 @@ async fn main() -> Result<(), anyhow::Error> {
.await?;

wallet.apply_update(update)?;
wallet.persist(&mut conn)?;
wallet.persist(&mut db)?;
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");
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())?;
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 txid = tx.compute_txid();
println!("Tx broadcasted! Txid: https://mempool.space/testnet4/tx/{txid}");

println!("Partial Sync...");
print!("SCANNING: ");
let mut printed: u32 = 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!();
wallet.apply_update(sync_update)?;
wallet.persist(&mut db)?;

// 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();
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})",
);

// 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: https://mempool.space/testnet4/tx/{}",
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();

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() {
println!("Applied {} evicted transactions", evicted_txs.len());
}

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(())
}
2 changes: 1 addition & 1 deletion examples/example_wallet_esplora_blocking/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading
Loading