Skip to content

Commit 8caaf60

Browse files
committed
add an example using the BIP329 crate with bdk_wallet
1 parent 3e61d2a commit 8caaf60

File tree

2 files changed

+255
-0
lines changed

2 files changed

+255
-0
lines changed

wallet/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ bdk_wallet = { path = ".", features = ["rusqlite", "file_store", "test-utils"] }
4646
bdk_file_store = { version = "0.18.1" }
4747
anyhow = "1"
4848
rand = "^0.8"
49+
bip329 = "0.4.0"
4950

5051
[package.metadata.docs.rs]
5152
all-features = true
@@ -60,3 +61,8 @@ required-features = ["all-keys"]
6061
name = "miniscriptc"
6162
path = "examples/compiler.rs"
6263
required-features = ["compiler"]
64+
65+
[[example]]
66+
name = "bip329_example"
67+
path = "examples/bip329_example.rs"
68+
required-features = ["keys-bip39"]

wallet/examples/bip329_example.rs

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
//! Example demonstrating how to use bdk_wallet with an external BIP-329 library
2+
//! for managing wallet labels persisted in a separate file.
3+
4+
extern crate anyhow;
5+
extern crate bdk_wallet;
6+
extern crate bip329;
7+
extern crate bitcoin;
8+
extern crate tempfile;
9+
10+
use anyhow::{anyhow, Context, Result};
11+
use bdk_wallet::{
12+
descriptor,
13+
keys::bip39::{Language, Mnemonic, WordCount},
14+
keys::GeneratableKey,
15+
keys::{DerivableKey, DescriptorSecretKey, ExtendedKey, GeneratedKey},
16+
miniscript::{self, Descriptor, DescriptorPublicKey},
17+
KeychainKind, Wallet,
18+
};
19+
use bip329::{AddressRecord, Label, LabelRef, Labels, TransactionRecord};
20+
use bitcoin::{address::NetworkUnchecked, bip32::DerivationPath, Address, Network, OutPoint, Txid};
21+
use std::{
22+
collections::{BTreeMap, HashMap, HashSet},
23+
io::ErrorKind,
24+
ops::DerefMut,
25+
path::PathBuf,
26+
str::FromStr,
27+
};
28+
use tempfile::tempdir;
29+
30+
// --- Helper Functions ---
31+
fn format_ref_str(item_ref: &LabelRef) -> String {
32+
match item_ref {
33+
LabelRef::Txid(txid) => format!("txid:{}", txid),
34+
LabelRef::Address(_) => format!("addr:{}", item_ref),
35+
LabelRef::Output(op) => format!("output:{}", op),
36+
LabelRef::Input(op) => format!("input:{}", op),
37+
LabelRef::PublicKey(pk_str) => format!("pubkey:{}", pk_str),
38+
LabelRef::Xpub(xpub_str) => format!("xpub:{}", xpub_str),
39+
}
40+
}
41+
fn format_bdk_addr_ref(addr: &Address) -> String {
42+
format!("addr:{}", addr)
43+
}
44+
fn format_bdk_txid_ref(txid: Txid) -> String {
45+
format!("txid:{}", txid)
46+
}
47+
fn format_bdk_outpoint_ref(op: OutPoint) -> String {
48+
format!("output:{}", op)
49+
}
50+
51+
// --- Main Example Logic ---
52+
fn main() -> Result<()> {
53+
println!("--- BDK Wallet + BIP-329 Label Example ---");
54+
55+
let temp_dir = tempdir().context("Failed to create temporary directory")?;
56+
let label_file_path: PathBuf = temp_dir.path().join("bdk_bip329_example_labels.jsonl");
57+
println!("Using temporary label file: {}", label_file_path.display());
58+
59+
let network = Network::Regtest;
60+
61+
// 1. Generate Keys and Descriptors Programmatically
62+
println!("Generating keys and descriptors...");
63+
let mnemonic: GeneratedKey<_, miniscript::Segwitv0> =
64+
Mnemonic::generate((WordCount::Words12, Language::English))
65+
.map_err(|_| anyhow!("Mnemonic generation failed"))?;
66+
let mnemonic_words = mnemonic.to_string();
67+
println!("Generated Mnemonic: {}", mnemonic_words);
68+
let mnemonic = Mnemonic::parse_in(Language::English, mnemonic_words)?;
69+
let xkey: ExtendedKey = mnemonic.into_extended_key()?;
70+
let master_xprv = xkey
71+
.into_xprv(network)
72+
.ok_or_else(|| anyhow!("Could not derive xprv for network {}", network))?;
73+
let external_path = DerivationPath::from_str("m/84h/1h/0h/0")?;
74+
let internal_path = DerivationPath::from_str("m/84h/1h/0h/1")?;
75+
76+
// CORRECTED Type Annotations:
77+
let (external_descriptor, _ext_keymap, _ext_networks): (
78+
Descriptor<DescriptorPublicKey>,
79+
BTreeMap<DescriptorPublicKey, DescriptorSecretKey>,
80+
HashSet<Network>,
81+
) = descriptor!(wpkh((master_xprv.clone(), external_path)))?;
82+
83+
let (internal_descriptor, _int_keymap, _int_networks): (
84+
Descriptor<DescriptorPublicKey>,
85+
BTreeMap<DescriptorPublicKey, DescriptorSecretKey>,
86+
HashSet<Network>,
87+
) = descriptor!(wpkh((master_xprv, internal_path)))?;
88+
89+
let external_descriptor_str = external_descriptor.to_string();
90+
let internal_descriptor_str = internal_descriptor.to_string();
91+
92+
println!("External Descriptor: {}", external_descriptor_str);
93+
println!("Internal Descriptor: {}", internal_descriptor_str);
94+
95+
// 2. Create the BDK Wallet
96+
let mut wallet = Wallet::create(external_descriptor_str, internal_descriptor_str)
97+
.network(network)
98+
.create_wallet_no_persist()
99+
.context("Failed to create wallet using generated descriptors")?;
100+
println!("Wallet created successfully.");
101+
102+
// Get example items
103+
let address1 = wallet.next_unused_address(KeychainKind::External);
104+
let address2 = wallet.next_unused_address(KeychainKind::External);
105+
let dummy_txid =
106+
Txid::from_str("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16")?;
107+
let dummy_outpoint = OutPoint::new(dummy_txid, 0);
108+
println!(
109+
"Wallet Addresses: Index {} -> {}",
110+
address1.index, address1.address
111+
);
112+
println!("Index {} -> {}", address2.index, address2.address);
113+
println!("Dummy TXID: {}", dummy_txid);
114+
println!("Dummy OutPoint: {}", dummy_outpoint);
115+
116+
// 3. Load Labels from temporary file (or create empty)
117+
println!("\n--- Loading Labels ---");
118+
// Use the PathBuf variable directly (borrowed)
119+
let mut labels = match Labels::try_from_file(&label_file_path) {
120+
Ok(loaded_labels) => {
121+
println!(
122+
"Loaded {} labels from temporary file '{}'.", // Updated message
123+
loaded_labels.len(),
124+
label_file_path.display()
125+
);
126+
loaded_labels
127+
}
128+
Err(bip329::error::ParseError::FileReadError(io_err))
129+
if io_err.kind() == ErrorKind::NotFound =>
130+
{
131+
println!(
132+
"Temporary label file '{}' not found, starting empty.", // Updated message
133+
label_file_path.display()
134+
);
135+
Labels::default()
136+
}
137+
Err(e) => {
138+
return Err(anyhow!(
139+
"Failed to load labels from {}: {}",
140+
label_file_path.display(),
141+
e
142+
))
143+
}
144+
};
145+
146+
// Build lookup map
147+
let mut label_lookup: HashMap<String, String> = HashMap::new();
148+
for label_entry in labels.iter() {
149+
if let Some(label_text) = label_entry.label() {
150+
let ref_str = format_ref_str(&label_entry.ref_());
151+
label_lookup.insert(ref_str, label_text.to_string());
152+
}
153+
}
154+
155+
// 4. Correlate Wallet Data with Labels
156+
println!("\n--- Current Labels for Wallet Items ---");
157+
let items_to_lookup: Vec<(&str, String)> = vec![
158+
("Address 1", format_bdk_addr_ref(&address1.address)),
159+
("Address 2", format_bdk_addr_ref(&address2.address)),
160+
("Dummy Tx", format_bdk_txid_ref(dummy_txid)),
161+
("Dummy UTXO", format_bdk_outpoint_ref(dummy_outpoint)),
162+
];
163+
164+
for (item_desc, item_ref_str) in &items_to_lookup {
165+
match label_lookup.get(item_ref_str) {
166+
Some(label_text) => println!("{} ({}): {}", item_desc, item_ref_str, label_text),
167+
None => println!("{} ({}): [No Label]", item_desc, item_ref_str),
168+
}
169+
}
170+
171+
// 5. Add/Update Labels in Memory
172+
println!("\n--- Adding/Updating Labels ---");
173+
let addr1_ref_str = format_bdk_addr_ref(&address1.address);
174+
let new_addr1_label = "Primary Receiving Address";
175+
let labels_vec = labels.deref_mut();
176+
match labels_vec
177+
.iter_mut()
178+
.find(|l| format_ref_str(&l.ref_()) == addr1_ref_str)
179+
{
180+
Some(label_entry) => {
181+
println!("Updating label for {}", addr1_ref_str);
182+
match label_entry {
183+
Label::Address(record) => record.label = Some(new_addr1_label.to_string()),
184+
_ => println!(
185+
"Warning: Found ref string {} but not Address label?",
186+
addr1_ref_str
187+
),
188+
}
189+
}
190+
None => {
191+
println!("Adding new label for {}", addr1_ref_str);
192+
let addr_unchecked: Address<NetworkUnchecked> =
193+
Address::from_str(&address1.address.to_string())?
194+
.require_network(network)?
195+
.into_unchecked();
196+
labels_vec.push(Label::Address(AddressRecord {
197+
ref_: addr_unchecked,
198+
label: Some(new_addr1_label.to_string()),
199+
}));
200+
}
201+
}
202+
let tx_ref_str = format_bdk_txid_ref(dummy_txid);
203+
if !labels_vec
204+
.iter()
205+
.any(|l| format_ref_str(&l.ref_()) == tx_ref_str)
206+
{
207+
println!("Adding new label for {}", tx_ref_str);
208+
labels_vec.push(Label::Transaction(TransactionRecord {
209+
ref_: dummy_txid,
210+
label: Some("Simulated Incoming TX".to_string()),
211+
origin: None,
212+
}));
213+
}
214+
215+
// 6. Export and Save Labels to temporary file
216+
println!("\n--- Exporting and Saving Labels ---");
217+
// Use the PathBuf variable directly (borrowed)
218+
match labels.export_to_file(&label_file_path) {
219+
Ok(_) => println!(
220+
"Labels successfully saved to temporary file '{}'", // Updated message
221+
label_file_path.display()
222+
),
223+
Err(e) => eprintln!("Error saving labels: {}", e),
224+
}
225+
226+
// 7. Demonstrate reading the temporary file back
227+
println!("\n--- Reading Labels Back from Temporary File ---"); // Updated message
228+
// Use the PathBuf variable directly (borrowed)
229+
match Labels::try_from_file(&label_file_path) {
230+
Ok(reloaded_labels) => {
231+
println!("Successfully reloaded {} labels:", reloaded_labels.len());
232+
for label_entry in reloaded_labels.iter() {
233+
if let Some(label_text) = label_entry.label() {
234+
println!(
235+
" {} -> {}",
236+
format_ref_str(&label_entry.ref_()),
237+
label_text
238+
);
239+
}
240+
}
241+
}
242+
Err(e) => eprintln!("Error reloading labels: {}", e),
243+
}
244+
245+
println!("\n--- Example Finished ---");
246+
// The `temp_dir` variable goes out of scope here, automatically deleting the directory
247+
// and the label file inside it.
248+
Ok(())
249+
}

0 commit comments

Comments
 (0)