Skip to content

Full Wallet Example

This page illustrates core wallet functionality, including:

  • Generating descriptors
  • Wallet creation, persistence, and loading
  • Full scan and light weight sync
  • Creating and broadcasting a transaction

Tip

The logic for this page is split between 2 separate examples in the examples source code. One to create descriptors and a second for everything else.If you are following along with the code examples you will need to copy and paste your private descriptors you get from the first example into the second. We leave descriptor creation in a separate example because bdk does not handle private descriptor (or private key) storage, that is up to the wallet developer.

Generating Descriptors

First we create signet descriptors for our wallet.

examples/rust/descriptors/src/main.rs
let mut seed: [u8; 32] = [0u8; 32];
rand::thread_rng().fill_bytes(&mut seed);

let network: Network = Network::Signet;
let xprv: Xpriv = Xpriv::new_master(network, &seed).unwrap();
println!("Generated Master Private Key:\n{}\nWarning: be very careful with private keys when using MainNet! We are logging these values for convenience only because this is an example on SigNet.\n", xprv);

let (descriptor, key_map, _) = Bip86(xprv, KeychainKind::External)
    .build(Network::Signet)
    .expect("Failed to build external descriptor");

let (change_descriptor, change_key_map, _) = Bip86(xprv, KeychainKind::Internal)
    .build(Network::Signet)
    .expect("Failed to build internal descriptor");

let descriptor_string_priv = descriptor.to_string_with_secret(&key_map);
let change_descriptor_string_priv = descriptor.to_string_with_secret(&change_key_map);

Notice we are creating private descriptors here in order to sign transactions later on.

Full Scan and Address Generation (First Run)

Next, lets use those descriptors to load up our wallet. Replace the placeholder descriptors in the full-wallet example with your private descriptors:

examples/rust/full-wallet/src/main.rs
1
2
3
4
5
const DESCRIPTOR_PRIVATE_EXTERNAL: &str = "[your private external descriptor here ...]";
const DESCRIPTOR_PRIVATE_INTERNAL: &str = "[your private internal descriptor here ...]";
// Example private descriptors
// const DESCRIPTOR_PRIVATE_EXTERNAL: &str = "tr(tprv8ZgxMBicQKsPdJuLWWArdBsWjqDA3W5WoREnfdgKEcCQB1FMKfSoaFz9JHZU71HwXAqTsjHripkLM62kUQar14SDD8brsmhFKqVUPXGrZLc/86'/1'/0'/0/*)#fv8tutn2";
// const DESCRIPTOR_PRIVATE_INTERNAL: &str = "tr(tprv8ZgxMBicQKsPdJuLWWArdBsWjqDA3W5WoREnfdgKEcCQB1FMKfSoaFz9JHZU71HwXAqTsjHripkLM62kUQar14SDD8brsmhFKqVUPXGrZLc/86'/1'/0'/1/*)#ccz2p7rj";

We are going to run this example twice. On the first run it will do a full scan for your wallet, persist that chain data, generate a new address for you, and display your current wallet balance, it will then attempt to build a transaction, but will fail becuase we don't have any funds yet. We will use the new address (from the first run) to request funds from the Mutinynet faucet so we can build a transaction on the second run. On the second run it will load the data from the previous run, do a light weight sync to check for updates (no need to repeat the full scan), and then build and broadcast a transaction. Let's go through this step by step.

examples/rust/full-wallet/src/main.rs
let mut conn = Connection::open(DB_PATH)?;

let wallet_opt = Wallet::load()
    .descriptor(KeychainKind::External, Some(DESCRIPTOR_PRIVATE_EXTERNAL))
    .descriptor(KeychainKind::Internal, Some(DESCRIPTOR_PRIVATE_INTERNAL))
    .extract_keys()
    .check_network(Network::Signet)
    .load_wallet(&mut conn)?;

let (mut wallet, is_new_wallet) = if let Some(loaded_wallet) = wallet_opt {
    (loaded_wallet, false)
} else {
    (Wallet::create(DESCRIPTOR_PRIVATE_EXTERNAL, DESCRIPTOR_PRIVATE_INTERNAL)
        .network(Network::Signet)
        .create_wallet(&mut conn)?, true)
};

In the quickstart example we simply used an in-memory wallet, with no persistence. But here we are saving wallet data to a file. Notice that we are providing our private descriptors during wallet load. This is because bdk never stores private keys, that responsibility is on the wallet developer (you). The data we are loading here does not include the private keys, but we want our wallet to have signing capabilities, so we need to provide our private descriptors during wallet load. If we get a wallet back from the load attempt, we'll use that, otherwise we'll create a new one. Since this is our first run nothing will be loaded and a new wallet will be created.

examples/rust/full-wallet/src/main.rs
let client: esplora_client::BlockingClient = Builder::new("https://mutinynet.com/api").build_blocking();
// Sync the wallet
if is_new_wallet {
    // Perform a full scan
    println!("Performing full scan...");
    let full_scan_request: FullScanRequestBuilder<KeychainKind> = wallet.start_full_scan();
    let update: FullScanResult<KeychainKind> = client.full_scan(full_scan_request, STOP_GAP, PARALLEL_REQUESTS)?;
    wallet.apply_update(update).unwrap();
} else {
    // Perform a regular sync
    println!("Performing regular sync...");
    let sync_request: SyncRequestBuilder<(KeychainKind, u32)> = wallet.start_sync_with_revealed_spks();
    let update: SyncResult = client.sync(sync_request, PARALLEL_REQUESTS)?;
    wallet.apply_update(update).unwrap();
};
wallet.persist(&mut conn)?;

Next we'll fetch data from our blockchain client. On the first run, we don't yet have any data, so we need to do a full scan. We then persist the data from the scan. Finally, we'll print out an address that we can use to request funds. You should also see the current balance printed out, it should be 0 since this is a brand new wallet. Note that we persist the wallet after generating the new address; this is to avoid re-using the same address as that would compromise our privacy (on subsequent runs you'll notice the address index incremented).

examples/rust/full-wallet/src/main.rs
1
2
3
4
// 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);
wallet.persist(&mut conn)?;

The process will then error out, indicating we don't have enough funds to send a transaction.

Request satoshis from the Mutinynet faucet

We can now use our new address to request some sats from the Mutinynet faucet. After requesting sats, you can view the transaction in their Mempool Explorer instance (click the link on the faucet confirmation page or put the txid in the search bar of the mempool explorer). After a minute or so you should see the transaction confirmed. We can also re-run the full-wallet example and see that our wallet now has some funds!

Load, Sync, and Send a Transaction (Second Run)

Now that we have some funds, we can re-run the full-wallet example. Since we persisted data from the previous run, this time our wallet will be loaded. You do not need to provide descriptors to load wallet data, however, if you don't you will not have signing capabilities, so here we do provide our private descriptors in the loading process:

examples/rust/full-wallet/src/main.rs
let mut conn = Connection::open(DB_PATH)?;

let wallet_opt = Wallet::load()
    .descriptor(KeychainKind::External, Some(DESCRIPTOR_PRIVATE_EXTERNAL))
    .descriptor(KeychainKind::Internal, Some(DESCRIPTOR_PRIVATE_INTERNAL))
    .extract_keys()
    .check_network(Network::Signet)
    .load_wallet(&mut conn)?;

let (mut wallet, is_new_wallet) = if let Some(loaded_wallet) = wallet_opt {
    (loaded_wallet, false)
} else {
    (Wallet::create(DESCRIPTOR_PRIVATE_EXTERNAL, DESCRIPTOR_PRIVATE_INTERNAL)
        .network(Network::Signet)
        .create_wallet(&mut conn)?, true)
};

Since we already have some data from the previous run, it will not do a full scan, but only a sync which is faster and less data intensive.

examples/rust/full-wallet/src/main.rs
let client: esplora_client::BlockingClient = Builder::new("https://mutinynet.com/api").build_blocking();
// Sync the wallet
if is_new_wallet {
    // Perform a full scan
    println!("Performing full scan...");
    let full_scan_request: FullScanRequestBuilder<KeychainKind> = wallet.start_full_scan();
    let update: FullScanResult<KeychainKind> = client.full_scan(full_scan_request, STOP_GAP, PARALLEL_REQUESTS)?;
    wallet.apply_update(update).unwrap();
} else {
    // Perform a regular sync
    println!("Performing regular sync...");
    let sync_request: SyncRequestBuilder<(KeychainKind, u32)> = wallet.start_sync_with_revealed_spks();
    let update: SyncResult = client.sync(sync_request, PARALLEL_REQUESTS)?;
    wallet.apply_update(update).unwrap();
};
wallet.persist(&mut conn)?;

Now that we have funds, let's prepare to send a transaction. We need to decide where to send the funds and how much to send.We will send funds back to the mutiny faucet return address. It's good practice to send test sats back to the faucet when you're done using them.

examples/rust/full-wallet/src/main.rs
1
2
3
4
5
6
7
// Use the Mutinynet faucet return address
let address = Address::from_str("tb1qd28npep0s8frcm3y7dxqajkcy2m40eysplyr9v")
    .unwrap()
    .require_network(Network::Signet)
    .unwrap();

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

Here we are preparing to send 5000 sats back to the mutiny faucet (it's good practice to send test sats back to the faucet when you're done using them).

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

examples/rust/transaction/src/main.rs
// Transaction Logic
let mut tx_builder = wallet.build_tx();
tx_builder.add_recipient(address.script_pubkey(), send_amount);

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

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

Again we can view the transaction in the Mutinynet explorer or re-run the full-wallet example to see that our wallet has less funds.