Skip to content

Quick Start Example

This page provides an overview of how BDK can be leveraged to create and sync a wallet using an Esplora client. You can find working code examples of this workflow in three programming languages: Rust, Swift, and Kotlin.

Tip

This page is up-to-date with version 1.0.0-beta.5 of bdk_wallet.

Create a new project

cargo init quickstart
cd quickstart

Create a new Swift project in Xcode.

Create a new Kotlin project.

Add required dependencies

Cargo.toml
1
2
3
4
5
6
7
8
[package]
name = "quickstart"
version = "0.1.0"
edition = "2021"

[dependencies]
bdk_wallet = { version = "=1.0.0-beta.5" }
bdk_esplora = { version = "=0.19.0", features = ["blocking"] }
  1. From the Xcode File menu, select Add Package Dependencies...
  2. Enter https://github.com/bitcoindevkit/bdk-swift into the package repository URL text field
build.gradle
repositories {
    mavenCentral()
}

dependencies {
    // for jvm
    implementation 'org.bitcoindevkit:bdk-jvm:<version>'
    // OR for android
    implementation 'org.bitcoindevkit:bdk-android:<version>'
}

Create a wallet, sync it and display the balance

We'll give a breakdown of the key pieces of this code in the next section.

examples/rust/quickstart/src/main.rs
use bdk_wallet::AddressInfo;
use bdk_wallet::KeychainKind;
use bdk_wallet::bitcoin::Network;
use bdk_wallet::Wallet;
use bdk_esplora::EsploraExt;
use bdk_esplora::esplora_client::Builder;
use bdk_esplora::esplora_client;
use bdk_wallet::chain::spk_client::{FullScanRequestBuilder, FullScanResult};

const STOP_GAP: usize = 50;
const PARALLEL_REQUESTS: usize = 1;

fn main() -> () {

    let descriptor: &str = "tr([12071a7c/86'/1'/0']tpubDCaLkqfh67Qr7ZuRrUNrCYQ54sMjHfsJ4yQSGb3aBr1yqt3yXpamRBUwnGSnyNnxQYu7rqeBiPfw3mjBcFNX4ky2vhjj9bDrGstkfUbLB9T/0/*)#z3x5097m";
    let change_descriptor: &str = "tr([12071a7c/86'/1'/0']tpubDCaLkqfh67Qr7ZuRrUNrCYQ54sMjHfsJ4yQSGb3aBr1yqt3yXpamRBUwnGSnyNnxQYu7rqeBiPfw3mjBcFNX4ky2vhjj9bDrGstkfUbLB9T/1/*)#n9r4jswr";

    // Create the wallet
    let mut wallet: Wallet = Wallet::create(descriptor, change_descriptor)
        .network(Network::Signet)
        .create_wallet_no_persist()
        .unwrap();

    // Sync the wallet
    let client: esplora_client::BlockingClient = Builder::new("https://mutinynet.com/api").build_blocking();

    println!("Syncing wallet...");
    let full_scan_request: FullScanRequestBuilder<KeychainKind> = wallet.start_full_scan();
    let update: FullScanResult<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());

    // Reveal a new address from your external keychain
    let address: AddressInfo = wallet.reveal_next_address(KeychainKind::External);
    println!("Generated address {} at index {}", address.address, address.index);

}
examples/swift/quickstart/main.swift
let descriptor = "tr([12071a7c/86'/1'/0']tpubDCaLkqfh67Qr7ZuRrUNrCYQ54sMjHfsJ4yQSGb3aBr1yqt3yXpamRBUwnGSnyNnxQYu7rqeBiPfw3mjBcFNX4ky2vhjj9bDrGstkfUbLB9T/0/*)#z3x5097m"
let changeDescriptor = "tr([12071a7c/86'/1'/0']tpubDCaLkqfh67Qr7ZuRrUNrCYQ54sMjHfsJ4yQSGb3aBr1yqt3yXpamRBUwnGSnyNnxQYu7rqeBiPfw3mjBcFNX4ky2vhjj9bDrGstkfUbLB9T/1/*)#n9r4jswr"

let wallet = try Wallet(
    descriptor: descriptor, 
    changeDescriptor: changeDescriptor, 
    network: Network.signet
)

let addressInfo = wallet.revealNextAddress(keychain: .external)
print("Generated address \(addressInfo.address) at index \(addressInfo.index)")

let esploraClient = EsploraClient(url: "https://mutinynet.com/api")

let syncRequest = wallet.startSyncWithRevealedSpks()
let update = try esploraClient.sync(
    syncRequest: syncRequest,
    parallelRequests: UInt64(5)
)
try wallet.applyUpdate(update: update)
let balance = wallet.balance()
print("Wallet balance: \(balance.total) sat")
examples/kotlin/quickstart/main.kt
val descriptor = "tr([12071a7c/86'/1'/0']tpubDCaLkqfh67Qr7ZuRrUNrCYQ54sMjHfsJ4yQSGb3aBr1yqt3yXpamRBUwnGSnyNnxQYu7rqeBiPfw3mjBcFNX4ky2vhjj9bDrGstkfUbLB9T/0/*)#z3x5097m"
val changeDescriptor = "tr([12071a7c/86'/1'/0']tpubDCaLkqfh67Qr7ZuRrUNrCYQ54sMjHfsJ4yQSGb3aBr1yqt3yXpamRBUwnGSnyNnxQYu7rqeBiPfw3mjBcFNX4ky2vhjj9bDrGstkfUbLB9T/1/*)#n9r4jswr"

val wallet = Wallet(
  descriptor, 
  changeDescriptor, 
  Network.SIGNET
)

val addressInfo = wallet.revealNextAddress(KeychainKind.EXTERNAL)
println("Generated address ${addressInfo.address} at index ${addressInfo.index}")

val esploraClient: EsploraClient = EsploraClient("https://mutinynet.com/api")

val syncRequest = wallet.startSyncWithRevealedSpks()
val update = try esploraClient.sync(
    syncRequest,
    5uL
)
wallet.applyUpdate(update)
val balance = wallet.balance()
println("Wallet balance: ${balance.total()} sat")

Build and run:

The wallet will take a few seconds to sync, then you should see the wallet balance printed in the terminal.

cargo build
cargo run

Run the project in Xcode.

Run the project in your IDE.

Let's take a closer look

Descriptors

First we need some descriptors to instantiate our wallet. In this example we use public key descriptors to simply display the balance of a wallet. To sign transactions you will need to use a wallet that is instantiated with private key descriptors. Refer to the Creating Descriptors page for information on how to generate your own private descriptors.

let descriptor: &str = "tr([12071a7c/86'/1'/0']tpubDCaLkqfh67Qr7ZuRrUNrCYQ54sMjHfsJ4yQSGb3aBr1yqt3yXpamRBUwnGSnyNnxQYu7rqeBiPfw3mjBcFNX4ky2vhjj9bDrGstkfUbLB9T/0/*)#z3x5097m";
let change_descriptor: &str = "tr([12071a7c/86'/1'/0']tpubDCaLkqfh67Qr7ZuRrUNrCYQ54sMjHfsJ4yQSGb3aBr1yqt3yXpamRBUwnGSnyNnxQYu7rqeBiPfw3mjBcFNX4ky2vhjj9bDrGstkfUbLB9T/1/*)#n9r4jswr";
let descriptor = "tr([12071a7c/86'/1'/0']tpubDCaLkqfh67Qr7ZuRrUNrCYQ54sMjHfsJ4yQSGb3aBr1yqt3yXpamRBUwnGSnyNnxQYu7rqeBiPfw3mjBcFNX4ky2vhjj9bDrGstkfUbLB9T/0/*)#z3x5097m"
let changeDescriptor = "tr([12071a7c/86'/1'/0']tpubDCaLkqfh67Qr7ZuRrUNrCYQ54sMjHfsJ4yQSGb3aBr1yqt3yXpamRBUwnGSnyNnxQYu7rqeBiPfw3mjBcFNX4ky2vhjj9bDrGstkfUbLB9T/1/*)#n9r4jswr"
val descriptor = "tr([12071a7c/86'/1'/0']tpubDCaLkqfh67Qr7ZuRrUNrCYQ54sMjHfsJ4yQSGb3aBr1yqt3yXpamRBUwnGSnyNnxQYu7rqeBiPfw3mjBcFNX4ky2vhjj9bDrGstkfUbLB9T/0/*)#z3x5097m"
val changeDescriptor = "tr([12071a7c/86'/1'/0']tpubDCaLkqfh67Qr7ZuRrUNrCYQ54sMjHfsJ4yQSGb3aBr1yqt3yXpamRBUwnGSnyNnxQYu7rqeBiPfw3mjBcFNX4ky2vhjj9bDrGstkfUbLB9T/1/*)#n9r4jswr"

These are taproot tr() descriptors using public keys on testnet (or signet) tpub as described in BIP86. The descriptor is an HD wallet with a path for generating addresses to give out externally for payment. We also have a second change_descriptor that we can use to generate addresses to pay ourseves change when sending payments (remeber that UTXOs must be spent if full, so you often want to make change).

Blockchain Client and Network

This example is using an Esplora client on the Mutinynet Signet network.

let client: esplora_client::BlockingClient = Builder::new("https://mutinynet.com/api").build_blocking();
let esploraClient = EsploraClient(url: "https://mutinynet.com/api")
val esploraClient: EsploraClient = EsploraClient("https://mutinynet.com/api")

Other options for blockchain clients include running an Electrum light wallet or using RPC on a bitcoind fullnode. We are using Esplora in this example.

This example also used the Signet network. You may alternatively want to run this example wallet using a locally hosted Regtest network. The details of how to set that up are beyond the scope of this example.

Scan

Once we have our wallet setup and connected to the network, we scan the network to detect UTXOs relevant to our wallet.

1
2
3
4
5
6
7
let full_scan_request: FullScanRequestBuilder<KeychainKind> = wallet.start_full_scan();
let update: FullScanResult<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());
1
2
3
4
5
6
7
8
let syncRequest = wallet.startSyncWithRevealedSpks()
let update = try esploraClient.sync(
    syncRequest: syncRequest,
    parallelRequests: UInt64(5)
)
try wallet.applyUpdate(update: update)
let balance = wallet.balance()
print("Wallet balance: \(balance.total) sat")
1
2
3
4
5
6
7
8
val syncRequest = wallet.startSyncWithRevealedSpks()
val update = try esploraClient.sync(
    syncRequest,
    5uL
)
wallet.applyUpdate(update)
val balance = wallet.balance()
println("Wallet balance: ${balance.total()} sat")

This scanning process is detailed on the Full Scan vs Sync page. The scanning process checks child pubkeys for the descriptors specified in the wallet to detect UTXOs that are relevant to the wallet. That data is then applied to the wallet.

Display Wallet Balance

Finally we can print the wallet.balance() to see how many sats we have available based on the information gathered in the scanning process.