Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bindings/uniffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ use rgb_lib::{
SyncKeychain as RgbLibSyncKeychain, SyncOptions as RgbLibSyncOptions, SyncStrategy, Token,
TokenLight, Transaction, TransactionType, Transfer as RgbLibTransfer, TransferKind,
TransferTransportEndpoint, TransportEndpoint as RgbLibTransportEndpoint, TypeOfTransition,
Unspent as RgbLibUnspent, UserRole, Utxo, Wallet as RgbLibWallet, WalletData,
Unspent as RgbLibUnspent, UserRole, Utxo, UtxoKeychain, Wallet as RgbLibWallet, WalletData,
WalletDescriptors, WitnessData,
},
};
Expand Down
8 changes: 8 additions & 0 deletions bindings/uniffi/src/rgb-lib.udl
Original file line number Diff line number Diff line change
Expand Up @@ -561,12 +561,20 @@ dictionary Unspent {
u32 pending_blinded;
};

[Remote]
enum UtxoKeychain {
"Colored",
"Vanilla",
};

[Remote]
dictionary Utxo {
Outpoint outpoint;
u64 btc_amount;
boolean colorable;
boolean exists;
UtxoKeychain? keychain;
u32? derivation_index;
};

[Remote]
Expand Down
3 changes: 2 additions & 1 deletion src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ pub use objects::{
PsbtOutputInfo, ReceiveData, Recipient, RecipientInfo, RecipientType, RgbAllocation,
RgbInputInfo, RgbInspection, RgbOperationInfo, RgbOutputInfo, RgbTransitionInfo, Token,
TokenLight, Transaction, TransactionType, Transfer, TransferKind, TransferTransportEndpoint,
TransportEndpoint, TypeOfTransition, Unspent, Utxo, WalletData, WalletDescriptors, WitnessData,
TransportEndpoint, TypeOfTransition, Unspent, Utxo, UtxoKeychain, WalletData,
WalletDescriptors, WitnessData,
};
#[cfg(any(feature = "electrum", feature = "esplora"))]
pub use objects::{
Expand Down
28 changes: 28 additions & 0 deletions src/wallet/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1355,6 +1355,24 @@ pub struct ReceiveDataInternal {
// UTXOs & unspents
// ────────────────────────────────────────────────────────────

/// The wallet keychain a UTXO belongs to.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub enum UtxoKeychain {
/// The colored keychain, holding colorable UTXOs
Colored,
/// The vanilla keychain, holding non-colorable UTXOs
Vanilla,
}

impl From<KeychainKind> for UtxoKeychain {
fn from(x: KeychainKind) -> UtxoKeychain {
match x {
KeychainKind::External => UtxoKeychain::Colored,
KeychainKind::Internal => UtxoKeychain::Vanilla,
}
}
}

/// A Bitcoin unspent transaction output.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "camel_case", serde(rename_all = "camelCase"))]
Expand All @@ -1367,6 +1385,12 @@ pub struct Utxo {
pub colorable: bool,
/// Defines if the UTXO already exists (TX that creates it has been broadcasted)
pub exists: bool,
/// Keychain of the UTXO. `None` when the UTXO is not (yet) part of the wallet's transaction
/// graph, e.g. when `exists` is false (pending witness receive or in-flight send change)
pub keychain: Option<UtxoKeychain>,
/// Derivation index of the UTXO's script pubkey. `None` in the same cases as
/// [`Utxo::keychain`]
pub derivation_index: Option<u32>,
}

impl From<DbTxo> for Utxo {
Expand All @@ -1379,6 +1403,8 @@ impl From<DbTxo> for Utxo {
.expect("DB should contain a valid u64 value"),
colorable: true,
exists: x.exists,
keychain: None,
derivation_index: None,
}
}
}
Expand All @@ -1390,6 +1416,8 @@ impl From<LocalOutput> for Utxo {
btc_amount: x.txout.value.to_sat(),
colorable: false,
exists: true,
keychain: Some(x.keychain.into()),
derivation_index: Some(x.derivation_index),
}
}
}
Expand Down
26 changes: 22 additions & 4 deletions src/wallet/offline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2175,10 +2175,28 @@ pub trait WalletOffline: WalletBackup {
.for_each(|u| u.rgb_allocations.retain(|a| a.settled));
}

let mut internal_unspents: Vec<Unspent> =
self.internal_unspents().map(Unspent::from).collect();

unspents.append(&mut internal_unspents);
let mut to_enrich: HashMap<BdkOutPoint, usize> = unspents
.iter()
.enumerate()
.map(|(i, u)| (BdkOutPoint::from(u.utxo.outpoint.clone()), i))
.collect();
let mut vanilla_unspents: Vec<Unspent> = vec![];
for output in self.bdk_wallet().list_output() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

list_output() iterates all spent + unspent outputs; that feels heavy for every list_unspents call. Could we enrich colored UTXOs with get_tx + derivation_of_spk and keep internal_unspents() for vanilla? That should also handle almost-spent colored UTXOs without a full graph walk.

match output.keychain {
KeychainKind::External => {
if let Some(i) = to_enrich.remove(&output.outpoint) {
unspents[i].utxo.keychain = Some(output.keychain.into());
unspents[i].utxo.derivation_index = Some(output.derivation_index);
}
}
KeychainKind::Internal => {
if !output.is_spent {
vanilla_unspents.push(Unspent::from(output));
}
}
}
}
unspents.append(&mut vanilla_unspents);

Ok(unspents)
}
Expand Down
54 changes: 54 additions & 0 deletions src/wallet/test/list_unspents.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
use super::*;

#[cfg(feature = "electrum")]
fn check_unspents_derivation(wallet: &Wallet, unspents: &[Unspent]) {
for unspent in unspents.iter().filter(|u| u.utxo.exists) {
let keychain = match unspent.utxo.keychain.unwrap() {
UtxoKeychain::Colored => KeychainKind::External,
UtxoKeychain::Vanilla => KeychainKind::Internal,
};
let derived_spk = wallet
.bdk_wallet()
.public_descriptor(keychain)
.at_derivation_index(unspent.utxo.derivation_index.unwrap())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests currently only cover Taproot wallets. Would you mind adding a SegWitV0 case so we're confident at_derivation_index / descriptor handling works for both witness versions?

.unwrap()
.script_pubkey();
let txid: bdk_wallet::bitcoin::Txid = unspent.utxo.outpoint.txid.parse().unwrap();
let tx = wallet.bdk_wallet().get_tx(txid).unwrap();
let actual_spk = tx.tx_node.tx.output[unspent.utxo.outpoint.vout as usize]
.script_pubkey
.clone();
assert_eq!(derived_spk, actual_spk);
}
}

#[cfg(feature = "electrum")]
#[test]
#[parallel]
Expand Down Expand Up @@ -31,6 +53,10 @@ fn success() {
let unspent_list_all = party.list_unspents(false);
assert_eq!(unspent_list_all.len(), 1);
assert!(unspent_list_all.iter().all(|u| !u.utxo.colorable));
assert!(unspent_list_all.iter().all(
|u| u.utxo.keychain == Some(UtxoKeychain::Vanilla) && u.utxo.derivation_index.is_some()
));
check_unspents_derivation(&party.wallet, &unspent_list_all);

party.create_utxos_default();

Expand All @@ -51,6 +77,19 @@ fn success() {
.count(),
1
);
let colorable_unspents: Vec<&Unspent> = unspent_list_all
.iter()
.filter(|u| u.utxo.colorable)
.collect();
assert!(colorable_unspents.iter().all(
|u| u.utxo.keychain == Some(UtxoKeychain::Colored) && u.utxo.derivation_index.is_some()
));
let derivation_indexes: HashSet<Option<u32>> = colorable_unspents
.iter()
.map(|u| u.utxo.derivation_index)
.collect();
assert_eq!(derivation_indexes.len(), colorable_unspents.len());
check_unspents_derivation(&party.wallet, &unspent_list_all);
let mut settled_allocations = vec![];
unspent_list_settled
.iter()
Expand Down Expand Up @@ -222,6 +261,21 @@ fn success() {
.count(),
1
);
// pending (exists = false) UTXOs are not in the bdk graph yet so their keychain and
// derivation index are unknown, while all existing UTXOs in the same response have them
assert!(
unspent_list_all
.iter()
.filter(|u| !u.utxo.exists)
.all(|u| u.utxo.keychain.is_none() && u.utxo.derivation_index.is_none())
);
assert!(
unspent_list_all
.iter()
.filter(|u| u.utxo.exists)
.all(|u| u.utxo.keychain.is_some() && u.utxo.derivation_index.is_some())
);
check_unspents_derivation(&party.wallet, &unspent_list_all);
let mut pending_allocations = vec![];
let mut settled_allocations = vec![];
unspent_list_all
Expand Down
Loading