Skip to content

Starter Example

So you want to build a bitcoin wallet using BDK. Great! Here is the rough outline of what you need to do just that. A standard, simple example of a bitcoin wallet in BDK-land would require 3 core pillars:

                    +-----------------+
                    |                 |
                    |     Wallet      |
                    |                 |
                    +-----------------+
                      /             \
                     /               \
    +----------------------+   +----------------------+
    | Persistence          |   | Blockchain Client    |
    |   - SQLite           |   |   - Electrum         |
    |   - Flat file        |   |   - Esplora          |
    |                      |   |   - Bitcoin Core RPC |
    |                      |   |   - CBF (Kyoto)      |
    +----------------------+   +----------------------+
  1. The bdk_wallet library, which will provide two core types: the Wallet and the TxBuilder. This library will handle all the domain logic related to keeping track of which UTXOs you own, what your total balance is, creating and signing transactions, etc.
  2. A blockchain client. Your wallet will need to keep track of blockchain data, like new transactions that have been added to the blockchain that impact your wallet, requesting these transactions from a Bitcoin Core node, an Electrum or Esplora server, etc.
  3. A persistence mechanism for saving wallet data between sessions (note that this is not actually required). Things like which addresses the wallet has revealed and what is the state of the blockchain on its last sync are things that are kept in persistence and can be loaded on startup.

Diving in!

This page provides a starter example showcasing how BDK can be used to create, sync, and manage a wallet using an Esplora client as a blockchain data source. Familiarity with this example will help you work through the more advanced pages in this section.

You can find working code for this example in a multitude of programming languages: Rust, Swift, Kotlin, Python, and WASM. Note that some additional language bindings are available for BDK, namely Dart (Flutter) and React Native.

Create a new project

cargo init starter-example
cd starter-example

Add required dependencies

Cargo.toml
[package]
name = "starter-example"
version = "0.1.0"
edition = "2021"

[dependencies]
bdk_wallet = { version = "3.1.0", features = ["rusqlite"] }
bdk_esplora = { version = "0.22.2", features = ["blocking"] }

Use descriptors

To create a wallet using BDK, we need some descriptors for our wallet. This example uses public descriptors (meaning they cannot be used to sign transactions) to create the wallet, sync, and build transactions, and the associated private descriptors to sign the PSBT. Refer to the Creating Descriptors page for information on how to generate your own private descriptors.

// Using the "awesome awesome awesome awesome awesome awesome awesome awesome awesome awesome awesome awesome" mnemonic and BIP-86
let descriptor: &str = "tr([5bc5d243/86'/1'/0']tpubDC72NVP1RK5qwy2QdEfWphDsUBAfBu7oiV6jEFooHP8tGQGFVUeFxhgZxuk1j6EQRJ1YsS3th2RyDgReRqCL4zqp4jtuV2z7gbiqDH2iyUS/0/*)#xh44xwsp";
let change_descriptor: &str = "tr([5bc5d243/86'/1'/0']tpubDC72NVP1RK5qwy2QdEfWphDsUBAfBu7oiV6jEFooHP8tGQGFVUeFxhgZxuk1j6EQRJ1YsS3th2RyDgReRqCL4zqp4jtuV2z7gbiqDH2iyUS/1/*)#hrs5mmqe";

These are taproot descriptors (tr()) using public keys on Regtest (tpub) as described in BIP86. The first descriptor is an HD wallet with a path for generating addresses to give out externally for payments. The second one is used by the wallet to generate addresses to pay ourselves change when sending payments.

Create or load a wallet

Next let's load up our wallet.

starter-example/src/main.rs
// Initiate the connection to the database
let mut conn = Connection::open(DB_PATH).expect("Can't open database");

// Create the wallet
let wallet_opt = Wallet::load()
    .descriptor(KeychainKind::External, Some(descriptor))
    .descriptor(KeychainKind::Internal, Some(change_descriptor))
    .check_network(Network::Regtest)
    .load_wallet(&mut conn)
    .unwrap();

let mut wallet = if let Some(loaded_wallet) = wallet_opt {
    loaded_wallet
} else {
    Wallet::create(descriptor, change_descriptor)
        .network(Network::Regtest)
        .create_wallet(&mut conn)
        .unwrap()
};

Sync the wallet

Now let's build an Esplora client and use it to request transaction history for the wallet.

starter-example/src/main.rs
// Sync the wallet
let client: esplora_client::BlockingClient =
    Builder::new("http://127.0.0.1:3002").build_blocking();

println!("Syncing wallet...");
let full_scan_request: FullScanRequestBuilder<KeychainKind> = wallet.start_full_scan();
let update: FullScanResponse<KeychainKind> = client
    .full_scan(full_scan_request, STOP_GAP, PARALLEL_REQUESTS)
    .unwrap();

// Apply the update from the full scan to the wallet
wallet.apply_update(update).unwrap();

let balance = wallet.balance();
println!("Wallet balance: {} sat", balance.total().to_sat());

In cases where you are using new descriptors that do not have a balance yet, the example will request a new address from the wallet and print it out so you can fund the wallet. Remember that this example uses Regtest coins!

starter-example/src/main.rs
if balance.total().to_sat() < 50000 {
    println!("Your wallet does not have sufficient balance for the following steps!");
    // Reveal a new address from your external keychain
    let address: AddressInfo = wallet.reveal_next_address(KeychainKind::External);
    println!(
        "Send Regtest coins to {} (index {})",
        address.address, address.index
    );
    wallet.persist(&mut conn).expect("Cannot persist");
    exit(0)
}

Create a transaction

For this step you'll need a wallet funded with some Regtest satoshis.

Let's prepare a transaction. The three core choices here are (1) where to send the funds, (2) how much to send, (3) what fees to pay. We will send funds to a generic regtest address using a feerate of 4 satoshis per vbyte.

starter-example/src/main.rs
// Use a faucet return address
let faucet_address = Address::from_str("bcrt1qxzh0r7mlztv3m8vxet5xxnsy9zh7j5tshh6vhp")
    .unwrap()
    .require_network(Network::Regtest)
    .unwrap();

let send_amount: Amount = Amount::from_sat(12345);

Here we are sending 12345 sats (make sure the wallet has at least this much balance, or change this value).

Finally we are ready to build the transaction:

starter-example/src/main.rs
let mut builder = wallet.build_tx();
builder
    .fee_rate(FeeRate::from_sat_per_vb(4).unwrap())
    .add_recipient(faucet_address.script_pubkey(), send_amount);

let mut psbt: Psbt = builder.finish().unwrap();

Sign and broadcast

Now that our PSBT is ready for signing, we load up the private keys in memory and use them to sign our PSBT, and finally broadcast it using the Esplora client.

starter-example/src/main.rs
// Using the "awesome awesome awesome awesome awesome awesome awesome awesome awesome awesome awesome awesome" mnemonic and BIP-86
let private_descriptor: &str = "tr(tprv8ZgxMBicQKsPdWAHbugK2tjtVtRjKGixYVZUdL7xLHMgXZS6BFbFi1UDb1CHT25Z5PU1F9j7wGxwUiRhqz9E3nZRztikGUV6HoRDYcqPhM4/86'/1'/0'/0/*)#x627tk5a";
let private_change_descriptor: &str = "tr(tprv8ZgxMBicQKsPdWAHbugK2tjtVtRjKGixYVZUdL7xLHMgXZS6BFbFi1UDb1CHT25Z5PU1F9j7wGxwUiRhqz9E3nZRztikGUV6HoRDYcqPhM4/86'/1'/0'/1/*)#hw0lkry9";

let (_, external_keymap) =
    Descriptor::parse_descriptor(wallet.secp_ctx(), private_descriptor).unwrap();
let (_, internal_keymap) =
    Descriptor::parse_descriptor(wallet.secp_ctx(), private_change_descriptor).unwrap();
let secp = bdk_wallet::bitcoin::secp256k1::Secp256k1::new();

let external_signers_container = SignersContainer::build(
    external_keymap,
    wallet.public_descriptor(KeychainKind::External),
    &secp,
);
let internal_signers_container = SignersContainer::build(
    internal_keymap,
    wallet.public_descriptor(KeychainKind::Internal),
    &secp,
);

let signers: &[&SignersContainer; 2] =
    &[&external_signers_container, &internal_signers_container];

let psbt_was_signed_and_finalized: bool = wallet
    .sign_with_signers(&mut psbt, signers, SignOptions::default())
    .unwrap();
assert!(psbt_was_signed_and_finalized);

let tx = psbt.extract_tx().unwrap();
client.broadcast(&tx).unwrap();
println!("Transaction broadcast! Txid: {}", tx.compute_txid());