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