Skip to content

Commit d0a0abc

Browse files
committed
Merge #274: docs(wallet): add sync operation to bdk_wallet examples
11bfc2f docs(wallet): for electrum, esplora examples use sqlite+testnet4, and mempool.space (Steve Myers) 5d2d25f docs(wallet): add bumping tx to examples (Vihiga Tyonum) fabd2d9 docs(wallet): fix clippy warnings (Vihiga Tyonum) 67ee040 docs(wallet): add `sync` to `bdk_wallet` examples (Vihiga Tyonum) Pull request description: <!-- You can erase any parts of this template not applicable to your Pull Request. --> ### Description This PR adds `sync` operation to `bdk_wallet` examples for `electrum` and `esplora` clients. Fixes #253 <!-- Describe the purpose of this PR, what's being adding and/or fixed --> ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `just p` before pushing ACKs for top commit: oleonardolima: tACK 11bfc2f Tree-SHA512: e946d4cebbe64216dcff1ab3c6c471f57a09d736ea52c658c3bff92e0f379f9f3fc446d446dcfabb87679b35cccc70b4560721e1afed677068147622d65ea451
2 parents 20edf98 + 11bfc2f commit d0a0abc

File tree

10 files changed

+457
-89
lines changed

10 files changed

+457
-89
lines changed

examples/example_wallet_electrum/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ version = "0.2.0"
44
edition = "2021"
55

66
[dependencies]
7-
bdk_wallet = { path = "../../wallet", features = ["file_store"] }
7+
bdk_wallet = { path = "../../wallet", features = ["rusqlite"] }
88
bdk_electrum = { version = "0.23.0" }
99
anyhow = "1"

examples/example_wallet_electrum/src/main.rs

Lines changed: 103 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
1-
use bdk_wallet::file_store::Store;
2-
use bdk_wallet::Wallet;
3-
use std::io::Write;
4-
51
use bdk_electrum::electrum_client;
62
use bdk_electrum::BdkElectrumClient;
73
use bdk_wallet::bitcoin::Amount;
4+
use bdk_wallet::bitcoin::FeeRate;
85
use bdk_wallet::bitcoin::Network;
96
use bdk_wallet::chain::collections::HashSet;
7+
use bdk_wallet::psbt::PsbtUtils;
8+
use bdk_wallet::rusqlite::Connection;
9+
use bdk_wallet::Wallet;
1010
use bdk_wallet::{KeychainKind, SignOptions};
11+
use std::io::Write;
12+
use std::thread::sleep;
13+
use std::time::Duration;
1114

12-
const DB_MAGIC: &str = "bdk_wallet_electrum_example";
1315
const SEND_AMOUNT: Amount = Amount::from_sat(5000);
1416
const STOP_GAP: usize = 50;
1517
const BATCH_SIZE: usize = 5;
1618

17-
const NETWORK: Network = Network::Testnet;
19+
const DB_PATH: &str = "bdk-example-electrum.sqlite";
20+
const NETWORK: Network = Network::Testnet4;
1821
const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
1922
const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
20-
const ELECTRUM_URL: &str = "ssl://electrum.blockstream.info:60002";
23+
const ELECTRUM_URL: &str = "ssl://mempool.space:40002";
2124

2225
fn main() -> Result<(), anyhow::Error> {
23-
let db_path = "bdk-electrum-example.db";
24-
25-
let (mut db, _) = Store::<bdk_wallet::ChangeSet>::load_or_create(DB_MAGIC.as_bytes(), db_path)?;
26-
26+
let mut db = Connection::open(DB_PATH)?;
2727
let wallet_opt = Wallet::load()
2828
.descriptor(KeychainKind::External, Some(EXTERNAL_DESC))
2929
.descriptor(KeychainKind::Internal, Some(INTERNAL_DESC))
@@ -44,7 +44,7 @@ fn main() -> Result<(), anyhow::Error> {
4444
let balance = wallet.balance();
4545
println!("Wallet balance before syncing: {}", balance.total());
4646

47-
print!("Syncing...");
47+
println!("Performing Full Sync...");
4848
let client = BdkElectrumClient::new(electrum_client::Client::new(ELECTRUM_URL)?);
4949

5050
// Populate the electrum client's transaction cache so it doesn't redownload transaction we
@@ -58,7 +58,9 @@ fn main() -> Result<(), anyhow::Error> {
5858
if once.insert(k) {
5959
print!("\nScanning keychain [{k:?}]");
6060
}
61-
print!(" {spk_i:<3}");
61+
if spk_i.is_multiple_of(5) {
62+
print!(" {spk_i:<3}");
63+
}
6264
stdout.flush().expect("must flush");
6365
}
6466
});
@@ -71,23 +73,108 @@ fn main() -> Result<(), anyhow::Error> {
7173
wallet.persist(&mut db)?;
7274

7375
let balance = wallet.balance();
74-
println!("Wallet balance after syncing: {}", balance.total());
76+
println!("Wallet balance after full sync: {}", balance.total());
77+
println!(
78+
"Wallet has {} transactions and {} utxos after full sync",
79+
wallet.transactions().count(),
80+
wallet.list_unspent().count()
81+
);
7582

7683
if balance.total() < SEND_AMOUNT {
7784
println!("Please send at least {SEND_AMOUNT} to the receiving address");
7885
std::process::exit(0);
7986
}
8087

88+
let target_fee_rate = FeeRate::from_sat_per_vb(1).unwrap();
8189
let mut tx_builder = wallet.build_tx();
8290
tx_builder.add_recipient(address.script_pubkey(), SEND_AMOUNT);
91+
tx_builder.fee_rate(target_fee_rate);
8392

8493
let mut psbt = tx_builder.finish()?;
8594
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
8695
assert!(finalized);
87-
96+
let original_fee = psbt.fee_amount().unwrap();
97+
let tx_feerate = psbt.fee_rate().unwrap();
8898
let tx = psbt.extract_tx()?;
8999
client.transaction_broadcast(&tx)?;
90-
println!("Tx broadcasted! Txid: {}", tx.compute_txid());
100+
let txid = tx.compute_txid();
101+
println!("Tx broadcasted! Txid: https://mempool.space/testnet4/tx/{txid}");
102+
103+
println!("Partial Sync...");
104+
print!("SCANNING: ");
105+
let mut last_printed = 0;
106+
let sync_request = wallet
107+
.start_sync_with_revealed_spks()
108+
.inspect(move |_, sync_progress| {
109+
let progress_percent =
110+
(100 * sync_progress.consumed()) as f32 / sync_progress.total() as f32;
111+
let progress_percent = progress_percent.round() as u32;
112+
if progress_percent.is_multiple_of(5) && progress_percent > last_printed {
113+
print!("{progress_percent}% ");
114+
std::io::stdout().flush().expect("must flush");
115+
last_printed = progress_percent;
116+
}
117+
});
118+
client.populate_tx_cache(wallet.tx_graph().full_txs().map(|tx_node| tx_node.tx));
119+
let sync_update = client.sync(sync_request, BATCH_SIZE, false)?;
120+
println!();
121+
wallet.apply_update(sync_update)?;
122+
wallet.persist(&mut db)?;
123+
124+
// bump fee rate for tx by at least 1 sat per vbyte
125+
let feerate = FeeRate::from_sat_per_vb(tx_feerate.to_sat_per_vb_ceil() + 1).unwrap();
126+
let mut builder = wallet.build_fee_bump(txid).expect("failed to bump tx");
127+
builder.fee_rate(feerate);
128+
let mut bumped_psbt = builder.finish().unwrap();
129+
let finalize_btx = wallet.sign(&mut bumped_psbt, SignOptions::default())?;
130+
assert!(finalize_btx);
131+
let new_fee = bumped_psbt.fee_amount().unwrap();
132+
let bumped_tx = bumped_psbt.extract_tx()?;
133+
assert_eq!(
134+
bumped_tx
135+
.output
136+
.iter()
137+
.find(|txout| txout.script_pubkey == address.script_pubkey())
138+
.unwrap()
139+
.value,
140+
SEND_AMOUNT,
141+
"Recipient output should remain unchanged"
142+
);
143+
assert!(
144+
new_fee > original_fee,
145+
"New fee ({new_fee}) should be higher than original ({original_fee})"
146+
);
147+
148+
// wait for first transaction to make it into the mempool and be indexed on mempool.space
149+
sleep(Duration::from_secs(10));
150+
client.transaction_broadcast(&bumped_tx)?;
151+
println!(
152+
"Broadcasted bumped tx. Txid: https://mempool.space/testnet4/tx/{}",
153+
bumped_tx.compute_txid()
154+
);
155+
156+
println!("Syncing after bumped tx broadcast...");
157+
let sync_request = wallet.start_sync_with_revealed_spks().inspect(|_, _| {});
158+
let sync_update = client.sync(sync_request, BATCH_SIZE, false)?;
159+
160+
let mut evicted_txs = Vec::new();
161+
for (txid, last_seen) in &sync_update.tx_update.evicted_ats {
162+
evicted_txs.push((*txid, *last_seen));
163+
}
164+
165+
wallet.apply_update(sync_update)?;
166+
if !evicted_txs.is_empty() {
167+
println!("Applied {} evicted transactions", evicted_txs.len());
168+
}
169+
wallet.persist(&mut db)?;
170+
171+
let balance_after_sync = wallet.balance();
172+
println!("Wallet balance after sync: {}", balance_after_sync.total());
173+
println!(
174+
"Wallet has {} transactions and {} utxos after partial sync",
175+
wallet.transactions().count(),
176+
wallet.list_unspent().count()
177+
);
91178

92179
Ok(())
93180
}
Lines changed: 119 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,48 @@
1-
use std::{collections::BTreeSet, io::Write};
2-
31
use anyhow::Ok;
42
use bdk_esplora::{esplora_client, EsploraAsyncExt};
53
use bdk_wallet::{
6-
bitcoin::{Amount, Network},
4+
bitcoin::{Amount, FeeRate, Network},
5+
psbt::PsbtUtils,
76
rusqlite::Connection,
87
KeychainKind, SignOptions, Wallet,
98
};
9+
use std::{collections::BTreeSet, io::Write};
10+
use tokio::time::{sleep, Duration};
1011

1112
const SEND_AMOUNT: Amount = Amount::from_sat(5000);
1213
const STOP_GAP: usize = 5;
1314
const PARALLEL_REQUESTS: usize = 5;
1415

1516
const DB_PATH: &str = "bdk-example-esplora-async.sqlite";
16-
const NETWORK: Network = Network::Signet;
17+
const NETWORK: Network = Network::Testnet4;
1718
const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
1819
const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
19-
const ESPLORA_URL: &str = "http://signet.bitcoindevkit.net";
20+
const ESPLORA_URL: &str = "https://mempool.space/testnet4/api";
2021

2122
#[tokio::main]
2223
async fn main() -> Result<(), anyhow::Error> {
23-
let mut conn = Connection::open(DB_PATH)?;
24-
24+
let mut db = Connection::open(DB_PATH)?;
2525
let wallet_opt = Wallet::load()
2626
.descriptor(KeychainKind::External, Some(EXTERNAL_DESC))
2727
.descriptor(KeychainKind::Internal, Some(INTERNAL_DESC))
2828
.extract_keys()
2929
.check_network(NETWORK)
30-
.load_wallet(&mut conn)?;
30+
.load_wallet(&mut db)?;
3131
let mut wallet = match wallet_opt {
3232
Some(wallet) => wallet,
3333
None => Wallet::create(EXTERNAL_DESC, INTERNAL_DESC)
3434
.network(NETWORK)
35-
.create_wallet(&mut conn)?,
35+
.create_wallet(&mut db)?,
3636
};
3737

3838
let address = wallet.next_unused_address(KeychainKind::External);
39-
wallet.persist(&mut conn)?;
40-
println!("Next unused address: ({}) {}", address.index, address);
39+
wallet.persist(&mut db)?;
40+
println!("Next unused address: ({}) {address}", address.index);
4141

4242
let balance = wallet.balance();
4343
println!("Wallet balance before syncing: {}", balance.total());
4444

45-
print!("Syncing...");
45+
println!("Full Sync...");
4646
let client = esplora_client::Builder::new(ESPLORA_URL).build_async()?;
4747

4848
let request = wallet.start_full_scan().inspect({
@@ -52,7 +52,9 @@ async fn main() -> Result<(), anyhow::Error> {
5252
if once.insert(keychain) {
5353
print!("\nScanning keychain [{keychain:?}]");
5454
}
55-
print!(" {spk_i:<3}");
55+
if spk_i.is_multiple_of(5) {
56+
print!(" {spk_i:<3}");
57+
}
5658
stdout.flush().expect("must flush")
5759
}
5860
});
@@ -62,27 +64,127 @@ async fn main() -> Result<(), anyhow::Error> {
6264
.await?;
6365

6466
wallet.apply_update(update)?;
65-
wallet.persist(&mut conn)?;
67+
wallet.persist(&mut db)?;
6668
println!();
6769

6870
let balance = wallet.balance();
69-
println!("Wallet balance after syncing: {}", balance.total());
71+
println!("Wallet balance after full sync: {}", balance.total());
72+
println!(
73+
"Wallet has {} transactions and {} utxos after full sync",
74+
wallet.transactions().count(),
75+
wallet.list_unspent().count()
76+
);
7077

7178
if balance.total() < SEND_AMOUNT {
7279
println!("Please send at least {SEND_AMOUNT} to the receiving address");
7380
std::process::exit(0);
7481
}
7582

83+
let target_fee_rate = FeeRate::from_sat_per_vb(1).unwrap();
7684
let mut tx_builder = wallet.build_tx();
7785
tx_builder.add_recipient(address.script_pubkey(), SEND_AMOUNT);
86+
tx_builder.fee_rate(target_fee_rate);
7887

7988
let mut psbt = tx_builder.finish()?;
8089
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
8190
assert!(finalized);
82-
91+
let original_fee = psbt.fee_amount().unwrap();
92+
let tx_feerate = psbt.fee_rate().unwrap();
8393
let tx = psbt.extract_tx()?;
8494
client.broadcast(&tx).await?;
85-
println!("Tx broadcasted! Txid: {}", tx.compute_txid());
95+
let txid = tx.compute_txid();
96+
println!("Tx broadcasted! Txid: https://mempool.space/testnet4/tx/{txid}");
97+
98+
println!("Partial Sync...");
99+
print!("SCANNING: ");
100+
let mut printed: u32 = 0;
101+
let sync_request = wallet
102+
.start_sync_with_revealed_spks()
103+
.inspect(move |_, sync_progress| {
104+
let progress_percent =
105+
(100 * sync_progress.consumed()) as f32 / sync_progress.total() as f32;
106+
let progress_percent = progress_percent.round() as u32;
107+
if progress_percent.is_multiple_of(5) && progress_percent > printed {
108+
print!("{progress_percent}% ");
109+
std::io::stdout().flush().expect("must flush");
110+
printed = progress_percent;
111+
}
112+
});
113+
let sync_update = client.sync(sync_request, PARALLEL_REQUESTS).await?;
114+
println!();
115+
wallet.apply_update(sync_update)?;
116+
wallet.persist(&mut db)?;
117+
118+
// bump fee rate for tx by at least 1 sat per vbyte
119+
let feerate = FeeRate::from_sat_per_vb(tx_feerate.to_sat_per_vb_ceil() + 1).unwrap();
120+
let mut builder = wallet.build_fee_bump(txid).expect("failed to bump tx");
121+
builder.fee_rate(feerate);
122+
let mut bumped_psbt = builder.finish().unwrap();
123+
let finalize_btx = wallet.sign(&mut bumped_psbt, SignOptions::default())?;
124+
assert!(finalize_btx);
125+
let new_fee = bumped_psbt.fee_amount().unwrap();
126+
let bumped_tx = bumped_psbt.extract_tx()?;
127+
assert_eq!(
128+
bumped_tx
129+
.output
130+
.iter()
131+
.find(|txout| txout.script_pubkey == address.script_pubkey())
132+
.unwrap()
133+
.value,
134+
SEND_AMOUNT,
135+
"Outputs should be the same"
136+
);
137+
assert!(
138+
new_fee > original_fee,
139+
"New fee ({new_fee}) should be higher than original ({original_fee})",
140+
);
141+
142+
// wait for first transaction to make it into the mempool and be indexed on mempool.space
143+
sleep(Duration::from_secs(10)).await;
144+
client.broadcast(&bumped_tx).await?;
145+
println!(
146+
"Broadcasted bumped tx. Txid: https://mempool.space/testnet4/tx/{}",
147+
bumped_tx.compute_txid()
148+
);
149+
150+
println!("syncing after broadcasting bumped tx...");
151+
print!("SCANNING: ");
152+
let sync_request = wallet
153+
.start_sync_with_revealed_spks()
154+
.inspect(move |_, sync_progress| {
155+
let progress_percent =
156+
(100 * sync_progress.consumed()) as f32 / sync_progress.total() as f32;
157+
let progress_percent = progress_percent.round() as u32;
158+
if progress_percent.is_multiple_of(10) && progress_percent > printed {
159+
print!("{progress_percent}% ");
160+
std::io::stdout().flush().expect("must flush");
161+
printed = progress_percent;
162+
}
163+
});
164+
let sync_update = client.sync(sync_request, PARALLEL_REQUESTS).await?;
165+
println!();
166+
167+
let mut evicted_txs = Vec::new();
168+
169+
for (txid, last_seen) in &sync_update.tx_update.evicted_ats {
170+
evicted_txs.push((*txid, *last_seen));
171+
}
172+
173+
wallet.apply_update(sync_update)?;
174+
175+
if !evicted_txs.is_empty() {
176+
println!("Applied {} evicted transactions", evicted_txs.len());
177+
}
178+
179+
wallet.persist(&mut db)?;
180+
181+
let balance_after_sync = wallet.balance();
182+
println!("Wallet balance after sync: {}", balance_after_sync.total());
183+
println!(
184+
"Wallet has {} transactions and {} utxos after partial sync",
185+
wallet.transactions().count(),
186+
wallet.list_unspent().count()
187+
);
86188

87189
Ok(())
88190
}

examples/example_wallet_esplora_blocking/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ publish = false
77
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
88

99
[dependencies]
10-
bdk_wallet = { path = "../../wallet", features = ["file_store"] }
10+
bdk_wallet = { path = "../../wallet", features = ["rusqlite"] }
1111
bdk_esplora = { version = "0.22.0", features = ["blocking"] }
1212
anyhow = "1"

0 commit comments

Comments
 (0)