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 theBDK 1.0
project page.The
bitcoindevkit/bdk-ffi
project has not yet been updated to use the newBDK 1.0
crates. For current status and timeline forBDK-FFI 1.0
see theBDK-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 levelWallet
type that is built from the low-level mechanisms provided by the other componentschain
: Tools for storing and indexing chain datafile_store
: A (experimental) persistence backend for storing chain data in a single file.esplora
: Extends theesplora-client
crate with methods to fetch chain data from an esplora HTTP server in the form thatbdk_chain
andWallet
can consume.electrum
: Extends theelectrum-client
crate with methods to fetch chain data from an electrum server in the form thatbdk_chain
andWallet
can consume.
Fully working examples of how to use these components are in /example-crates
:
example_cli
: Library used by theexample_*
crates. Provides utilities for syncing, showing the balance, generating addresses and creating transactions without using the bdkWallet
.example_electrum
: A command line Bitcoin wallet application built on top ofexample_cli
and theelectrum
crate. It shows the power of the bdk tools (chain
+file_store
+electrum
), without depending on the mainbdk
library.wallet_esplora_blocking
: Uses theWallet
to sync and spend using the Esplora blocking interface.wallet_esplora_async
: Uses theWallet
to sync and spend using the Esplora asynchronous interface.wallet_electrum
: Uses theWallet
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.
- Create a new Rust project:
cargo init my_bdk_app cd my_bdk_app
- Add
bdk
to yourCargo.toml
file. Find the latestbdk@1
release oncrates.io
, for example:cargo add bdk@1.0.0-alpha.1
- 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.
- Clone the repo:
git clone https://github.com/bitcoindevkit/bdk.git cd bdk
- 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:
- the "external" keychain output descriptor
- an optional "internal" keychain output descriptor
- optional signing keys
- storage for wallet state data
- a view of the current blockchain
- an indexed graph of wallet transactions
The common operations that can be performed with a BDK wallet are:
- get a new address
- list unspent transaction outputs
- list transactions
- get the wallet balance
- build a new unsigned transaction
- build a transaction to bump the fee of an unconfirmed transaction
- sign an unsigned or partially signed transaction
- finalize a partially signed transaction
- cancel an unconfirmed transaction
File Store
Blockchain Clients
Esplora
Electrum
Chain
Wallet with Async Explora
-
Create a new Rust project:
cargo init my_bdk_app cd my_bdk_app
-
Add
bdk
to yourCargo.toml
file.Find the latest(use pre-released branch for now):BDK@1.0.0
release oncrates.io
cargo add bdk --git "https://github.com/notmandatory/bdk.git" --branch "test/esplora_tests"
-
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"
-
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(()) }
-
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); }
-
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(); }
-
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); } }
-
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); } }
-
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); } }
-
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); }; }
- 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); } }
- 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); } }
- 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() ); } }