# Introduction
This article should serve as a "getting started" guide for developers who are considering integrating BDK in their projects: it tries to introduce the reader to the basic concepts behind the library and some of its modules and components that can be used to build a very simple functioning Bitcoin wallet. All the information written in this article are valid for the latest published version (opens new window).
# Design Goals
The main goal of the library is to be a solid foundation for Bitcoin wallets of any kind, on any platform: in practice, this means that the library should be:
- Very well-reviewed and tested
- Lightweight, so that it can be used easily on mobile devices as well
- Extendable, so that it can be adapted to perfectly suit different use-cases
- Generalized, meaning that it supports different types of Bitcoin scripts and wallets through the use of descriptors
- Reasonably easy to use, exposing a "high level" interface to the user and hiding all the complexity inside
These goals have a direct impact on the design of the internal components of the library, and as a consequence on the APIs that are exposed to the final user, which might in some cases feel counter-intuitive at first. Throughout the article, we will try to focus on those points and try to explain them as best as we can.
# The Wallet
Structure
The Wallet
(opens new window) structure is in many ways the heart of the library: it represents an instance of a wallet and exposes some APIs to perform all the typical operations one might want to do with a Bitcoin wallet, such as generating a new address, listing the transactions received, creating a transaction, etc.
A Wallet
instance can be constructed given at least one descriptor which would be used to derive both External
(opens new window) and Internal
(opens new window) addresses, or two if one prefers to keep them separated. External
addresses are the ones returned by the generic Wallet::get_address()
(opens new window) call, while Internal
addresses are generated internally to receive the change whenever a new transaction is created. But they can be generated on demand too using Wallet::get_internal_address()
(opens new window) call.
A Wallet
also needs at least one other component to function properly, its Database
(opens new window): it will be used as a cache to store the list of transactions synchronized with the blockchain, the UTXOs, the addresses generated, and a few other things. It's important to note that the Database
will never store any secret. Securely storing keys is explicitly left to the user of the library to implement, mainly because there isn't really one good way to do it, that would work reliably on every platform. On
mobile devices, for instance, the OS' keychain could be used, to allow unlocking the secrets with the use of biometric data (FaceID or fingerprint), while on desktop platform there isn't generally a similar framework available and the user would have to implement something that meets their needs. It's not excluded that in the future we could provide a "reference implementation" of secure multi-platform storage for keys, but that would very likely be released as a separate module outside of the Wallet
structure, or potentially even as a separate library that could be reused for other applications as well.
Going back to our Wallet
: given a descriptor and a Database
we can build an "air-gapped" or "Offline" wallet. Basically, we can make a wallet that physically can't connect to the Bitcoin network. It will still be able to generate addresses and sign PSBTs (opens new window), but with a greatly reduced attack surface because a sizable part of the code that handles the logic to synchronize with the network would be entirely omitted in the final executable binary.
This is how a Wallet
can be created. Notice that we are using MemoryDatabase
(opens new window) as our Database
. We'll get to that in a second.
use bdk::{
bitcoin::Network,
database::MemoryDatabase,
Wallet,
wallet::AddressIndex,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)";
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/1/*)";
let wallet: Wallet<MemoryDatabase> = Wallet::new(
external_descriptor,
Some(internal_descriptor),
Network::Testnet,
MemoryDatabase::new(),
)?;
Ok(())
}
Once we have our Wallet
instance we can generate a new address and print it out:
// ...
let address = wallet.get_address(AddressIndex::New)?;
println!("Generated Address: {}", address);
Building and running this code will print out:
Generated Address: tb1q7w0t936xp5p994qx506xj53gjdcmzjr2mkqghn
Before we've talked about the benefits of an air-gapped wallet, but we should also talk about the disadvantages: the biggest one is the fact that it cannot create new transactions because it doesn't know which UTXOs belong to the wallet. To get this information we generally need to sync
with the network, but this wallet can't physically do that.
To fix this we can add one more component in our code: a Blockchain
(opens new window) backend. In particular, we are going to use the ElectrumBlockchain
(opens new window) which syncs with an Electrum
server, and then we will use this blockchain to sync
our wallet database with current state of the network since that's available out of the box in BDK and is pretty fast.
We can update our code to look something like this:
use bdk::{
blockchain::ElectrumBlockchain,
bitcoin::Network,
database::MemoryDatabase,
electrum_client::Client,
wallet::{Wallet, AddressIndex},
};
// ...
let client = Client::new("ssl://electrum.blockstream.info:60002")?;
let blockchain = ElectrumBlockchain::from(client);
Specifically here, we create an ElectrumBlockchain
and connect to Blockstream's public Electrum Testnet servers over SSL.
Now, since we are running in the Testnet
network, we can try to get some funds from a faucet online to this address we've generated. Once we have an incoming transaction we can do the first sync
of our wallet.
This is again something that might seem counterintuitive at first: why do we have to manually ask the Wallet
to sync itself? Can't it do it periodically in background? The answer is that yes, that would definitely be possible, but it would remove some control on what's happening inside the wallet from the user. This can be especially problematic on mobile platforms, where the OS tries very aggressively to suspend apps in background to save battery. Having a thread running and trying to make network requests while the app is in background would very likely cause errors or potentially crashes somewhere. So, for this reason this operation has to be performed manually, to allow the user to call that function only at the right time.
use bdk::SyncOptions;
// ...
wallet.sync(&blockchain, SyncOptions::default())?;
The SyncOptions
determines some sync time behaviors, like progress update, etc. For this case the default
sync option with no progress update is adequate. This will make queries to the Electrum server and store the list of transactions and UTXOs in our Database
. In this case, we are using a MemoryDatabase
, so those data are only going to be kept in RAM and dropped once our Wallet
is dropped. This is very useful for playing around and experimenting, but not so great for real-world wallets: for that, you can use sled (opens new window) which is supported out of the box, or even use a custom database. More on that later!
So, now that we've synced with the blockchain we can create our first transaction. First of all, we will print out the balance of our wallet to make sure that our wallet has seen the incoming transaction. Then we will create the actual transaction and we will specify some flags using the TxBuilder
(opens new window). To finish it off, we will ask the wallet to sign the transaction and then broadcast it to the network.
Right now we will not get into details of all the available options in TxBuilder
since that is definitely out of the scope of a "getting started" guide. For now, you can just imagine the builder as your way to tell the library how to build transactions. We'll come back to this in a future article.
use bdk::bitcoin::Address;
use std::str::FromStr;
// ...
let balance = wallet.get_balance()?;
println!("Wallet balance in SAT: {}", balance);
let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?;
let mut tx_builder = wallet.build_tx();
tx_builder
.add_recipient(faucet_address.script_pubkey(), (balance.trusted_pending + balance.confirmed) / 2)
.enable_rbf();
let (mut psbt, tx_details) = tx_builder.finish()?;
println!("Transaction details: {:#?}", tx_details);
In this case, we are sending back half the balance to the faucet's address and we are also enabling RBF since the default fees are at 1 satoshi/vbyte. With RBF we will be able to bump the fees of the transaction, should it get stuck in the mempool due to the low fee rate.
All that's left to do once we have our unsigned PSBT is to sign it:
// ...
use bdk::SignOptions;
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
assert!(finalized, "Tx has not been finalized");
println!("Transaction Signed: {}", finalized);
And then broadcast it:
// ...
use bdk::blockchain::Blockchain;
let raw_transaction = psbt.extract_tx();
let txid = raw_transaction.txid();
blockchain.broadcast(&raw_transaction)?;
println!(
"Transaction sent! TXID: {txid}.\nExplorer URL: https://blockstream.info/testnet/tx/{txid}",
txid = txid
);
# Custom Database and Blockchain types
We briefly mentioned before that for our example we used the MemoryDatabase
, but that it could also be swapped for a different one: this is one example of the modularity of BDK. By default, some database types are already implemented in the library, namely the MemoryDatabase (opens new window) which only keeps data in RAM, the Sled (opens new window) database that can store data on a filesystem, and the SqliteDatabase (opens new window) that can store data into a SQLite database. But since the Database
trait is public, users of the library can also implement different database types more suitable for their use-case.
The same is true for the Blockchain
types: the library provides (through the use of opt-in features) implementations for the Electrum
, Esplora
, CompactFilters
(Neutrino) and Bitcoin Core rpc
backends. Those again can also be
swapped with custom types if the user desires to do so.
# Conclusion
Hopefully, this article will help you get started with BDK! This is just a very quick and gentle introduction to the library, and only barely scratches the surface of what's inside: we will keep publishing more articles in the future to explain some of the more advanced features of BDK, like key generation, using complex descriptors with multiple keys and/or timelocks, using external signers, etc.
If you'd like to learn more about the library feel free to ask any questions in the comment section down below, or join our Discord Community (opens new window) to chat with us directly!