From 6a1ac7f80a7f97cd3c6264fb54f2d1e3b1f95130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 12 May 2023 17:43:05 +0800 Subject: [PATCH] [examples_redesign] Implemented `example_electrum` This is a version of `keychain_tracker_electrum` that uses the redesigned structures instead. --- Cargo.toml | 1 + example-crates/example_cli/src/lib.rs | 29 +- example-crates/example_electrum/Cargo.toml | 11 + example-crates/example_electrum/src/main.rs | 315 ++++++++++++++++++++ 4 files changed, 344 insertions(+), 12 deletions(-) create mode 100644 example-crates/example_electrum/Cargo.toml create mode 100644 example-crates/example_electrum/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 4d0f4f4d..48ecaa88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/file_store", "crates/electrum", "example-crates/example_cli", + "example-crates/example_electrum", "example-crates/keychain_tracker_electrum", "example-crates/keychain_tracker_esplora", "example-crates/keychain_tracker_example_cli", diff --git a/example-crates/example_cli/src/lib.rs b/example-crates/example_cli/src/lib.rs index 30be503f..6ac40455 100644 --- a/example-crates/example_cli/src/lib.rs +++ b/example-crates/example_cli/src/lib.rs @@ -54,7 +54,7 @@ impl Append for ChangeSet { } fn is_empty(&self) -> bool { - todo!() + self.indexed_additions.is_empty() && self.extension.is_empty() } } @@ -666,7 +666,7 @@ pub fn planned_utxos( graph: &Mutex>, db: &Mutex>, - chain: &O, + chain: &Mutex, keymap: &HashMap, network: Network, broadcast: impl FnOnce(&Transaction) -> anyhow::Result<()>, @@ -684,26 +684,31 @@ where } Commands::Balance => { let graph = &*graph.lock().unwrap(); + let chain = &*chain.lock().unwrap(); run_balance_cmd(graph, chain).map_err(anyhow::Error::from) } Commands::TxOut { txout_cmd } => { let graph = &*graph.lock().unwrap(); + let chain = &*chain.lock().unwrap(); run_txo_cmd(graph, chain, network, txout_cmd) } Commands::Send { value, address, coin_select, - } => run_send_cmd( - graph, - db, - chain, - keymap, - coin_select, - address, - value, - broadcast, - ), + } => { + let chain = &*chain.lock().unwrap(); + run_send_cmd( + graph, + db, + chain, + keymap, + coin_select, + address, + value, + broadcast, + ) + } } } diff --git a/example-crates/example_electrum/Cargo.toml b/example-crates/example_electrum/Cargo.toml new file mode 100644 index 00000000..49d158e9 --- /dev/null +++ b/example-crates/example_electrum/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "example_electrum" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bdk_chain = { path = "../../crates/chain", features = ["serde"] } +bdk_electrum = { path = "../../crates/electrum" } +example_cli = { path = "../example_cli" } diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs new file mode 100644 index 00000000..6b67e8a7 --- /dev/null +++ b/example-crates/example_electrum/src/main.rs @@ -0,0 +1,315 @@ +use std::{ + collections::BTreeMap, + io::{self, Write}, + sync::Mutex, +}; + +use bdk_chain::{ + bitcoin::{Address, BlockHash, Network, OutPoint, Txid}, + indexed_tx_graph::IndexedAdditions, + local_chain::{self, LocalChain}, + Append, ConfirmationHeightAnchor, +}; +use bdk_electrum::{ + electrum_client::{self, ElectrumApi}, + v2::{ElectrumExt, ElectrumUpdate}, +}; +use example_cli::{ + anyhow::{self, Context}, + clap::{self, Parser, Subcommand}, +}; + +const DB_MAGIC: &[u8] = b"bdk_example_electrum"; +const DB_PATH: &str = ".bdk_electrum_example.db"; +const ASSUME_FINAL_DEPTH: usize = 10; + +#[derive(Subcommand, Debug, Clone)] +enum ElectrumCommands { + /// Scans the addresses in the wallet using the esplora 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 esplora 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, +} + +fn main() -> anyhow::Result<()> { + let (args, keymap, graph, db, chain_changeset) = + example_cli::init::( + DB_MAGIC, DB_PATH, + )?; + + let chain = Mutex::new({ + let mut chain = LocalChain::default(); + chain.apply_changeset(chain_changeset); + 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", + }; + 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, c) = { + 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 c = chain + .blocks() + .iter() + .rev() + .take(ASSUME_FINAL_DEPTH) + .map(|(k, v)| (*k, *v)) + .collect::>(); + + (keychain_spks, c) + }; + + client + .scan( + &c, + 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().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, v.clone())) + .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.observed_as.is_confirmed()) + .map(|canonical_tx| canonical_tx.node.txid) + .collect::>(); + + txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| { + eprintln!("Checking if {} is confirmed yet", txid); + })); + } + + let c = chain + .blocks() + .iter() + .rev() + .take(ASSUME_FINAL_DEPTH) + .map(|(k, v)| (*k, *v)) + .collect::>(); + + // drop lock on graph and chain + drop((graph, chain)); + + let update = client + .scan_without_keychain(&c, spks, txids, outpoints, scan_options.batch_size) + .context("scanning the blockchain")?; + ElectrumUpdate { + graph_update: update.graph_update, + chain_update: update.chain_update, + keychain_update: BTreeMap::new(), + } + } + }; + + let missing_txids = { + let graph = &*graph.lock().unwrap(); + response + .missing_full_txs(graph.graph()) + .cloned() + .collect::>() + }; + + let new_txs = client + .batch_transaction_get(&missing_txids) + .context("fetching full transactions")?; + let now = std::time::UNIX_EPOCH + .elapsed() + .expect("must get time") + .as_secs(); + let final_update = response.finalize(Some(now), new_txs); + + let db_changeset = { + let mut chain = chain.lock().unwrap(); + let mut graph = graph.lock().unwrap(); + + let chain_changeset = chain.apply_update(final_update.chain)?; + + let indexed_additions = { + let mut additions = IndexedAdditions::::default(); + let (_, index_additions) = graph.index.reveal_to_target_multi(&final_update.keychain); + additions.append(IndexedAdditions { + index_additions, + ..Default::default() + }); + additions.append(graph.apply_update(final_update.graph)); + additions + }; + + example_cli::ChangeSet { + indexed_additions, + extension: chain_changeset, + } + }; + + let mut db = db.lock().unwrap(); + db.stage(db_changeset); + db.commit()?; + Ok(()) +}