diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index f6146a42b3..f4dd274eff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -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) } 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 70693f21b9..0bbfacf00b 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 @@ -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() + .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()