]> Untitled Git - bdk/commitdiff
chore: rename example-crates directory to examples
authorSteve Myers <steve@notmandatory.org>
Sat, 5 Apr 2025 01:31:47 +0000 (20:31 -0500)
committerSteve Myers <steve@notmandatory.org>
Sat, 5 Apr 2025 03:25:37 +0000 (22:25 -0500)
24 files changed:
.github/workflows/cont_integration.yml
Cargo.toml
README.md
crates/electrum/src/lib.rs
crates/esplora/README.md
example-crates/example_bitcoind_rpc_polling/Cargo.toml [deleted file]
example-crates/example_bitcoind_rpc_polling/README.md [deleted file]
example-crates/example_bitcoind_rpc_polling/src/main.rs [deleted file]
example-crates/example_cli/Cargo.toml [deleted file]
example-crates/example_cli/src/lib.rs [deleted file]
example-crates/example_electrum/Cargo.toml [deleted file]
example-crates/example_electrum/src/main.rs [deleted file]
example-crates/example_esplora/Cargo.toml [deleted file]
example-crates/example_esplora/src/main.rs [deleted file]
examples/example_bitcoind_rpc_polling/Cargo.toml [new file with mode: 0644]
examples/example_bitcoind_rpc_polling/README.md [new file with mode: 0644]
examples/example_bitcoind_rpc_polling/src/main.rs [new file with mode: 0644]
examples/example_cli/Cargo.toml [new file with mode: 0644]
examples/example_cli/src/lib.rs [new file with mode: 0644]
examples/example_electrum/Cargo.toml [new file with mode: 0644]
examples/example_electrum/src/main.rs [new file with mode: 0644]
examples/example_esplora/Cargo.toml [new file with mode: 0644]
examples/example_esplora/src/main.rs [new file with mode: 0644]
nursery/README.md

index 8769ca1f37a8f2a55c331ccab079695a5acc0eb7..e517c2bf86c118bb899e20545facd91aadf39db1 100644 (file)
@@ -190,5 +190,5 @@ jobs:
       - name: Rust Cache
         uses: Swatinem/rust-cache@v2.7.7
       - name: Build
-        working-directory: example-crates/${{ matrix.example-dir }}
+        working-directory: examples/${{ matrix.example-dir }}
         run: cargo build
index 3f96ad1243ce1fab12ca9233364fb448c1fb9a82..0a62ba43ae6e638d12f1eae13f5f3297d1d6d376 100644 (file)
@@ -8,10 +8,10 @@ members = [
     "crates/esplora",
     "crates/bitcoind_rpc",
     "crates/testenv",
-    "example-crates/example_cli",
-    "example-crates/example_electrum",
-    "example-crates/example_esplora",
-    "example-crates/example_bitcoind_rpc_polling",
+    "examples/example_cli",
+    "examples/example_electrum",
+    "examples/example_esplora",
+    "examples/example_bitcoind_rpc_polling",
 ]
 
 [workspace.package]
index e3fc7480f42982714578fd29068e3d3d3a495442..27d5448eebe863b94a8e5280edbe1f5bc6deb31a 100644 (file)
--- a/README.md
+++ b/README.md
@@ -33,20 +33,20 @@ The workspace in this repository contains several crates in the `/crates` direct
 | Sub-Directory | Description | Badges |
 |---------------|-------------|--------|
 | [`chain`](./crates/chain) | Tools for storing and indexing chain data. | ![Chain Crate Info](https://img.shields.io/crates/v/bdk_chain.svg) ![Chain API Docs](https://img.shields.io/badge/docs.rs-bdk_chain-green) |
-| [`core`](./crates/core) | A collection of core structures used by the [`bdk_chain`], [`bdk_wallet`], and bdk chain data source crates. | ![Core Crate Info](https://img.shields.io/crates/v/bdk_core.svg) ![Core API Docs](https://img.shields.io/badge/docs.rs-bdk_core-green) |
+| [`core`](./crates/core) | A collection of core structures used by the [`bdk_chain`], [`bdk_wallet`], and BDK's chain data source crates. | ![Core Crate Info](https://img.shields.io/crates/v/bdk_core.svg) ![Core API Docs](https://img.shields.io/badge/docs.rs-bdk_core-green) |
 | [`esplora`](./crates/esplora) | Extends the [`esplora-client`] crate with methods to fetch chain data from an esplora HTTP server in the form that [`bdk_chain`] and `Wallet` can consume. | ![Esplora Crate Info](https://img.shields.io/crates/v/bdk_esplora.svg) ![Esplora API Docs](https://img.shields.io/badge/docs.rs-bdk_esplora-green) |
 | [`electrum`](./crates/electrum) | Extends the [`electrum-client`] crate with methods to fetch chain data from an electrum server in the form that [`bdk_chain`] and `Wallet` can consume. | ![Electrum Crate Info](https://img.shields.io/crates/v/bdk_electrum.svg) ![Electrum API Docs](https://img.shields.io/badge/docs.rs-bdk_electrum-green) |
-| [`bitcoind_rpc`](./crates/bitcoind_rpc) | Extends [`bitcoincore-rpc`] for emitting blockchain data from the `bitcoind` RPC interface. | ![BitcoinD RPC Crate Info](https://img.shields.io/crates/v/bdk_bitcoind_rpc.svg) ![BitcoinD RPC API Docs](https://img.shields.io/badge/docs.rs-bdk_bitcoind_rpc-green) |
+| [`bitcoind_rpc`](./crates/bitcoind_rpc) | Extends [`bitcoincore-rpc`] for emitting blockchain data from the `bitcoind` RPC interface in the form that [`bdk_chain`] and `Wallet` can consume. | ![BitcoinD RPC Crate Info](https://img.shields.io/crates/v/bdk_bitcoind_rpc.svg) ![BitcoinD RPC API Docs](https://img.shields.io/badge/docs.rs-bdk_bitcoind_rpc-green) |
 | [`file_store`](./crates/file_store) | Persistence backend for storing chain data in a single file. Intended for testing and development purposes, not for production. | ![File Store Crate Info](https://img.shields.io/crates/v/bdk_file_store.svg) ![File Store API Docs](https://img.shields.io/badge/docs.rs-bdk_file_store-green) |
 
 The [`bdk_wallet`] repository and crate contains a higher level `Wallet` type that depends on the above lower-level mechanism crates. 
 
-Fully working examples of how to use these components are in `/example-crates`:
+Fully working examples of how to use these components are in `/examples`:
 
-- [`example_cli`](./example-crates/example_cli): Library used by the `example_*` crates. Provides utilities for syncing, showing the balance, generating addresses and creating transactions without using the bdk_wallet `Wallet`.
-- [`example_electrum`](./example-crates/example_electrum): A command line Bitcoin wallet application built on top of `example_cli` and the `electrum` crate. It shows the power of the bdk tools (`chain` + `file_store` + `electrum`), without depending on the main `bdk_wallet` library.
-- [`example_esplora`](./example-crates/example_esplora): A command line Bitcoin wallet application built on top of `example_cli` and the `esplora` crate. It shows the power of the bdk tools (`chain` + `file_store` + `esplora`), without depending on the main `bdk_wallet` library.
-- [`example_bitcoind_rpc_polling`](./example-crates/example_bitcoind_rpc_polling): A command line Bitcoin wallet application built on top of `example_cli` and the `bitcoind_rpc` crate. It shows the power of the bdk tools (`chain` + `file_store` + `bitcoind_rpc`), without depending on the main `bdk_wallet` library.
+- [`example_cli`](examples/example_cli): Library used by the `example_*` crates. Provides utilities for syncing, showing the balance, generating addresses and creating transactions without using the bdk_wallet `Wallet`.
+- [`example_electrum`](examples/example_electrum): A command line Bitcoin wallet application built on top of `example_cli` and the `electrum` crate. It shows the power of the bdk tools (`chain` + `file_store` + `electrum`), without depending on the main `bdk_wallet` library.
+- [`example_esplora`](examples/example_esplora): A command line Bitcoin wallet application built on top of `example_cli` and the `esplora` crate. It shows the power of the bdk tools (`chain` + `file_store` + `esplora`), without depending on the main `bdk_wallet` library.
+- [`example_bitcoind_rpc_polling`](examples/example_bitcoind_rpc_polling): A command line Bitcoin wallet application built on top of `example_cli` and the `bitcoind_rpc` crate. It shows the power of the bdk tools (`chain` + `file_store` + `bitcoind_rpc`), without depending on the main `bdk_wallet` library.
 
 [`rust-miniscript`]: https://github.com/rust-bitcoin/rust-miniscript
 [`rust-bitcoin`]: https://github.com/rust-bitcoin/rust-bitcoin
index 8bc87321f8a1b31822e6e26a8182a65dd48e1c4c..d791f962e6e9b4e82d96615286a49491d9958df6 100644 (file)
@@ -13,7 +13,7 @@
 //!
 //! Refer to [`example_electrum`] for a complete example.
 //!
-//! [`example_electrum`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_electrum
+//! [`example_electrum`]: https://github.com/bitcoindevkit/bdk/tree/master/examples/example_electrum
 //! [`SyncResponse`]: bdk_core::spk_client::SyncResponse
 //! [`FullScanResponse`]: bdk_core::spk_client::FullScanResponse
 
index 244deb20ffabe2c78b6af5ada568b1e9aaf5aedb..4a6acb377dc2c8675d6916232ae40d13b68ec6b7 100644 (file)
@@ -41,7 +41,7 @@ use bdk_esplora::EsploraExt;
 use bdk_esplora::EsploraAsyncExt;
 ```
 
-For full examples, refer to [`example_wallet_esplora_blocking`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_wallet_esplora_blocking) and [`example_wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_wallet_esplora_async).
+For full examples, refer to [`example_wallet_esplora_blocking`](https://github.com/bitcoindevkit/bdk/tree/master/examples/example_wallet_esplora_blocking) and [`example_wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/examples/example_wallet_esplora_async).
 
 [`esplora-client`]: https://docs.rs/esplora-client/
 [`bdk_chain`]: https://docs.rs/bdk-chain/
diff --git a/example-crates/example_bitcoind_rpc_polling/Cargo.toml b/example-crates/example_bitcoind_rpc_polling/Cargo.toml
deleted file mode 100644 (file)
index 6728bb1..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-[package]
-name = "example_bitcoind_rpc_polling"
-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_bitcoind_rpc = { path = "../../crates/bitcoind_rpc" }
-example_cli = { path = "../example_cli" }
-ctrlc = { version = "^2" }
diff --git a/example-crates/example_bitcoind_rpc_polling/README.md b/example-crates/example_bitcoind_rpc_polling/README.md
deleted file mode 100644 (file)
index fef82ab..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-# Example RPC CLI
-
-### Simple Regtest Test
-
-1. Start local regtest bitcoind.
-   ```
-    mkdir -p /tmp/regtest/bitcoind
-    bitcoind -regtest -server -fallbackfee=0.0002 -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -datadir=/tmp/regtest/bitcoind -daemon
-   ```
-2. Create a test bitcoind wallet and set bitcoind env.
-   ```
-   bitcoin-cli -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -named createwallet wallet_name="test"
-   export RPC_URL=127.0.0.1:18443
-   export RPC_USER=<your-rpc-username>
-   export RPC_PASS=<your-rpc-password>
-   ```
-3. Get test bitcoind wallet info.
-   ```
-   bitcoin-cli -rpcwallet="test" -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -datadir=/tmp/regtest/bitcoind -regtest getwalletinfo
-   ```
-4. Get new test bitcoind wallet address.
-   ```
-   BITCOIND_ADDRESS=$(bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> getnewaddress)
-   echo $BITCOIND_ADDRESS
-   ```
-5. Generate 101 blocks with reward to test bitcoind wallet address.
-   ```
-   bitcoin-cli -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> generatetoaddress 101 $BITCOIND_ADDRESS
-   ```
-6. Verify test bitcoind wallet balance.
-   ```
-   bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> getbalances
-   ```
-7. Set descriptor env and get address from RPC CLI wallet.
-   ```
-   export DESCRIPTOR="wpkh(tprv8ZgxMBicQKsPfK9BTf82oQkHhawtZv19CorqQKPFeaHDMA4dXYX6eWsJGNJ7VTQXWmoHdrfjCYuDijcRmNFwSKcVhswzqs4fugE8turndGc/1/*)"
-   cargo run -- --network regtest address next
-   ```
-8. Send 5 test bitcoin to RPC CLI wallet.
-   ```
-   bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> sendtoaddress <address> 5
-   ```
-9. Sync blockchain with RPC CLI wallet.
-   ```
-   cargo run -- --network regtest sync
-   <CNTRL-C to stop syncing>
-   ```
-10. Get RPC CLI wallet unconfirmed balances.
-   ```
-   cargo run -- --network regtest balance
-   ```
-11. Generate 1 block with reward to test bitcoind wallet address.
-   ```
-   bitcoin-cli -datadir=/tmp/regtest/bitcoind -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -regtest generatetoaddress 10 $BITCOIND_ADDRESS
-   ```
-12. Sync the blockchain with RPC CLI wallet.
-   ```
-   cargo run -- --network regtest sync
-   <CNTRL-C to stop syncing>
-   ```
-13. Get RPC CLI wallet confirmed balances.
-   ```
-   cargo run -- --network regtest balance
-   ```
-14. Get RPC CLI wallet transactions.
-   ```
-   cargo run -- --network regtest txout list
-   ```
\ No newline at end of file
diff --git a/example-crates/example_bitcoind_rpc_polling/src/main.rs b/example-crates/example_bitcoind_rpc_polling/src/main.rs
deleted file mode 100644 (file)
index 83cb25f..0000000
+++ /dev/null
@@ -1,370 +0,0 @@
-use std::{
-    path::PathBuf,
-    sync::{
-        atomic::{AtomicBool, Ordering},
-        Arc,
-    },
-    time::{Duration, Instant},
-};
-
-use bdk_bitcoind_rpc::{
-    bitcoincore_rpc::{Auth, Client, RpcApi},
-    Emitter,
-};
-use bdk_chain::{
-    bitcoin::{Block, Transaction},
-    local_chain, Merge,
-};
-use example_cli::{
-    anyhow,
-    clap::{self, Args, Subcommand},
-    ChangeSet, Keychain,
-};
-
-const DB_MAGIC: &[u8] = b"bdk_example_rpc";
-const DB_PATH: &str = ".bdk_example_rpc.db";
-
-/// The mpsc channel bound for emissions from [`Emitter`].
-const CHANNEL_BOUND: usize = 10;
-/// Delay for printing status to stdout.
-const STDOUT_PRINT_DELAY: Duration = Duration::from_secs(6);
-/// Delay between mempool emissions.
-const MEMPOOL_EMIT_DELAY: Duration = Duration::from_secs(30);
-/// Delay for committing to persistence.
-const DB_COMMIT_DELAY: Duration = Duration::from_secs(60);
-
-#[derive(Debug)]
-enum Emission {
-    Block(bdk_bitcoind_rpc::BlockEvent<Block>),
-    Mempool(Vec<(Transaction, u64)>),
-    Tip(u32),
-}
-
-#[derive(Args, Debug, Clone)]
-struct RpcArgs {
-    /// RPC URL
-    #[clap(env = "RPC_URL", long, default_value = "127.0.0.1:8332")]
-    url: String,
-    /// RPC auth cookie file
-    #[clap(env = "RPC_COOKIE", long)]
-    rpc_cookie: Option<PathBuf>,
-    /// RPC auth username
-    #[clap(env = "RPC_USER", long)]
-    rpc_user: Option<String>,
-    /// RPC auth password
-    #[clap(env = "RPC_PASS", long)]
-    rpc_password: Option<String>,
-    /// Starting block height to fallback to if no point of agreement if found
-    #[clap(env = "FALLBACK_HEIGHT", long, default_value = "0")]
-    fallback_height: u32,
-}
-
-impl From<RpcArgs> for Auth {
-    fn from(args: RpcArgs) -> Self {
-        match (args.rpc_cookie, args.rpc_user, args.rpc_password) {
-            (None, None, None) => Self::None,
-            (Some(path), _, _) => Self::CookieFile(path),
-            (_, Some(user), Some(pass)) => Self::UserPass(user, pass),
-            (_, Some(_), None) => panic!("rpc auth: missing rpc_pass"),
-            (_, None, Some(_)) => panic!("rpc auth: missing rpc_user"),
-        }
-    }
-}
-
-impl RpcArgs {
-    fn new_client(&self) -> anyhow::Result<Client> {
-        Ok(Client::new(
-            &self.url,
-            match (&self.rpc_cookie, &self.rpc_user, &self.rpc_password) {
-                (None, None, None) => Auth::None,
-                (Some(path), _, _) => Auth::CookieFile(path.clone()),
-                (_, Some(user), Some(pass)) => Auth::UserPass(user.clone(), pass.clone()),
-                (_, Some(_), None) => panic!("rpc auth: missing rpc_pass"),
-                (_, None, Some(_)) => panic!("rpc auth: missing rpc_user"),
-            },
-        )?)
-    }
-}
-
-#[derive(Subcommand, Debug, Clone)]
-enum RpcCommands {
-    /// Syncs local state with remote state via RPC (starting from last point of agreement) and
-    /// stores/indexes relevant transactions
-    Sync {
-        #[clap(flatten)]
-        rpc_args: RpcArgs,
-    },
-    /// Sync by having the emitter logic in a separate thread
-    Live {
-        #[clap(flatten)]
-        rpc_args: RpcArgs,
-    },
-}
-
-fn main() -> anyhow::Result<()> {
-    let start = Instant::now();
-
-    let example_cli::Init {
-        args,
-        graph,
-        chain,
-        db,
-        network,
-    } = match example_cli::init_or_load::<RpcCommands, RpcArgs>(DB_MAGIC, DB_PATH)? {
-        Some(init) => init,
-        None => return Ok(()),
-    };
-
-    let rpc_cmd = match args.command {
-        example_cli::Commands::ChainSpecific(rpc_cmd) => rpc_cmd,
-        general_cmd => {
-            return example_cli::handle_commands(
-                &graph,
-                &chain,
-                &db,
-                network,
-                |rpc_args, tx| {
-                    let client = rpc_args.new_client()?;
-                    client.send_raw_transaction(tx)?;
-                    Ok(())
-                },
-                general_cmd,
-            );
-        }
-    };
-
-    match rpc_cmd {
-        RpcCommands::Sync { rpc_args } => {
-            let RpcArgs {
-                fallback_height, ..
-            } = rpc_args;
-
-            let chain_tip = chain.lock().unwrap().tip();
-            let rpc_client = rpc_args.new_client()?;
-            let mut emitter = Emitter::new(&rpc_client, chain_tip, fallback_height);
-            let mut db_stage = ChangeSet::default();
-
-            let mut last_db_commit = Instant::now();
-            let mut last_print = Instant::now();
-
-            while let Some(emission) = emitter.next_block()? {
-                let height = emission.block_height();
-
-                let mut chain = chain.lock().unwrap();
-                let mut graph = graph.lock().unwrap();
-
-                let chain_changeset = chain
-                    .apply_update(emission.checkpoint)
-                    .expect("must always apply as we receive blocks in order from emitter");
-                let graph_changeset = graph.apply_block_relevant(&emission.block, height);
-                db_stage.merge(ChangeSet {
-                    local_chain: chain_changeset,
-                    tx_graph: graph_changeset.tx_graph,
-                    indexer: graph_changeset.indexer,
-                    ..Default::default()
-                });
-
-                // commit staged db changes in intervals
-                if last_db_commit.elapsed() >= DB_COMMIT_DELAY {
-                    let db = &mut *db.lock().unwrap();
-                    last_db_commit = Instant::now();
-                    if let Some(changeset) = db_stage.take() {
-                        db.append(&changeset)?;
-                    }
-                    println!(
-                        "[{:>10}s] committed to db (took {}s)",
-                        start.elapsed().as_secs_f32(),
-                        last_db_commit.elapsed().as_secs_f32()
-                    );
-                }
-
-                // print synced-to height and current balance in intervals
-                if last_print.elapsed() >= STDOUT_PRINT_DELAY {
-                    last_print = Instant::now();
-                    let synced_to = chain.tip();
-                    let balance = {
-                        graph.graph().balance(
-                            &*chain,
-                            synced_to.block_id(),
-                            graph.index.outpoints().iter().cloned(),
-                            |(k, _), _| k == &Keychain::Internal,
-                        )
-                    };
-                    println!(
-                        "[{:>10}s] synced to {} @ {} | total: {}",
-                        start.elapsed().as_secs_f32(),
-                        synced_to.hash(),
-                        synced_to.height(),
-                        balance.total()
-                    );
-                }
-            }
-
-            let mempool_txs = emitter.mempool()?;
-            let graph_changeset = graph
-                .lock()
-                .unwrap()
-                .batch_insert_relevant_unconfirmed(mempool_txs);
-            {
-                let db = &mut *db.lock().unwrap();
-                db_stage.merge(ChangeSet {
-                    tx_graph: graph_changeset.tx_graph,
-                    indexer: graph_changeset.indexer,
-                    ..Default::default()
-                });
-                if let Some(changeset) = db_stage.take() {
-                    db.append(&changeset)?;
-                }
-            }
-        }
-        RpcCommands::Live { rpc_args } => {
-            let RpcArgs {
-                fallback_height, ..
-            } = rpc_args;
-            let sigterm_flag = start_ctrlc_handler();
-
-            let last_cp = chain.lock().unwrap().tip();
-
-            println!(
-                "[{:>10}s] starting emitter thread...",
-                start.elapsed().as_secs_f32()
-            );
-            let (tx, rx) = std::sync::mpsc::sync_channel::<Emission>(CHANNEL_BOUND);
-            let emission_jh = std::thread::spawn(move || -> anyhow::Result<()> {
-                let rpc_client = rpc_args.new_client()?;
-                let mut emitter = Emitter::new(&rpc_client, last_cp, fallback_height);
-
-                let mut block_count = rpc_client.get_block_count()? as u32;
-                tx.send(Emission::Tip(block_count))?;
-
-                loop {
-                    match emitter.next_block()? {
-                        Some(block_emission) => {
-                            let height = block_emission.block_height();
-                            if sigterm_flag.load(Ordering::Acquire) {
-                                break;
-                            }
-                            if height > block_count {
-                                block_count = rpc_client.get_block_count()? as u32;
-                                tx.send(Emission::Tip(block_count))?;
-                            }
-                            tx.send(Emission::Block(block_emission))?;
-                        }
-                        None => {
-                            if await_flag(&sigterm_flag, MEMPOOL_EMIT_DELAY) {
-                                break;
-                            }
-                            println!("preparing mempool emission...");
-                            let now = Instant::now();
-                            tx.send(Emission::Mempool(emitter.mempool()?))?;
-                            println!("mempool emission prepared in {}s", now.elapsed().as_secs());
-                            continue;
-                        }
-                    };
-                }
-
-                println!("emitter thread shutting down...");
-                Ok(())
-            });
-
-            let mut tip_height = 0_u32;
-            let mut last_db_commit = Instant::now();
-            let mut last_print = Option::<Instant>::None;
-            let mut db_stage = ChangeSet::default();
-
-            for emission in rx {
-                let mut graph = graph.lock().unwrap();
-                let mut chain = chain.lock().unwrap();
-
-                let (chain_changeset, graph_changeset) = match emission {
-                    Emission::Block(block_emission) => {
-                        let height = block_emission.block_height();
-                        let chain_changeset = chain
-                            .apply_update(block_emission.checkpoint)
-                            .expect("must always apply as we receive blocks in order from emitter");
-                        let graph_changeset =
-                            graph.apply_block_relevant(&block_emission.block, height);
-                        (chain_changeset, graph_changeset)
-                    }
-                    Emission::Mempool(mempool_txs) => {
-                        let graph_changeset = graph.batch_insert_relevant_unconfirmed(mempool_txs);
-                        (local_chain::ChangeSet::default(), graph_changeset)
-                    }
-                    Emission::Tip(h) => {
-                        tip_height = h;
-                        continue;
-                    }
-                };
-
-                db_stage.merge(ChangeSet {
-                    local_chain: chain_changeset,
-                    tx_graph: graph_changeset.tx_graph,
-                    indexer: graph_changeset.indexer,
-                    ..Default::default()
-                });
-
-                if last_db_commit.elapsed() >= DB_COMMIT_DELAY {
-                    let db = &mut *db.lock().unwrap();
-                    last_db_commit = Instant::now();
-                    if let Some(changeset) = db_stage.take() {
-                        db.append(&changeset)?;
-                    }
-                    println!(
-                        "[{:>10}s] committed to db (took {}s)",
-                        start.elapsed().as_secs_f32(),
-                        last_db_commit.elapsed().as_secs_f32()
-                    );
-                }
-
-                if last_print.map_or(Duration::MAX, |i| i.elapsed()) >= STDOUT_PRINT_DELAY {
-                    last_print = Some(Instant::now());
-                    let synced_to = chain.tip();
-                    let balance = {
-                        graph.graph().balance(
-                            &*chain,
-                            synced_to.block_id(),
-                            graph.index.outpoints().iter().cloned(),
-                            |(k, _), _| k == &Keychain::Internal,
-                        )
-                    };
-                    println!(
-                        "[{:>10}s] synced to {} @ {} / {} | total: {}",
-                        start.elapsed().as_secs_f32(),
-                        synced_to.hash(),
-                        synced_to.height(),
-                        tip_height,
-                        balance.total()
-                    );
-                }
-            }
-
-            emission_jh.join().expect("must join emitter thread")?;
-        }
-    }
-
-    Ok(())
-}
-
-#[allow(dead_code)]
-fn start_ctrlc_handler() -> Arc<AtomicBool> {
-    let flag = Arc::new(AtomicBool::new(false));
-    let cloned_flag = flag.clone();
-
-    ctrlc::set_handler(move || cloned_flag.store(true, Ordering::Release));
-
-    flag
-}
-
-#[allow(dead_code)]
-fn await_flag(flag: &AtomicBool, duration: Duration) -> bool {
-    let start = Instant::now();
-    loop {
-        if flag.load(Ordering::Acquire) {
-            return true;
-        }
-        if start.elapsed() >= duration {
-            return false;
-        }
-        std::thread::sleep(Duration::from_secs(1));
-    }
-}
diff --git a/example-crates/example_cli/Cargo.toml b/example-crates/example_cli/Cargo.toml
deleted file mode 100644 (file)
index 0a467db..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-[package]
-name = "example_cli"
-version = "0.2.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", "miniscript"]}
-bdk_coin_select = "0.4"
-bdk_file_store = { path = "../../crates/file_store" }
-bitcoin = { version = "0.32.0", features = ["base64"], default-features = false }
-
-anyhow = "1"
-clap = { version = "4.5.17", features = ["derive", "env"] }
-rand = "0.8"
-serde = { version = "1", features = ["derive"] }
-serde_json = "1.0"
diff --git a/example-crates/example_cli/src/lib.rs b/example-crates/example_cli/src/lib.rs
deleted file mode 100644 (file)
index 2cb2784..0000000
+++ /dev/null
@@ -1,954 +0,0 @@
-use serde_json::json;
-use std::cmp;
-use std::collections::HashMap;
-use std::env;
-use std::fmt;
-use std::str::FromStr;
-use std::sync::Mutex;
-
-use anyhow::bail;
-use anyhow::Context;
-use bdk_chain::bitcoin::{
-    absolute, address::NetworkUnchecked, bip32, consensus, constants, hex::DisplayHex, relative,
-    secp256k1::Secp256k1, transaction, Address, Amount, Network, NetworkKind, PrivateKey, Psbt,
-    PublicKey, Sequence, Transaction, TxIn, TxOut,
-};
-use bdk_chain::miniscript::{
-    descriptor::{DescriptorSecretKey, SinglePubKey},
-    plan::{Assets, Plan},
-    psbt::PsbtExt,
-    Descriptor, DescriptorPublicKey, ForEachKey,
-};
-use bdk_chain::ConfirmationBlockTime;
-use bdk_chain::{
-    indexed_tx_graph,
-    indexer::keychain_txout::{self, KeychainTxOutIndex},
-    local_chain::{self, LocalChain},
-    tx_graph, ChainOracle, DescriptorExt, FullTxOut, IndexedTxGraph, Merge,
-};
-use bdk_coin_select::{
-    metrics::LowestFee, Candidate, ChangePolicy, CoinSelector, DrainWeights, FeeRate, Target,
-    TargetFee, TargetOutputs,
-};
-use bdk_file_store::Store;
-use clap::{Parser, Subcommand};
-use rand::prelude::*;
-
-pub use anyhow;
-pub use clap;
-
-/// Alias for a `IndexedTxGraph` with specific `Anchor` and `Indexer`.
-pub type KeychainTxGraph = IndexedTxGraph<ConfirmationBlockTime, KeychainTxOutIndex<Keychain>>;
-
-/// ChangeSet
-#[derive(Default, Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)]
-pub struct ChangeSet {
-    /// Descriptor for recipient addresses.
-    pub descriptor: Option<Descriptor<DescriptorPublicKey>>,
-    /// Descriptor for change addresses.
-    pub change_descriptor: Option<Descriptor<DescriptorPublicKey>>,
-    /// Stores the network type of the transaction data.
-    pub network: Option<Network>,
-    /// Changes to the [`LocalChain`].
-    pub local_chain: local_chain::ChangeSet,
-    /// Changes to [`TxGraph`](tx_graph::TxGraph).
-    pub tx_graph: tx_graph::ChangeSet<ConfirmationBlockTime>,
-    /// Changes to [`KeychainTxOutIndex`].
-    pub indexer: keychain_txout::ChangeSet,
-}
-
-#[derive(Parser)]
-#[clap(author, version, about, long_about = None)]
-#[clap(propagate_version = true)]
-pub struct Args<CS: clap::Subcommand, S: clap::Args> {
-    #[clap(subcommand)]
-    pub command: Commands<CS, S>,
-}
-
-#[derive(Subcommand, Debug, Clone)]
-pub enum Commands<CS: clap::Subcommand, S: clap::Args> {
-    /// Initialize a new data store.
-    Init {
-        /// Network
-        #[clap(long, short, default_value = "signet")]
-        network: Network,
-        /// Descriptor
-        #[clap(env = "DESCRIPTOR")]
-        descriptor: String,
-        /// Change descriptor
-        #[clap(long, short, env = "CHANGE_DESCRIPTOR")]
-        change_descriptor: Option<String>,
-    },
-    #[clap(flatten)]
-    ChainSpecific(CS),
-    /// Address generation and inspection.
-    Address {
-        #[clap(subcommand)]
-        addr_cmd: AddressCmd,
-    },
-    /// Get the wallet balance.
-    Balance,
-    /// TxOut related commands.
-    #[clap(name = "txout")]
-    TxOut {
-        #[clap(subcommand)]
-        txout_cmd: TxOutCmd,
-    },
-    /// PSBT operations
-    Psbt {
-        #[clap(subcommand)]
-        psbt_cmd: PsbtCmd<S>,
-    },
-    /// Generate new BIP86 descriptors.
-    Generate {
-        /// Network
-        #[clap(long, short, default_value = "signet")]
-        network: Network,
-    },
-}
-
-#[derive(Subcommand, Debug, Clone)]
-pub enum AddressCmd {
-    /// Get the next unused address.
-    Next,
-    /// Get a new address regardless of the existing unused addresses.
-    New,
-    /// List all addresses
-    List {
-        /// List change addresses
-        #[clap(long)]
-        change: bool,
-    },
-    /// Get last revealed address index for each keychain.
-    Index,
-}
-
-#[derive(Subcommand, Debug, Clone)]
-pub enum TxOutCmd {
-    /// List transaction outputs.
-    List {
-        /// Return only spent outputs.
-        #[clap(short, long)]
-        spent: bool,
-        /// Return only unspent outputs.
-        #[clap(short, long)]
-        unspent: bool,
-        /// Return only confirmed outputs.
-        #[clap(long)]
-        confirmed: bool,
-        /// Return only unconfirmed outputs.
-        #[clap(long)]
-        unconfirmed: bool,
-    },
-}
-
-#[derive(Subcommand, Debug, Clone)]
-pub enum PsbtCmd<S: clap::Args> {
-    /// Create a new PSBT.
-    New {
-        /// Amount to send in satoshis
-        #[clap(required = true)]
-        value: u64,
-        /// Recipient address
-        #[clap(required = true)]
-        address: Address<NetworkUnchecked>,
-        /// Set the feerate of the tx (sat/vbyte)
-        #[clap(long, short, default_value = "1.0")]
-        feerate: Option<f32>,
-        /// Set max absolute timelock (from consensus value)
-        #[clap(long, short)]
-        after: Option<u32>,
-        /// Set max relative timelock (from consensus value)
-        #[clap(long, short)]
-        older: Option<u32>,
-        /// Coin selection algorithm
-        #[clap(long, short, default_value = "bnb")]
-        coin_select: CoinSelectionAlgo,
-        /// Debug print the PSBT
-        #[clap(long, short)]
-        debug: bool,
-    },
-    /// Sign with a hot signer
-    Sign {
-        /// Private descriptor [env: DESCRIPTOR=]
-        #[clap(long, short)]
-        descriptor: Option<String>,
-        /// PSBT
-        #[clap(long, short, required = true)]
-        psbt: String,
-    },
-    /// Extract transaction
-    Extract {
-        /// PSBT
-        #[clap(long, short, required = true)]
-        psbt: String,
-        /// Whether to try broadcasting the tx
-        #[clap(long, short)]
-        broadcast: bool,
-        #[clap(flatten)]
-        chain_specific: S,
-    },
-}
-
-#[derive(
-    Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, serde::Deserialize, serde::Serialize,
-)]
-pub enum Keychain {
-    External,
-    Internal,
-}
-
-impl fmt::Display for Keychain {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            Keychain::External => write!(f, "external"),
-            Keychain::Internal => write!(f, "internal"),
-        }
-    }
-}
-
-#[derive(Clone, Debug, Default)]
-pub enum CoinSelectionAlgo {
-    LargestFirst,
-    SmallestFirst,
-    OldestFirst,
-    NewestFirst,
-    #[default]
-    BranchAndBound,
-}
-
-impl FromStr for CoinSelectionAlgo {
-    type Err = anyhow::Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        use CoinSelectionAlgo::*;
-        Ok(match s {
-            "largest-first" => LargestFirst,
-            "smallest-first" => SmallestFirst,
-            "oldest-first" => OldestFirst,
-            "newest-first" => NewestFirst,
-            "bnb" => BranchAndBound,
-            unknown => bail!("unknown coin selection algorithm '{}'", unknown),
-        })
-    }
-}
-
-impl fmt::Display for CoinSelectionAlgo {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        use CoinSelectionAlgo::*;
-        write!(
-            f,
-            "{}",
-            match self {
-                LargestFirst => "largest-first",
-                SmallestFirst => "smallest-first",
-                OldestFirst => "oldest-first",
-                NewestFirst => "newest-first",
-                BranchAndBound => "bnb",
-            }
-        )
-    }
-}
-
-// Records changes to the internal keychain when we
-// have to include a change output during tx creation.
-#[derive(Debug)]
-pub struct ChangeInfo {
-    pub change_keychain: Keychain,
-    pub indexer: keychain_txout::ChangeSet,
-    pub index: u32,
-}
-
-pub fn create_tx<O: ChainOracle>(
-    graph: &mut KeychainTxGraph,
-    chain: &O,
-    assets: &Assets,
-    cs_algorithm: CoinSelectionAlgo,
-    address: Address,
-    value: u64,
-    feerate: f32,
-) -> anyhow::Result<(Psbt, Option<ChangeInfo>)>
-where
-    O::Error: std::error::Error + Send + Sync + 'static,
-{
-    let mut changeset = keychain_txout::ChangeSet::default();
-
-    // get planned utxos
-    let mut plan_utxos = planned_utxos(graph, chain, assets)?;
-
-    // sort utxos if cs-algo requires it
-    match cs_algorithm {
-        CoinSelectionAlgo::LargestFirst => {
-            plan_utxos.sort_by_key(|(_, utxo)| cmp::Reverse(utxo.txout.value))
-        }
-        CoinSelectionAlgo::SmallestFirst => plan_utxos.sort_by_key(|(_, utxo)| utxo.txout.value),
-        CoinSelectionAlgo::OldestFirst => plan_utxos.sort_by_key(|(_, utxo)| utxo.chain_position),
-        CoinSelectionAlgo::NewestFirst => {
-            plan_utxos.sort_by_key(|(_, utxo)| cmp::Reverse(utxo.chain_position))
-        }
-        CoinSelectionAlgo::BranchAndBound => plan_utxos.shuffle(&mut thread_rng()),
-    }
-
-    // build candidate set
-    let candidates: Vec<Candidate> = plan_utxos
-        .iter()
-        .map(|(plan, utxo)| {
-            Candidate::new(
-                utxo.txout.value.to_sat(),
-                plan.satisfaction_weight() as u64,
-                plan.witness_version().is_some(),
-            )
-        })
-        .collect();
-
-    // create recipient output(s)
-    let mut outputs = vec![TxOut {
-        value: Amount::from_sat(value),
-        script_pubkey: address.script_pubkey(),
-    }];
-
-    let (change_keychain, _) = graph
-        .index
-        .keychains()
-        .last()
-        .expect("must have a keychain");
-
-    let ((change_index, change_script), index_changeset) = graph
-        .index
-        .next_unused_spk(change_keychain)
-        .expect("Must exist");
-    changeset.merge(index_changeset);
-
-    let mut change_output = TxOut {
-        value: Amount::ZERO,
-        script_pubkey: change_script,
-    };
-
-    let change_desc = graph
-        .index
-        .keychains()
-        .find(|(k, _)| k == &change_keychain)
-        .expect("must exist")
-        .1;
-
-    let min_drain_value = change_desc.dust_value().to_sat();
-
-    let target = Target {
-        outputs: TargetOutputs::fund_outputs(
-            outputs
-                .iter()
-                .map(|output| (output.weight().to_wu(), output.value.to_sat())),
-        ),
-        fee: TargetFee {
-            rate: FeeRate::from_sat_per_vb(feerate),
-            ..Default::default()
-        },
-    };
-
-    let change_policy = ChangePolicy {
-        min_value: min_drain_value,
-        drain_weights: DrainWeights::TR_KEYSPEND,
-    };
-
-    // run coin selection
-    let mut selector = CoinSelector::new(&candidates);
-    match cs_algorithm {
-        CoinSelectionAlgo::BranchAndBound => {
-            let metric = LowestFee {
-                target,
-                long_term_feerate: FeeRate::from_sat_per_vb(10.0),
-                change_policy,
-            };
-            match selector.run_bnb(metric, 10_000) {
-                Ok(_) => {}
-                Err(_) => selector
-                    .select_until_target_met(target)
-                    .context("selecting coins")?,
-            }
-        }
-        _ => selector
-            .select_until_target_met(target)
-            .context("selecting coins")?,
-    }
-
-    // get the selected plan utxos
-    let selected: Vec<_> = selector.apply_selection(&plan_utxos).collect();
-
-    // if the selection tells us to use change and the change value is sufficient, we add it as an output
-    let mut change_info = Option::<ChangeInfo>::None;
-    let drain = selector.drain(target, change_policy);
-    if drain.value > min_drain_value {
-        change_output.value = Amount::from_sat(drain.value);
-        outputs.push(change_output);
-        change_info = Some(ChangeInfo {
-            change_keychain,
-            indexer: changeset,
-            index: change_index,
-        });
-        outputs.shuffle(&mut thread_rng());
-    }
-
-    let unsigned_tx = Transaction {
-        version: transaction::Version::TWO,
-        lock_time: assets
-            .absolute_timelock
-            .unwrap_or(absolute::LockTime::from_height(
-                chain.get_chain_tip()?.height,
-            )?),
-        input: selected
-            .iter()
-            .map(|(plan, utxo)| TxIn {
-                previous_output: utxo.outpoint,
-                sequence: plan
-                    .relative_timelock
-                    .map_or(Sequence::ENABLE_RBF_NO_LOCKTIME, Sequence::from),
-                ..Default::default()
-            })
-            .collect(),
-        output: outputs,
-    };
-
-    // update psbt with plan
-    let mut psbt = Psbt::from_unsigned_tx(unsigned_tx)?;
-    for (i, (plan, utxo)) in selected.iter().enumerate() {
-        let psbt_input = &mut psbt.inputs[i];
-        plan.update_psbt_input(psbt_input);
-        psbt_input.witness_utxo = Some(utxo.txout.clone());
-    }
-
-    Ok((psbt, change_info))
-}
-
-// Alias the elements of `planned_utxos`
-pub type PlanUtxo = (Plan, FullTxOut<ConfirmationBlockTime>);
-
-pub fn planned_utxos<O: ChainOracle>(
-    graph: &KeychainTxGraph,
-    chain: &O,
-    assets: &Assets,
-) -> Result<Vec<PlanUtxo>, O::Error> {
-    let chain_tip = chain.get_chain_tip()?;
-    let outpoints = graph.index.outpoints();
-    graph
-        .graph()
-        .try_filter_chain_unspents(chain, chain_tip, outpoints.iter().cloned())?
-        .filter_map(|((k, i), full_txo)| -> Option<Result<PlanUtxo, _>> {
-            let desc = graph
-                .index
-                .keychains()
-                .find(|(keychain, _)| *keychain == k)
-                .expect("keychain must exist")
-                .1
-                .at_derivation_index(i)
-                .expect("i can't be hardened");
-
-            let plan = desc.plan(assets).ok()?;
-
-            Some(Ok((plan, full_txo)))
-        })
-        .collect()
-}
-
-pub fn handle_commands<CS: clap::Subcommand, S: clap::Args>(
-    graph: &Mutex<KeychainTxGraph>,
-    chain: &Mutex<LocalChain>,
-    db: &Mutex<Store<ChangeSet>>,
-    network: Network,
-    broadcast_fn: impl FnOnce(S, &Transaction) -> anyhow::Result<()>,
-    cmd: Commands<CS, S>,
-) -> anyhow::Result<()> {
-    match cmd {
-        Commands::Init { .. } => unreachable!("handled by init command"),
-        Commands::Generate { .. } => unreachable!("handled by generate command"),
-        Commands::ChainSpecific(_) => unreachable!("example code should handle this!"),
-        Commands::Address { addr_cmd } => {
-            let graph = &mut *graph.lock().unwrap();
-            let index = &mut graph.index;
-
-            match addr_cmd {
-                AddressCmd::Next | AddressCmd::New => {
-                    let spk_chooser = match addr_cmd {
-                        AddressCmd::Next => KeychainTxOutIndex::next_unused_spk,
-                        AddressCmd::New => KeychainTxOutIndex::reveal_next_spk,
-                        _ => unreachable!("only these two variants exist in match arm"),
-                    };
-
-                    let ((spk_i, spk), index_changeset) =
-                        spk_chooser(index, Keychain::External).expect("Must exist");
-                    let db = &mut *db.lock().unwrap();
-                    db.append(&ChangeSet {
-                        indexer: index_changeset,
-                        ..Default::default()
-                    })?;
-                    let addr = Address::from_script(spk.as_script(), network)?;
-                    println!("[address @ {}] {}", spk_i, addr);
-                    Ok(())
-                }
-                AddressCmd::Index => {
-                    for (keychain, derivation_index) in index.last_revealed_indices() {
-                        println!("{:?}: {}", keychain, derivation_index);
-                    }
-                    Ok(())
-                }
-                AddressCmd::List { change } => {
-                    let target_keychain = match change {
-                        true => Keychain::Internal,
-                        false => Keychain::External,
-                    };
-                    for (spk_i, spk) in index.revealed_keychain_spks(target_keychain) {
-                        let address = Address::from_script(spk.as_script(), network)
-                            .expect("should always be able to derive address");
-                        println!(
-                            "{:?} {} used:{}",
-                            spk_i,
-                            address,
-                            index.is_used(target_keychain, spk_i)
-                        );
-                    }
-                    Ok(())
-                }
-            }
-        }
-        Commands::Balance => {
-            let graph = &*graph.lock().unwrap();
-            let chain = &*chain.lock().unwrap();
-            fn print_balances<'a>(
-                title_str: &'a str,
-                items: impl IntoIterator<Item = (&'a str, Amount)>,
-            ) {
-                println!("{}:", title_str);
-                for (name, amount) in items.into_iter() {
-                    println!("    {:<10} {:>12} sats", name, amount.to_sat())
-                }
-            }
-
-            let balance = graph.graph().try_balance(
-                chain,
-                chain.get_chain_tip()?,
-                graph.index.outpoints().iter().cloned(),
-                |(k, _), _| k == &Keychain::Internal,
-            )?;
-
-            let confirmed_total = balance.confirmed + balance.immature;
-            let unconfirmed_total = balance.untrusted_pending + balance.trusted_pending;
-
-            print_balances(
-                "confirmed",
-                [
-                    ("total", confirmed_total),
-                    ("spendable", balance.confirmed),
-                    ("immature", balance.immature),
-                ],
-            );
-            print_balances(
-                "unconfirmed",
-                [
-                    ("total", unconfirmed_total),
-                    ("trusted", balance.trusted_pending),
-                    ("untrusted", balance.untrusted_pending),
-                ],
-            );
-
-            Ok(())
-        }
-        Commands::TxOut { txout_cmd } => {
-            let graph = &*graph.lock().unwrap();
-            let chain = &*chain.lock().unwrap();
-            let chain_tip = chain.get_chain_tip()?;
-            let outpoints = graph.index.outpoints();
-
-            match txout_cmd {
-                TxOutCmd::List {
-                    spent,
-                    unspent,
-                    confirmed,
-                    unconfirmed,
-                } => {
-                    let txouts = graph
-                        .graph()
-                        .try_filter_chain_txouts(chain, chain_tip, outpoints.iter().cloned())?
-                        .filter(|(_, full_txo)| match (spent, unspent) {
-                            (true, false) => full_txo.spent_by.is_some(),
-                            (false, true) => full_txo.spent_by.is_none(),
-                            _ => true,
-                        })
-                        .filter(|(_, full_txo)| match (confirmed, unconfirmed) {
-                            (true, false) => full_txo.chain_position.is_confirmed(),
-                            (false, true) => !full_txo.chain_position.is_confirmed(),
-                            _ => true,
-                        })
-                        .collect::<Vec<_>>();
-
-                    for (spk_i, full_txo) in txouts {
-                        let addr = Address::from_script(&full_txo.txout.script_pubkey, network)?;
-                        println!(
-                            "{:?} {} {} {} spent:{:?}",
-                            spk_i, full_txo.txout.value, full_txo.outpoint, addr, full_txo.spent_by
-                        )
-                    }
-                    Ok(())
-                }
-            }
-        }
-        Commands::Psbt { psbt_cmd } => match psbt_cmd {
-            PsbtCmd::New {
-                value,
-                address,
-                feerate,
-                after,
-                older,
-                coin_select,
-                debug,
-            } => {
-                let address = address.require_network(network)?;
-
-                let (psbt, change_info) = {
-                    let mut graph = graph.lock().unwrap();
-                    let chain = chain.lock().unwrap();
-
-                    // collect assets we can sign for
-                    let mut pks = vec![];
-                    for (_, desc) in graph.index.keychains() {
-                        desc.for_each_key(|k| {
-                            pks.push(k.clone());
-                            true
-                        });
-                    }
-                    let mut assets = Assets::new().add(pks);
-                    if let Some(n) = after {
-                        assets = assets.after(absolute::LockTime::from_consensus(n));
-                    }
-                    if let Some(n) = older {
-                        assets = assets.older(relative::LockTime::from_consensus(n)?);
-                    }
-
-                    create_tx(
-                        &mut graph,
-                        &*chain,
-                        &assets,
-                        coin_select,
-                        address,
-                        value,
-                        feerate.expect("must have feerate"),
-                    )?
-                };
-
-                if let Some(ChangeInfo {
-                    change_keychain,
-                    indexer,
-                    index,
-                }) = change_info
-                {
-                    // We must first persist to disk the fact that we've got a new address from the
-                    // change keychain so future scans will find the tx we're about to broadcast.
-                    // If we're unable to persist this, then we don't want to broadcast.
-                    {
-                        let db = &mut *db.lock().unwrap();
-                        db.append(&ChangeSet {
-                            indexer,
-                            ..Default::default()
-                        })?;
-                    }
-
-                    // We don't want other callers/threads to use this address while we're using it
-                    // but we also don't want to scan the tx we just created because it's not
-                    // technically in the blockchain yet.
-                    graph
-                        .lock()
-                        .unwrap()
-                        .index
-                        .mark_used(change_keychain, index);
-                }
-
-                if debug {
-                    dbg!(psbt);
-                } else {
-                    // print base64 encoded psbt
-                    let fee = psbt.fee()?.to_sat();
-                    let mut obj = serde_json::Map::new();
-                    obj.insert("psbt".to_string(), json!(psbt.to_string()));
-                    obj.insert("fee".to_string(), json!(fee));
-                    println!("{}", serde_json::to_string_pretty(&obj)?);
-                };
-
-                Ok(())
-            }
-            PsbtCmd::Sign { psbt, descriptor } => {
-                let mut psbt = Psbt::from_str(&psbt)?;
-
-                let desc_str = match descriptor {
-                    Some(s) => s,
-                    None => env::var("DESCRIPTOR").context("unable to sign")?,
-                };
-
-                let secp = Secp256k1::new();
-                let (_, keymap) = Descriptor::parse_descriptor(&secp, &desc_str)?;
-                if keymap.is_empty() {
-                    bail!("unable to sign")
-                }
-
-                // note: we're only looking at the first entry in the keymap
-                // the idea is to find something that impls `GetKey`
-                let sign_res = match keymap.iter().next().expect("not empty") {
-                    (DescriptorPublicKey::Single(single_pub), DescriptorSecretKey::Single(prv)) => {
-                        let pk = match single_pub.key {
-                            SinglePubKey::FullKey(pk) => pk,
-                            SinglePubKey::XOnly(_) => unimplemented!("single xonly pubkey"),
-                        };
-                        let keys: HashMap<PublicKey, PrivateKey> = [(pk, prv.key)].into();
-                        psbt.sign(&keys, &secp)
-                    }
-                    (_, DescriptorSecretKey::XPrv(k)) => psbt.sign(&k.xkey, &secp),
-                    _ => unimplemented!("multi xkey signer"),
-                };
-
-                let _ = sign_res
-                    .map_err(|errors| anyhow::anyhow!("failed to sign PSBT {:?}", errors))?;
-
-                let mut obj = serde_json::Map::new();
-                obj.insert("psbt".to_string(), json!(psbt.to_string()));
-                println!("{}", serde_json::to_string_pretty(&obj)?);
-
-                Ok(())
-            }
-            PsbtCmd::Extract {
-                broadcast,
-                chain_specific,
-                psbt,
-            } => {
-                let mut psbt = Psbt::from_str(&psbt)?;
-                psbt.finalize_mut(&Secp256k1::new())
-                    .map_err(|errors| anyhow::anyhow!("failed to finalize PSBT {errors:?}"))?;
-
-                let tx = psbt.extract_tx()?;
-
-                if broadcast {
-                    let mut graph = graph.lock().unwrap();
-
-                    match broadcast_fn(chain_specific, &tx) {
-                        Ok(_) => {
-                            println!("Broadcasted Tx: {}", tx.compute_txid());
-
-                            let changeset = graph.insert_tx(tx);
-
-                            // We know the tx is at least unconfirmed now. Note if persisting here fails,
-                            // it's not a big deal since we can always find it again from the
-                            // blockchain.
-                            db.lock().unwrap().append(&ChangeSet {
-                                tx_graph: changeset.tx_graph,
-                                indexer: changeset.indexer,
-                                ..Default::default()
-                            })?;
-                        }
-                        Err(e) => {
-                            // We failed to broadcast, so allow our change address to be used in the future
-                            let (change_keychain, _) = graph
-                                .index
-                                .keychains()
-                                .last()
-                                .expect("must have a keychain");
-                            let change_index = tx.output.iter().find_map(|txout| {
-                                let spk = txout.script_pubkey.clone();
-                                match graph.index.index_of_spk(spk) {
-                                    Some(&(keychain, index)) if keychain == change_keychain => {
-                                        Some((keychain, index))
-                                    }
-                                    _ => None,
-                                }
-                            });
-                            if let Some((keychain, index)) = change_index {
-                                graph.index.unmark_used(keychain, index);
-                            }
-                            bail!(e);
-                        }
-                    }
-                } else {
-                    // encode raw tx hex
-                    let hex = consensus::serialize(&tx).to_lower_hex_string();
-                    let mut obj = serde_json::Map::new();
-                    obj.insert("tx".to_string(), json!(hex));
-                    println!("{}", serde_json::to_string_pretty(&obj)?);
-                }
-
-                Ok(())
-            }
-        },
-    }
-}
-
-/// The initial state returned by [`init_or_load`].
-pub struct Init<CS: clap::Subcommand, S: clap::Args> {
-    /// CLI args
-    pub args: Args<CS, S>,
-    /// Indexed graph
-    pub graph: Mutex<KeychainTxGraph>,
-    /// Local chain
-    pub chain: Mutex<LocalChain>,
-    /// Database
-    pub db: Mutex<Store<ChangeSet>>,
-    /// Network
-    pub network: Network,
-}
-
-/// Loads from persistence or creates new
-pub fn init_or_load<CS: clap::Subcommand, S: clap::Args>(
-    db_magic: &[u8],
-    db_path: &str,
-) -> anyhow::Result<Option<Init<CS, S>>> {
-    let args = Args::<CS, S>::parse();
-
-    match args.command {
-        // initialize new db
-        Commands::Init { .. } => initialize::<CS, S>(args, db_magic, db_path).map(|_| None),
-        // generate keys
-        Commands::Generate { network } => generate_bip86_helper(network).map(|_| None),
-        // try load
-        _ => {
-            let (db, changeset) =
-                Store::<ChangeSet>::load(db_magic, db_path).context("could not open file store")?;
-
-            let changeset = changeset.expect("should not be empty");
-
-            let network = changeset.network.expect("changeset network");
-
-            let chain = Mutex::new({
-                let (mut chain, _) =
-                    LocalChain::from_genesis_hash(constants::genesis_block(network).block_hash());
-                chain.apply_changeset(&changeset.local_chain)?;
-                chain
-            });
-
-            let graph = Mutex::new({
-                // insert descriptors and apply loaded changeset
-                let mut index = KeychainTxOutIndex::default();
-                if let Some(desc) = changeset.descriptor {
-                    index.insert_descriptor(Keychain::External, desc)?;
-                }
-                if let Some(change_desc) = changeset.change_descriptor {
-                    index.insert_descriptor(Keychain::Internal, change_desc)?;
-                }
-                let mut graph = KeychainTxGraph::new(index);
-                graph.apply_changeset(indexed_tx_graph::ChangeSet {
-                    tx_graph: changeset.tx_graph,
-                    indexer: changeset.indexer,
-                });
-                graph
-            });
-
-            let db = Mutex::new(db);
-
-            Ok(Some(Init {
-                args,
-                graph,
-                chain,
-                db,
-                network,
-            }))
-        }
-    }
-}
-
-/// Initialize db backend.
-fn initialize<CS, S>(args: Args<CS, S>, db_magic: &[u8], db_path: &str) -> anyhow::Result<()>
-where
-    CS: clap::Subcommand,
-    S: clap::Args,
-{
-    if let Commands::Init {
-        network,
-        descriptor,
-        change_descriptor,
-    } = args.command
-    {
-        let mut changeset = ChangeSet::default();
-
-        // parse descriptors
-        let secp = Secp256k1::new();
-        let mut index = KeychainTxOutIndex::default();
-        let (descriptor, _) =
-            Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &descriptor)?;
-        let _ = index.insert_descriptor(Keychain::External, descriptor.clone())?;
-        changeset.descriptor = Some(descriptor);
-
-        if let Some(desc) = change_descriptor {
-            let (change_descriptor, _) =
-                Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &desc)?;
-            let _ = index.insert_descriptor(Keychain::Internal, change_descriptor.clone())?;
-            changeset.change_descriptor = Some(change_descriptor);
-        }
-
-        // create new
-        let (_, chain_changeset) =
-            LocalChain::from_genesis_hash(constants::genesis_block(network).block_hash());
-        changeset.network = Some(network);
-        changeset.local_chain = chain_changeset;
-        let mut db = Store::<ChangeSet>::create(db_magic, db_path)?;
-        db.append(&changeset)?;
-        println!("New database {db_path}");
-    }
-
-    Ok(())
-}
-
-/// Generate BIP86 descriptors.
-fn generate_bip86_helper(network: impl Into<NetworkKind>) -> anyhow::Result<()> {
-    let secp = Secp256k1::new();
-    let mut seed = [0x00; 32];
-    thread_rng().fill_bytes(&mut seed);
-
-    let m = bip32::Xpriv::new_master(network, &seed)?;
-    let fp = m.fingerprint(&secp);
-    let path = if m.network.is_mainnet() {
-        "86h/0h/0h"
-    } else {
-        "86h/1h/0h"
-    };
-
-    let descriptors: Vec<String> = [0, 1]
-        .iter()
-        .map(|i| format!("tr([{fp}]{m}/{path}/{i}/*)"))
-        .collect();
-    let external_desc = &descriptors[0];
-    let internal_desc = &descriptors[1];
-    let (descriptor, keymap) =
-        <Descriptor<DescriptorPublicKey>>::parse_descriptor(&secp, external_desc)?;
-    let (internal_descriptor, internal_keymap) =
-        <Descriptor<DescriptorPublicKey>>::parse_descriptor(&secp, internal_desc)?;
-    println!("Public");
-    println!("{}", descriptor);
-    println!("{}", internal_descriptor);
-    println!("\nPrivate");
-    println!("{}", descriptor.to_string_with_secret(&keymap));
-    println!(
-        "{}",
-        internal_descriptor.to_string_with_secret(&internal_keymap)
-    );
-
-    Ok(())
-}
-
-impl Merge for ChangeSet {
-    fn merge(&mut self, other: Self) {
-        if other.descriptor.is_some() {
-            self.descriptor = other.descriptor;
-        }
-        if other.change_descriptor.is_some() {
-            self.change_descriptor = other.change_descriptor;
-        }
-        if other.network.is_some() {
-            self.network = other.network;
-        }
-        Merge::merge(&mut self.local_chain, other.local_chain);
-        Merge::merge(&mut self.tx_graph, other.tx_graph);
-        Merge::merge(&mut self.indexer, other.indexer);
-    }
-
-    fn is_empty(&self) -> bool {
-        self.descriptor.is_none()
-            && self.change_descriptor.is_none()
-            && self.network.is_none()
-            && self.local_chain.is_empty()
-            && self.tx_graph.is_empty()
-            && self.indexer.is_empty()
-    }
-}
diff --git a/example-crates/example_electrum/Cargo.toml b/example-crates/example_electrum/Cargo.toml
deleted file mode 100644 (file)
index 9dcd540..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-[package]
-name = "example_electrum"
-version = "0.2.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
deleted file mode 100644 (file)
index b6c93a1..0000000
+++ /dev/null
@@ -1,288 +0,0 @@
-use std::io::{self, Write};
-
-use bdk_chain::{
-    bitcoin::Network,
-    collections::BTreeSet,
-    indexed_tx_graph,
-    spk_client::{FullScanRequest, SyncRequest},
-    ConfirmationBlockTime, Merge,
-};
-use bdk_electrum::{
-    electrum_client::{self, Client, ElectrumApi},
-    BdkElectrumClient,
-};
-use example_cli::{
-    self,
-    anyhow::{self, Context},
-    clap::{self, Parser, Subcommand},
-    ChangeSet, 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,
-        #[clap(flatten)]
-        electrum_args: ElectrumArgs,
-    },
-    /// 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,
-        #[clap(flatten)]
-        electrum_args: ElectrumArgs,
-    },
-}
-
-impl ElectrumCommands {
-    fn electrum_args(&self) -> ElectrumArgs {
-        match self {
-            ElectrumCommands::Scan { electrum_args, .. } => electrum_args.clone(),
-            ElectrumCommands::Sync { electrum_args, .. } => electrum_args.clone(),
-        }
-    }
-}
-
-#[derive(clap::Args, Debug, Clone)]
-pub struct ElectrumArgs {
-    /// The electrum url to use to connect to. If not provided it will use a default electrum server
-    /// for your chosen network.
-    electrum_url: Option<String>,
-}
-
-impl ElectrumArgs {
-    pub fn client(&self, network: Network) -> anyhow::Result<Client> {
-        let electrum_url = self.electrum_url.as_deref().unwrap_or(match 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!(network, Network::Bitcoin))
-            .build();
-
-        Ok(electrum_client::Client::from_config(electrum_url, config)?)
-    }
-}
-
-#[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 example_cli::Init {
-        args,
-        graph,
-        chain,
-        db,
-        network,
-    } = match example_cli::init_or_load::<ElectrumCommands, ElectrumArgs>(DB_MAGIC, DB_PATH)? {
-        Some(init) => init,
-        None => return Ok(()),
-    };
-
-    let electrum_cmd = match &args.command {
-        example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd,
-        general_cmd => {
-            return example_cli::handle_commands(
-                &graph,
-                &chain,
-                &db,
-                network,
-                |electrum_args, tx| {
-                    let client = electrum_args.client(network)?;
-                    client.transaction_broadcast(tx)?;
-                    Ok(())
-                },
-                general_cmd.clone(),
-            );
-        }
-    };
-
-    let client = BdkElectrumClient::new(electrum_cmd.electrum_args().client(network)?);
-
-    // Tell the electrum client about the txs we've already got locally so it doesn't re-download them
-    client.populate_tx_cache(
-        graph
-            .lock()
-            .unwrap()
-            .graph()
-            .full_txs()
-            .map(|tx_node| tx_node.tx),
-    );
-
-    let (chain_update, tx_update, keychain_update) = match electrum_cmd.clone() {
-        ElectrumCommands::Scan {
-            stop_gap,
-            scan_options,
-            ..
-        } => {
-            let request = {
-                let graph = &*graph.lock().unwrap();
-                let chain = &*chain.lock().unwrap();
-
-                FullScanRequest::builder()
-                    .chain_tip(chain.tip())
-                    .spks_for_keychain(
-                        Keychain::External,
-                        graph
-                            .index
-                            .unbounded_spk_iter(Keychain::External)
-                            .into_iter()
-                            .flatten(),
-                    )
-                    .spks_for_keychain(
-                        Keychain::Internal,
-                        graph
-                            .index
-                            .unbounded_spk_iter(Keychain::Internal)
-                            .into_iter()
-                            .flatten(),
-                    )
-                    .inspect({
-                        let mut once = BTreeSet::new();
-                        move |k, spk_i, _| {
-                            if once.insert(k) {
-                                eprint!("\nScanning {}: {} ", k, spk_i);
-                            } else {
-                                eprint!("{} ", spk_i);
-                            }
-                            io::stdout().flush().expect("must flush");
-                        }
-                    })
-            };
-
-            let res = client
-                .full_scan::<_>(request, stop_gap, scan_options.batch_size, false)
-                .context("scanning the blockchain")?;
-            (
-                res.chain_update,
-                res.tx_update,
-                Some(res.last_active_indices),
-            )
-        }
-        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();
-
-            if !(all_spks || unused_spks || utxos || unconfirmed) {
-                unused_spks = true;
-                unconfirmed = true;
-                utxos = true;
-            } else if all_spks {
-                unused_spks = false;
-            }
-
-            let chain_tip = chain.tip();
-            let mut request =
-                SyncRequest::builder()
-                    .chain_tip(chain_tip.clone())
-                    .inspect(|item, progress| {
-                        let pc = (100 * progress.consumed()) as f32 / progress.total() as f32;
-                        eprintln!("[ SCANNING {:03.0}% ] {}", pc, item);
-                    });
-
-            request = request.expected_spk_txids(graph.list_expected_spk_txids(
-                &*chain,
-                chain_tip.block_id(),
-                ..,
-            ));
-            if all_spks {
-                request = request.spks_with_indexes(graph.index.revealed_spks(..));
-            }
-            if unused_spks {
-                request = request.spks_with_indexes(graph.index.unused_spks());
-            }
-            if utxos {
-                let init_outpoints = graph.index.outpoints();
-                request = request.outpoints(
-                    graph
-                        .graph()
-                        .filter_chain_unspents(
-                            &*chain,
-                            chain_tip.block_id(),
-                            init_outpoints.iter().cloned(),
-                        )
-                        .map(|(_, utxo)| utxo.outpoint),
-                );
-            };
-            if unconfirmed {
-                request = request.txids(
-                    graph
-                        .graph()
-                        .list_canonical_txs(&*chain, chain_tip.block_id())
-                        .filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
-                        .map(|canonical_tx| canonical_tx.tx_node.txid),
-                );
-            }
-
-            let res = client
-                .sync(request, scan_options.batch_size, false)
-                .context("scanning the blockchain")?;
-
-            // drop lock on graph and chain
-            drop((graph, chain));
-
-            (res.chain_update, res.tx_update, None)
-        }
-    };
-
-    let db_changeset = {
-        let mut chain = chain.lock().unwrap();
-        let mut graph = graph.lock().unwrap();
-
-        let chain_changeset = chain.apply_update(chain_update.expect("request has chain tip"))?;
-
-        let mut indexed_tx_graph_changeset =
-            indexed_tx_graph::ChangeSet::<ConfirmationBlockTime, _>::default();
-        if let Some(keychain_update) = keychain_update {
-            let keychain_changeset = graph.index.reveal_to_target_multi(&keychain_update);
-            indexed_tx_graph_changeset.merge(keychain_changeset.into());
-        }
-        indexed_tx_graph_changeset.merge(graph.apply_update(tx_update));
-
-        ChangeSet {
-            local_chain: chain_changeset,
-            tx_graph: indexed_tx_graph_changeset.tx_graph,
-            indexer: indexed_tx_graph_changeset.indexer,
-            ..Default::default()
-        }
-    };
-
-    let mut db = db.lock().unwrap();
-    db.append(&db_changeset)?;
-    Ok(())
-}
diff --git a/example-crates/example_esplora/Cargo.toml b/example-crates/example_esplora/Cargo.toml
deleted file mode 100644 (file)
index ccad862..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-[package]
-name = "example_esplora"
-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_esplora = { path = "../../crates/esplora", features = ["blocking"] }
-example_cli = { path = "../example_cli" }
-
diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs
deleted file mode 100644 (file)
index 8ef39c2..0000000
+++ /dev/null
@@ -1,292 +0,0 @@
-use core::f32;
-use std::{
-    collections::BTreeSet,
-    io::{self, Write},
-};
-
-use bdk_chain::{
-    bitcoin::Network,
-    keychain_txout::FullScanRequestBuilderExt,
-    spk_client::{FullScanRequest, SyncRequest},
-    Merge,
-};
-use bdk_esplora::{esplora_client, EsploraExt};
-use example_cli::{
-    anyhow::{self, Context},
-    clap::{self, Parser, Subcommand},
-    ChangeSet, Keychain,
-};
-
-const DB_MAGIC: &[u8] = b"bdk_example_esplora";
-const DB_PATH: &str = ".bdk_example_esplora.db";
-
-#[derive(Subcommand, Debug, Clone)]
-enum EsploraCommands {
-    /// 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, short = 'g', default_value = "10")]
-        stop_gap: usize,
-        #[clap(flatten)]
-        scan_options: ScanOptions,
-        #[clap(flatten)]
-        esplora_args: EsploraArgs,
-    },
-    /// Scan for particular addresses and unconfirmed transactions 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,
-        #[clap(flatten)]
-        esplora_args: EsploraArgs,
-    },
-}
-
-impl EsploraCommands {
-    fn esplora_args(&self) -> EsploraArgs {
-        match self {
-            EsploraCommands::Scan { esplora_args, .. } => esplora_args.clone(),
-            EsploraCommands::Sync { esplora_args, .. } => esplora_args.clone(),
-        }
-    }
-}
-
-#[derive(clap::Args, Debug, Clone)]
-pub struct EsploraArgs {
-    /// The esplora url endpoint to connect to.
-    #[clap(long, short = 'u', env = "ESPLORA_SERVER")]
-    esplora_url: Option<String>,
-}
-
-impl EsploraArgs {
-    pub fn client(&self, network: Network) -> anyhow::Result<esplora_client::BlockingClient> {
-        let esplora_url = self.esplora_url.as_deref().unwrap_or(match network {
-            Network::Bitcoin => "https://blockstream.info/api",
-            Network::Testnet => "https://blockstream.info/testnet/api",
-            Network::Regtest => "http://localhost:3002",
-            Network::Signet => "http://signet.bitcoindevkit.net",
-            _ => panic!("unsupported network"),
-        });
-
-        let client = esplora_client::Builder::new(esplora_url).build_blocking();
-        Ok(client)
-    }
-}
-
-#[derive(Parser, Debug, Clone, PartialEq)]
-pub struct ScanOptions {
-    /// Max number of concurrent esplora server requests.
-    #[clap(long, default_value = "2")]
-    pub parallel_requests: usize,
-}
-
-fn main() -> anyhow::Result<()> {
-    let example_cli::Init {
-        args,
-        graph,
-        chain,
-        db,
-        network,
-    } = match example_cli::init_or_load::<EsploraCommands, EsploraArgs>(DB_MAGIC, DB_PATH)? {
-        Some(init) => init,
-        None => return Ok(()),
-    };
-
-    let esplora_cmd = match &args.command {
-        // These are commands that are handled by this example (sync, scan).
-        example_cli::Commands::ChainSpecific(esplora_cmd) => esplora_cmd,
-        // These are general commands handled by example_cli. Execute the cmd and return.
-        general_cmd => {
-            return example_cli::handle_commands(
-                &graph,
-                &chain,
-                &db,
-                network,
-                |esplora_args, tx| {
-                    let client = esplora_args.client(network)?;
-                    client
-                        .broadcast(tx)
-                        .map(|_| ())
-                        .map_err(anyhow::Error::from)
-                },
-                general_cmd.clone(),
-            );
-        }
-    };
-
-    let client = esplora_cmd.esplora_args().client(network)?;
-    // Prepare the `IndexedTxGraph` and `LocalChain` updates based on whether we are scanning or
-    // syncing.
-    //
-    // 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.
-    //
-    // Syncing: We only check for specified spks, utxos and txids to update their confirmation
-    //   status or fetch missing transactions.
-    let (local_chain_changeset, indexed_tx_graph_changeset) = match &esplora_cmd {
-        EsploraCommands::Scan {
-            stop_gap,
-            scan_options,
-            ..
-        } => {
-            let request = {
-                let chain_tip = chain.lock().expect("mutex must not be poisoned").tip();
-                let indexed_graph = &*graph.lock().expect("mutex must not be poisoned");
-                FullScanRequest::builder()
-                    .chain_tip(chain_tip)
-                    .spks_from_indexer(&indexed_graph.index)
-                    .inspect({
-                        let mut once = BTreeSet::<Keychain>::new();
-                        move |keychain, spk_i, _| {
-                            if once.insert(keychain) {
-                                eprint!("\nscanning {}: ", keychain);
-                            }
-                            eprint!("{} ", spk_i);
-                            // Flush early to ensure we print at every iteration.
-                            let _ = io::stderr().flush();
-                        }
-                    })
-                    .build()
-            };
-
-            // The client scans keychain spks for transaction histories, stopping after `stop_gap`
-            // is reached. It returns a `TxGraph` update (`tx_update`) and a structure that
-            // represents the last active spk derivation indices of keychains
-            // (`keychain_indices_update`).
-            let update = client
-                .full_scan(request, *stop_gap, scan_options.parallel_requests)
-                .context("scanning for transactions")?;
-
-            let mut graph = graph.lock().expect("mutex must not be poisoned");
-            let mut chain = chain.lock().expect("mutex must not be poisoned");
-            // Because we did a stop gap based scan we are likely to have some updates to our
-            // deriviation indices. Usually before a scan you are on a fresh wallet with no
-            // addresses derived so we need to derive up to last active addresses the scan found
-            // before adding the transactions.
-            (
-                chain.apply_update(update.chain_update.expect("request included chain tip"))?,
-                {
-                    let index_changeset = graph
-                        .index
-                        .reveal_to_target_multi(&update.last_active_indices);
-                    let mut indexed_tx_graph_changeset = graph.apply_update(update.tx_update);
-                    indexed_tx_graph_changeset.merge(index_changeset.into());
-                    indexed_tx_graph_changeset
-                },
-            )
-        }
-        EsploraCommands::Sync {
-            mut unused_spks,
-            all_spks,
-            mut utxos,
-            mut unconfirmed,
-            scan_options,
-            ..
-        } => {
-            if !(*all_spks || unused_spks || utxos || unconfirmed) {
-                // If nothing is specifically selected, we select everything (except all spks).
-                unused_spks = true;
-                unconfirmed = true;
-                utxos = true;
-            } else if *all_spks {
-                // If all spks is selected, we don't need to also select unused spks (as unused spks
-                // is a subset of all spks).
-                unused_spks = false;
-            }
-
-            let local_tip = chain.lock().expect("mutex must not be poisoned").tip();
-            // Spks, outpoints and txids we want updates on will be accumulated here.
-            let mut request =
-                SyncRequest::builder()
-                    .chain_tip(local_tip.clone())
-                    .inspect(|item, progress| {
-                        let pc = (100 * progress.consumed()) as f32 / progress.total() as f32;
-                        eprintln!("[ SCANNING {:03.0}% ] {}", pc, item);
-                        // Flush early to ensure we print at every iteration.
-                        let _ = io::stderr().flush();
-                    });
-
-            // Get a short lock on the structures to get spks, utxos, and txs that we are interested
-            // in.
-            {
-                let graph = graph.lock().unwrap();
-                let chain = chain.lock().unwrap();
-                request = request.expected_spk_txids(graph.list_expected_spk_txids(
-                    &*chain,
-                    local_tip.block_id(),
-                    ..,
-                ));
-                if *all_spks {
-                    request = request.spks_with_indexes(graph.index.revealed_spks(..));
-                }
-                if unused_spks {
-                    request = request.spks_with_indexes(graph.index.unused_spks());
-                }
-                if utxos {
-                    // We want to search for whether the UTXO is spent, and spent by which
-                    // transaction. We provide the outpoint of the UTXO to
-                    // `EsploraExt::update_tx_graph_without_keychain`.
-                    let init_outpoints = graph.index.outpoints();
-                    request = request.outpoints(
-                        graph
-                            .graph()
-                            .filter_chain_unspents(
-                                &*chain,
-                                local_tip.block_id(),
-                                init_outpoints.iter().cloned(),
-                            )
-                            .map(|(_, utxo)| utxo.outpoint),
-                    );
-                };
-                if unconfirmed {
-                    // We want to search for whether the unconfirmed transaction is now confirmed.
-                    // We provide the unconfirmed txids to
-                    // `EsploraExt::update_tx_graph_without_keychain`.
-                    request = request.txids(
-                        graph
-                            .graph()
-                            .list_canonical_txs(&*chain, local_tip.block_id())
-                            .filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
-                            .map(|canonical_tx| canonical_tx.tx_node.txid),
-                    );
-                }
-            }
-
-            let update = client.sync(request, scan_options.parallel_requests)?;
-
-            (
-                chain
-                    .lock()
-                    .unwrap()
-                    .apply_update(update.chain_update.expect("request has chain tip"))?,
-                graph.lock().unwrap().apply_update(update.tx_update),
-            )
-        }
-    };
-
-    println!();
-
-    // We persist the changes
-    let mut db = db.lock().unwrap();
-    db.append(&ChangeSet {
-        local_chain: local_chain_changeset,
-        tx_graph: indexed_tx_graph_changeset.tx_graph,
-        indexer: indexed_tx_graph_changeset.indexer,
-        ..Default::default()
-    })?;
-    Ok(())
-}
diff --git a/examples/example_bitcoind_rpc_polling/Cargo.toml b/examples/example_bitcoind_rpc_polling/Cargo.toml
new file mode 100644 (file)
index 0000000..6728bb1
--- /dev/null
@@ -0,0 +1,12 @@
+[package]
+name = "example_bitcoind_rpc_polling"
+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_bitcoind_rpc = { path = "../../crates/bitcoind_rpc" }
+example_cli = { path = "../example_cli" }
+ctrlc = { version = "^2" }
diff --git a/examples/example_bitcoind_rpc_polling/README.md b/examples/example_bitcoind_rpc_polling/README.md
new file mode 100644 (file)
index 0000000..fef82ab
--- /dev/null
@@ -0,0 +1,68 @@
+# Example RPC CLI
+
+### Simple Regtest Test
+
+1. Start local regtest bitcoind.
+   ```
+    mkdir -p /tmp/regtest/bitcoind
+    bitcoind -regtest -server -fallbackfee=0.0002 -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -datadir=/tmp/regtest/bitcoind -daemon
+   ```
+2. Create a test bitcoind wallet and set bitcoind env.
+   ```
+   bitcoin-cli -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -named createwallet wallet_name="test"
+   export RPC_URL=127.0.0.1:18443
+   export RPC_USER=<your-rpc-username>
+   export RPC_PASS=<your-rpc-password>
+   ```
+3. Get test bitcoind wallet info.
+   ```
+   bitcoin-cli -rpcwallet="test" -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -datadir=/tmp/regtest/bitcoind -regtest getwalletinfo
+   ```
+4. Get new test bitcoind wallet address.
+   ```
+   BITCOIND_ADDRESS=$(bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> getnewaddress)
+   echo $BITCOIND_ADDRESS
+   ```
+5. Generate 101 blocks with reward to test bitcoind wallet address.
+   ```
+   bitcoin-cli -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> generatetoaddress 101 $BITCOIND_ADDRESS
+   ```
+6. Verify test bitcoind wallet balance.
+   ```
+   bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> getbalances
+   ```
+7. Set descriptor env and get address from RPC CLI wallet.
+   ```
+   export DESCRIPTOR="wpkh(tprv8ZgxMBicQKsPfK9BTf82oQkHhawtZv19CorqQKPFeaHDMA4dXYX6eWsJGNJ7VTQXWmoHdrfjCYuDijcRmNFwSKcVhswzqs4fugE8turndGc/1/*)"
+   cargo run -- --network regtest address next
+   ```
+8. Send 5 test bitcoin to RPC CLI wallet.
+   ```
+   bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> sendtoaddress <address> 5
+   ```
+9. Sync blockchain with RPC CLI wallet.
+   ```
+   cargo run -- --network regtest sync
+   <CNTRL-C to stop syncing>
+   ```
+10. Get RPC CLI wallet unconfirmed balances.
+   ```
+   cargo run -- --network regtest balance
+   ```
+11. Generate 1 block with reward to test bitcoind wallet address.
+   ```
+   bitcoin-cli -datadir=/tmp/regtest/bitcoind -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -regtest generatetoaddress 10 $BITCOIND_ADDRESS
+   ```
+12. Sync the blockchain with RPC CLI wallet.
+   ```
+   cargo run -- --network regtest sync
+   <CNTRL-C to stop syncing>
+   ```
+13. Get RPC CLI wallet confirmed balances.
+   ```
+   cargo run -- --network regtest balance
+   ```
+14. Get RPC CLI wallet transactions.
+   ```
+   cargo run -- --network regtest txout list
+   ```
\ No newline at end of file
diff --git a/examples/example_bitcoind_rpc_polling/src/main.rs b/examples/example_bitcoind_rpc_polling/src/main.rs
new file mode 100644 (file)
index 0000000..83cb25f
--- /dev/null
@@ -0,0 +1,370 @@
+use std::{
+    path::PathBuf,
+    sync::{
+        atomic::{AtomicBool, Ordering},
+        Arc,
+    },
+    time::{Duration, Instant},
+};
+
+use bdk_bitcoind_rpc::{
+    bitcoincore_rpc::{Auth, Client, RpcApi},
+    Emitter,
+};
+use bdk_chain::{
+    bitcoin::{Block, Transaction},
+    local_chain, Merge,
+};
+use example_cli::{
+    anyhow,
+    clap::{self, Args, Subcommand},
+    ChangeSet, Keychain,
+};
+
+const DB_MAGIC: &[u8] = b"bdk_example_rpc";
+const DB_PATH: &str = ".bdk_example_rpc.db";
+
+/// The mpsc channel bound for emissions from [`Emitter`].
+const CHANNEL_BOUND: usize = 10;
+/// Delay for printing status to stdout.
+const STDOUT_PRINT_DELAY: Duration = Duration::from_secs(6);
+/// Delay between mempool emissions.
+const MEMPOOL_EMIT_DELAY: Duration = Duration::from_secs(30);
+/// Delay for committing to persistence.
+const DB_COMMIT_DELAY: Duration = Duration::from_secs(60);
+
+#[derive(Debug)]
+enum Emission {
+    Block(bdk_bitcoind_rpc::BlockEvent<Block>),
+    Mempool(Vec<(Transaction, u64)>),
+    Tip(u32),
+}
+
+#[derive(Args, Debug, Clone)]
+struct RpcArgs {
+    /// RPC URL
+    #[clap(env = "RPC_URL", long, default_value = "127.0.0.1:8332")]
+    url: String,
+    /// RPC auth cookie file
+    #[clap(env = "RPC_COOKIE", long)]
+    rpc_cookie: Option<PathBuf>,
+    /// RPC auth username
+    #[clap(env = "RPC_USER", long)]
+    rpc_user: Option<String>,
+    /// RPC auth password
+    #[clap(env = "RPC_PASS", long)]
+    rpc_password: Option<String>,
+    /// Starting block height to fallback to if no point of agreement if found
+    #[clap(env = "FALLBACK_HEIGHT", long, default_value = "0")]
+    fallback_height: u32,
+}
+
+impl From<RpcArgs> for Auth {
+    fn from(args: RpcArgs) -> Self {
+        match (args.rpc_cookie, args.rpc_user, args.rpc_password) {
+            (None, None, None) => Self::None,
+            (Some(path), _, _) => Self::CookieFile(path),
+            (_, Some(user), Some(pass)) => Self::UserPass(user, pass),
+            (_, Some(_), None) => panic!("rpc auth: missing rpc_pass"),
+            (_, None, Some(_)) => panic!("rpc auth: missing rpc_user"),
+        }
+    }
+}
+
+impl RpcArgs {
+    fn new_client(&self) -> anyhow::Result<Client> {
+        Ok(Client::new(
+            &self.url,
+            match (&self.rpc_cookie, &self.rpc_user, &self.rpc_password) {
+                (None, None, None) => Auth::None,
+                (Some(path), _, _) => Auth::CookieFile(path.clone()),
+                (_, Some(user), Some(pass)) => Auth::UserPass(user.clone(), pass.clone()),
+                (_, Some(_), None) => panic!("rpc auth: missing rpc_pass"),
+                (_, None, Some(_)) => panic!("rpc auth: missing rpc_user"),
+            },
+        )?)
+    }
+}
+
+#[derive(Subcommand, Debug, Clone)]
+enum RpcCommands {
+    /// Syncs local state with remote state via RPC (starting from last point of agreement) and
+    /// stores/indexes relevant transactions
+    Sync {
+        #[clap(flatten)]
+        rpc_args: RpcArgs,
+    },
+    /// Sync by having the emitter logic in a separate thread
+    Live {
+        #[clap(flatten)]
+        rpc_args: RpcArgs,
+    },
+}
+
+fn main() -> anyhow::Result<()> {
+    let start = Instant::now();
+
+    let example_cli::Init {
+        args,
+        graph,
+        chain,
+        db,
+        network,
+    } = match example_cli::init_or_load::<RpcCommands, RpcArgs>(DB_MAGIC, DB_PATH)? {
+        Some(init) => init,
+        None => return Ok(()),
+    };
+
+    let rpc_cmd = match args.command {
+        example_cli::Commands::ChainSpecific(rpc_cmd) => rpc_cmd,
+        general_cmd => {
+            return example_cli::handle_commands(
+                &graph,
+                &chain,
+                &db,
+                network,
+                |rpc_args, tx| {
+                    let client = rpc_args.new_client()?;
+                    client.send_raw_transaction(tx)?;
+                    Ok(())
+                },
+                general_cmd,
+            );
+        }
+    };
+
+    match rpc_cmd {
+        RpcCommands::Sync { rpc_args } => {
+            let RpcArgs {
+                fallback_height, ..
+            } = rpc_args;
+
+            let chain_tip = chain.lock().unwrap().tip();
+            let rpc_client = rpc_args.new_client()?;
+            let mut emitter = Emitter::new(&rpc_client, chain_tip, fallback_height);
+            let mut db_stage = ChangeSet::default();
+
+            let mut last_db_commit = Instant::now();
+            let mut last_print = Instant::now();
+
+            while let Some(emission) = emitter.next_block()? {
+                let height = emission.block_height();
+
+                let mut chain = chain.lock().unwrap();
+                let mut graph = graph.lock().unwrap();
+
+                let chain_changeset = chain
+                    .apply_update(emission.checkpoint)
+                    .expect("must always apply as we receive blocks in order from emitter");
+                let graph_changeset = graph.apply_block_relevant(&emission.block, height);
+                db_stage.merge(ChangeSet {
+                    local_chain: chain_changeset,
+                    tx_graph: graph_changeset.tx_graph,
+                    indexer: graph_changeset.indexer,
+                    ..Default::default()
+                });
+
+                // commit staged db changes in intervals
+                if last_db_commit.elapsed() >= DB_COMMIT_DELAY {
+                    let db = &mut *db.lock().unwrap();
+                    last_db_commit = Instant::now();
+                    if let Some(changeset) = db_stage.take() {
+                        db.append(&changeset)?;
+                    }
+                    println!(
+                        "[{:>10}s] committed to db (took {}s)",
+                        start.elapsed().as_secs_f32(),
+                        last_db_commit.elapsed().as_secs_f32()
+                    );
+                }
+
+                // print synced-to height and current balance in intervals
+                if last_print.elapsed() >= STDOUT_PRINT_DELAY {
+                    last_print = Instant::now();
+                    let synced_to = chain.tip();
+                    let balance = {
+                        graph.graph().balance(
+                            &*chain,
+                            synced_to.block_id(),
+                            graph.index.outpoints().iter().cloned(),
+                            |(k, _), _| k == &Keychain::Internal,
+                        )
+                    };
+                    println!(
+                        "[{:>10}s] synced to {} @ {} | total: {}",
+                        start.elapsed().as_secs_f32(),
+                        synced_to.hash(),
+                        synced_to.height(),
+                        balance.total()
+                    );
+                }
+            }
+
+            let mempool_txs = emitter.mempool()?;
+            let graph_changeset = graph
+                .lock()
+                .unwrap()
+                .batch_insert_relevant_unconfirmed(mempool_txs);
+            {
+                let db = &mut *db.lock().unwrap();
+                db_stage.merge(ChangeSet {
+                    tx_graph: graph_changeset.tx_graph,
+                    indexer: graph_changeset.indexer,
+                    ..Default::default()
+                });
+                if let Some(changeset) = db_stage.take() {
+                    db.append(&changeset)?;
+                }
+            }
+        }
+        RpcCommands::Live { rpc_args } => {
+            let RpcArgs {
+                fallback_height, ..
+            } = rpc_args;
+            let sigterm_flag = start_ctrlc_handler();
+
+            let last_cp = chain.lock().unwrap().tip();
+
+            println!(
+                "[{:>10}s] starting emitter thread...",
+                start.elapsed().as_secs_f32()
+            );
+            let (tx, rx) = std::sync::mpsc::sync_channel::<Emission>(CHANNEL_BOUND);
+            let emission_jh = std::thread::spawn(move || -> anyhow::Result<()> {
+                let rpc_client = rpc_args.new_client()?;
+                let mut emitter = Emitter::new(&rpc_client, last_cp, fallback_height);
+
+                let mut block_count = rpc_client.get_block_count()? as u32;
+                tx.send(Emission::Tip(block_count))?;
+
+                loop {
+                    match emitter.next_block()? {
+                        Some(block_emission) => {
+                            let height = block_emission.block_height();
+                            if sigterm_flag.load(Ordering::Acquire) {
+                                break;
+                            }
+                            if height > block_count {
+                                block_count = rpc_client.get_block_count()? as u32;
+                                tx.send(Emission::Tip(block_count))?;
+                            }
+                            tx.send(Emission::Block(block_emission))?;
+                        }
+                        None => {
+                            if await_flag(&sigterm_flag, MEMPOOL_EMIT_DELAY) {
+                                break;
+                            }
+                            println!("preparing mempool emission...");
+                            let now = Instant::now();
+                            tx.send(Emission::Mempool(emitter.mempool()?))?;
+                            println!("mempool emission prepared in {}s", now.elapsed().as_secs());
+                            continue;
+                        }
+                    };
+                }
+
+                println!("emitter thread shutting down...");
+                Ok(())
+            });
+
+            let mut tip_height = 0_u32;
+            let mut last_db_commit = Instant::now();
+            let mut last_print = Option::<Instant>::None;
+            let mut db_stage = ChangeSet::default();
+
+            for emission in rx {
+                let mut graph = graph.lock().unwrap();
+                let mut chain = chain.lock().unwrap();
+
+                let (chain_changeset, graph_changeset) = match emission {
+                    Emission::Block(block_emission) => {
+                        let height = block_emission.block_height();
+                        let chain_changeset = chain
+                            .apply_update(block_emission.checkpoint)
+                            .expect("must always apply as we receive blocks in order from emitter");
+                        let graph_changeset =
+                            graph.apply_block_relevant(&block_emission.block, height);
+                        (chain_changeset, graph_changeset)
+                    }
+                    Emission::Mempool(mempool_txs) => {
+                        let graph_changeset = graph.batch_insert_relevant_unconfirmed(mempool_txs);
+                        (local_chain::ChangeSet::default(), graph_changeset)
+                    }
+                    Emission::Tip(h) => {
+                        tip_height = h;
+                        continue;
+                    }
+                };
+
+                db_stage.merge(ChangeSet {
+                    local_chain: chain_changeset,
+                    tx_graph: graph_changeset.tx_graph,
+                    indexer: graph_changeset.indexer,
+                    ..Default::default()
+                });
+
+                if last_db_commit.elapsed() >= DB_COMMIT_DELAY {
+                    let db = &mut *db.lock().unwrap();
+                    last_db_commit = Instant::now();
+                    if let Some(changeset) = db_stage.take() {
+                        db.append(&changeset)?;
+                    }
+                    println!(
+                        "[{:>10}s] committed to db (took {}s)",
+                        start.elapsed().as_secs_f32(),
+                        last_db_commit.elapsed().as_secs_f32()
+                    );
+                }
+
+                if last_print.map_or(Duration::MAX, |i| i.elapsed()) >= STDOUT_PRINT_DELAY {
+                    last_print = Some(Instant::now());
+                    let synced_to = chain.tip();
+                    let balance = {
+                        graph.graph().balance(
+                            &*chain,
+                            synced_to.block_id(),
+                            graph.index.outpoints().iter().cloned(),
+                            |(k, _), _| k == &Keychain::Internal,
+                        )
+                    };
+                    println!(
+                        "[{:>10}s] synced to {} @ {} / {} | total: {}",
+                        start.elapsed().as_secs_f32(),
+                        synced_to.hash(),
+                        synced_to.height(),
+                        tip_height,
+                        balance.total()
+                    );
+                }
+            }
+
+            emission_jh.join().expect("must join emitter thread")?;
+        }
+    }
+
+    Ok(())
+}
+
+#[allow(dead_code)]
+fn start_ctrlc_handler() -> Arc<AtomicBool> {
+    let flag = Arc::new(AtomicBool::new(false));
+    let cloned_flag = flag.clone();
+
+    ctrlc::set_handler(move || cloned_flag.store(true, Ordering::Release));
+
+    flag
+}
+
+#[allow(dead_code)]
+fn await_flag(flag: &AtomicBool, duration: Duration) -> bool {
+    let start = Instant::now();
+    loop {
+        if flag.load(Ordering::Acquire) {
+            return true;
+        }
+        if start.elapsed() >= duration {
+            return false;
+        }
+        std::thread::sleep(Duration::from_secs(1));
+    }
+}
diff --git a/examples/example_cli/Cargo.toml b/examples/example_cli/Cargo.toml
new file mode 100644 (file)
index 0000000..0a467db
--- /dev/null
@@ -0,0 +1,18 @@
+[package]
+name = "example_cli"
+version = "0.2.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", "miniscript"]}
+bdk_coin_select = "0.4"
+bdk_file_store = { path = "../../crates/file_store" }
+bitcoin = { version = "0.32.0", features = ["base64"], default-features = false }
+
+anyhow = "1"
+clap = { version = "4.5.17", features = ["derive", "env"] }
+rand = "0.8"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1.0"
diff --git a/examples/example_cli/src/lib.rs b/examples/example_cli/src/lib.rs
new file mode 100644 (file)
index 0000000..2cb2784
--- /dev/null
@@ -0,0 +1,954 @@
+use serde_json::json;
+use std::cmp;
+use std::collections::HashMap;
+use std::env;
+use std::fmt;
+use std::str::FromStr;
+use std::sync::Mutex;
+
+use anyhow::bail;
+use anyhow::Context;
+use bdk_chain::bitcoin::{
+    absolute, address::NetworkUnchecked, bip32, consensus, constants, hex::DisplayHex, relative,
+    secp256k1::Secp256k1, transaction, Address, Amount, Network, NetworkKind, PrivateKey, Psbt,
+    PublicKey, Sequence, Transaction, TxIn, TxOut,
+};
+use bdk_chain::miniscript::{
+    descriptor::{DescriptorSecretKey, SinglePubKey},
+    plan::{Assets, Plan},
+    psbt::PsbtExt,
+    Descriptor, DescriptorPublicKey, ForEachKey,
+};
+use bdk_chain::ConfirmationBlockTime;
+use bdk_chain::{
+    indexed_tx_graph,
+    indexer::keychain_txout::{self, KeychainTxOutIndex},
+    local_chain::{self, LocalChain},
+    tx_graph, ChainOracle, DescriptorExt, FullTxOut, IndexedTxGraph, Merge,
+};
+use bdk_coin_select::{
+    metrics::LowestFee, Candidate, ChangePolicy, CoinSelector, DrainWeights, FeeRate, Target,
+    TargetFee, TargetOutputs,
+};
+use bdk_file_store::Store;
+use clap::{Parser, Subcommand};
+use rand::prelude::*;
+
+pub use anyhow;
+pub use clap;
+
+/// Alias for a `IndexedTxGraph` with specific `Anchor` and `Indexer`.
+pub type KeychainTxGraph = IndexedTxGraph<ConfirmationBlockTime, KeychainTxOutIndex<Keychain>>;
+
+/// ChangeSet
+#[derive(Default, Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)]
+pub struct ChangeSet {
+    /// Descriptor for recipient addresses.
+    pub descriptor: Option<Descriptor<DescriptorPublicKey>>,
+    /// Descriptor for change addresses.
+    pub change_descriptor: Option<Descriptor<DescriptorPublicKey>>,
+    /// Stores the network type of the transaction data.
+    pub network: Option<Network>,
+    /// Changes to the [`LocalChain`].
+    pub local_chain: local_chain::ChangeSet,
+    /// Changes to [`TxGraph`](tx_graph::TxGraph).
+    pub tx_graph: tx_graph::ChangeSet<ConfirmationBlockTime>,
+    /// Changes to [`KeychainTxOutIndex`].
+    pub indexer: keychain_txout::ChangeSet,
+}
+
+#[derive(Parser)]
+#[clap(author, version, about, long_about = None)]
+#[clap(propagate_version = true)]
+pub struct Args<CS: clap::Subcommand, S: clap::Args> {
+    #[clap(subcommand)]
+    pub command: Commands<CS, S>,
+}
+
+#[derive(Subcommand, Debug, Clone)]
+pub enum Commands<CS: clap::Subcommand, S: clap::Args> {
+    /// Initialize a new data store.
+    Init {
+        /// Network
+        #[clap(long, short, default_value = "signet")]
+        network: Network,
+        /// Descriptor
+        #[clap(env = "DESCRIPTOR")]
+        descriptor: String,
+        /// Change descriptor
+        #[clap(long, short, env = "CHANGE_DESCRIPTOR")]
+        change_descriptor: Option<String>,
+    },
+    #[clap(flatten)]
+    ChainSpecific(CS),
+    /// Address generation and inspection.
+    Address {
+        #[clap(subcommand)]
+        addr_cmd: AddressCmd,
+    },
+    /// Get the wallet balance.
+    Balance,
+    /// TxOut related commands.
+    #[clap(name = "txout")]
+    TxOut {
+        #[clap(subcommand)]
+        txout_cmd: TxOutCmd,
+    },
+    /// PSBT operations
+    Psbt {
+        #[clap(subcommand)]
+        psbt_cmd: PsbtCmd<S>,
+    },
+    /// Generate new BIP86 descriptors.
+    Generate {
+        /// Network
+        #[clap(long, short, default_value = "signet")]
+        network: Network,
+    },
+}
+
+#[derive(Subcommand, Debug, Clone)]
+pub enum AddressCmd {
+    /// Get the next unused address.
+    Next,
+    /// Get a new address regardless of the existing unused addresses.
+    New,
+    /// List all addresses
+    List {
+        /// List change addresses
+        #[clap(long)]
+        change: bool,
+    },
+    /// Get last revealed address index for each keychain.
+    Index,
+}
+
+#[derive(Subcommand, Debug, Clone)]
+pub enum TxOutCmd {
+    /// List transaction outputs.
+    List {
+        /// Return only spent outputs.
+        #[clap(short, long)]
+        spent: bool,
+        /// Return only unspent outputs.
+        #[clap(short, long)]
+        unspent: bool,
+        /// Return only confirmed outputs.
+        #[clap(long)]
+        confirmed: bool,
+        /// Return only unconfirmed outputs.
+        #[clap(long)]
+        unconfirmed: bool,
+    },
+}
+
+#[derive(Subcommand, Debug, Clone)]
+pub enum PsbtCmd<S: clap::Args> {
+    /// Create a new PSBT.
+    New {
+        /// Amount to send in satoshis
+        #[clap(required = true)]
+        value: u64,
+        /// Recipient address
+        #[clap(required = true)]
+        address: Address<NetworkUnchecked>,
+        /// Set the feerate of the tx (sat/vbyte)
+        #[clap(long, short, default_value = "1.0")]
+        feerate: Option<f32>,
+        /// Set max absolute timelock (from consensus value)
+        #[clap(long, short)]
+        after: Option<u32>,
+        /// Set max relative timelock (from consensus value)
+        #[clap(long, short)]
+        older: Option<u32>,
+        /// Coin selection algorithm
+        #[clap(long, short, default_value = "bnb")]
+        coin_select: CoinSelectionAlgo,
+        /// Debug print the PSBT
+        #[clap(long, short)]
+        debug: bool,
+    },
+    /// Sign with a hot signer
+    Sign {
+        /// Private descriptor [env: DESCRIPTOR=]
+        #[clap(long, short)]
+        descriptor: Option<String>,
+        /// PSBT
+        #[clap(long, short, required = true)]
+        psbt: String,
+    },
+    /// Extract transaction
+    Extract {
+        /// PSBT
+        #[clap(long, short, required = true)]
+        psbt: String,
+        /// Whether to try broadcasting the tx
+        #[clap(long, short)]
+        broadcast: bool,
+        #[clap(flatten)]
+        chain_specific: S,
+    },
+}
+
+#[derive(
+    Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, serde::Deserialize, serde::Serialize,
+)]
+pub enum Keychain {
+    External,
+    Internal,
+}
+
+impl fmt::Display for Keychain {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Keychain::External => write!(f, "external"),
+            Keychain::Internal => write!(f, "internal"),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Default)]
+pub enum CoinSelectionAlgo {
+    LargestFirst,
+    SmallestFirst,
+    OldestFirst,
+    NewestFirst,
+    #[default]
+    BranchAndBound,
+}
+
+impl FromStr for CoinSelectionAlgo {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        use CoinSelectionAlgo::*;
+        Ok(match s {
+            "largest-first" => LargestFirst,
+            "smallest-first" => SmallestFirst,
+            "oldest-first" => OldestFirst,
+            "newest-first" => NewestFirst,
+            "bnb" => BranchAndBound,
+            unknown => bail!("unknown coin selection algorithm '{}'", unknown),
+        })
+    }
+}
+
+impl fmt::Display for CoinSelectionAlgo {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        use CoinSelectionAlgo::*;
+        write!(
+            f,
+            "{}",
+            match self {
+                LargestFirst => "largest-first",
+                SmallestFirst => "smallest-first",
+                OldestFirst => "oldest-first",
+                NewestFirst => "newest-first",
+                BranchAndBound => "bnb",
+            }
+        )
+    }
+}
+
+// Records changes to the internal keychain when we
+// have to include a change output during tx creation.
+#[derive(Debug)]
+pub struct ChangeInfo {
+    pub change_keychain: Keychain,
+    pub indexer: keychain_txout::ChangeSet,
+    pub index: u32,
+}
+
+pub fn create_tx<O: ChainOracle>(
+    graph: &mut KeychainTxGraph,
+    chain: &O,
+    assets: &Assets,
+    cs_algorithm: CoinSelectionAlgo,
+    address: Address,
+    value: u64,
+    feerate: f32,
+) -> anyhow::Result<(Psbt, Option<ChangeInfo>)>
+where
+    O::Error: std::error::Error + Send + Sync + 'static,
+{
+    let mut changeset = keychain_txout::ChangeSet::default();
+
+    // get planned utxos
+    let mut plan_utxos = planned_utxos(graph, chain, assets)?;
+
+    // sort utxos if cs-algo requires it
+    match cs_algorithm {
+        CoinSelectionAlgo::LargestFirst => {
+            plan_utxos.sort_by_key(|(_, utxo)| cmp::Reverse(utxo.txout.value))
+        }
+        CoinSelectionAlgo::SmallestFirst => plan_utxos.sort_by_key(|(_, utxo)| utxo.txout.value),
+        CoinSelectionAlgo::OldestFirst => plan_utxos.sort_by_key(|(_, utxo)| utxo.chain_position),
+        CoinSelectionAlgo::NewestFirst => {
+            plan_utxos.sort_by_key(|(_, utxo)| cmp::Reverse(utxo.chain_position))
+        }
+        CoinSelectionAlgo::BranchAndBound => plan_utxos.shuffle(&mut thread_rng()),
+    }
+
+    // build candidate set
+    let candidates: Vec<Candidate> = plan_utxos
+        .iter()
+        .map(|(plan, utxo)| {
+            Candidate::new(
+                utxo.txout.value.to_sat(),
+                plan.satisfaction_weight() as u64,
+                plan.witness_version().is_some(),
+            )
+        })
+        .collect();
+
+    // create recipient output(s)
+    let mut outputs = vec![TxOut {
+        value: Amount::from_sat(value),
+        script_pubkey: address.script_pubkey(),
+    }];
+
+    let (change_keychain, _) = graph
+        .index
+        .keychains()
+        .last()
+        .expect("must have a keychain");
+
+    let ((change_index, change_script), index_changeset) = graph
+        .index
+        .next_unused_spk(change_keychain)
+        .expect("Must exist");
+    changeset.merge(index_changeset);
+
+    let mut change_output = TxOut {
+        value: Amount::ZERO,
+        script_pubkey: change_script,
+    };
+
+    let change_desc = graph
+        .index
+        .keychains()
+        .find(|(k, _)| k == &change_keychain)
+        .expect("must exist")
+        .1;
+
+    let min_drain_value = change_desc.dust_value().to_sat();
+
+    let target = Target {
+        outputs: TargetOutputs::fund_outputs(
+            outputs
+                .iter()
+                .map(|output| (output.weight().to_wu(), output.value.to_sat())),
+        ),
+        fee: TargetFee {
+            rate: FeeRate::from_sat_per_vb(feerate),
+            ..Default::default()
+        },
+    };
+
+    let change_policy = ChangePolicy {
+        min_value: min_drain_value,
+        drain_weights: DrainWeights::TR_KEYSPEND,
+    };
+
+    // run coin selection
+    let mut selector = CoinSelector::new(&candidates);
+    match cs_algorithm {
+        CoinSelectionAlgo::BranchAndBound => {
+            let metric = LowestFee {
+                target,
+                long_term_feerate: FeeRate::from_sat_per_vb(10.0),
+                change_policy,
+            };
+            match selector.run_bnb(metric, 10_000) {
+                Ok(_) => {}
+                Err(_) => selector
+                    .select_until_target_met(target)
+                    .context("selecting coins")?,
+            }
+        }
+        _ => selector
+            .select_until_target_met(target)
+            .context("selecting coins")?,
+    }
+
+    // get the selected plan utxos
+    let selected: Vec<_> = selector.apply_selection(&plan_utxos).collect();
+
+    // if the selection tells us to use change and the change value is sufficient, we add it as an output
+    let mut change_info = Option::<ChangeInfo>::None;
+    let drain = selector.drain(target, change_policy);
+    if drain.value > min_drain_value {
+        change_output.value = Amount::from_sat(drain.value);
+        outputs.push(change_output);
+        change_info = Some(ChangeInfo {
+            change_keychain,
+            indexer: changeset,
+            index: change_index,
+        });
+        outputs.shuffle(&mut thread_rng());
+    }
+
+    let unsigned_tx = Transaction {
+        version: transaction::Version::TWO,
+        lock_time: assets
+            .absolute_timelock
+            .unwrap_or(absolute::LockTime::from_height(
+                chain.get_chain_tip()?.height,
+            )?),
+        input: selected
+            .iter()
+            .map(|(plan, utxo)| TxIn {
+                previous_output: utxo.outpoint,
+                sequence: plan
+                    .relative_timelock
+                    .map_or(Sequence::ENABLE_RBF_NO_LOCKTIME, Sequence::from),
+                ..Default::default()
+            })
+            .collect(),
+        output: outputs,
+    };
+
+    // update psbt with plan
+    let mut psbt = Psbt::from_unsigned_tx(unsigned_tx)?;
+    for (i, (plan, utxo)) in selected.iter().enumerate() {
+        let psbt_input = &mut psbt.inputs[i];
+        plan.update_psbt_input(psbt_input);
+        psbt_input.witness_utxo = Some(utxo.txout.clone());
+    }
+
+    Ok((psbt, change_info))
+}
+
+// Alias the elements of `planned_utxos`
+pub type PlanUtxo = (Plan, FullTxOut<ConfirmationBlockTime>);
+
+pub fn planned_utxos<O: ChainOracle>(
+    graph: &KeychainTxGraph,
+    chain: &O,
+    assets: &Assets,
+) -> Result<Vec<PlanUtxo>, O::Error> {
+    let chain_tip = chain.get_chain_tip()?;
+    let outpoints = graph.index.outpoints();
+    graph
+        .graph()
+        .try_filter_chain_unspents(chain, chain_tip, outpoints.iter().cloned())?
+        .filter_map(|((k, i), full_txo)| -> Option<Result<PlanUtxo, _>> {
+            let desc = graph
+                .index
+                .keychains()
+                .find(|(keychain, _)| *keychain == k)
+                .expect("keychain must exist")
+                .1
+                .at_derivation_index(i)
+                .expect("i can't be hardened");
+
+            let plan = desc.plan(assets).ok()?;
+
+            Some(Ok((plan, full_txo)))
+        })
+        .collect()
+}
+
+pub fn handle_commands<CS: clap::Subcommand, S: clap::Args>(
+    graph: &Mutex<KeychainTxGraph>,
+    chain: &Mutex<LocalChain>,
+    db: &Mutex<Store<ChangeSet>>,
+    network: Network,
+    broadcast_fn: impl FnOnce(S, &Transaction) -> anyhow::Result<()>,
+    cmd: Commands<CS, S>,
+) -> anyhow::Result<()> {
+    match cmd {
+        Commands::Init { .. } => unreachable!("handled by init command"),
+        Commands::Generate { .. } => unreachable!("handled by generate command"),
+        Commands::ChainSpecific(_) => unreachable!("example code should handle this!"),
+        Commands::Address { addr_cmd } => {
+            let graph = &mut *graph.lock().unwrap();
+            let index = &mut graph.index;
+
+            match addr_cmd {
+                AddressCmd::Next | AddressCmd::New => {
+                    let spk_chooser = match addr_cmd {
+                        AddressCmd::Next => KeychainTxOutIndex::next_unused_spk,
+                        AddressCmd::New => KeychainTxOutIndex::reveal_next_spk,
+                        _ => unreachable!("only these two variants exist in match arm"),
+                    };
+
+                    let ((spk_i, spk), index_changeset) =
+                        spk_chooser(index, Keychain::External).expect("Must exist");
+                    let db = &mut *db.lock().unwrap();
+                    db.append(&ChangeSet {
+                        indexer: index_changeset,
+                        ..Default::default()
+                    })?;
+                    let addr = Address::from_script(spk.as_script(), network)?;
+                    println!("[address @ {}] {}", spk_i, addr);
+                    Ok(())
+                }
+                AddressCmd::Index => {
+                    for (keychain, derivation_index) in index.last_revealed_indices() {
+                        println!("{:?}: {}", keychain, derivation_index);
+                    }
+                    Ok(())
+                }
+                AddressCmd::List { change } => {
+                    let target_keychain = match change {
+                        true => Keychain::Internal,
+                        false => Keychain::External,
+                    };
+                    for (spk_i, spk) in index.revealed_keychain_spks(target_keychain) {
+                        let address = Address::from_script(spk.as_script(), network)
+                            .expect("should always be able to derive address");
+                        println!(
+                            "{:?} {} used:{}",
+                            spk_i,
+                            address,
+                            index.is_used(target_keychain, spk_i)
+                        );
+                    }
+                    Ok(())
+                }
+            }
+        }
+        Commands::Balance => {
+            let graph = &*graph.lock().unwrap();
+            let chain = &*chain.lock().unwrap();
+            fn print_balances<'a>(
+                title_str: &'a str,
+                items: impl IntoIterator<Item = (&'a str, Amount)>,
+            ) {
+                println!("{}:", title_str);
+                for (name, amount) in items.into_iter() {
+                    println!("    {:<10} {:>12} sats", name, amount.to_sat())
+                }
+            }
+
+            let balance = graph.graph().try_balance(
+                chain,
+                chain.get_chain_tip()?,
+                graph.index.outpoints().iter().cloned(),
+                |(k, _), _| k == &Keychain::Internal,
+            )?;
+
+            let confirmed_total = balance.confirmed + balance.immature;
+            let unconfirmed_total = balance.untrusted_pending + balance.trusted_pending;
+
+            print_balances(
+                "confirmed",
+                [
+                    ("total", confirmed_total),
+                    ("spendable", balance.confirmed),
+                    ("immature", balance.immature),
+                ],
+            );
+            print_balances(
+                "unconfirmed",
+                [
+                    ("total", unconfirmed_total),
+                    ("trusted", balance.trusted_pending),
+                    ("untrusted", balance.untrusted_pending),
+                ],
+            );
+
+            Ok(())
+        }
+        Commands::TxOut { txout_cmd } => {
+            let graph = &*graph.lock().unwrap();
+            let chain = &*chain.lock().unwrap();
+            let chain_tip = chain.get_chain_tip()?;
+            let outpoints = graph.index.outpoints();
+
+            match txout_cmd {
+                TxOutCmd::List {
+                    spent,
+                    unspent,
+                    confirmed,
+                    unconfirmed,
+                } => {
+                    let txouts = graph
+                        .graph()
+                        .try_filter_chain_txouts(chain, chain_tip, outpoints.iter().cloned())?
+                        .filter(|(_, full_txo)| match (spent, unspent) {
+                            (true, false) => full_txo.spent_by.is_some(),
+                            (false, true) => full_txo.spent_by.is_none(),
+                            _ => true,
+                        })
+                        .filter(|(_, full_txo)| match (confirmed, unconfirmed) {
+                            (true, false) => full_txo.chain_position.is_confirmed(),
+                            (false, true) => !full_txo.chain_position.is_confirmed(),
+                            _ => true,
+                        })
+                        .collect::<Vec<_>>();
+
+                    for (spk_i, full_txo) in txouts {
+                        let addr = Address::from_script(&full_txo.txout.script_pubkey, network)?;
+                        println!(
+                            "{:?} {} {} {} spent:{:?}",
+                            spk_i, full_txo.txout.value, full_txo.outpoint, addr, full_txo.spent_by
+                        )
+                    }
+                    Ok(())
+                }
+            }
+        }
+        Commands::Psbt { psbt_cmd } => match psbt_cmd {
+            PsbtCmd::New {
+                value,
+                address,
+                feerate,
+                after,
+                older,
+                coin_select,
+                debug,
+            } => {
+                let address = address.require_network(network)?;
+
+                let (psbt, change_info) = {
+                    let mut graph = graph.lock().unwrap();
+                    let chain = chain.lock().unwrap();
+
+                    // collect assets we can sign for
+                    let mut pks = vec![];
+                    for (_, desc) in graph.index.keychains() {
+                        desc.for_each_key(|k| {
+                            pks.push(k.clone());
+                            true
+                        });
+                    }
+                    let mut assets = Assets::new().add(pks);
+                    if let Some(n) = after {
+                        assets = assets.after(absolute::LockTime::from_consensus(n));
+                    }
+                    if let Some(n) = older {
+                        assets = assets.older(relative::LockTime::from_consensus(n)?);
+                    }
+
+                    create_tx(
+                        &mut graph,
+                        &*chain,
+                        &assets,
+                        coin_select,
+                        address,
+                        value,
+                        feerate.expect("must have feerate"),
+                    )?
+                };
+
+                if let Some(ChangeInfo {
+                    change_keychain,
+                    indexer,
+                    index,
+                }) = change_info
+                {
+                    // We must first persist to disk the fact that we've got a new address from the
+                    // change keychain so future scans will find the tx we're about to broadcast.
+                    // If we're unable to persist this, then we don't want to broadcast.
+                    {
+                        let db = &mut *db.lock().unwrap();
+                        db.append(&ChangeSet {
+                            indexer,
+                            ..Default::default()
+                        })?;
+                    }
+
+                    // We don't want other callers/threads to use this address while we're using it
+                    // but we also don't want to scan the tx we just created because it's not
+                    // technically in the blockchain yet.
+                    graph
+                        .lock()
+                        .unwrap()
+                        .index
+                        .mark_used(change_keychain, index);
+                }
+
+                if debug {
+                    dbg!(psbt);
+                } else {
+                    // print base64 encoded psbt
+                    let fee = psbt.fee()?.to_sat();
+                    let mut obj = serde_json::Map::new();
+                    obj.insert("psbt".to_string(), json!(psbt.to_string()));
+                    obj.insert("fee".to_string(), json!(fee));
+                    println!("{}", serde_json::to_string_pretty(&obj)?);
+                };
+
+                Ok(())
+            }
+            PsbtCmd::Sign { psbt, descriptor } => {
+                let mut psbt = Psbt::from_str(&psbt)?;
+
+                let desc_str = match descriptor {
+                    Some(s) => s,
+                    None => env::var("DESCRIPTOR").context("unable to sign")?,
+                };
+
+                let secp = Secp256k1::new();
+                let (_, keymap) = Descriptor::parse_descriptor(&secp, &desc_str)?;
+                if keymap.is_empty() {
+                    bail!("unable to sign")
+                }
+
+                // note: we're only looking at the first entry in the keymap
+                // the idea is to find something that impls `GetKey`
+                let sign_res = match keymap.iter().next().expect("not empty") {
+                    (DescriptorPublicKey::Single(single_pub), DescriptorSecretKey::Single(prv)) => {
+                        let pk = match single_pub.key {
+                            SinglePubKey::FullKey(pk) => pk,
+                            SinglePubKey::XOnly(_) => unimplemented!("single xonly pubkey"),
+                        };
+                        let keys: HashMap<PublicKey, PrivateKey> = [(pk, prv.key)].into();
+                        psbt.sign(&keys, &secp)
+                    }
+                    (_, DescriptorSecretKey::XPrv(k)) => psbt.sign(&k.xkey, &secp),
+                    _ => unimplemented!("multi xkey signer"),
+                };
+
+                let _ = sign_res
+                    .map_err(|errors| anyhow::anyhow!("failed to sign PSBT {:?}", errors))?;
+
+                let mut obj = serde_json::Map::new();
+                obj.insert("psbt".to_string(), json!(psbt.to_string()));
+                println!("{}", serde_json::to_string_pretty(&obj)?);
+
+                Ok(())
+            }
+            PsbtCmd::Extract {
+                broadcast,
+                chain_specific,
+                psbt,
+            } => {
+                let mut psbt = Psbt::from_str(&psbt)?;
+                psbt.finalize_mut(&Secp256k1::new())
+                    .map_err(|errors| anyhow::anyhow!("failed to finalize PSBT {errors:?}"))?;
+
+                let tx = psbt.extract_tx()?;
+
+                if broadcast {
+                    let mut graph = graph.lock().unwrap();
+
+                    match broadcast_fn(chain_specific, &tx) {
+                        Ok(_) => {
+                            println!("Broadcasted Tx: {}", tx.compute_txid());
+
+                            let changeset = graph.insert_tx(tx);
+
+                            // We know the tx is at least unconfirmed now. Note if persisting here fails,
+                            // it's not a big deal since we can always find it again from the
+                            // blockchain.
+                            db.lock().unwrap().append(&ChangeSet {
+                                tx_graph: changeset.tx_graph,
+                                indexer: changeset.indexer,
+                                ..Default::default()
+                            })?;
+                        }
+                        Err(e) => {
+                            // We failed to broadcast, so allow our change address to be used in the future
+                            let (change_keychain, _) = graph
+                                .index
+                                .keychains()
+                                .last()
+                                .expect("must have a keychain");
+                            let change_index = tx.output.iter().find_map(|txout| {
+                                let spk = txout.script_pubkey.clone();
+                                match graph.index.index_of_spk(spk) {
+                                    Some(&(keychain, index)) if keychain == change_keychain => {
+                                        Some((keychain, index))
+                                    }
+                                    _ => None,
+                                }
+                            });
+                            if let Some((keychain, index)) = change_index {
+                                graph.index.unmark_used(keychain, index);
+                            }
+                            bail!(e);
+                        }
+                    }
+                } else {
+                    // encode raw tx hex
+                    let hex = consensus::serialize(&tx).to_lower_hex_string();
+                    let mut obj = serde_json::Map::new();
+                    obj.insert("tx".to_string(), json!(hex));
+                    println!("{}", serde_json::to_string_pretty(&obj)?);
+                }
+
+                Ok(())
+            }
+        },
+    }
+}
+
+/// The initial state returned by [`init_or_load`].
+pub struct Init<CS: clap::Subcommand, S: clap::Args> {
+    /// CLI args
+    pub args: Args<CS, S>,
+    /// Indexed graph
+    pub graph: Mutex<KeychainTxGraph>,
+    /// Local chain
+    pub chain: Mutex<LocalChain>,
+    /// Database
+    pub db: Mutex<Store<ChangeSet>>,
+    /// Network
+    pub network: Network,
+}
+
+/// Loads from persistence or creates new
+pub fn init_or_load<CS: clap::Subcommand, S: clap::Args>(
+    db_magic: &[u8],
+    db_path: &str,
+) -> anyhow::Result<Option<Init<CS, S>>> {
+    let args = Args::<CS, S>::parse();
+
+    match args.command {
+        // initialize new db
+        Commands::Init { .. } => initialize::<CS, S>(args, db_magic, db_path).map(|_| None),
+        // generate keys
+        Commands::Generate { network } => generate_bip86_helper(network).map(|_| None),
+        // try load
+        _ => {
+            let (db, changeset) =
+                Store::<ChangeSet>::load(db_magic, db_path).context("could not open file store")?;
+
+            let changeset = changeset.expect("should not be empty");
+
+            let network = changeset.network.expect("changeset network");
+
+            let chain = Mutex::new({
+                let (mut chain, _) =
+                    LocalChain::from_genesis_hash(constants::genesis_block(network).block_hash());
+                chain.apply_changeset(&changeset.local_chain)?;
+                chain
+            });
+
+            let graph = Mutex::new({
+                // insert descriptors and apply loaded changeset
+                let mut index = KeychainTxOutIndex::default();
+                if let Some(desc) = changeset.descriptor {
+                    index.insert_descriptor(Keychain::External, desc)?;
+                }
+                if let Some(change_desc) = changeset.change_descriptor {
+                    index.insert_descriptor(Keychain::Internal, change_desc)?;
+                }
+                let mut graph = KeychainTxGraph::new(index);
+                graph.apply_changeset(indexed_tx_graph::ChangeSet {
+                    tx_graph: changeset.tx_graph,
+                    indexer: changeset.indexer,
+                });
+                graph
+            });
+
+            let db = Mutex::new(db);
+
+            Ok(Some(Init {
+                args,
+                graph,
+                chain,
+                db,
+                network,
+            }))
+        }
+    }
+}
+
+/// Initialize db backend.
+fn initialize<CS, S>(args: Args<CS, S>, db_magic: &[u8], db_path: &str) -> anyhow::Result<()>
+where
+    CS: clap::Subcommand,
+    S: clap::Args,
+{
+    if let Commands::Init {
+        network,
+        descriptor,
+        change_descriptor,
+    } = args.command
+    {
+        let mut changeset = ChangeSet::default();
+
+        // parse descriptors
+        let secp = Secp256k1::new();
+        let mut index = KeychainTxOutIndex::default();
+        let (descriptor, _) =
+            Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &descriptor)?;
+        let _ = index.insert_descriptor(Keychain::External, descriptor.clone())?;
+        changeset.descriptor = Some(descriptor);
+
+        if let Some(desc) = change_descriptor {
+            let (change_descriptor, _) =
+                Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &desc)?;
+            let _ = index.insert_descriptor(Keychain::Internal, change_descriptor.clone())?;
+            changeset.change_descriptor = Some(change_descriptor);
+        }
+
+        // create new
+        let (_, chain_changeset) =
+            LocalChain::from_genesis_hash(constants::genesis_block(network).block_hash());
+        changeset.network = Some(network);
+        changeset.local_chain = chain_changeset;
+        let mut db = Store::<ChangeSet>::create(db_magic, db_path)?;
+        db.append(&changeset)?;
+        println!("New database {db_path}");
+    }
+
+    Ok(())
+}
+
+/// Generate BIP86 descriptors.
+fn generate_bip86_helper(network: impl Into<NetworkKind>) -> anyhow::Result<()> {
+    let secp = Secp256k1::new();
+    let mut seed = [0x00; 32];
+    thread_rng().fill_bytes(&mut seed);
+
+    let m = bip32::Xpriv::new_master(network, &seed)?;
+    let fp = m.fingerprint(&secp);
+    let path = if m.network.is_mainnet() {
+        "86h/0h/0h"
+    } else {
+        "86h/1h/0h"
+    };
+
+    let descriptors: Vec<String> = [0, 1]
+        .iter()
+        .map(|i| format!("tr([{fp}]{m}/{path}/{i}/*)"))
+        .collect();
+    let external_desc = &descriptors[0];
+    let internal_desc = &descriptors[1];
+    let (descriptor, keymap) =
+        <Descriptor<DescriptorPublicKey>>::parse_descriptor(&secp, external_desc)?;
+    let (internal_descriptor, internal_keymap) =
+        <Descriptor<DescriptorPublicKey>>::parse_descriptor(&secp, internal_desc)?;
+    println!("Public");
+    println!("{}", descriptor);
+    println!("{}", internal_descriptor);
+    println!("\nPrivate");
+    println!("{}", descriptor.to_string_with_secret(&keymap));
+    println!(
+        "{}",
+        internal_descriptor.to_string_with_secret(&internal_keymap)
+    );
+
+    Ok(())
+}
+
+impl Merge for ChangeSet {
+    fn merge(&mut self, other: Self) {
+        if other.descriptor.is_some() {
+            self.descriptor = other.descriptor;
+        }
+        if other.change_descriptor.is_some() {
+            self.change_descriptor = other.change_descriptor;
+        }
+        if other.network.is_some() {
+            self.network = other.network;
+        }
+        Merge::merge(&mut self.local_chain, other.local_chain);
+        Merge::merge(&mut self.tx_graph, other.tx_graph);
+        Merge::merge(&mut self.indexer, other.indexer);
+    }
+
+    fn is_empty(&self) -> bool {
+        self.descriptor.is_none()
+            && self.change_descriptor.is_none()
+            && self.network.is_none()
+            && self.local_chain.is_empty()
+            && self.tx_graph.is_empty()
+            && self.indexer.is_empty()
+    }
+}
diff --git a/examples/example_electrum/Cargo.toml b/examples/example_electrum/Cargo.toml
new file mode 100644 (file)
index 0000000..9dcd540
--- /dev/null
@@ -0,0 +1,11 @@
+[package]
+name = "example_electrum"
+version = "0.2.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/examples/example_electrum/src/main.rs b/examples/example_electrum/src/main.rs
new file mode 100644 (file)
index 0000000..b6c93a1
--- /dev/null
@@ -0,0 +1,288 @@
+use std::io::{self, Write};
+
+use bdk_chain::{
+    bitcoin::Network,
+    collections::BTreeSet,
+    indexed_tx_graph,
+    spk_client::{FullScanRequest, SyncRequest},
+    ConfirmationBlockTime, Merge,
+};
+use bdk_electrum::{
+    electrum_client::{self, Client, ElectrumApi},
+    BdkElectrumClient,
+};
+use example_cli::{
+    self,
+    anyhow::{self, Context},
+    clap::{self, Parser, Subcommand},
+    ChangeSet, 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,
+        #[clap(flatten)]
+        electrum_args: ElectrumArgs,
+    },
+    /// 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,
+        #[clap(flatten)]
+        electrum_args: ElectrumArgs,
+    },
+}
+
+impl ElectrumCommands {
+    fn electrum_args(&self) -> ElectrumArgs {
+        match self {
+            ElectrumCommands::Scan { electrum_args, .. } => electrum_args.clone(),
+            ElectrumCommands::Sync { electrum_args, .. } => electrum_args.clone(),
+        }
+    }
+}
+
+#[derive(clap::Args, Debug, Clone)]
+pub struct ElectrumArgs {
+    /// The electrum url to use to connect to. If not provided it will use a default electrum server
+    /// for your chosen network.
+    electrum_url: Option<String>,
+}
+
+impl ElectrumArgs {
+    pub fn client(&self, network: Network) -> anyhow::Result<Client> {
+        let electrum_url = self.electrum_url.as_deref().unwrap_or(match 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!(network, Network::Bitcoin))
+            .build();
+
+        Ok(electrum_client::Client::from_config(electrum_url, config)?)
+    }
+}
+
+#[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 example_cli::Init {
+        args,
+        graph,
+        chain,
+        db,
+        network,
+    } = match example_cli::init_or_load::<ElectrumCommands, ElectrumArgs>(DB_MAGIC, DB_PATH)? {
+        Some(init) => init,
+        None => return Ok(()),
+    };
+
+    let electrum_cmd = match &args.command {
+        example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd,
+        general_cmd => {
+            return example_cli::handle_commands(
+                &graph,
+                &chain,
+                &db,
+                network,
+                |electrum_args, tx| {
+                    let client = electrum_args.client(network)?;
+                    client.transaction_broadcast(tx)?;
+                    Ok(())
+                },
+                general_cmd.clone(),
+            );
+        }
+    };
+
+    let client = BdkElectrumClient::new(electrum_cmd.electrum_args().client(network)?);
+
+    // Tell the electrum client about the txs we've already got locally so it doesn't re-download them
+    client.populate_tx_cache(
+        graph
+            .lock()
+            .unwrap()
+            .graph()
+            .full_txs()
+            .map(|tx_node| tx_node.tx),
+    );
+
+    let (chain_update, tx_update, keychain_update) = match electrum_cmd.clone() {
+        ElectrumCommands::Scan {
+            stop_gap,
+            scan_options,
+            ..
+        } => {
+            let request = {
+                let graph = &*graph.lock().unwrap();
+                let chain = &*chain.lock().unwrap();
+
+                FullScanRequest::builder()
+                    .chain_tip(chain.tip())
+                    .spks_for_keychain(
+                        Keychain::External,
+                        graph
+                            .index
+                            .unbounded_spk_iter(Keychain::External)
+                            .into_iter()
+                            .flatten(),
+                    )
+                    .spks_for_keychain(
+                        Keychain::Internal,
+                        graph
+                            .index
+                            .unbounded_spk_iter(Keychain::Internal)
+                            .into_iter()
+                            .flatten(),
+                    )
+                    .inspect({
+                        let mut once = BTreeSet::new();
+                        move |k, spk_i, _| {
+                            if once.insert(k) {
+                                eprint!("\nScanning {}: {} ", k, spk_i);
+                            } else {
+                                eprint!("{} ", spk_i);
+                            }
+                            io::stdout().flush().expect("must flush");
+                        }
+                    })
+            };
+
+            let res = client
+                .full_scan::<_>(request, stop_gap, scan_options.batch_size, false)
+                .context("scanning the blockchain")?;
+            (
+                res.chain_update,
+                res.tx_update,
+                Some(res.last_active_indices),
+            )
+        }
+        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();
+
+            if !(all_spks || unused_spks || utxos || unconfirmed) {
+                unused_spks = true;
+                unconfirmed = true;
+                utxos = true;
+            } else if all_spks {
+                unused_spks = false;
+            }
+
+            let chain_tip = chain.tip();
+            let mut request =
+                SyncRequest::builder()
+                    .chain_tip(chain_tip.clone())
+                    .inspect(|item, progress| {
+                        let pc = (100 * progress.consumed()) as f32 / progress.total() as f32;
+                        eprintln!("[ SCANNING {:03.0}% ] {}", pc, item);
+                    });
+
+            request = request.expected_spk_txids(graph.list_expected_spk_txids(
+                &*chain,
+                chain_tip.block_id(),
+                ..,
+            ));
+            if all_spks {
+                request = request.spks_with_indexes(graph.index.revealed_spks(..));
+            }
+            if unused_spks {
+                request = request.spks_with_indexes(graph.index.unused_spks());
+            }
+            if utxos {
+                let init_outpoints = graph.index.outpoints();
+                request = request.outpoints(
+                    graph
+                        .graph()
+                        .filter_chain_unspents(
+                            &*chain,
+                            chain_tip.block_id(),
+                            init_outpoints.iter().cloned(),
+                        )
+                        .map(|(_, utxo)| utxo.outpoint),
+                );
+            };
+            if unconfirmed {
+                request = request.txids(
+                    graph
+                        .graph()
+                        .list_canonical_txs(&*chain, chain_tip.block_id())
+                        .filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
+                        .map(|canonical_tx| canonical_tx.tx_node.txid),
+                );
+            }
+
+            let res = client
+                .sync(request, scan_options.batch_size, false)
+                .context("scanning the blockchain")?;
+
+            // drop lock on graph and chain
+            drop((graph, chain));
+
+            (res.chain_update, res.tx_update, None)
+        }
+    };
+
+    let db_changeset = {
+        let mut chain = chain.lock().unwrap();
+        let mut graph = graph.lock().unwrap();
+
+        let chain_changeset = chain.apply_update(chain_update.expect("request has chain tip"))?;
+
+        let mut indexed_tx_graph_changeset =
+            indexed_tx_graph::ChangeSet::<ConfirmationBlockTime, _>::default();
+        if let Some(keychain_update) = keychain_update {
+            let keychain_changeset = graph.index.reveal_to_target_multi(&keychain_update);
+            indexed_tx_graph_changeset.merge(keychain_changeset.into());
+        }
+        indexed_tx_graph_changeset.merge(graph.apply_update(tx_update));
+
+        ChangeSet {
+            local_chain: chain_changeset,
+            tx_graph: indexed_tx_graph_changeset.tx_graph,
+            indexer: indexed_tx_graph_changeset.indexer,
+            ..Default::default()
+        }
+    };
+
+    let mut db = db.lock().unwrap();
+    db.append(&db_changeset)?;
+    Ok(())
+}
diff --git a/examples/example_esplora/Cargo.toml b/examples/example_esplora/Cargo.toml
new file mode 100644 (file)
index 0000000..ccad862
--- /dev/null
@@ -0,0 +1,12 @@
+[package]
+name = "example_esplora"
+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_esplora = { path = "../../crates/esplora", features = ["blocking"] }
+example_cli = { path = "../example_cli" }
+
diff --git a/examples/example_esplora/src/main.rs b/examples/example_esplora/src/main.rs
new file mode 100644 (file)
index 0000000..8ef39c2
--- /dev/null
@@ -0,0 +1,292 @@
+use core::f32;
+use std::{
+    collections::BTreeSet,
+    io::{self, Write},
+};
+
+use bdk_chain::{
+    bitcoin::Network,
+    keychain_txout::FullScanRequestBuilderExt,
+    spk_client::{FullScanRequest, SyncRequest},
+    Merge,
+};
+use bdk_esplora::{esplora_client, EsploraExt};
+use example_cli::{
+    anyhow::{self, Context},
+    clap::{self, Parser, Subcommand},
+    ChangeSet, Keychain,
+};
+
+const DB_MAGIC: &[u8] = b"bdk_example_esplora";
+const DB_PATH: &str = ".bdk_example_esplora.db";
+
+#[derive(Subcommand, Debug, Clone)]
+enum EsploraCommands {
+    /// 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, short = 'g', default_value = "10")]
+        stop_gap: usize,
+        #[clap(flatten)]
+        scan_options: ScanOptions,
+        #[clap(flatten)]
+        esplora_args: EsploraArgs,
+    },
+    /// Scan for particular addresses and unconfirmed transactions 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,
+        #[clap(flatten)]
+        esplora_args: EsploraArgs,
+    },
+}
+
+impl EsploraCommands {
+    fn esplora_args(&self) -> EsploraArgs {
+        match self {
+            EsploraCommands::Scan { esplora_args, .. } => esplora_args.clone(),
+            EsploraCommands::Sync { esplora_args, .. } => esplora_args.clone(),
+        }
+    }
+}
+
+#[derive(clap::Args, Debug, Clone)]
+pub struct EsploraArgs {
+    /// The esplora url endpoint to connect to.
+    #[clap(long, short = 'u', env = "ESPLORA_SERVER")]
+    esplora_url: Option<String>,
+}
+
+impl EsploraArgs {
+    pub fn client(&self, network: Network) -> anyhow::Result<esplora_client::BlockingClient> {
+        let esplora_url = self.esplora_url.as_deref().unwrap_or(match network {
+            Network::Bitcoin => "https://blockstream.info/api",
+            Network::Testnet => "https://blockstream.info/testnet/api",
+            Network::Regtest => "http://localhost:3002",
+            Network::Signet => "http://signet.bitcoindevkit.net",
+            _ => panic!("unsupported network"),
+        });
+
+        let client = esplora_client::Builder::new(esplora_url).build_blocking();
+        Ok(client)
+    }
+}
+
+#[derive(Parser, Debug, Clone, PartialEq)]
+pub struct ScanOptions {
+    /// Max number of concurrent esplora server requests.
+    #[clap(long, default_value = "2")]
+    pub parallel_requests: usize,
+}
+
+fn main() -> anyhow::Result<()> {
+    let example_cli::Init {
+        args,
+        graph,
+        chain,
+        db,
+        network,
+    } = match example_cli::init_or_load::<EsploraCommands, EsploraArgs>(DB_MAGIC, DB_PATH)? {
+        Some(init) => init,
+        None => return Ok(()),
+    };
+
+    let esplora_cmd = match &args.command {
+        // These are commands that are handled by this example (sync, scan).
+        example_cli::Commands::ChainSpecific(esplora_cmd) => esplora_cmd,
+        // These are general commands handled by example_cli. Execute the cmd and return.
+        general_cmd => {
+            return example_cli::handle_commands(
+                &graph,
+                &chain,
+                &db,
+                network,
+                |esplora_args, tx| {
+                    let client = esplora_args.client(network)?;
+                    client
+                        .broadcast(tx)
+                        .map(|_| ())
+                        .map_err(anyhow::Error::from)
+                },
+                general_cmd.clone(),
+            );
+        }
+    };
+
+    let client = esplora_cmd.esplora_args().client(network)?;
+    // Prepare the `IndexedTxGraph` and `LocalChain` updates based on whether we are scanning or
+    // syncing.
+    //
+    // 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.
+    //
+    // Syncing: We only check for specified spks, utxos and txids to update their confirmation
+    //   status or fetch missing transactions.
+    let (local_chain_changeset, indexed_tx_graph_changeset) = match &esplora_cmd {
+        EsploraCommands::Scan {
+            stop_gap,
+            scan_options,
+            ..
+        } => {
+            let request = {
+                let chain_tip = chain.lock().expect("mutex must not be poisoned").tip();
+                let indexed_graph = &*graph.lock().expect("mutex must not be poisoned");
+                FullScanRequest::builder()
+                    .chain_tip(chain_tip)
+                    .spks_from_indexer(&indexed_graph.index)
+                    .inspect({
+                        let mut once = BTreeSet::<Keychain>::new();
+                        move |keychain, spk_i, _| {
+                            if once.insert(keychain) {
+                                eprint!("\nscanning {}: ", keychain);
+                            }
+                            eprint!("{} ", spk_i);
+                            // Flush early to ensure we print at every iteration.
+                            let _ = io::stderr().flush();
+                        }
+                    })
+                    .build()
+            };
+
+            // The client scans keychain spks for transaction histories, stopping after `stop_gap`
+            // is reached. It returns a `TxGraph` update (`tx_update`) and a structure that
+            // represents the last active spk derivation indices of keychains
+            // (`keychain_indices_update`).
+            let update = client
+                .full_scan(request, *stop_gap, scan_options.parallel_requests)
+                .context("scanning for transactions")?;
+
+            let mut graph = graph.lock().expect("mutex must not be poisoned");
+            let mut chain = chain.lock().expect("mutex must not be poisoned");
+            // Because we did a stop gap based scan we are likely to have some updates to our
+            // deriviation indices. Usually before a scan you are on a fresh wallet with no
+            // addresses derived so we need to derive up to last active addresses the scan found
+            // before adding the transactions.
+            (
+                chain.apply_update(update.chain_update.expect("request included chain tip"))?,
+                {
+                    let index_changeset = graph
+                        .index
+                        .reveal_to_target_multi(&update.last_active_indices);
+                    let mut indexed_tx_graph_changeset = graph.apply_update(update.tx_update);
+                    indexed_tx_graph_changeset.merge(index_changeset.into());
+                    indexed_tx_graph_changeset
+                },
+            )
+        }
+        EsploraCommands::Sync {
+            mut unused_spks,
+            all_spks,
+            mut utxos,
+            mut unconfirmed,
+            scan_options,
+            ..
+        } => {
+            if !(*all_spks || unused_spks || utxos || unconfirmed) {
+                // If nothing is specifically selected, we select everything (except all spks).
+                unused_spks = true;
+                unconfirmed = true;
+                utxos = true;
+            } else if *all_spks {
+                // If all spks is selected, we don't need to also select unused spks (as unused spks
+                // is a subset of all spks).
+                unused_spks = false;
+            }
+
+            let local_tip = chain.lock().expect("mutex must not be poisoned").tip();
+            // Spks, outpoints and txids we want updates on will be accumulated here.
+            let mut request =
+                SyncRequest::builder()
+                    .chain_tip(local_tip.clone())
+                    .inspect(|item, progress| {
+                        let pc = (100 * progress.consumed()) as f32 / progress.total() as f32;
+                        eprintln!("[ SCANNING {:03.0}% ] {}", pc, item);
+                        // Flush early to ensure we print at every iteration.
+                        let _ = io::stderr().flush();
+                    });
+
+            // Get a short lock on the structures to get spks, utxos, and txs that we are interested
+            // in.
+            {
+                let graph = graph.lock().unwrap();
+                let chain = chain.lock().unwrap();
+                request = request.expected_spk_txids(graph.list_expected_spk_txids(
+                    &*chain,
+                    local_tip.block_id(),
+                    ..,
+                ));
+                if *all_spks {
+                    request = request.spks_with_indexes(graph.index.revealed_spks(..));
+                }
+                if unused_spks {
+                    request = request.spks_with_indexes(graph.index.unused_spks());
+                }
+                if utxos {
+                    // We want to search for whether the UTXO is spent, and spent by which
+                    // transaction. We provide the outpoint of the UTXO to
+                    // `EsploraExt::update_tx_graph_without_keychain`.
+                    let init_outpoints = graph.index.outpoints();
+                    request = request.outpoints(
+                        graph
+                            .graph()
+                            .filter_chain_unspents(
+                                &*chain,
+                                local_tip.block_id(),
+                                init_outpoints.iter().cloned(),
+                            )
+                            .map(|(_, utxo)| utxo.outpoint),
+                    );
+                };
+                if unconfirmed {
+                    // We want to search for whether the unconfirmed transaction is now confirmed.
+                    // We provide the unconfirmed txids to
+                    // `EsploraExt::update_tx_graph_without_keychain`.
+                    request = request.txids(
+                        graph
+                            .graph()
+                            .list_canonical_txs(&*chain, local_tip.block_id())
+                            .filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
+                            .map(|canonical_tx| canonical_tx.tx_node.txid),
+                    );
+                }
+            }
+
+            let update = client.sync(request, scan_options.parallel_requests)?;
+
+            (
+                chain
+                    .lock()
+                    .unwrap()
+                    .apply_update(update.chain_update.expect("request has chain tip"))?,
+                graph.lock().unwrap().apply_update(update.tx_update),
+            )
+        }
+    };
+
+    println!();
+
+    // We persist the changes
+    let mut db = db.lock().unwrap();
+    db.append(&ChangeSet {
+        local_chain: local_chain_changeset,
+        tx_graph: indexed_tx_graph_changeset.tx_graph,
+        indexer: indexed_tx_graph_changeset.indexer,
+        ..Default::default()
+    })?;
+    Ok(())
+}
index 9759162850dbdbbcb73c63b954667afca31f787e..d76d6500504de3c5072fc08c2e434b167ae4b202 100644 (file)
@@ -2,4 +2,4 @@
 
 This is a directory for crates that are experimental and have not been released yet.
 Keep in mind that they may never be released.
-Things in `/example-crates` may use them to demonstrate how things might look in the future.
+Things in `/examples` may use them to demonstrate how things might look in the future.