diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/wallet_esplora_async/src/main.rs index ff1bbfb6d9..b4ba511a9d 100644 --- a/example-crates/wallet_esplora_async/src/main.rs +++ b/example-crates/wallet_esplora_async/src/main.rs @@ -1,5 +1,6 @@ use std::{io::Write, str::FromStr}; +use bdk::bitcoin::{OutPoint, ScriptBuf, Txid}; use bdk::{ bitcoin::{Address, Network}, wallet::{AddressIndex, Update}, @@ -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> { - let db_path = std::env::temp_dir().join("bdk-esplora-async-example"); - let db = Store::::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::::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()); + + // 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 = 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 = wallet + .spk_index() + .all_spks() + .into_iter() + .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 = 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 = 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!( @@ -78,11 +202,14 @@ async fn main() -> Result<(), Box> { 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(); @@ -91,8 +218,37 @@ async fn main() -> Result<(), Box> { 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" +}