use std::{ collections::BTreeMap, io::{self, Write}, sync::Mutex, }; use bdk_chain::{ bitcoin::{Address, Network, OutPoint, ScriptBuf, Txid}, indexed_tx_graph::{self, IndexedTxGraph}, keychain, local_chain::{self, LocalChain}, Append, ConfirmationHeightAnchor, }; use bdk_electrum::{ electrum_client::{self, ElectrumApi}, ElectrumExt, ElectrumUpdate, }; use example_cli::{ anyhow::{self, Context}, clap::{self, Parser, Subcommand}, Keychain, }; const DB_MAGIC: &[u8] = b"bdk_example_electrum"; const DB_PATH: &str = ".bdk_example_electrum.db"; #[derive(Subcommand, Debug, Clone)] enum ElectrumCommands { /// Scans the addresses in the wallet using the electrum API. Scan { /// When a gap this large has been found for a keychain, it will stop. #[clap(long, default_value = "5")] stop_gap: usize, #[clap(flatten)] scan_options: ScanOptions, }, /// Scans particular addresses using the electrum API. Sync { /// Scan all the unused addresses. #[clap(long)] unused_spks: bool, /// Scan every address that you have derived. #[clap(long)] all_spks: bool, /// Scan unspent outpoints for spends or changes to confirmation status of residing tx. #[clap(long)] utxos: bool, /// Scan unconfirmed transactions for updates. #[clap(long)] unconfirmed: bool, #[clap(flatten)] scan_options: ScanOptions, }, } #[derive(Parser, Debug, Clone, PartialEq)] pub struct ScanOptions { /// Set batch size for each script_history call to electrum client. #[clap(long, default_value = "25")] pub batch_size: usize, } type ChangeSet = ( local_chain::ChangeSet, indexed_tx_graph::ChangeSet>, ); fn main() -> anyhow::Result<()> { let (args, keymap, index, db, (disk_local_chain, disk_tx_graph)) = example_cli::init::(DB_MAGIC, DB_PATH)?; let graph = Mutex::new({ let mut graph = IndexedTxGraph::new(index); graph.apply_changeset(disk_tx_graph); graph }); let chain = Mutex::new(LocalChain::from_changeset(disk_local_chain)); let electrum_url = match args.network { Network::Bitcoin => "ssl://electrum.blockstream.info:50002", Network::Testnet => "ssl://electrum.blockstream.info:60002", Network::Regtest => "tcp://localhost:60401", Network::Signet => "tcp://signet-electrumx.wakiyamap.dev:50001", _ => panic!("Unknown network"), }; let config = electrum_client::Config::builder() .validate_domain(matches!(args.network, Network::Bitcoin)) .build(); let client = electrum_client::Client::from_config(electrum_url, config)?; let electrum_cmd = match &args.command { example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd, general_cmd => { let res = example_cli::handle_commands( &graph, &db, &chain, &keymap, args.network, |tx| { client .transaction_broadcast(tx) .map(|_| ()) .map_err(anyhow::Error::from) }, general_cmd.clone(), ); db.lock().unwrap().commit()?; return res; } }; let response = match electrum_cmd.clone() { ElectrumCommands::Scan { stop_gap, scan_options, } => { let (keychain_spks, tip) = { let graph = &*graph.lock().unwrap(); let chain = &*chain.lock().unwrap(); let keychain_spks = graph .index .spks_of_all_keychains() .into_iter() .map(|(keychain, iter)| { let mut first = true; let spk_iter = iter.inspect(move |(i, _)| { if first { eprint!("\nscanning {}: ", keychain); first = false; } eprint!("{} ", i); let _ = io::stdout().flush(); }); (keychain, spk_iter) }) .collect::>(); let tip = chain.tip(); (keychain_spks, tip) }; client .scan( tip, keychain_spks, core::iter::empty(), core::iter::empty(), stop_gap, scan_options.batch_size, ) .context("scanning the blockchain")? } ElectrumCommands::Sync { mut unused_spks, all_spks, mut utxos, mut unconfirmed, scan_options, } => { // Get a short lock on the tracker to get the spks we're interested in let graph = graph.lock().unwrap(); let chain = chain.lock().unwrap(); let chain_tip = chain.tip().map(|cp| cp.block_id()).unwrap_or_default(); if !(all_spks || unused_spks || utxos || unconfirmed) { unused_spks = true; unconfirmed = true; utxos = true; } else if all_spks { unused_spks = false; } let mut spks: Box> = Box::new(core::iter::empty()); if all_spks { let all_spks = graph .index .all_spks() .iter() .map(|(k, v)| (*k, v.clone())) .collect::>(); spks = Box::new(spks.chain(all_spks.into_iter().map(|(index, script)| { eprintln!("scanning {:?}", index); script }))); } if unused_spks { let unused_spks = graph .index .unused_spks(..) .map(|(k, v)| (*k, ScriptBuf::from(v))) .collect::>(); spks = Box::new(spks.chain(unused_spks.into_iter().map(|(index, script)| { eprintln!( "Checking if address {} {:?} has been used", Address::from_script(&script, args.network).unwrap(), index ); script }))); } let mut outpoints: Box> = Box::new(core::iter::empty()); if utxos { let init_outpoints = graph.index.outpoints().iter().cloned(); let utxos = graph .graph() .filter_chain_unspents(&*chain, chain_tip, init_outpoints) .map(|(_, utxo)| utxo) .collect::>(); outpoints = Box::new( utxos .into_iter() .inspect(|utxo| { eprintln!( "Checking if outpoint {} (value: {}) has been spent", utxo.outpoint, utxo.txout.value ); }) .map(|utxo| utxo.outpoint), ); }; let mut txids: Box> = Box::new(core::iter::empty()); if unconfirmed { let unconfirmed_txids = graph .graph() .list_chain_txs(&*chain, chain_tip) .filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed()) .map(|canonical_tx| canonical_tx.tx_node.txid) .collect::>(); txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| { eprintln!("Checking if {} is confirmed yet", txid); })); } let tip = chain.tip(); // drop lock on graph and chain drop((graph, chain)); let electrum_update = client .scan_without_keychain(tip, spks, txids, outpoints, scan_options.batch_size) .context("scanning the blockchain")?; (electrum_update, BTreeMap::new()) } }; let ( ElectrumUpdate { chain_update, relevant_txids, }, keychain_update, ) = response; let missing_txids = { let graph = &*graph.lock().unwrap(); relevant_txids.missing_full_txs(graph.graph()) }; let now = std::time::UNIX_EPOCH .elapsed() .expect("must get time") .as_secs(); let graph_update = relevant_txids.into_tx_graph(&client, Some(now), missing_txids)?; let db_changeset = { let mut chain = chain.lock().unwrap(); let mut graph = graph.lock().unwrap(); let chain = chain.apply_update(chain_update)?; let indexed_tx_graph = { let mut changeset = indexed_tx_graph::ChangeSet::::default(); let (_, indexer) = graph.index.reveal_to_target_multi(&keychain_update); changeset.append(indexed_tx_graph::ChangeSet { indexer, ..Default::default() }); changeset.append(graph.apply_update(graph_update)); changeset }; (chain, indexed_tx_graph) }; let mut db = db.lock().unwrap(); db.stage(db_changeset); db.commit()?; Ok(()) }