Skip to content

Commit

Permalink
refactor(example): update wallet_esplora_async to use new EsploraAsyn…
Browse files Browse the repository at this point in the history
…cExt scan and sync functions
  • Loading branch information
notmandatory committed Nov 2, 2023
1 parent fbcbbcc commit 070ee02
Showing 1 changed file with 200 additions and 44 deletions.
244 changes: 200 additions & 44 deletions example-crates/wallet_esplora_async/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{io::Write, str::FromStr};

use bdk::bitcoin::{OutPoint, ScriptBuf, Txid};
use bdk::{
bitcoin::{Address, Network},
wallet::{AddressIndex, Update},
Expand All @@ -9,66 +10,189 @@ use bdk_esplora::{esplora_client, EsploraAsyncExt};
use bdk_file_store::Store;

const DB_MAGIC: &str = "bdk_wallet_esplora_async_example";
const CHAIN_DATA_FILE: &str = "chain.dat";
const SEND_AMOUNT: u64 = 5000;
const STOP_GAP: usize = 50;
const PARALLEL_REQUESTS: usize = 5;
const ESPLORA_SERVER_URL: &str = "http://signet.bitcoindevkit.net";

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let db_path = std::env::temp_dir().join("bdk-esplora-async-example");
let db = Store::<bdk::wallet::ChangeSet>::new_from_path(DB_MAGIC.as_bytes(), db_path)?;
let network = Network::Signet;

// let db_path = std::env::temp_dir().join("bdk-esplora-async-example");
let db = Store::<bdk::wallet::ChangeSet>::new_from_path(DB_MAGIC.as_bytes(), CHAIN_DATA_FILE)?;
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";

let mut wallet = Wallet::new(
external_descriptor,
Some(internal_descriptor),
db,
Network::Testnet,
)?;
// Create a wallet and get a new address and current wallet balance before syncing
let mut wallet = Wallet::new(external_descriptor, Some(internal_descriptor), db, network)?;

let address = wallet.get_address(AddressIndex::New);
println!("Generated Address: {}", address);

let balance = wallet.get_balance();
println!("Wallet balance before syncing: {} sats", balance.total());
println!("Wallet balance before syncing: confirmed {} sats, trusted_pending {} sats, untrusted pending {} sats", balance.confirmed, balance.trusted_pending, balance.untrusted_pending);

print!("Syncing...");
let client =
esplora_client::Builder::new("https://blockstream.info/testnet/api").build_async()?;
// Create an async esplora client
let client = esplora_client::Builder::new(ESPLORA_SERVER_URL).build_async()?;

// Get wallet's previous chain tip
let prev_tip = wallet.latest_checkpoint();
let keychain_spks = wallet
.spks_of_all_keychains()
.into_iter()
.map(|(k, k_spks)| {
let mut once = Some(());
let mut stdout = std::io::stdout();
let k_spks = k_spks
.inspect(move |(spk_i, _)| match once.take() {
Some(_) => print!("\nScanning keychain [{:?}]", k),
None => print!(" {:<3}", spk_i),

// Scanning: We are iterating through spks of all keychains and scanning for transactions for
// each spk. We start with the lowest derivation index spk and stop scanning after `stop_gap`
// number of consecutive spks have no transaction history. A Scan is done in situations of
// wallet restoration. It is a special case. Applications should use "sync" style updates
// after an initial scan.
if prompt("Scan wallet") {
let keychain_spks = wallet
.spks_of_all_keychains()
.into_iter()
// This `map` is purely for logging.
.map(|(keychain, iter)| {
let mut first = true;
let spk_iter = iter.inspect(move |(i, _)| {
if first {
// TODO impl Display for Keychain
print!("\nScanning keychain [{:?}]", keychain);
first = false;
}
print!("{} ", i);
// Flush early to ensure we print at every iteration.
let _ = std::io::stdout().flush();
});
(keychain, spk_iter)
})
.collect();
println!();

let (last_active_indices, graph_update, chain_update) = client
.scan(
keychain_spks,
wallet.local_chain(),
prev_tip,
STOP_GAP,
PARALLEL_REQUESTS,
)
.await?;

let wallet_update = Update {
last_active_indices,
graph: graph_update,
chain: Some(chain_update),
};
wallet.apply_update(wallet_update)?;
wallet.commit()?;
println!("Scan completed.");
}
// Syncing: We only check for specified spks, utxos and txids to update their confirmation
// status or fetch missing transactions.
else {
let mut spks = Box::new(Vec::new());

Check failure on line 92 in example-crates/wallet_esplora_async/src/main.rs

View workflow job for this annotation

GitHub Actions / clippy

`Box::new(_)` of default value

error: `Box::new(_)` of default value --> example-crates/wallet_esplora_async/src/main.rs:92:24 | 92 | let mut spks = Box::new(Vec::new()); | ^^^^^^^^^^^^^^^^^^^^ help: try: `Box::<std::vec::Vec<bdk::bitcoin::ScriptBuf>>::default()` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#box_default = note: `-D clippy::box-default` implied by `-D warnings`

// Sync only unused SPKs
if prompt("Sync only unused SPKs") {
// TODO add Wallet::unused_spks() function, gives all unused tracked spks
let unused_spks: Vec<ScriptBuf> = wallet
.spk_index()
.unused_spks(..)
.into_iter()
.map(|((keychain, index), script)| {
eprintln!(
"Checking if keychain: {:?}, index: {}, address: {} has been used",
keychain,
index,
Address::from_script(script, network).unwrap(),
);
// Flush early to ensure we print at every iteration.
let _ = std::io::stderr().flush();
ScriptBuf::from(script)
})
.inspect(move |_| stdout.flush().expect("must flush"));
(k, k_spks)
})
.collect();
let (update_graph, last_active_indices) = client
.scan_txs_with_keychains(keychain_spks, None, None, STOP_GAP, PARALLEL_REQUESTS)
.await?;
let missing_heights = update_graph.missing_heights(wallet.local_chain());
let chain_update = client.update_local_chain(prev_tip, missing_heights).await?;
let update = Update {
last_active_indices,
graph: update_graph,
chain: Some(chain_update),
};
wallet.apply_update(update)?;
wallet.commit()?;
println!();
.collect();
spks = Box::new(unused_spks);
println!("Syncing unused SPKs...");
}
// Sync all SPKs
else if prompt("Sync all SPKs") {
// TODO add Wallet::all_spks() function, gives all tracked spks
let all_spks: Vec<ScriptBuf> = wallet
.spk_index()
.all_spks()
.into_iter()

Check failure on line 122 in example-crates/wallet_esplora_async/src/main.rs

View workflow job for this annotation

GitHub Actions / clippy

this `.into_iter()` call is equivalent to `.iter()` and will not consume the `BTreeMap`

error: this `.into_iter()` call is equivalent to `.iter()` and will not consume the `BTreeMap` --> example-crates/wallet_esplora_async/src/main.rs:122:18 | 122 | .into_iter() | ^^^^^^^^^ help: call directly: `iter` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#into_iter_on_ref = note: `-D clippy::into-iter-on-ref` implied by `-D warnings`
.map(|((keychain, index), script)| {
eprintln!(
"Checking if keychain: {:?}, index: {}, address: {} has been used",
keychain,
index,
Address::from_script(script.as_script(), network).unwrap(),
);
// Flush early to ensure we print at every iteration.
let _ = std::io::stderr().flush();
(*script).clone()
})
.collect();
spks = Box::new(all_spks);
println!("Syncing all SPKs...");
}

// Sync UTXOs

// We want to search for whether our UTXOs are spent, and spent by which transaction.
let outpoints: Vec<OutPoint> = wallet
.list_unspent()
.inspect(|utxo| {
eprintln!(
"Checking if outpoint {} (value: {}) has been spent",
utxo.outpoint, utxo.txout.value
);
// Flush early to ensure we print at every iteration.
let _ = std::io::stderr().flush();
})
.map(|utxo| utxo.outpoint)
.collect();

// Sync unconfirmed TX

// We want to search for whether our unconfirmed transactions are now confirmed.
// TODO add .unconfirmed_txs()
let txids: Vec<Txid> = wallet
.transactions()
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
.map(|canonical_tx| canonical_tx.tx_node.txid)
.inspect(|txid| {
eprintln!("Checking if {} is confirmed yet", txid);
// Flush early to ensure we print at every iteration.
let _ = std::io::stderr().flush();
})
.collect();

let (graph_update, chain_update) = client
.sync(
*spks,
wallet.local_chain(),
prev_tip,
outpoints,
txids,
PARALLEL_REQUESTS,
)
.await?;

let wallet_update = Update {
last_active_indices: Default::default(),
graph: graph_update,
chain: Some(chain_update),
};

wallet.apply_update(wallet_update)?;
wallet.commit()?;
println!("Sync completed.");
}

let balance = wallet.get_balance();
println!("Wallet balance after syncing: {} sats", balance.total());
dbg!(&balance);
println!("Wallet balance after update: confirmed {} sats, trusted_pending {} sats, untrusted pending {} sats",
balance.confirmed, balance.trusted_pending, balance.untrusted_pending);

if balance.total() < SEND_AMOUNT {
println!(
Expand All @@ -78,11 +202,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
std::process::exit(0);
}

let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?
.require_network(Network::Testnet)?;
// Create TX to return sats to signet faucet https://signetfaucet.com/
let faucet_address = Address::from_str("tb1qg3lau83hm9e9tdvzr5k7aqtw3uv0dwkfct4xdn")?
.require_network(network)?;

let mut tx_builder = wallet.build_tx();
tx_builder
// .drain_to(faucet_address.script_pubkey())
// .drain_wallet()
.add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT)
.enable_rbf();

Expand All @@ -91,8 +218,37 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
assert!(finalized);

let tx = psbt.extract_tx();
client.broadcast(&tx).await?;
println!("Tx broadcasted! Txid: {}", tx.txid());
let (sent, received) = wallet.sent_and_received(&tx);
let fee = wallet.calculate_fee(&tx).expect("fee");
let fee_rate = wallet
.calculate_fee_rate(&tx)
.expect("fee rate")
.as_sat_per_vb();
println!(
"Created tx sending {} sats to {}",
sent - received - fee,
faucet_address
);
println!(
"Fee is {} sats, fee rate is {:.2} sats/vbyte",
fee, fee_rate
);

if prompt("Broadcast") {
client.broadcast(&tx).await?;
println!(
"Tx broadcast! https://mempool.space/signet/tx/{}",
tx.txid()
);
}

Ok(())
}

fn prompt(question: &str) -> bool {
print!("{}? (Y/N) ", question);
std::io::stdout().flush().expect("stdout flush");
let mut answer = String::new();
std::io::stdin().read_line(&mut answer).expect("answer");
answer.trim().to_ascii_lowercase() == "y"
}

0 comments on commit 070ee02

Please sign in to comment.