1use crate::{Bip329, LabelTarget};
2use bdk_wallet::Wallet;
3
4use crate::Error;
5use crate::changeset::{LabelChangeset, MergeStrategy};
6use crate::persist::LabelPersister;
7use crate::{export, import};
8use bip329::{Label, LabelRef};
9use std::io::{BufRead, Write};
10
11pub struct LabelledWallet<'a> {
16 pub wallet: &'a mut Wallet,
18 pub labels: &'a mut LabelChangeset,
20}
21
22impl Bip329 for LabelledWallet<'_> {
23 fn add_label(
24 &mut self,
25 target: impl Into<LabelTarget>,
26 label_text: impl Into<String>,
27 ) -> Result<Label, Error> {
28 let new_label = match target.into() {
29 LabelTarget::Txid(txid) => {
30 let known_txid = self.wallet.list_output().any(|o| o.outpoint.txid == txid);
31 if !known_txid {
32 return Err(Error::Validation(format!(
33 "txid {txid} not found in wallet"
34 )));
35 }
36 Label::Transaction(bip329::TransactionRecord {
37 ref_: txid,
38 label: Some(label_text.into()),
39 origin: None,
40 })
41 }
42 LabelTarget::Address(addr) => {
43 let checked_address = addr.assume_checked_ref();
44 if !self.wallet.is_mine(checked_address.script_pubkey()) {
45 return Err(Error::Validation(format!(
46 "address {:?} does not belong to this wallet",
47 addr
48 )));
49 }
50 Label::Address(bip329::AddressRecord {
51 ref_: addr,
52 label: Some(label_text.into()),
53 })
54 }
55 LabelTarget::PublicKey(pk) => Label::PublicKey(bip329::PublicKeyRecord {
56 ref_: pk,
57 label: Some(label_text.into()),
58 }),
59 LabelTarget::Input(outpoint) => {
60 let known_input = self.wallet.list_output().any(|o| o.outpoint == outpoint);
61 if !known_input {
62 return Err(Error::Validation(format!(
63 "outpoint {outpoint} not found in wallet"
64 )));
65 }
66 Label::Input(bip329::InputRecord {
67 ref_: outpoint,
68 label: Some(label_text.into()),
69 })
70 }
71 LabelTarget::Output(outpoint) => {
72 let known_input = self.wallet.list_output().any(|o| o.outpoint == outpoint);
73 if !known_input {
74 return Err(Error::Validation(format!(
75 "outpoint {outpoint} not found in wallet"
76 )));
77 }
78
79 let spendable = self
80 .labels
81 .get(&LabelRef::Output(outpoint))
82 .and_then(|l| match l {
83 Label::Output(rec) => Some(rec.spendable),
84 _ => None,
85 })
86 .unwrap_or(true);
87
88 Label::Output(bip329::OutputRecord {
89 ref_: outpoint,
90 label: Some(label_text.into()),
91 spendable,
92 })
93 }
94 LabelTarget::Xpub(xpub) => Label::ExtendedPublicKey(bip329::ExtendedPublicKeyRecord {
95 ref_: xpub,
96 label: Some(label_text.into()),
97 }),
98 };
99
100 self.labels.insert(new_label.clone());
101
102 Ok(new_label)
103 }
104
105 fn import_labels<R: BufRead>(
106 &mut self,
107 reader: R,
108 strategy: MergeStrategy,
109 ) -> Result<(), Error> {
110 let imported_labels = import(reader)?;
111 self.labels.merge(imported_labels, strategy);
112 Ok(())
113 }
114
115 fn export_labels<W: Write>(&self, writer: W) -> Result<(), Error> {
116 export(self.labels, writer)
117 }
118}
119
120impl LabelledWallet<'_> {
121 pub fn persist<P: LabelPersister>(&mut self, persister: &mut P) -> Result<(), Error> {
124 if !self.labels.has_staged_changes() {
125 return Ok(());
126 }
127 let diff = self.labels.diff();
128 persister
129 .append_changeset(&diff)
130 .map_err(|e| Error::Custom(Box::new(e)))?;
131 self.labels.clear_staged();
132
133 Ok(())
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use crate::changeset::LabelChangeset;
140 use crate::{InputTarget, OutputTarget};
141 use bdk_wallet::test_utils::{get_funded_wallet, get_test_wpkh_and_change_desc};
142 use bdk_wallet::{KeychainKind, Wallet};
143 use bip329::{
144 AddressRecord, ExtendedPublicKeyRecord, InputRecord, Label, OutputRecord, PublicKeyRecord,
145 TransactionRecord,
146 };
147 use bitcoin::Address;
148 use bitcoin::Network;
149 use bitcoin::address::NetworkUnchecked;
150 use bitcoin::bip32::Xpub;
151 use bitcoin::{OutPoint, PublicKey, Txid};
152 use std::matches;
153 use std::str::FromStr;
154
155 use super::*;
156
157 fn test_wallet() -> Wallet {
160 let external_desc =
161 "wpkh(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)";
162 let internal_desc =
163 "wpkh(03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7)";
164
165 Wallet::create(external_desc, internal_desc)
166 .network(Network::Testnet)
167 .create_wallet_no_persist()
168 .expect("Failed to create wallet")
169 }
170
171 fn funded_test_wallet() -> (Wallet, Txid) {
178 let (desc, change_desc) = get_test_wpkh_and_change_desc();
179 get_funded_wallet(desc, change_desc)
180 }
181
182 fn unspent_and_spent_outpoints(wallet: &Wallet) -> (OutPoint, OutPoint) {
186 let unspent_outpoint = wallet
187 .list_unspent()
188 .next()
189 .expect("funded wallet should have one unspent output")
190 .outpoint;
191
192 let spent_outpoint = wallet
193 .list_output()
194 .map(|o| o.outpoint)
195 .find(|op| *op != unspent_outpoint)
196 .expect("funded wallet should also have a spent output (tx1's input)");
197
198 (unspent_outpoint, spent_outpoint)
199 }
200
201 fn as_unchecked(address: Address) -> Address<NetworkUnchecked> {
202 address
203 .to_string()
204 .parse()
205 .expect("Wallet derived address should be converted to Network Unchecked")
206 }
207
208 #[test]
209 fn test_add_label_variant_mapping() {
210 let (mut wallet, funding_txid) = funded_test_wallet();
211
212 let (unspent_outpoint, spent_outpoint) = unspent_and_spent_outpoints(&wallet);
213 let owned_address = wallet.reveal_next_address(KeychainKind::External).address;
214
215 let dummy_pubkey = PublicKey::from_str(
216 "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
217 )
218 .unwrap();
219
220 let dummy_xpub = Xpub::from_str("xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8").unwrap();
221
222 let mut changeset = LabelChangeset::new();
223
224 let mut labelled_wallet = LabelledWallet {
225 wallet: &mut wallet,
226 labels: &mut changeset,
227 };
228
229 let transaction_label = labelled_wallet
230 .add_label(funding_txid, "Payment for Machinery")
231 .expect("Failed to add transaction label");
232
233 let address_label = labelled_wallet
234 .add_label(as_unchecked(owned_address.clone()), "Employee address")
235 .expect("Failed to add address label");
236
237 let pubkey_label = labelled_wallet
238 .add_label(dummy_pubkey, "My wallet's public key")
239 .expect("Failed to add address label");
240
241 let input_label = labelled_wallet
242 .add_label(InputTarget(spent_outpoint), "My transaction's input")
243 .expect("Failed to add address label");
244
245 let output_label = labelled_wallet
246 .add_label(OutputTarget(unspent_outpoint), "My transaction's Output")
247 .expect("Failed to add address label");
248
249 let xpub_label = labelled_wallet
250 .add_label(dummy_xpub, "My wallet's extended public key")
251 .expect("Failed to add address label");
252
253 assert!(matches!(
254 transaction_label,
255 Label::Transaction(TransactionRecord {
256 ref_: _,
257 label: Some(_),
258 origin: _,
259 })
260 ));
261
262 assert!(matches!(
263 address_label,
264 Label::Address(AddressRecord {
265 ref_: _,
266 label: Some(_)
267 })
268 ));
269
270 assert!(matches!(
271 pubkey_label,
272 Label::PublicKey(PublicKeyRecord {
273 ref_: _,
274 label: Some(_),
275 })
276 ));
277
278 assert!(matches!(
279 input_label,
280 Label::Input(InputRecord {
281 ref_: _,
282 label: Some(_),
283 })
284 ));
285
286 assert!(matches!(
287 output_label,
288 Label::Output(OutputRecord {
289 ref_: _,
290 label: Some(_),
291 spendable: true,
292 })
293 ));
294
295 assert!(matches!(
296 xpub_label,
297 Label::ExtendedPublicKey(ExtendedPublicKeyRecord {
298 ref_: _,
299 label: Some(_),
300 })
301 ));
302 }
303
304 #[test]
305 fn test_add_label_rejects_address_not_owned_by_wallet() {
306 let mut wallet = test_wallet();
307 let mut changeset = LabelChangeset::new();
308 let mut labelled_wallet = LabelledWallet {
309 wallet: &mut wallet,
310 labels: &mut changeset,
311 };
312
313 let unowned_address = bitcoin::Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")
314 .expect("failed to parse address");
315
316 let result = labelled_wallet.add_label(unowned_address, "Not for this wallet");
317
318 assert!(matches!(result, Err(Error::Validation(_))));
319 }
320
321 #[test]
322 fn test_add_label_rejects_unknown_outpoint() {
323 let mut wallet = test_wallet();
324 let mut changeset = LabelChangeset::new();
325 let mut labelled_wallet = LabelledWallet {
326 wallet: &mut wallet,
327 labels: &mut changeset,
328 };
329
330 let dummy_txid =
331 Txid::from_str("0000000000000000000000000000000000000000000000000000000000000000")
332 .unwrap();
333 let unknown_outpoint = OutPoint::new(dummy_txid, 0);
334
335 let output_result =
336 labelled_wallet.add_label(OutputTarget(unknown_outpoint), "Not a real UTXO");
337 let input_result =
338 labelled_wallet.add_label(InputTarget(unknown_outpoint), "Not a real input");
339
340 assert!(matches!(output_result, Err(Error::Validation(_))));
341 assert!(matches!(input_result, Err(Error::Validation(_))));
342 }
343
344 #[test]
345 fn test_add_label_preserves_existing_spendable_state() {
346 let (mut wallet, _funding_txid) = funded_test_wallet();
347 let (unspent_outpoint, _spent_outpoint) = unspent_and_spent_outpoints(&wallet);
348
349 let mut changeset = LabelChangeset::new();
350
351 changeset.insert(Label::Output(bip329::OutputRecord {
352 ref_: unspent_outpoint,
353 label: Some("Dummy Label".to_string()),
354 spendable: false,
355 }));
356
357 let mut labelled_wallet = LabelledWallet {
358 wallet: &mut wallet,
359 labels: &mut changeset,
360 };
361
362 let relabelled_output = labelled_wallet
363 .add_label(OutputTarget(unspent_outpoint), "My transaction's Output")
364 .expect("Failed to add address label");
365
366 assert!(matches!(
367 relabelled_output,
368 Label::Output(OutputRecord {
369 spendable: false,
370 ..
371 })
372 ))
373 }
374
375 #[test]
376 fn test_add_label_defaults_new_outputs_to_spendadle_true() {
377 let (mut wallet, _funding_txid) = funded_test_wallet();
378 let (unspent_outpoint, _spent_outpoint) = unspent_and_spent_outpoints(&wallet);
379
380 let mut changeset = LabelChangeset::new();
381
382 let mut labelled_wallet = LabelledWallet {
383 wallet: &mut wallet,
384 labels: &mut changeset,
385 };
386
387 let new_output = labelled_wallet
388 .add_label(OutputTarget(unspent_outpoint), "New Output")
389 .expect("Failed to add address label");
390
391 assert!(matches!(
392 new_output,
393 Label::Output(OutputRecord {
394 spendable: true,
395 ..
396 })
397 ))
398 }
399
400 #[test]
401 fn test_mock_persister_captures_only_staged_labels() {
402 let (mut wallet, funding_txid) = funded_test_wallet();
403
404 let mut changeset = LabelChangeset::new();
405
406 let mut labelled_wallet = LabelledWallet {
407 wallet: &mut wallet,
408 labels: &mut changeset,
409 };
410
411 use std::convert::Infallible;
412 pub struct MockPersister {
413 pub received_changesets: Vec<LabelChangeset>,
414 }
415
416 impl LabelPersister for MockPersister {
417 type Error = Infallible;
418
419 fn read_labels(&self) -> Result<LabelChangeset, Self::Error> {
420 Ok(LabelChangeset::default())
421 }
422
423 fn append_changeset(&mut self, changeset: &LabelChangeset) -> Result<(), Self::Error> {
424 self.received_changesets.push(changeset.clone());
425 Ok(())
426 }
427 }
428
429 let mut mock_persister = MockPersister {
430 received_changesets: vec![],
431 };
432
433 assert_eq!(mock_persister.received_changesets.len(), 0);
434
435 let transaction_label = labelled_wallet
436 .add_label(funding_txid, "Payment for Machinery")
437 .expect("Failed to add transaction label");
438
439 labelled_wallet
440 .persist(&mut mock_persister)
441 .expect("first persist should succeed");
442
443 assert_eq!(mock_persister.received_changesets.len(), 1);
444 assert_eq!(mock_persister.received_changesets[0].len(), 1);
445
446 labelled_wallet
447 .persist(&mut mock_persister)
448 .expect("second, no-op persist should succeed");
449
450 assert_eq!(
451 mock_persister.received_changesets.len(),
452 1,
453 "persist() must not re-send unchanged labels"
454 );
455
456 assert!(matches!(
457 transaction_label,
458 Label::Transaction(TransactionRecord {
459 ref_: _,
460 label: Some(_),
461 origin: _,
462 })
463 ));
464
465 let persisted_changeset = &mock_persister.received_changesets[0];
466
467 assert_eq!(
468 persisted_changeset.get(&transaction_label.ref_()),
469 Some(&transaction_label)
470 );
471 }
472
473 #[test]
474 fn test_wallet_io_delegation_roundtrip() {
475 let mut source_wallet = test_wallet();
476 let owned_address = source_wallet
477 .reveal_next_address(KeychainKind::External)
478 .address;
479
480 let mut source_changeset = LabelChangeset::new();
481
482 let mut source_labelled_wallet = LabelledWallet {
483 wallet: &mut source_wallet,
484 labels: &mut source_changeset,
485 };
486
487 assert_eq!(source_labelled_wallet.labels.len(), 0);
488
489 let address_label = source_labelled_wallet
490 .add_label(as_unchecked(owned_address.clone()), "Employee address")
491 .expect("Failed to add address label");
492
493 assert_eq!(source_labelled_wallet.labels.len(), 1);
494
495 let mut buffer = Vec::new();
496
497 source_labelled_wallet
498 .export_labels(&mut buffer)
499 .expect("Failed to export labels");
500
501 assert!(
502 !buffer.is_empty(),
503 "The exported buffer should contain data"
504 );
505
506 let mut dest_wallet = test_wallet();
507 let mut dest_changeset = LabelChangeset::new();
508 let mut dest_labelled_wallet = LabelledWallet {
509 wallet: &mut dest_wallet,
510 labels: &mut dest_changeset,
511 };
512
513 assert_eq!(dest_labelled_wallet.labels.len(), 0);
514
515 let reader = std::io::Cursor::new(buffer);
516
517 dest_labelled_wallet
518 .import_labels(reader, MergeStrategy::Overwrite)
519 .expect("Failed to import labels");
520
521 assert_eq!(dest_labelled_wallet.labels.len(), 1);
522
523 assert_eq!(
524 dest_labelled_wallet.labels.get(&address_label.ref_()),
525 Some(&address_label)
526 );
527 }
528}