Skip to content

Commit aa2cc6b

Browse files
committed
feat(wallet): BIP 329 import/export labels
Adds BIP 329 label import export functionality into the `bdk_wallet` crate. This is feature-gated to the `"labels"` feature.
1 parent 17a9850 commit aa2cc6b

File tree

3 files changed

+136
-0
lines changed

3 files changed

+136
-0
lines changed

crates/wallet/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ all-keys = ["keys-bip39"]
3232
keys-bip39 = ["bip39"]
3333
rusqlite = ["bdk_chain/rusqlite"]
3434
file_store = ["bdk_file_store"]
35+
labels = []
3536

3637
[dev-dependencies]
3738
lazy_static = "1.4"

crates/wallet/src/types.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
// licenses.
1111

1212
use alloc::boxed::Box;
13+
#[cfg(feature = "labels")]
14+
use alloc::string::String;
1315
use core::convert::AsRef;
1416

1517
use bdk_chain::ConfirmationTime;
@@ -63,6 +65,46 @@ pub struct LocalOutput {
6365
pub derivation_index: u32,
6466
/// The confirmation time for transaction containing this utxo
6567
pub confirmation_time: ConfirmationTime,
68+
#[cfg(feature = "labels")]
69+
/// The label for this UTXO according to
70+
/// [BIP 329](https://github.com/bitcoin/bips/blob/master/bip-0329.mediawiki)
71+
/// format
72+
pub label: Option<Label>,
73+
}
74+
75+
#[cfg(feature = "labels")]
76+
/// A label for a [`LocalOutput`], used to identify the purpose of the UTXO.
77+
///
78+
/// # Note
79+
///
80+
/// The labels follow the [BIP 329](https://github.com/bitcoin/bips/blob/master/bip-0329.mediawiki)
81+
/// export/import format.
82+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
83+
#[non_exhaustive] // We might add more types in the future
84+
pub enum Label {
85+
/// Input Outpoint: Transaction id and input index separated by a colon
86+
Input(String),
87+
/// Output Outpoint: Transaction id and input index separated by a colon
88+
Output(String),
89+
}
90+
91+
#[cfg(feature = "labels")]
92+
impl Label {
93+
/// Gets the [`Label`]
94+
pub fn label(&self) -> &str {
95+
match self {
96+
Label::Input(s) => s,
97+
Label::Output(s) => s,
98+
}
99+
}
100+
101+
/// Edits the [`Label`]
102+
pub fn edit(&mut self, new_label: String) {
103+
match self {
104+
Label::Input(s) => *s = new_label,
105+
Label::Output(s) => *s = new_label,
106+
}
107+
}
66108
}
67109

68110
/// A [`Utxo`] with its `satisfaction_weight`.

crates/wallet/src/wallet/mod.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ use chain::Staged;
4646
use core::fmt;
4747
use core::mem;
4848
use core::ops::Deref;
49+
#[cfg(feature = "labels")]
50+
use core::str::FromStr;
4951
use rand_core::RngCore;
5052

5153
use descriptor::error::Error as DescriptorError;
@@ -297,6 +299,32 @@ impl fmt::Display for ApplyBlockError {
297299
#[cfg(feature = "std")]
298300
impl std::error::Error for ApplyBlockError {}
299301

302+
#[cfg(feature = "labels")]
303+
/// The error type when importing [`Label`]s into a [`Wallet`].
304+
#[derive(Debug, PartialEq)]
305+
pub enum LabelError {
306+
/// There was a problem with the [`Label::Input`] type.
307+
Input(String),
308+
/// There was a problem with the [`Label::Output`] type.
309+
Output(String),
310+
/// There was a problem with the [`Label::Address`] type.
311+
Address(String),
312+
}
313+
314+
#[cfg(feature = "labels")]
315+
impl fmt::Display for LabelError {
316+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317+
match self {
318+
LabelError::Input(l) => write!(f, "input label error: {l}"),
319+
LabelError::Output(l) => write!(f, "output label error: {l}"),
320+
LabelError::Address(l) => write!(f, "address label error: {l}"),
321+
}
322+
}
323+
}
324+
325+
#[cfg(all(feature = "std", feature = "labels"))]
326+
impl std::error::Error for LabelError {}
327+
300328
impl Wallet {
301329
/// Build a new [`Wallet`].
302330
///
@@ -1570,6 +1598,8 @@ impl Wallet {
15701598
is_spent: true,
15711599
derivation_index,
15721600
confirmation_time,
1601+
#[cfg(feature = "labels")]
1602+
label: None,
15731603
}),
15741604
satisfaction_weight,
15751605
}
@@ -2310,6 +2340,67 @@ impl Wallet {
23102340
.batch_insert_relevant_unconfirmed(unconfirmed_txs);
23112341
self.stage.merge(indexed_graph_changeset.into());
23122342
}
2343+
2344+
#[cfg(feature = "labels")]
2345+
/// Exports the labels of all the wallet's UTXOs.
2346+
///
2347+
/// # Note
2348+
///
2349+
/// The labels follow the [BIP 329](https://github.com/bitcoin/bips/blob/master/bip-0329.mediawiki)
2350+
/// export/import format.
2351+
pub fn export_labels(&self) -> Vec<Option<Label>> {
2352+
let labels = self.list_output().map(|utxo| utxo.label).collect();
2353+
labels
2354+
}
2355+
2356+
#[cfg(feature = "labels")]
2357+
/// Imports labels into a wallet
2358+
///
2359+
/// # Note
2360+
///
2361+
/// The labels follow the [BIP 329](https://github.com/bitcoin/bips/blob/master/bip-0329.mediawiki)
2362+
/// export/import format.
2363+
pub fn import_labels(&self, labels: Vec<Label>) -> Result<(), LabelError> {
2364+
labels.iter().try_for_each(|label| {
2365+
match label {
2366+
Label::Input(l) => {
2367+
// parse the string into an OutPoint and then match
2368+
let parts = l.split(':').collect::<Vec<_>>();
2369+
if parts.len() != 2 {
2370+
return Err(LabelError::Input(l.to_string()));
2371+
} else {
2372+
let outpoint = OutPoint::new(
2373+
// TODO: deal with the Error types in expect
2374+
Txid::from_str(parts[0]).expect("valid txid"),
2375+
u32::from_str(parts[1]).expect("valid vout"),
2376+
);
2377+
let local_output = self.get_utxo(outpoint);
2378+
if let Some(mut local_output) = local_output {
2379+
local_output.label = Some(label.clone());
2380+
};
2381+
}
2382+
}
2383+
Label::Output(l) => {
2384+
// parse the string into an OutPoint and then match
2385+
let parts = l.split(':').collect::<Vec<_>>();
2386+
if parts.len() != 2 {
2387+
return Err(LabelError::Input(l.to_string()));
2388+
} else {
2389+
let outpoint = OutPoint::new(
2390+
// TODO: deal with the Error types in expect
2391+
Txid::from_str(parts[0]).expect("valid txid"),
2392+
u32::from_str(parts[1]).expect("valid vout"),
2393+
);
2394+
let local_output = self.get_utxo(outpoint);
2395+
if let Some(mut local_output) = local_output {
2396+
local_output.label = Some(label.clone());
2397+
};
2398+
}
2399+
}
2400+
}
2401+
Ok(())
2402+
})
2403+
}
23132404
}
23142405

23152406
/// Methods to construct sync/full-scan requests for spk-based chain sources.
@@ -2386,6 +2477,8 @@ fn new_local_utxo(
23862477
confirmation_time: full_txo.chain_position.into(),
23872478
keychain,
23882479
derivation_index,
2480+
#[cfg(feature = "labels")]
2481+
label: None,
23892482
}
23902483
}
23912484

0 commit comments

Comments
 (0)