From bae6e8dcb521871a7571206f74b5ef6a6fe7cfee Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 25 May 2026 21:57:46 +0000 Subject: [PATCH] fix(cashu): correct skip-on-decrypt-fail + auto-resume orphan mint quotes Two small follow-ups to the audit refactor: * recomputeUnspent: replace the broken `getOrPut { ... return@forEach }` pattern (which short-circuited the outer loop on a single decryption failure, skipping remaining tokens) with an explicit containsKey guard. Decryption failures are now individually skipped without affecting other tokens in the same pass. * CashuWalletScreen: when the wallet opens and pendingQuotes (live flow from CashuWalletState) is non-empty, automatically resume the most recent kind:7374 by re-polling the mint and reopening the receive dialog. Without this, a user who backgrounded the app mid-mint would see no indication their pending invoice exists. https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP --- .../model/nip60Cashu/CashuWalletState.kt | 16 +++++++++++----- .../screen/loggedIn/wallet/CashuWalletScreen.kt | 11 +++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuWalletState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuWalletState.kt index 3569b71936..c975cb54e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuWalletState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip60Cashu/CashuWalletState.kt @@ -324,12 +324,18 @@ class CashuWalletState( private suspend fun recomputeUnspent() { val all = tokenEvents.values.toList() // Decrypt anything we haven't seen before; reuse cached TokenContent - // for events we've already decrypted. + // for events we've already decrypted. Decryption failures are + // skipped — the proof set rebuilds the next time a re-key happens. all.forEach { evt -> - tokenContents.getOrPut(evt.id) { - runCatching { evt.tokenContent(signer) } - .onFailure { Log.w("CashuWallet") { "Failed to decrypt token ${evt.id.take(8)}: ${it.message}" } } - .getOrNull() ?: return@getOrPut return@forEach + if (!tokenContents.containsKey(evt.id)) { + val content = + runCatching { evt.tokenContent(signer) } + .onFailure { + Log.w("CashuWallet") { + "Failed to decrypt token ${evt.id.take(8)}: ${it.message}" + } + }.getOrNull() + if (content != null) tokenContents[evt.id] = content } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt index 1cb204a811..4d7dd0ffb3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt @@ -97,12 +97,23 @@ fun CashuWalletScreen( val mints by viewModel.mints.collectAsState() val balanceSats by viewModel.balanceSats.collectAsState() val history by viewModel.history.collectAsState() + val pendingQuotes by viewModel.pendingQuotes.collectAsState() var receiveOpen by remember { mutableStateOf(false) } var sendLnOpen by remember { mutableStateOf(false) } var sendTokenOpen by remember { mutableStateOf(false) } var redeemOpen by remember { mutableStateOf(false) } + // If the user has an unfinished kind:7374 quote, surface it the next time + // they open the wallet so the in-flight invoice isn't lost. Auto-resumes + // the most recent quote on first composition. + LaunchedEffect(pendingQuotes) { + if (!receiveOpen && pendingQuotes.isNotEmpty()) { + viewModel.resumeMintQuote(pendingQuotes.first()) + receiveOpen = true + } + } + Scaffold( topBar = { TopAppBar(