Introduction

The Bitcoin Dev Kit (BDK) project was created to provide well engineered and reviewed components for building bitcoin based applications. The core components of BDK are written in the Rust language and live in the bitcoindevkit/bdk repository. The core BDK components are built upon the excellent rust-bitcoin and rust-miniscript crates.

The BDK team also maintains the bitcoindevkit/bdk-ffi repository which provide cross platform versions of the high level BDK APIs. Current supported platforms are: Kotlin (android, linux, MacOS), Swift (iOS, MacOS), and Python (linux, MacOS, Windows).

⚠ The BDK developers are in the process of rewriting major components of the software to be release in an upcoming 1.0 version. BDK 1.0 is a still under active development and should be considered "alpha" quality. This means APIs may change and full testing and documentation has not been completed. For current status and release timeline please see the BDK 1.0 project page.

The bitcoindevkit/bdk-ffi project has not yet been updated to use the new BDK 1.0 crates. For current status and timeline for BDK-FFI 1.0 see the BDK-FFI project page.

🔨 Project Organization

Within the bitcoindevkit/bdk repository the BDK team maintains a suite of rust crates which provide both an easy to use high level API as well as powerful lower level components to used when building more advanced bitcoin software.

The project is split up into several crates in the /crates directory:

  • bdk: Contains the central high level Wallet type that is built from the low-level mechanisms provided by the other components
  • chain: Tools for storing and indexing chain data
  • file_store: A (experimental) persistence backend for storing chain data in a single file.
  • 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: 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.

Fully working examples of how to use these components are in /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.
  • 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 library.
  • wallet_esplora_blocking: Uses the Wallet to sync and spend using the Esplora blocking interface.
  • wallet_esplora_async: Uses the Wallet to sync and spend using the Esplora asynchronous interface.
  • wallet_electrum: Uses the Wallet to sync and spend using Electrum.

😃 Join our community

Open source is fundamental to this project and we would love to connect with you.

Please feel free to open or comment on PRs and issues in any of the bitcoindevkit repos or join us on the BDK discord server, come say hi!

Quick Start

Install Rust

See the Rust "Getting Started" page to install the Rust development tools.

Using BDK in a Rust project

Follow these steps to use BDK in your own rust project with the async esplora blockchain client.

⚠ For now use the latest master branch versions of BDK crates.

  1. Create a new Rust project:
    cargo init my_bdk_app
    cd my_bdk_app
    
  2. Add bdk to your Cargo.toml file. Find the latest bdk@1 release on crates.io, for example:
     cargo add bdk@1.0.0-alpha.1
    
  3. Add other required dependencies:
    cargo add bdk_esplora@0.3.0
    cargo add bdk_file_store@0.2.0
    cargo add tokio@1 --features "rt,rt-multi-thread,macros"
    

See the Wallet with Async Esplora tutorial for how to create and sync a wallet.

Contributing to BDK

To contribute to BDK first install the stable version of rust, clone the repository master branch and verify you can run the tests for all features.

  1. Clone the repo:
    git clone https://github.com/bitcoindevkit/bdk.git
    cd bdk
    
  2. Build and run the tests:
    cargo test --all-features
    

BDK Wallet

The BDK Wallet struct provides as high level interface to perform common bitcoin wallet operations. In BDK a wallet encapsulates:

  1. the "external" keychain output descriptor
  2. an optional "internal" keychain output descriptor
  3. optional signing keys
  4. storage for wallet state data
  5. a view of the current blockchain
  6. an indexed graph of wallet transactions

The common operations that can be performed with a BDK wallet are:

  1. get a new address
  2. list unspent transaction outputs
  3. list transactions
  4. get the wallet balance
  5. build a new unsigned transaction
  6. build a transaction to bump the fee of an unconfirmed transaction
  7. sign an unsigned or partially signed transaction
  8. finalize a partially signed transaction
  9. cancel an unconfirmed transaction

File Store

Blockchain Clients

Esplora

Electrum

Chain

Wallet with Async Explora

  1. Create a new Rust project:

    cargo init my_bdk_app
    cd my_bdk_app
    
  2. Add bdk to your Cargo.toml file. Find the latest BDK@1.0.0 release on crates.io (use pre-released branch for now):

     cargo add bdk --git "https://github.com/notmandatory/bdk.git" --branch "test/esplora_tests"
    
  3. Add other required dependencies:

    cargo add bdk_esplora --git "https://github.com/notmandatory/bdk.git" --branch "test/esplora_tests"
    cargo add bdk_file_store --git "https://github.com/notmandatory/bdk.git" --branch "test/esplora_tests"
    cargo add tokio@1 --features "rt,rt-multi-thread,macros"
    
  4. Edit src/main.rs, replace with below code to load or create and save new descriptors:

    use std::fs::File;
    use std::io::Read;
    use std::string::ToString;
    use std::{io::Write, str::FromStr};
    
    use bdk::bitcoin::bip32;
    use bdk::bitcoin::bip32::ExtendedPrivKey;
    use bdk::bitcoin::secp256k1::{rand, rand::RngCore, Secp256k1};
    
    use bdk::{bitcoin::Network, descriptor};
    
    use bdk::descriptor::IntoWalletDescriptor;
    use bdk::keys::IntoDescriptorKey;
    
    const CONFIG_FILE: &str = "config.txt";
    
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create and load or save new descriptors
    
        let secp = Secp256k1::new();
        let network = Network::Signet;
    
        // get descriptors from config.txt file, if file is missing create a new ones
        let descriptors = match File::open(CONFIG_FILE) {
            // load descriptors from file
            Ok(mut file) => {
                let mut config = String::new();
                file.read_to_string(&mut config)?;
                let descriptor_strings: [_; 2] = config
                    .split("|")
                    .map(|d| d.to_string())
                    .collect::<Vec<_>>()
                    .try_into()
                    .unwrap();
                let external_descriptor = descriptor_strings[0]
                    .into_wallet_descriptor(&secp, network)
                    .unwrap();
                let internal_descriptor = descriptor_strings[1]
                    .into_wallet_descriptor(&secp, network)
                    .unwrap();
                (external_descriptor, internal_descriptor)
            }
            Err(_) => {
                // create new descriptors and save them to the file
                let mut seed = [0u8; 32];
                rand::thread_rng().fill_bytes(&mut seed);
                let xprv = ExtendedPrivKey::new_master(network, &seed).unwrap();
                let bip86_external = bip32::DerivationPath::from_str("m/86'/1'/0'/0/0").unwrap();
                let bip86_internal = bip32::DerivationPath::from_str("m/86'/1'/0'/0/1").unwrap();
                let external_key = (xprv, bip86_external).into_descriptor_key().unwrap();
                let internal_key = (xprv, bip86_internal).into_descriptor_key().unwrap();
                let external_descriptor = descriptor!(tr(external_key))
                    .unwrap()
                    .into_wallet_descriptor(&secp, network)
                    .unwrap();
                let internal_descriptor = descriptor!(tr(internal_key))
                    .unwrap()
                    .into_wallet_descriptor(&secp, network)
                    .unwrap();
                // save descriptor strings to file
                let mut file = File::create(CONFIG_FILE).unwrap();
                println!("Created new descriptor config file: config.txt");
                let config = format!(
                    "{}|{}",
                    &external_descriptor
                        .0
                        .to_string_with_secret(&external_descriptor.1),
                    &internal_descriptor
                        .0
                        .to_string_with_secret(&internal_descriptor.1)
                );
                file.write(config.as_bytes()).unwrap();
                (external_descriptor, internal_descriptor)
            }
        };
    
        let external_descriptor = descriptors.0;
        let internal_descriptor = descriptors.1;
        println!(
            "External descriptor: {}",
            &external_descriptor
                .0
                .to_string_with_secret(&external_descriptor.1)
        );
        println!(
            "Internal descriptor: {}\n",
            &internal_descriptor
                .0
                .to_string_with_secret(&internal_descriptor.1)
        );
    
        Ok(())
    }
  5. Add code to create a wallet and get a new address and current wallet balance:

    #![allow(unused)]
    fn main() {
    use bdk::{bitcoin::Network, descriptor, Wallet};
    use bdk::wallet::AddressIndex;
    use bdk_file_store::Store;
    
    const CHAIN_DATA_FILE: &str = "chain.dat";
    const DB_MAGIC: &[u8] = "TABCONF24".as_bytes();
    }
    #![allow(unused)]
    fn main() {
    // Create a wallet and get a new address and current wallet balance
    
     let db = Store::<bdk::wallet::ChangeSet>::new_from_path(DB_MAGIC, CHAIN_DATA_FILE)?;
    
     // Create a new wallet
     let mut wallet = Wallet::new(external_descriptor, Some(internal_descriptor), db, network)?;
    
     // Get a new wallet address
     let address = wallet.get_address(AddressIndex::New);
     println!("Generated Address: {:?}", address);
    
     // Get the wallet balance before syncing
     let balance = wallet.get_balance();
     println!("Wallet balance before syncing: confirmed {} sats, trusted_pending {} sats, untrusted pending {} sats", balance.confirmed, balance.trusted_pending, balance.untrusted_pending);
    }
  6. Add code to create an async esplora client:

    #![allow(unused)]
    fn main() {
    use bdk_esplora::esplora_client;
    }
    #![allow(unused)]
    fn main() {
     // Create an async esplora client
    
     let client = esplora_client::Builder::new("http://signet.bitcoindevkit.net").build_async()?;
     let prev_tip = wallet.latest_checkpoint();
    }
  7. Add code to scans keychain SPKs for transaction histories, stopping after stop_gap is reached:

    #![allow(unused)]
    fn main() {
    use std::collections::BTreeMap;
    use std::{io, io::Write, str::FromStr};
    use bdk::chain::keychain::WalletUpdate;
    use bdk::{bitcoin::Network, descriptor, KeychainKind, Wallet};
    use bdk_esplora::{esplora_client, EsploraAsyncExt};
    
    const STOP_GAP: usize = 50;
    const PARALLEL_REQUESTS: usize = 5;
    }
    #![allow(unused)]
    fn main() {
     // Prepare the `IndexedTxGraph` update 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.
     if prompt("Scan wallet") {
         let keychain_spks = wallet
             .spks_of_all_keychains()
             .into_iter()
             // This `map` is purely for logging.
             .map(|(keychain, iter)| {
                 let mut first = true;
                 let spk_iter = iter.inspect(move |(i, _)| {
                     if first {
                         // TODO impl Display for Keychain
                         eprint!(
                             "\nscanning {}: ",
                             match keychain {
                                 KeychainKind::External => "External",
                                 KeychainKind::Internal => "Internal",
                             }
                         );
                         first = false;
                     }
                     eprint!("{} ", i);
                     // Flush early to ensure we print at every iteration.
                     let _ = io::stderr().flush();
                 });
                 (keychain, spk_iter)
             })
             .collect::<BTreeMap<_, _>>();
    
         // The client scans keychain spks for transaction histories, stopping after `stop_gap`
         // is reached. It returns a `TxGraph` update (`graph_update`) and a structure that
         // represents the last active spk derivation indices of keychains
         // (`keychain_indices_update`).
         let (graph_update, last_active_indices) = client
             .update_tx_graph(
                 keychain_spks,
                 core::iter::empty(),
                 core::iter::empty(),
                 STOP_GAP,
                 PARALLEL_REQUESTS,
             )
             .await?;
    
         println!();
         let missing_heights = wallet.tx_graph().missing_heights(wallet.local_chain());
         let chain_update = client
             .update_local_chain(prev_tip.clone(), missing_heights)
             .await?;
    
         let update = WalletUpdate {
             last_active_indices,
             graph: graph_update,
             chain: chain_update,
         };
         wallet.apply_update(update)?;
         wallet.commit()?;
         println!("Scan completed.");
    
         let balance = wallet.get_balance();
         println!("Wallet balance after scanning: confirmed {} sats, trusted_pending {} sats, untrusted pending {} sats",
                  balance.confirmed, balance.trusted_pending, balance.untrusted_pending);
     }
    }
  8. Add code to sync wallet by checking for history on all derived SPKs:

    #![allow(unused)]
    fn main() {
    use bdk::bitcoin::{bip32, Address, OutPoint, ScriptBuf, Txid};
    }
    #![allow(unused)]
    fn main() {
     // Syncing: We only check for specified spks, utxos and txids to update their confirmation
     //   status or fetch missing transactions.
     else {
         // Spks, outpoints and txids we want updates on will be accumulated here.
         let mut spks: Box<Vec<ScriptBuf>> = Box::new(Vec::new());
         let mut outpoints: Box<dyn Iterator<Item = OutPoint> + Send> =
             Box::new(core::iter::empty());
         let mut txids: Box<dyn Iterator<Item = Txid> + Send> = Box::new(core::iter::empty());
    
         // Sync all SPKs
         if prompt("Sync all SPKs") {
             // TODO add Wallet::all_spks() function, gives all tracked spks
             let all_spks: Vec<ScriptBuf> = wallet
                 .spk_index()
                 .all_spks()
                 .into_iter()
                 .map(|((keychain, index), script)| {
                     eprintln!(
                         "Checking if keychain: {}, index: {}, address: {} has been used",
                         match keychain {
                             KeychainKind::External => "External",
                             KeychainKind::Internal => "Internal",
                         },
                         index,
                         Address::from_script(script.as_script(), network).unwrap(),
                     );
                     // Flush early to ensure we print at every iteration.
                     let _ = io::stderr().flush();
                     (*script).clone()
                 })
                 .collect();
             spks = Box::new(all_spks);
         }
    
         let graph_update = client
             .update_tx_graph_without_keychain(spks.into_iter(), txids, outpoints, PARALLEL_REQUESTS)
             .await?;
    
         let missing_heights = wallet.tx_graph().missing_heights(wallet.local_chain());
         let chain_update = client.update_local_chain(prev_tip, missing_heights).await?;
    
         let update = WalletUpdate {
             // no update to active indices
             last_active_indices: BTreeMap::new(),
             graph: graph_update,
             chain: chain_update,
         };
         wallet.apply_update(update)?;
         wallet.commit()?;
         println!("Sync completed.");
    
         let balance = wallet.get_balance();
         println!("Wallet balance after syncing: confirmed {} sats, trusted_pending {} sats, untrusted pending {} sats",
                  balance.confirmed, balance.trusted_pending, balance.untrusted_pending);
     }
    }
  9. Add code to sync wallet by checking for history on only unused SPKs:

    #![allow(unused)]
    fn main() {
         // Sync only unused SPKs
         else if prompt("Sync only unused SPKs") {
             // TODO add Wallet::unused_spks() function, gives all unused tracked spks
             let unused_spks: Vec<ScriptBuf> = wallet
                 .spk_index()
                 .unused_spks(..)
                 .into_iter()
                 .map(|((keychain, index), script)| {
                     eprintln!(
                         "Checking if keychain: {}, index: {}, address: {} has been used",
                         match keychain {
                             KeychainKind::External => "External",
                             KeychainKind::Internal => "Internal",
                         },
                         index,
                         Address::from_script(script, network).unwrap(),
                     );
                     // Flush early to ensure we print at every iteration.
                     let _ = io::stderr().flush();
                     ScriptBuf::from(script)
                 })
                 .collect();
             spks = Box::new(unused_spks);
         }
    }
  10. Add code to sync wallet UTXOs to see if any have been spent:

#![allow(unused)]
fn main() {
     // Sync UTXOs
     if prompt("Sync 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 utxo_outpoints = wallet
             .list_unspent()
             .inspect(|utxo| {
                 eprintln!(
                     "Checking if outpoint {} (value: {}) has been spent",
                     utxo.outpoint, utxo.txout.value
                 );
                 // Flush early to ensure we print at every iteration.
                 let _ = io::stderr().flush();
             })
             .map(|utxo| utxo.outpoint);
         outpoints = Box::new(utxo_outpoints);
     };
}
  1. Add code to sync wallet unconfirmed TXs:
#![allow(unused)]
fn main() {
     // Sync unconfirmed TX
     if prompt("Sync unconfirmed TX") {
         // We want to search for whether the unconfirmed transaction is now confirmed.
         // We provide the unconfirmed txids to
         // `EsploraExt::update_tx_graph_without_keychain`.
         let unconfirmed_txids = wallet
             .transactions()
             .filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
             .map(|canonical_tx| canonical_tx.tx_node.txid)
             .inspect(|txid| {
                 eprintln!("Checking if {} is confirmed yet", txid);
                 // Flush early to ensure we print at every iteration.
                 let _ = io::stderr().flush();
             });
         txids = Box::new(unconfirmed_txids);
     }
}
  1. Add code to check the new wallet balance and request a deposit if required:
#![allow(unused)]
fn main() {
const SEND_AMOUNT: u64 = 5000;
}
#![allow(unused)]
fn main() {
 // Check balance and request deposit if required
 if balance.total() < SEND_AMOUNT {
     println!(
         "Please send at least {} sats to {} using: https://signetfaucet.com/",
         SEND_AMOUNT, address.address
     );
     std::process::exit(0);
 }
}
  1. Add code to create a TX to return sats to the signet faucet:
#![allow(unused)]
fn main() {
 // Create TX to return sats to signet faucet https://signetfaucet.com/
 let faucet_address = Address::from_str("tb1qg3lau83hm9e9tdvzr5k7aqtw3uv0dwkfct4xdn")?
     .require_network(network)?;

 let mut tx_builder = wallet.build_tx();
 tx_builder
     .add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT)
     // .drain_to(faucet_address.script_pubkey())
     // .drain_wallet()
     .fee_rate(FeeRate::from_sat_per_vb(2.1))
     .enable_rbf();

 let mut psbt = tx_builder.finish()?;
 let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
 assert!(finalized);

 let tx = psbt.extract_tx();
 let (sent, received) = wallet.sent_and_received(&tx);
 let fee = wallet.calculate_fee(&tx).expect("fee");
 let fee_rate = wallet
     .calculate_fee_rate(&tx)
     .expect("fee rate")
     .as_sat_per_vb();
 println!(
     "Created tx sending {} sats to {}",
     sent - received - fee,
     faucet_address
 );
 println!(
     "Fee is {} sats, fee rate is {:.2} sats/vbyte",
     fee, fee_rate
 );

 if prompt("Broadcast") {
     client.broadcast(&tx).await?;
     println!(
         "Tx broadcast! https://mempool.space/signet/tx/{}",
         tx.txid()
     );
 }
}