diff --git a/bindings/uniffi/src/lib.rs b/bindings/uniffi/src/lib.rs index d52218e..020f882 100644 --- a/bindings/uniffi/src/lib.rs +++ b/bindings/uniffi/src/lib.rs @@ -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, }, }; diff --git a/bindings/uniffi/src/rgb-lib.udl b/bindings/uniffi/src/rgb-lib.udl index 6d0355a..7ccd6fd 100644 --- a/bindings/uniffi/src/rgb-lib.udl +++ b/bindings/uniffi/src/rgb-lib.udl @@ -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] diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 989b06d..c5e7ad9 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -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::{ diff --git a/src/wallet/objects.rs b/src/wallet/objects.rs index c23ca22..60132fe 100644 --- a/src/wallet/objects.rs +++ b/src/wallet/objects.rs @@ -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 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"))] @@ -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, + /// Derivation index of the UTXO's script pubkey. `None` in the same cases as + /// [`Utxo::keychain`] + pub derivation_index: Option, } impl From for Utxo { @@ -1379,6 +1403,8 @@ impl From for Utxo { .expect("DB should contain a valid u64 value"), colorable: true, exists: x.exists, + keychain: None, + derivation_index: None, } } } @@ -1390,6 +1416,8 @@ impl From for Utxo { btc_amount: x.txout.value.to_sat(), colorable: false, exists: true, + keychain: Some(x.keychain.into()), + derivation_index: Some(x.derivation_index), } } } diff --git a/src/wallet/offline.rs b/src/wallet/offline.rs index b5b81a3..78c6c07 100644 --- a/src/wallet/offline.rs +++ b/src/wallet/offline.rs @@ -2175,10 +2175,28 @@ pub trait WalletOffline: WalletBackup { .for_each(|u| u.rgb_allocations.retain(|a| a.settled)); } - let mut internal_unspents: Vec = - self.internal_unspents().map(Unspent::from).collect(); - - unspents.append(&mut internal_unspents); + let mut to_enrich: HashMap = unspents + .iter() + .enumerate() + .map(|(i, u)| (BdkOutPoint::from(u.utxo.outpoint.clone()), i)) + .collect(); + let mut vanilla_unspents: Vec = vec![]; + for output in self.bdk_wallet().list_output() { + 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) } diff --git a/src/wallet/test/list_unspents.rs b/src/wallet/test/list_unspents.rs index 3276f4f..70d2605 100644 --- a/src/wallet/test/list_unspents.rs +++ b/src/wallet/test/list_unspents.rs @@ -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()) + .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] @@ -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(); @@ -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> = 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() @@ -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