From: Steve Myers Date: Sat, 5 Apr 2025 01:31:47 +0000 (-0500) Subject: chore: rename example-crates directory to examples X-Git-Tag: core-0.5.0~6^2 X-Git-Url: http://internal-gitweb-vhost/script/%22https:/database/scripts/enum.BuildSatisfaction.html?a=commitdiff_plain;h=1d7ad3122e74266e0833d7b590eb3f7bf3882655;p=bdk chore: rename example-crates directory to examples --- diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index 8769ca1f..e517c2bf 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -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 diff --git a/Cargo.toml b/Cargo.toml index 3f96ad12..0a62ba43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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] diff --git a/README.md b/README.md index e3fc7480..27d5448e 100644 --- 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 diff --git a/crates/electrum/src/lib.rs b/crates/electrum/src/lib.rs index 8bc87321..d791f962 100644 --- a/crates/electrum/src/lib.rs +++ b/crates/electrum/src/lib.rs @@ -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 diff --git a/crates/esplora/README.md b/crates/esplora/README.md index 244deb20..4a6acb37 100644 --- a/crates/esplora/README.md +++ b/crates/esplora/README.md @@ -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 index 6728bb13..00000000 --- a/example-crates/example_bitcoind_rpc_polling/Cargo.toml +++ /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 index fef82ab1..00000000 --- a/example-crates/example_bitcoind_rpc_polling/README.md +++ /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= -rpcpassword= -datadir=/tmp/regtest/bitcoind -daemon - ``` -2. Create a test bitcoind wallet and set bitcoind env. - ``` - bitcoin-cli -datadir=/tmp/regtest/bitcoind -regtest -rpcuser= -rpcpassword= -named createwallet wallet_name="test" - export RPC_URL=127.0.0.1:18443 - export RPC_USER= - export RPC_PASS= - ``` -3. Get test bitcoind wallet info. - ``` - bitcoin-cli -rpcwallet="test" -rpcuser= -rpcpassword= -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= -rpcpassword= getnewaddress) - echo $BITCOIND_ADDRESS - ``` -5. Generate 101 blocks with reward to test bitcoind wallet address. - ``` - bitcoin-cli -datadir=/tmp/regtest/bitcoind -regtest -rpcuser= -rpcpassword= generatetoaddress 101 $BITCOIND_ADDRESS - ``` -6. Verify test bitcoind wallet balance. - ``` - bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser= -rpcpassword= 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= -rpcpassword= sendtoaddress
5 - ``` -9. Sync blockchain with RPC CLI wallet. - ``` - cargo run -- --network regtest sync - - ``` -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= -rpcpassword= -regtest generatetoaddress 10 $BITCOIND_ADDRESS - ``` -12. Sync the blockchain with RPC CLI wallet. - ``` - cargo run -- --network regtest sync - - ``` -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 index 83cb25f8..00000000 --- a/example-crates/example_bitcoind_rpc_polling/src/main.rs +++ /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), - 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, - /// RPC auth username - #[clap(env = "RPC_USER", long)] - rpc_user: Option, - /// RPC auth password - #[clap(env = "RPC_PASS", long)] - rpc_password: Option, - /// 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 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 { - 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::(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::(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::::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 { - 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 index 0a467db8..00000000 --- a/example-crates/example_cli/Cargo.toml +++ /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 index 2cb27849..00000000 --- a/example-crates/example_cli/src/lib.rs +++ /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>; - -/// ChangeSet -#[derive(Default, Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct ChangeSet { - /// Descriptor for recipient addresses. - pub descriptor: Option>, - /// Descriptor for change addresses. - pub change_descriptor: Option>, - /// Stores the network type of the transaction data. - pub network: Option, - /// Changes to the [`LocalChain`]. - pub local_chain: local_chain::ChangeSet, - /// Changes to [`TxGraph`](tx_graph::TxGraph). - pub tx_graph: tx_graph::ChangeSet, - /// Changes to [`KeychainTxOutIndex`]. - pub indexer: keychain_txout::ChangeSet, -} - -#[derive(Parser)] -#[clap(author, version, about, long_about = None)] -#[clap(propagate_version = true)] -pub struct Args { - #[clap(subcommand)] - pub command: Commands, -} - -#[derive(Subcommand, Debug, Clone)] -pub enum Commands { - /// 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, - }, - #[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, - }, - /// 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 { - /// Create a new PSBT. - New { - /// Amount to send in satoshis - #[clap(required = true)] - value: u64, - /// Recipient address - #[clap(required = true)] - address: Address, - /// Set the feerate of the tx (sat/vbyte) - #[clap(long, short, default_value = "1.0")] - feerate: Option, - /// Set max absolute timelock (from consensus value) - #[clap(long, short)] - after: Option, - /// Set max relative timelock (from consensus value) - #[clap(long, short)] - older: Option, - /// 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, - /// 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 { - 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( - graph: &mut KeychainTxGraph, - chain: &O, - assets: &Assets, - cs_algorithm: CoinSelectionAlgo, - address: Address, - value: u64, - feerate: f32, -) -> anyhow::Result<(Psbt, Option)> -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 = 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::::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); - -pub fn planned_utxos( - graph: &KeychainTxGraph, - chain: &O, - assets: &Assets, -) -> Result, 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> { - 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( - graph: &Mutex, - chain: &Mutex, - db: &Mutex>, - network: Network, - broadcast_fn: impl FnOnce(S, &Transaction) -> anyhow::Result<()>, - cmd: Commands, -) -> 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, - ) { - 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::>(); - - 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 = [(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 { - /// CLI args - pub args: Args, - /// Indexed graph - pub graph: Mutex, - /// Local chain - pub chain: Mutex, - /// Database - pub db: Mutex>, - /// Network - pub network: Network, -} - -/// Loads from persistence or creates new -pub fn init_or_load( - db_magic: &[u8], - db_path: &str, -) -> anyhow::Result>> { - let args = Args::::parse(); - - match args.command { - // initialize new db - Commands::Init { .. } => initialize::(args, db_magic, db_path).map(|_| None), - // generate keys - Commands::Generate { network } => generate_bip86_helper(network).map(|_| None), - // try load - _ => { - let (db, changeset) = - Store::::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(args: Args, 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::::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::::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::::create(db_magic, db_path)?; - db.append(&changeset)?; - println!("New database {db_path}"); - } - - Ok(()) -} - -/// Generate BIP86 descriptors. -fn generate_bip86_helper(network: impl Into) -> 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 = [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) = - >::parse_descriptor(&secp, external_desc)?; - let (internal_descriptor, internal_keymap) = - >::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 index 9dcd5400..00000000 --- a/example-crates/example_electrum/Cargo.toml +++ /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 index b6c93a1d..00000000 --- a/example-crates/example_electrum/src/main.rs +++ /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, -} - -impl ElectrumArgs { - pub fn client(&self, network: Network) -> anyhow::Result { - 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::(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::::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 index ccad862e..00000000 --- a/example-crates/example_esplora/Cargo.toml +++ /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 index 8ef39c2f..00000000 --- a/example-crates/example_esplora/src/main.rs +++ /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, -} - -impl EsploraArgs { - pub fn client(&self, network: Network) -> anyhow::Result { - 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::(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::::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 index 00000000..6728bb13 --- /dev/null +++ b/examples/example_bitcoind_rpc_polling/Cargo.toml @@ -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 index 00000000..fef82ab1 --- /dev/null +++ b/examples/example_bitcoind_rpc_polling/README.md @@ -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= -rpcpassword= -datadir=/tmp/regtest/bitcoind -daemon + ``` +2. Create a test bitcoind wallet and set bitcoind env. + ``` + bitcoin-cli -datadir=/tmp/regtest/bitcoind -regtest -rpcuser= -rpcpassword= -named createwallet wallet_name="test" + export RPC_URL=127.0.0.1:18443 + export RPC_USER= + export RPC_PASS= + ``` +3. Get test bitcoind wallet info. + ``` + bitcoin-cli -rpcwallet="test" -rpcuser= -rpcpassword= -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= -rpcpassword= getnewaddress) + echo $BITCOIND_ADDRESS + ``` +5. Generate 101 blocks with reward to test bitcoind wallet address. + ``` + bitcoin-cli -datadir=/tmp/regtest/bitcoind -regtest -rpcuser= -rpcpassword= generatetoaddress 101 $BITCOIND_ADDRESS + ``` +6. Verify test bitcoind wallet balance. + ``` + bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser= -rpcpassword= 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= -rpcpassword= sendtoaddress
5 + ``` +9. Sync blockchain with RPC CLI wallet. + ``` + cargo run -- --network regtest sync + + ``` +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= -rpcpassword= -regtest generatetoaddress 10 $BITCOIND_ADDRESS + ``` +12. Sync the blockchain with RPC CLI wallet. + ``` + cargo run -- --network regtest sync + + ``` +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 index 00000000..83cb25f8 --- /dev/null +++ b/examples/example_bitcoind_rpc_polling/src/main.rs @@ -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), + 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, + /// RPC auth username + #[clap(env = "RPC_USER", long)] + rpc_user: Option, + /// RPC auth password + #[clap(env = "RPC_PASS", long)] + rpc_password: Option, + /// 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 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 { + 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::(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::(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::::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 { + 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 index 00000000..0a467db8 --- /dev/null +++ b/examples/example_cli/Cargo.toml @@ -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 index 00000000..2cb27849 --- /dev/null +++ b/examples/example_cli/src/lib.rs @@ -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>; + +/// ChangeSet +#[derive(Default, Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct ChangeSet { + /// Descriptor for recipient addresses. + pub descriptor: Option>, + /// Descriptor for change addresses. + pub change_descriptor: Option>, + /// Stores the network type of the transaction data. + pub network: Option, + /// Changes to the [`LocalChain`]. + pub local_chain: local_chain::ChangeSet, + /// Changes to [`TxGraph`](tx_graph::TxGraph). + pub tx_graph: tx_graph::ChangeSet, + /// Changes to [`KeychainTxOutIndex`]. + pub indexer: keychain_txout::ChangeSet, +} + +#[derive(Parser)] +#[clap(author, version, about, long_about = None)] +#[clap(propagate_version = true)] +pub struct Args { + #[clap(subcommand)] + pub command: Commands, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum Commands { + /// 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, + }, + #[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, + }, + /// 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 { + /// Create a new PSBT. + New { + /// Amount to send in satoshis + #[clap(required = true)] + value: u64, + /// Recipient address + #[clap(required = true)] + address: Address, + /// Set the feerate of the tx (sat/vbyte) + #[clap(long, short, default_value = "1.0")] + feerate: Option, + /// Set max absolute timelock (from consensus value) + #[clap(long, short)] + after: Option, + /// Set max relative timelock (from consensus value) + #[clap(long, short)] + older: Option, + /// 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, + /// 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 { + 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( + graph: &mut KeychainTxGraph, + chain: &O, + assets: &Assets, + cs_algorithm: CoinSelectionAlgo, + address: Address, + value: u64, + feerate: f32, +) -> anyhow::Result<(Psbt, Option)> +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 = 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::::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); + +pub fn planned_utxos( + graph: &KeychainTxGraph, + chain: &O, + assets: &Assets, +) -> Result, 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> { + 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( + graph: &Mutex, + chain: &Mutex, + db: &Mutex>, + network: Network, + broadcast_fn: impl FnOnce(S, &Transaction) -> anyhow::Result<()>, + cmd: Commands, +) -> 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, + ) { + 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::>(); + + 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 = [(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 { + /// CLI args + pub args: Args, + /// Indexed graph + pub graph: Mutex, + /// Local chain + pub chain: Mutex, + /// Database + pub db: Mutex>, + /// Network + pub network: Network, +} + +/// Loads from persistence or creates new +pub fn init_or_load( + db_magic: &[u8], + db_path: &str, +) -> anyhow::Result>> { + let args = Args::::parse(); + + match args.command { + // initialize new db + Commands::Init { .. } => initialize::(args, db_magic, db_path).map(|_| None), + // generate keys + Commands::Generate { network } => generate_bip86_helper(network).map(|_| None), + // try load + _ => { + let (db, changeset) = + Store::::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(args: Args, 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::::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::::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::::create(db_magic, db_path)?; + db.append(&changeset)?; + println!("New database {db_path}"); + } + + Ok(()) +} + +/// Generate BIP86 descriptors. +fn generate_bip86_helper(network: impl Into) -> 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 = [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) = + >::parse_descriptor(&secp, external_desc)?; + let (internal_descriptor, internal_keymap) = + >::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 index 00000000..9dcd5400 --- /dev/null +++ b/examples/example_electrum/Cargo.toml @@ -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 index 00000000..b6c93a1d --- /dev/null +++ b/examples/example_electrum/src/main.rs @@ -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, +} + +impl ElectrumArgs { + pub fn client(&self, network: Network) -> anyhow::Result { + 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::(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::::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 index 00000000..ccad862e --- /dev/null +++ b/examples/example_esplora/Cargo.toml @@ -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 index 00000000..8ef39c2f --- /dev/null +++ b/examples/example_esplora/src/main.rs @@ -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, +} + +impl EsploraArgs { + pub fn client(&self, network: Network) -> anyhow::Result { + 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::(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::::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/nursery/README.md b/nursery/README.md index 97591628..d76d6500 100644 --- a/nursery/README.md +++ b/nursery/README.md @@ -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.