]> Untitled Git - bdk/commitdiff
[examples_redesign] Implemented `example_electrum`
author志宇 <hello@evanlinjin.me>
Fri, 12 May 2023 09:43:05 +0000 (17:43 +0800)
committer志宇 <hello@evanlinjin.me>
Sat, 3 Jun 2023 19:32:17 +0000 (03:32 +0800)
This is a version of `keychain_tracker_electrum` that uses the
redesigned structures instead.

Cargo.toml
example-crates/example_cli/src/lib.rs
example-crates/example_electrum/Cargo.toml [new file with mode: 0644]
example-crates/example_electrum/src/main.rs [new file with mode: 0644]

index 4d0f4f4d7dd67f8ee8a325fc3f71800a433f920c..48ecaa88a29fe0bbcc4907b123ffbb471cb8ff68 100644 (file)
@@ -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",
index 30be503fa7a5987a272fe58ba146197e32e7bac5..6ac40455ce865241e47081ce5c9b64dd0ac352bf 100644 (file)
@@ -54,7 +54,7 @@ impl<A: Anchor, X: Append> Append for ChangeSet<A, X> {
     }
 
     fn is_empty(&self) -> bool {
-        todo!()
+        self.indexed_additions.is_empty() && self.extension.is_empty()
     }
 }
 
@@ -666,7 +666,7 @@ pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDeri
 pub fn handle_commands<C: clap::Subcommand, A: Anchor, O: ChainOracle, X>(
     graph: &Mutex<KeychainTxGraph<A>>,
     db: &Mutex<Database<A, X>>,
-    chain: &O,
+    chain: &Mutex<O>,
     keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
     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 (file)
index 0000000..49d158e
--- /dev/null
@@ -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 (file)
index 0000000..6b67e8a
--- /dev/null
@@ -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::<ElectrumCommands, ConfirmationHeightAnchor, local_chain::ChangeSet>(
+            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::<BTreeMap<_, _>>();
+
+                let c = chain
+                    .blocks()
+                    .iter()
+                    .rev()
+                    .take(ASSUME_FINAL_DEPTH)
+                    .map(|(k, v)| (*k, *v))
+                    .collect::<BTreeMap<u32, BlockHash>>();
+
+                (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<dyn Iterator<Item = bdk_chain::bitcoin::Script>> =
+                Box::new(core::iter::empty());
+            if all_spks {
+                let all_spks = graph
+                    .index
+                    .all_spks()
+                    .iter()
+                    .map(|(k, v)| (*k, v.clone()))
+                    .collect::<Vec<_>>();
+                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::<Vec<_>>();
+                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<dyn Iterator<Item = OutPoint>> = 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::<Vec<_>>();
+
+                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<dyn Iterator<Item = Txid>> = 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::<Vec<Txid>>();
+
+                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::<BTreeMap<u32, BlockHash>>();
+
+            // 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::<Vec<_>>()
+    };
+
+    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::<ConfirmationHeightAnchor, _>::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(())
+}