From 730582cea4a65ea143fe83922d3c55e4fc2e4287 Mon Sep 17 00:00:00 2001 From: Leeyah-123 Date: Mon, 23 Mar 2026 19:46:02 +0100 Subject: [PATCH] example(bdk_wallet): add BIP-329 wallet labels example using bip329 crate --- Cargo.toml | 6 + examples/bip329_labels.rs | 318 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 324 insertions(+) create mode 100644 examples/bip329_labels.rs diff --git a/Cargo.toml b/Cargo.toml index 8fd4f229..a3cd0130 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ bdk_wallet = { path = ".", features = ["rusqlite", "file_store", "test-utils"] } clap = { version = "4.5.60", features = ["derive", "env"] } ctrlc = "3.5.2" rand = "0.8" +bip329 = "0.4" tempfile = "3" tokio = { version = "1.38.1", features = ["rt", "rt-multi-thread", "macros"] } @@ -77,3 +78,8 @@ name = "esplora_blocking" [[example]] name = "bitcoind_rpc" + +[[example]] +name = "bip329_labels" +path = "examples/bip329_labels.rs" +required-features = ["keys-bip39"] diff --git a/examples/bip329_labels.rs b/examples/bip329_labels.rs new file mode 100644 index 00000000..6d7e2365 --- /dev/null +++ b/examples/bip329_labels.rs @@ -0,0 +1,318 @@ +//! # BDK Wallet + BIP-329 Labels Example +//! +//! This example demonstrates how to use [`bdk_wallet`] together with the +//! [`bip329`] crate to attach and persist human-readable labels to wallet +//! items (addresses, transactions, and outputs) in the standard BIP-329 JSONL +//! format. +//! +//! ## What this example covers +//! +//! 1. Creating a BDK wallet from a generated BIP-39 mnemonic. +//! 2. Revealing receive addresses and labelling them. +//! 3. Building `TransactionRecord`s for wallet transactions. +//! 4. Labelling UTXOs via `OutputRecord` with `spendable` coin-control hints. +//! 5. Exporting all labels to a BIP-329 JSONL file. +//! 6. Reloading those labels and doing efficient lookups with `Labels::into_string_map`. +//! 7. Updating a label in-place (`retain` + `push`) and re-exporting to stdout so you can see the +//! raw JSONL format. +//! +//! ## Running +//! +//! ```shell +//! cargo run --example bip329_labels --features keys-bip39 +//! ``` + +use anyhow::{anyhow, Context, Result}; +use bdk_wallet::{ + keys::{ + bip39::{Language, Mnemonic, WordCount}, + DerivableKey, ExtendedKey, GeneratableKey, GeneratedKey, + }, + miniscript, AddressInfo, KeychainKind, Wallet, +}; +use bip329::{AddressRecord, Label, Labels, OutputRecord, TransactionRecord}; +use bitcoin::{address::NetworkUnchecked, Address, Network, OutPoint, Txid}; +use std::{io::ErrorKind, path::Path, str::FromStr}; +use tempfile::tempdir; + +// ── helpers ────────────────────────────────────────────────────────────────── + +/// Derive BIP-84 (native SegWit) receive + change descriptors from a mnemonic. +fn descriptors_from_mnemonic(mnemonic: &Mnemonic, network: Network) -> Result<(String, String)> { + let xkey: ExtendedKey = mnemonic + .clone() + .into_extended_key() + .context("mnemonic → xkey")?; + let xprv = xkey + .into_xprv(network.into()) + .ok_or_else(|| anyhow!("could not derive xprv for {network}"))?; + + // BIP-84: m/84h/coin_typeh/0h/{0,1}/* + let coin = match network { + Network::Bitcoin => 0, + _ => 1, + }; + Ok(( + format!("wpkh({xprv}/84h/{coin}h/0h/0/*)"), + format!("wpkh({xprv}/84h/{coin}h/0h/1/*)"), + )) +} + +/// Load a `Labels` collection from `path`, returning an empty one if the file +/// does not yet exist. +fn load_or_default(path: &Path) -> Result { + match Labels::try_from_file(path) { + Ok(l) => Ok(l), + Err(bip329::error::ParseError::FileReadError(e)) if e.kind() == ErrorKind::NotFound => { + Ok(Labels::default()) + } + Err(e) => Err(anyhow!( + "failed to load labels from {}: {e}", + path.display() + )), + } +} + +/// Return a BIP-84 derivation path string for a BDK `AddressInfo`. +/// +/// BIP-329 does not have a dedicated `keypath` field on `AddressRecord` in the +/// current crate version, but the derivation path is useful context to print +/// alongside labels so that other wallets can verify or re-derive the address. +fn keypath_for(info: &AddressInfo, network: Network) -> String { + let coin = match network { + Network::Bitcoin => 0, + _ => 1, + }; + let change = match info.keychain { + KeychainKind::External => 0, + KeychainKind::Internal => 1, + }; + format!("m/84h/{coin}h/0h/{change}/{}", info.index) +} + +// ── main ───────────────────────────────────────────────────────────────────── + +#[allow(clippy::print_stdout)] +fn main() -> Result<()> { + // ── 1. Create a BDK wallet from a freshly generated mnemonic ───────────── + + let network = Network::Regtest; + + let mnemonic: GeneratedKey<_, miniscript::Segwitv0> = + Mnemonic::generate((WordCount::Words12, Language::English)) + .map_err(|_| anyhow!("mnemonic generation failed"))?; + let mnemonic = Mnemonic::parse_in(Language::English, mnemonic.to_string())?; + + println!("Mnemonic: {mnemonic}"); + + let (ext_desc, int_desc) = descriptors_from_mnemonic(&mnemonic, network)?; + + let mut wallet = Wallet::create(ext_desc, int_desc) + .network(network) + .create_wallet_no_persist() + .context("wallet creation failed")?; + + println!("\n── Wallet ready ({network}) ─────────────────────────────────\n"); + + // ── 2. Reveal addresses and build Address labels ────────────────────────── + // + // `reveal_next_address` increments the derivation index. Call + // `wallet.persist(&mut conn)` after each reveal so the new index is saved + // to disk — skipping this step risks handing out the same address twice. + + let savings = wallet.reveal_next_address(KeychainKind::External); + let exchange = wallet.reveal_next_address(KeychainKind::External); + let change = wallet.reveal_next_address(KeychainKind::Internal); + + // BIP-329 `AddressRecord` stores the address and an optional label string. + // The address must be in `NetworkUnchecked` form as bip329 accepts labels + // for any network. + let to_unchecked = |addr: &Address| -> Address { + Address::from_str(&addr.to_string()) + .expect("address was just derived") + .into_unchecked() + }; + + // Build the label vec — we use `Label::Address(AddressRecord { .. })`. + // Note: in addition to the label string it is good practice to log the + // BIP-84 derivation path alongside the record for cross-wallet portability. + let mut labels: Vec