Skip to content

Simple Starter Example

Overview

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:

  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 examples of this example in three programming languages: Rust, Swift, and Kotlin. (Note: some additional language bindings are available for BDK, see 3rd Party Bindings).

Tip

To complete this example from top to bottom, you'll need to create new descriptors and replace the ones provided. Once you do so, you'll run the example twice; on first run the wallet will not have any balance and will exit with an address to send funds to. Once that's done, you can run the example again and the wallet will be able to perform the later steps, namely creating and broadcasting a new transaction.

Create a new project

=== "Rust"

```shell
cargo init starter-example
cd starter-example
```

=== "Swift"

```shell
swift package init --type executable
```

=== "Kotlin"

```shell
gradle init
```

Add required dependencies

=== "Rust"

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

[dependencies]
bdk_wallet = { version = "2.2.0", features = ["rusqlite"] }
bdk_esplora = { version = "0.22.1", features = ["blocking"] }

```

=== "Swift"

```toml title="Package.swift"

```
Or, if you're building an iOS app:

1. From the Xcode File menu, select Add Package Dependencies...
2. Enter `https://github.com/bitcoindevkit/bdk-swift` into the package repository URL search field and bdk-swift should come up
3. For the Dependency Rule select `Exact Version`, enter the version number (same as Package.swift) and click Add Package

=== "Kotlin"

```kotlin title="build.gradle.kts"
repositories {
    mavenCentral()
}

dependencies {
    // for JVM
    implementation("org.bitcoindevkit:bdk-jvm:1.1.0")
    // for Android
    implementation("org.bitcoindevkit:bdk-android:1.1.0")
}
```

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) on Signet. Step 7 and below will fail unless you replace those public descriptors with private ones of your own and fund them using Signet coins through a faucet. Refer to the Creating Descriptors page for information on how to generate your own private descriptors.

Warning

Note that if you replace the descriptors after running the example using the provided ones, you must delete or rename the database file or will get an error on wallet load.

=== "Rust"

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

=== "Swift"

```swift
let descriptor = try Descriptor(descriptor: "tr([12071a7c/86'/1'/0']tpubDCaLkqfh67Qr7ZuRrUNrCYQ54sMjHfsJ4yQSGb3aBr1yqt3yXpamRBUwnGSnyNnxQYu7rqeBiPfw3mjBcFNX4ky2vhjj9bDrGstkfUbLB9T/0/*)#z3x5097m", network: Network.signet)
let changeDescriptor = try Descriptor(descriptor: "tr([12071a7c/86'/1'/0']tpubDCaLkqfh67Qr7ZuRrUNrCYQ54sMjHfsJ4yQSGb3aBr1yqt3yXpamRBUwnGSnyNnxQYu7rqeBiPfw3mjBcFNX4ky2vhjj9bDrGstkfUbLB9T/1/*)#n9r4jswr", network: Network.signet)
```

=== "Kotlin"

```kotlin
    val descriptor = Descriptor("tr([12071a7c/86'/1'/0']tpubDCaLkqfh67Qr7ZuRrUNrCYQ54sMjHfsJ4yQSGb3aBr1yqt3yXpamRBUwnGSnyNnxQYu7rqeBiPfw3mjBcFNX4ky2vhjj9bDrGstkfUbLB9T/0/*)#z3x5097m", Network.SIGNET)
    val changeDescriptor = Descriptor("tr([12071a7c/86'/1'/0']tpubDCaLkqfh67Qr7ZuRrUNrCYQ54sMjHfsJ4yQSGb3aBr1yqt3yXpamRBUwnGSnyNnxQYu7rqeBiPfw3mjBcFNX4ky2vhjj9bDrGstkfUbLB9T/1/*)#n9r4jswr", Network.SIGNET)
```

These are taproot descriptors (tr()) using public keys on Signet (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 (remember that UTXOs must be spent in full, so you often need to make change).

Create or load a wallet

Next let's load up our wallet.

=== "Rust"

```rust title="examples/rust/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))
        // .extract_keys() // uncomment this line when using private descriptors
        .check_network(Network::Signet)
        .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::Signet)
            .create_wallet(&mut conn)
            .unwrap()
    };
```

=== "Swift"

```swift
let wallet: Wallet
let connection: Connection

if FileManager.default.fileExists(atPath: dbFilePath.path) {
    print("Loading up existing wallet")
    connection = try Connection(path: dbFilePath.path)
    wallet = try Wallet.load(
        descriptor: descriptor,
        changeDescriptor: changeDescriptor,
        connection: connection
    )
} else {
    print("Creating new wallet")
    connection = try Connection(path: dbFilePath.path)
    wallet = try Wallet(
        descriptor: descriptor,
        changeDescriptor: changeDescriptor,
        network: Network.signet,
        connection: connection
    )
}
```

=== "Kotlin"

```kotlin title="examples/kotlin/starter-example/src/.../App.kt"
    val persistenceExists = File(PERSISTENCE_FILE_PATH).exists()
    val connection = Connection(PERSISTENCE_FILE_PATH)

    val wallet = if (persistenceExists) {
        println("Loading up existing wallet")
        Wallet.load(
            descriptor = descriptor,
            changeDescriptor = changeDescriptor,
            connection = connection
        )
    } else {
        println("Creating new wallet")
        Wallet(
            descriptor = descriptor,
            changeDescriptor = changeDescriptor,
            network = Network.SIGNET,
            connection = connection
        )
    }
```

Sync the wallet

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

Test 1

use bdk_wallet::{AddressInfo, KeychainKind};
use std::process::exit;
use std::str::FromStr;

const STOP_GAP: usize = 20;
const PARALLEL_REQUESTS: usize = 1;
const DB_PATH: &str = "starter.sqlite3";

#[allow(deprecated)]

Test 2

    // Sync the wallet
    let client: esplora_client::BlockingClient =
        Builder::new("https://blockstream.info/signet/api/").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());

=== "Rust"

```rust title="examples/rust/starter-example/src/main.rs"
    // Sync the wallet
    let client: esplora_client::BlockingClient =
        Builder::new("https://blockstream.info/signet/api/").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());
```

=== "Swift"

```swift
let esploraClient = EsploraClient(url: "https://blockstream.info/signet/api/")
let fullScanRequest = try wallet.startFullScan().build()
let update = try esploraClient.fullScan(
    request: fullScanRequest,
    stopGap: UInt64(10),
    parallelRequests: UInt64(1)
)
try wallet.applyUpdate(update: update)
let balance = wallet.balance()
print("Wallet balance: \(balance.total.toSat()) sat")
```

=== "Kotlin"

```kotlin title="examples/kotlin/starter-example/src/.../App.kt"
    val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL)
    val fullScanRequest: FullScanRequest = wallet.startFullScan().build()
    val update = esploraClient.fullScan(
        request = fullScanRequest,
        stopGap = 10uL,
        parallelRequests = 1uL
    )
    wallet.applyUpdate(update)
    val balance = wallet.balance().total.toSat()
    println("Balance: $balance")
```

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 Signet coins!

=== "Rust"

```rust title="examples/rust/starter-example/src/main.rs"
    if balance.total().to_sat() < 5000 {
        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 Signet coins to {} (address generated at index {})",
            address.address, address.index
        );
        wallet.persist(&mut conn).expect("Cannot persist");
        exit(0)
    }
```

=== "Swift"

```swift
if (balance.total.toSat() < UInt64(5000)) {
    print("Your wallet does not have sufficient balance for the following steps!");
    let address = wallet.revealNextAddress(keychain: KeychainKind.external)
    print("Send Signet coins to address \(address.address) (address generated at index \(address.index))")
    try wallet.persist(connection: connection)
    exit(0)
}
```

=== "Kotlin"

```kotlin title="examples/kotlin/starter-example/src/.../App.kt"
    if (balance < 5000uL) {
        println("Your wallet does not have sufficient balance for the following steps!");
        val address = wallet.revealNextAddress(KeychainKind.EXTERNAL)
        println("Send Signet coins to address ${address.address} (address generated at index ${address.index})")
        wallet.persist(connection)
        exitProcess(0)
    }
```

Send a transaction

For this step you'll need a wallet built with private keys, funded with some Signet satoshis. You can find a faucet here to get some coins.

Let's prepare to send a transaction. The two core choices here are where to send the funds and how much to send. We will send funds back to the faucet return address; it's good practice to send test sats back to the faucet when you're done using them.

=== "Rust"

```rust title="examples/rust/starter-example/src/main.rs"
    // Use a faucet return address
    let faucet_address =
        Address::from_str("tb1p4tp4l6glyr2gs94neqcpr5gha7344nfyznfkc8szkreflscsdkgqsdent4")
            .unwrap()
            .require_network(Network::Signet)
            .unwrap();

    let send_amount: Amount = Amount::from_sat(4000);
```

=== "Swift"

```swift
let faucetAddress: Address = try Address(address: "tb1p4tp4l6glyr2gs94neqcpr5gha7344nfyznfkc8szkreflscsdkgqsdent4", network: Network.signet)
let amount: Amount = Amount.fromSat(fromSat: UInt64(4000))
```

=== "Kotlin"

```kotlin title="examples/kotlin/starter-example/src/.../App.kt"
    val esploraClient: EsploraClient = EsploraClient(SIGNET_ESPLORA_URL)
    val fullScanRequest: FullScanRequest = wallet.startFullScan().build()
    val update = esploraClient.fullScan(
        request = fullScanRequest,
        stopGap = 10uL,
        parallelRequests = 1uL
    )
    wallet.applyUpdate(update)
    val balance = wallet.balance().total.toSat()
    println("Balance: $balance")
```

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

Finally we are ready to build, sign, and broadcast the transaction:

=== "Rust"

```rust title="examples/rust/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();

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

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

=== "Swift"

```swift
let psbt: Psbt = try TxBuilder()
    .addRecipient(script: faucetAddress.scriptPubkey(), amount: amount)
    .feeRate(feeRate: try FeeRate.fromSatPerVb(satPerVb: UInt64(7)))
    .finish(wallet: wallet)

try wallet.sign(psbt: psbt)
let tx: Transaction = try psbt.extractTx()
esploraClient.broadcast(tx)
print("Transaction broadcast successfully! Txid: \(tx.computeTxid())")
```

=== "Kotlin"

```kotlin title="examples/kotlin/starter-example/src/.../App.kt"
    val psbt: Psbt = TxBuilder()
        .addRecipient(script = faucetAddress.scriptPubkey(), amount = amount)
        .feeRate(FeeRate.fromSatPerVb(7uL))
        .finish(wallet)

    wallet.sign(psbt)
    val tx: Transaction = psbt.extractTx()
    esploraClient.broadcast(tx)
    println("Transaction broadcast successfully! Txid: ${tx.computeTxid()}")
```

We can view our transaction on the mempool.space Signet explorer.