|
1 | | -use std::collections::HashMap; |
| 1 | +use std::collections::{HashMap, HashSet}; |
2 | 2 | use std::fmt::Debug; |
3 | 3 | use std::str::FromStr; |
4 | 4 | use std::sync::{Arc, RwLock}; |
@@ -257,6 +257,29 @@ impl CashuKvDatabase { |
257 | 257 | format!("proof_{}", hex::encode(proof.y.serialize())) |
258 | 258 | } |
259 | 259 |
|
| 260 | + fn update_proofs_cache( |
| 261 | + cache: &mut Vec<ProofInfo>, added: Vec<ProofInfo>, removed_ys: Vec<PublicKey>, |
| 262 | + ) { |
| 263 | + let mut added_ys = HashSet::new(); |
| 264 | + let mut added_deduped = Vec::with_capacity(added.len()); |
| 265 | + for proof in added.into_iter().rev() { |
| 266 | + if added_ys.insert(proof.y) { |
| 267 | + added_deduped.push(proof); |
| 268 | + } |
| 269 | + } |
| 270 | + added_deduped.reverse(); |
| 271 | + |
| 272 | + let removed_ys: HashSet<_> = removed_ys.into_iter().collect(); |
| 273 | + |
| 274 | + // Match the KV store's proof_<Y> uniqueness by replacing cached entries with |
| 275 | + // the same Y before appending the updated proof data. |
| 276 | + cache.retain(|proof| !added_ys.contains(&proof.y)); |
| 277 | + cache.extend(added_deduped); |
| 278 | + |
| 279 | + // Remove proofs with matching Y values |
| 280 | + cache.retain(|proof| !removed_ys.contains(&proof.y)); |
| 281 | + } |
| 282 | + |
260 | 283 | fn generate_mint_key(mint_url: &MintUrl) -> String { |
261 | 284 | // Generate a deterministic hash of the mint URL for use as a key |
262 | 285 | let mut hasher = DefaultHasher::new(); |
@@ -736,12 +759,7 @@ impl WalletDatabase<cdk::cdk_database::Error> for CashuKvDatabase { |
736 | 759 | // Update cache |
737 | 760 | { |
738 | 761 | let mut cache = self.proofs_cache.write().unwrap(); |
739 | | - |
740 | | - // Add new proofs |
741 | | - cache.extend(added); |
742 | | - |
743 | | - // Remove proofs with matching Y values |
744 | | - cache.retain(|proof| !removed_ys.contains(&proof.y)); |
| 762 | + Self::update_proofs_cache(&mut cache, added, removed_ys); |
745 | 763 | } |
746 | 764 |
|
747 | 765 | Ok(()) |
@@ -1262,3 +1280,69 @@ pub(super) async fn write_has_recovered( |
1262 | 1280 | .await |
1263 | 1281 | .map_err(TrustedError::IOError) |
1264 | 1282 | } |
| 1283 | + |
| 1284 | +#[cfg(test)] |
| 1285 | +mod tests { |
| 1286 | + use super::*; |
| 1287 | + use cdk::Amount; |
| 1288 | + use cdk::nuts::Proof; |
| 1289 | + use cdk::secret::Secret; |
| 1290 | + |
| 1291 | + fn proof_info(secret: &str, amount: u64) -> ProofInfo { |
| 1292 | + let proof = Proof::new( |
| 1293 | + Amount::from(amount), |
| 1294 | + Id::from_str("009a1f293253e41e").unwrap(), |
| 1295 | + Secret::new(secret), |
| 1296 | + PublicKey::from_str( |
| 1297 | + "024369d2d22a80ecf78f3937da9d5f30c1b9f74f0c32684d583cca0fa6a61cdcfc", |
| 1298 | + ) |
| 1299 | + .unwrap(), |
| 1300 | + ); |
| 1301 | + |
| 1302 | + ProofInfo::new( |
| 1303 | + proof, |
| 1304 | + MintUrl::from_str("https://mint.example.com").unwrap(), |
| 1305 | + State::Unspent, |
| 1306 | + CurrencyUnit::Sat, |
| 1307 | + ) |
| 1308 | + .unwrap() |
| 1309 | + } |
| 1310 | + |
| 1311 | + #[test] |
| 1312 | + fn update_proofs_cache_replaces_existing_proof_with_same_y() { |
| 1313 | + let original = proof_info("same cached proof", 1); |
| 1314 | + let replacement = proof_info("same cached proof", 2); |
| 1315 | + let mut cache = vec![original.clone()]; |
| 1316 | + |
| 1317 | + CashuKvDatabase::update_proofs_cache(&mut cache, vec![replacement.clone()], vec![]); |
| 1318 | + |
| 1319 | + assert_eq!(cache, vec![replacement]); |
| 1320 | + assert_eq!(cache.iter().filter(|proof| proof.y == original.y).count(), 1); |
| 1321 | + } |
| 1322 | + |
| 1323 | + #[test] |
| 1324 | + fn update_proofs_cache_deduplicates_added_proofs_by_y() { |
| 1325 | + let original = proof_info("duplicate added proof", 1); |
| 1326 | + let replacement = proof_info("duplicate added proof", 2); |
| 1327 | + let mut cache = Vec::new(); |
| 1328 | + |
| 1329 | + CashuKvDatabase::update_proofs_cache( |
| 1330 | + &mut cache, |
| 1331 | + vec![original.clone(), replacement.clone()], |
| 1332 | + vec![], |
| 1333 | + ); |
| 1334 | + |
| 1335 | + assert_eq!(cache, vec![replacement]); |
| 1336 | + assert_eq!(cache.iter().filter(|proof| proof.y == original.y).count(), 1); |
| 1337 | + } |
| 1338 | + |
| 1339 | + #[test] |
| 1340 | + fn update_proofs_cache_removal_wins_over_added_proof() { |
| 1341 | + let proof = proof_info("removed cached proof", 1); |
| 1342 | + let mut cache = vec![proof.clone()]; |
| 1343 | + |
| 1344 | + CashuKvDatabase::update_proofs_cache(&mut cache, vec![proof.clone()], vec![proof.y]); |
| 1345 | + |
| 1346 | + assert!(cache.is_empty()); |
| 1347 | + } |
| 1348 | +} |
0 commit comments