Skip to content
Open
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
74 changes: 67 additions & 7 deletions orange-sdk/src/trusted_wallet/cashu/cashu_store.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::str::FromStr;
use std::sync::{Arc, RwLock};
Expand Down Expand Up @@ -257,6 +257,21 @@ impl CashuKvDatabase {
format!("proof_{}", hex::encode(proof.y.serialize()))
}

fn update_proofs_cache(
cache: &mut Vec<ProofInfo>, added: Vec<ProofInfo>, removed_ys: Vec<PublicKey>,
) {
let added_ys: HashSet<_> = added.iter().map(|proof| proof.y).collect();
let removed_ys: HashSet<_> = removed_ys.into_iter().collect();

// Match the KV store's proof_<Y> uniqueness by replacing cached entries with
// the same Y before appending the updated proof data.
cache.retain(|proof| !added_ys.contains(&proof.y));
cache.extend(added);
Copy link
Copy Markdown
Contributor

@thesimplekid thesimplekid May 19, 2026

Choose a reason for hiding this comment

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

To fully protect from duplicates we could de-duplicate the added vec, though cdk should not attempt to add duplicate proofs like this anyway so maybe not needed.


// Remove proofs with matching Y values
cache.retain(|proof| !removed_ys.contains(&proof.y));
}

fn generate_mint_key(mint_url: &MintUrl) -> String {
// Generate a deterministic hash of the mint URL for use as a key
let mut hasher = DefaultHasher::new();
Expand Down Expand Up @@ -736,12 +751,7 @@ impl WalletDatabase<cdk::cdk_database::Error> for CashuKvDatabase {
// Update cache
{
let mut cache = self.proofs_cache.write().unwrap();

// Add new proofs
cache.extend(added);

// Remove proofs with matching Y values
cache.retain(|proof| !removed_ys.contains(&proof.y));
Self::update_proofs_cache(&mut cache, added, removed_ys);
}

Ok(())
Expand Down Expand Up @@ -1262,3 +1272,53 @@ pub(super) async fn write_has_recovered(
.await
.map_err(TrustedError::IOError)
}

#[cfg(test)]
mod tests {
use super::*;
use cdk::Amount;
use cdk::nuts::Proof;
use cdk::secret::Secret;

fn proof_info(secret: &str, amount: u64) -> ProofInfo {
let proof = Proof::new(
Amount::from(amount),
Id::from_str("009a1f293253e41e").unwrap(),
Secret::new(secret),
PublicKey::from_str(
"024369d2d22a80ecf78f3937da9d5f30c1b9f74f0c32684d583cca0fa6a61cdcfc",
)
.unwrap(),
);

ProofInfo::new(
proof,
MintUrl::from_str("https://mint.example.com").unwrap(),
State::Unspent,
CurrencyUnit::Sat,
)
.unwrap()
}

#[test]
fn update_proofs_cache_replaces_existing_proof_with_same_y() {
let original = proof_info("same cached proof", 1);
let replacement = proof_info("same cached proof", 2);
let mut cache = vec![original.clone()];

CashuKvDatabase::update_proofs_cache(&mut cache, vec![replacement.clone()], vec![]);

assert_eq!(cache, vec![replacement]);
assert_eq!(cache.iter().filter(|proof| proof.y == original.y).count(), 1);
}

#[test]
fn update_proofs_cache_removal_wins_over_added_proof() {
let proof = proof_info("removed cached proof", 1);
let mut cache = vec![proof.clone()];

CashuKvDatabase::update_proofs_cache(&mut cache, vec![proof.clone()], vec![proof.y]);

assert!(cache.is_empty());
}
}
Loading