From ae059a723883d6deb07840950049810937582d02 Mon Sep 17 00:00:00 2001 From: DanS Date: Sun, 22 Mar 2026 09:14:08 -0500 Subject: [PATCH] Fix change output detection using IVK instead of static address list - Use try_sapling_note_decryption with IVKs to detect wallet-owned outputs - Correctly identifies change sent to diversified addresses - Prevents change outputs from appearing as external recipients in tx history --- lib/src/lightwallet.rs | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs index 2e41b08..a2bebb6 100644 --- a/lib/src/lightwallet.rs +++ b/lib/src/lightwallet.rs @@ -1452,11 +1452,12 @@ pub fn scan_full_tx(&self, tx: &Transaction, height: i32, datetime: u64) { // Also scan the output to see if it can be decoded with our OutgoingViewKey // If it can, then we sent this transaction, so we should be able to get // the memo and value for our records - // First, collect all our z addresses, to check for change - // Collect z addresses - let z_addresses = self.zkeys.read().unwrap().iter().map( |zk| { - encode_payment_address(self.config.hrp_sapling_address(), &zk.zaddress) - }).collect::>(); + + // Collect IVKs to detect change outputs to diversified addresses + let ivks_for_change: Vec<_> = self.zkeys.read().unwrap().iter() + .map(|zk| zk.extfvk.fvk.vk.ivk()) + .collect(); + // Search all ovks that we have let ovks: Vec<_> = self.zkeys.read().unwrap().iter() .map(|zk| zk.extfvk.fvk.ovk.clone()) @@ -1472,13 +1473,18 @@ pub fn scan_full_tx(&self, tx: &Transaction, height: i32, datetime: u64) { Some((note, payment_address, memo)) => { let address = encode_payment_address(self.config.hrp_sapling_address(), &payment_address); - // Check if this is change, and if it also doesn't have a memo, don't add - // to the outgoing metadata. - // If this is change (i.e., funds sent to ourself) AND has a memo, then - // presumably the users is writing a memo to themself, so we will add it to - // the outgoing metadata, even though it might be confusing in the UI, but hopefully - // the user can make sense of it. - if z_addresses.contains(&address) && memo.to_utf8().is_none() { + + // Check if this output belongs to our wallet using IVK decryption. + // This correctly detects change sent to diversified addresses, + // not just the wallet's default z-address. + let epk_prime = output.ephemeral_key.as_prime_order(&JUBJUB).unwrap(); + let is_to_self = ivks_for_change.iter().any(|ivk| { + try_sapling_note_decryption(ivk, &epk_prime, &output.cmu, &output.enc_ciphertext).is_some() + }); + + // If this is change (funds to ourself) without a memo, skip it. + // If the user sent a memo to themselves, keep it in outgoing metadata. + if is_to_self && memo.to_utf8().is_none() { continue; } // Update the WalletTx