mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 00:16:59 +00:00
fix(cashu): pending-invoice card lingering + thumbs-up mint recommendation never landing
Two bugs sharing the same root cause — LocalCache silently dropping or
losing events that downstream wallet state depended on.
1. Discard Invoice didn't remove the pending banner.
`CashuWalletState` listens on `cache.live.deletedEventBundles` to
prune `quoteEvents` when a NIP-09 delete of a kind:7374 is processed.
That stream is only emitted from `LocalCache.deleteNote`, which is
only reached when `consume(DeletionEvent)` finds the target Note
still resident in `notes` — a `LargeSoftCache<HexKey, Note>` backed
by `WeakReference`s (commons/.../LargeSoftCache.kt). Weak references
can be cleared on any GC cycle, so on a moderately busy device the
quote Note is often gone between publish and the deletion round-trip;
the cache's deleteNote path then no-ops, `_deletedEventBundles`
never fires, and our `quoteEvents` map keeps the deleted entry until
process death — manifesting as a pending-invoice banner that the
user can't dismiss.
Fix: process our own kind:5 deletions inline in the
`newEventBundles` collector (which DOES see every kind:5 we publish,
independent of soft-cache state) by extracting `deleteEventIds()`
and calling the existing `removeEvents()`. The wallet flows now stay
in sync regardless of weak-ref collection.
2. Thumbs-up on a mint never appeared in My Mint Recommendations.
`LocalCache.justConsumeAndUpdateIndexes` dispatches by event type and
falls into a `else -> Log.w("Event Not Supported")` branch for
anything missing a `when` arm — silently dropping the event. None of
the three NIP-87 events (`CashuMintEvent`, `FedimintEvent`,
`MintRecommendationEvent`) had a dispatch entry, so when the wallet
published a kind:38000 the cache rejected it, `newEventBundles`
never emitted, and `CashuWalletState.applyEvents` never indexed it.
Same broken path for mint announcements arriving from
`CashuMintDirectoryFilterAssembler`'s relay subscription.
Fix: add three dispatch entries routing all NIP-87 events through
`consumeRegularEvent`. They're parameterized-replaceable per spec
but none extend `AddressableEvent` in Quartz today, so
`consumeBaseReplaceable`'s `check(event is AddressableEvent)` would
throw — `consumeRegularEvent` works because the downstream consumers
(`CashuMintDirectoryState`, `CashuWalletState.applyEvents`) already
dedupe by `(pubKey, dTag)` and keep the newest.
https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP
This commit is contained in:
@@ -234,6 +234,9 @@ import com.vitorpamplona.quartz.nip78AppData.AppSpecificDataEvent
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip85TrustedAssertions.list.TrustProviderListEvent
|
||||
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent
|
||||
import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent
|
||||
import com.vitorpamplona.quartz.nip87Ecash.fedimint.FedimintEvent
|
||||
import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||
@@ -3133,6 +3136,32 @@ object LocalCache : ILocalCache, ICacheProvider {
|
||||
consumeRegularEvent(event, relay, wasVerified)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// NIP-87 Cashu mint discovery + recommendations
|
||||
// ============================================================
|
||||
// All three are kind 3xxxx (parameterized-replaceable per the
|
||||
// spec) but neither CashuMintEvent / FedimintEvent /
|
||||
// MintRecommendationEvent extends AddressableEvent in Quartz
|
||||
// today, so consumeBaseReplaceable's `check(event is
|
||||
// AddressableEvent)` would crash. Route through
|
||||
// consumeRegularEvent — downstream consumers
|
||||
// (CashuMintDirectoryState, CashuWalletState) already dedupe
|
||||
// by (pubKey, dTag) and keep the newest. Without these
|
||||
// entries the dispatch falls into the "Event Not Supported"
|
||||
// else branch and silently drops the event, so our own
|
||||
// kind:38000 thumbs-up never lands in the cache.
|
||||
is CashuMintEvent -> {
|
||||
consumeRegularEvent(event, relay, wasVerified)
|
||||
}
|
||||
|
||||
is FedimintEvent -> {
|
||||
consumeRegularEvent(event, relay, wasVerified)
|
||||
}
|
||||
|
||||
is MintRecommendationEvent -> {
|
||||
consumeRegularEvent(event, relay, wasVerified)
|
||||
}
|
||||
|
||||
is ChannelCreateEvent -> {
|
||||
consume(event, relay, wasVerified)
|
||||
}
|
||||
|
||||
+29
-1
@@ -31,6 +31,7 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
import com.vitorpamplona.quartz.nip60Cashu.history.CashuSpendingHistoryEvent
|
||||
import com.vitorpamplona.quartz.nip60Cashu.quote.CashuMintQuoteEvent
|
||||
import com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenEvent
|
||||
@@ -278,7 +279,34 @@ class CashuWalletState(
|
||||
jobs +=
|
||||
scope.launch(Dispatchers.Default) {
|
||||
cache.live.newEventBundles.collect { notes ->
|
||||
val ours = notes.mapNotNull { it.event }.filter(::isRelevantEvent)
|
||||
val all = notes.mapNotNull { it.event }
|
||||
|
||||
// Process our own NIP-09 deletions inline rather than
|
||||
// relying on LocalCache's `deletedEventBundles`. That
|
||||
// path only fires when `consume(DeletionEvent)` finds
|
||||
// the target Note still resident in `notes` — a
|
||||
// LargeSoftCache backed by WeakReferences, which can
|
||||
// be cleared on any GC cycle. When the weak ref is
|
||||
// gone (common between publish and the round-trip on
|
||||
// a busy device), the cache's deleteNote/removedNote
|
||||
// chain silently no-ops and our quoteEvents /
|
||||
// tokenEvents / etc. retain the logically-deleted
|
||||
// entries indefinitely — which manifested as the
|
||||
// pending-invoice card not disappearing after the
|
||||
// user tapped Discard. The kind:5 event itself does
|
||||
// reach us via newEventBundles regardless of soft-
|
||||
// cache state, so we extract its target ids and
|
||||
// remove from our maps directly.
|
||||
val ourDeleteIds =
|
||||
all
|
||||
.asSequence()
|
||||
.filterIsInstance<DeletionEvent>()
|
||||
.filter { it.pubKey == pubKey }
|
||||
.flatMap { it.deleteEventIds().asSequence() }
|
||||
.toSet()
|
||||
if (ourDeleteIds.isNotEmpty()) removeEvents(ourDeleteIds)
|
||||
|
||||
val ours = all.filter(::isRelevantEvent)
|
||||
if (ours.isNotEmpty()) {
|
||||
applyEvents(ours)
|
||||
recomputePending()
|
||||
|
||||
Reference in New Issue
Block a user