From e76566fba2ba3a6cdc90fb2e85c0b341b12bae10 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 22:14:38 +0000 Subject: [PATCH] fix: address review findings on the event rename branch - Deprecated aliases for merged classes now keep their old companion API: TokenEvent.build(encryptedContent), NutzapRedemptionEvent.build(nutzap, content), SoftwareReleaseEvent.build(appId, version, channel, assets) and buildDTag resolve to deprecated forwarders instead of an incompatible builder. Covered by DeprecatedMergedEventApiTest. - CashuWalletOps.redeemNutzap now tags the nutzap sender (NIP-61 ["p", sender]), using the notifySender helper from the merge. - TokenReference.parseFromTag requires a 64-char id, like ETag, so redeemedReferences() and redeemedNutzaps() agree; drop the per-tag list allocation. - Kind 30063 search: check content is non-blank before the two NIP-82 tag scans. - SoftwareApp: findAll/findLatest release lookups no longer re-run the NIP-82 and d-tag checks that the addressables scan already applied; cheap d-tag check first. - Deprecate asSoftwareRelease(): EventFactory already builds ReleaseArtifactSetEvent, and re-wrapping another kind breaks its id/sig. - Replace inline fully-qualified names touched by the rename with imports. - nostr-expert event-hierarchy doc: describe the real NIP-51 class hierarchy. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015VvJ8XpeYqBe8bYT3bJ7UD --- .../references/event-hierarchy.md | 22 ++++-- .../amethyst/ui/note/types/SoftwareApp.kt | 22 +++--- .../create/NewCalendarCollectionViewModel.kt | 2 +- .../appfunctions/AmethystAppFunctions.kt | 3 +- .../commons/cashu/ops/CashuWalletOps.kt | 6 +- .../commons/viewmodels/NestViewModel.kt | 3 +- .../vitorpamplona/amethyst/desktop/Main.kt | 3 +- .../DesktopRelaySubscriptionsCoordinator.kt | 3 +- .../desktop/ui/note/DesktopRichText.kt | 3 +- .../release/Nip82ReleaseDetector.kt | 5 ++ .../release/SoftwareReleaseEvent.kt | 32 +++++++++ .../ReleaseArtifactSetEvent.kt | 5 +- .../nip60Cashu/history/TokenReference.kt | 6 +- .../redemption/NutzapRedemptionEvent.kt | 16 +++++ .../quartz/nip61Nutzaps/token/TokenEvent.kt | 16 +++++ .../SoftwareApplicationEventTest.kt | 9 +-- .../utils/DeprecatedMergedEventApiTest.kt | 68 +++++++++++++++++++ 17 files changed, 187 insertions(+), 37 deletions(-) create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/DeprecatedMergedEventApiTest.kt diff --git a/.claude/skills/nostr-expert/references/event-hierarchy.md b/.claude/skills/nostr-expert/references/event-hierarchy.md index cf6f6dfd12..a2c288c9de 100644 --- a/.claude/skills/nostr-expert/references/event-hierarchy.md +++ b/.claude/skills/nostr-expert/references/event-hierarchy.md @@ -95,14 +95,22 @@ class LongFormContentEvent(...) : BaseAddressableEvent(...) // Addressable via kind:pubkey:d-tag ``` -### Lists (kinds 10000-30004) +### Lists (NIP-51) +Each list kind is its own class under `nip51Lists/`, not a subtype of a shared list event. +Lists with private (NIP-44 encrypted) items extend one of two bases: ```kotlin -sealed class FollowSetEvent : BaseAddressableEvent { - object MuteList : FollowSetEvent(10000) - object PinList : FollowSetEvent(10001) - object BookmarkList : FollowSetEvent(10003) - // ... 18 list types total -} +// Base for sets (kind 30000-39999); some replaceable lists use it too (MuteListEvent, InterestListEvent) +abstract class PrivateTagArrayEvent(...) : BaseAddressableEvent(...) +// Replaceable lists (kind 10000-19999) +abstract class PrivateReplaceableTagArrayEvent(...) : BaseReplaceableEvent(...) + +class MuteListEvent(...) : PrivateTagArrayEvent(...) // kind 10000 +class PinListEvent(...) : BaseReplaceableEvent(...) // kind 10001, public only +class BookmarkListEvent(...) : PrivateReplaceableTagArrayEvent(...) // kind 10003 +class InterestListEvent(...) : PrivateTagArrayEvent(...) // kind 10015 +class FollowSetEvent(...) : PrivateTagArrayEvent(...) // kind 30000 follow sets +class BookmarkSetEvent(...) : PrivateTagArrayEvent(...) // kind 30003 bookmark sets +class StarterPackEvent(...) : BaseAddressableEvent(...) // kind 39089 starter packs ``` ## Event Interfaces diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt index 8067e9db46..2caa900e85 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/SoftwareApp.kt @@ -242,7 +242,7 @@ fun produceLatestReleaseVersion(app: SoftwareApplicationEvent): State { return flow.collectAsStateWithLifecycle(initialValue = null) } -fun findLatestNip82Release(app: SoftwareApplicationEvent): ReleaseArtifactSetEvent? = latestNip82Release(nip82ReleaseNotesFor(app), app) +fun findLatestNip82Release(app: SoftwareApplicationEvent): ReleaseArtifactSetEvent? = nip82ReleasesFor(app).maxByOrNull { it.createdAt } /** Picks the newest NIP-82 release for [app] out of an already-narrowed [notes] collection. */ private fun latestNip82Release( @@ -256,12 +256,13 @@ private fun latestNip82Release( } /** kind-30063 addressables authored by [app] whose `d` tag is `@`. */ -private fun nip82ReleaseNotesFor(app: SoftwareApplicationEvent): Set { +private fun nip82ReleasesFor(app: SoftwareApplicationEvent): List { val prefix = "${app.dTag()}@" - return LocalCache.addressables.filterIntoSet(ReleaseArtifactSetEvent.KIND, app.pubKey) { _, addr -> - val ev = addr.event ?: return@filterIntoSet false - ev.isNip82SoftwareRelease() && ev.dTag().startsWith(prefix) - } + return LocalCache.addressables + .filterIntoSet(ReleaseArtifactSetEvent.KIND, app.pubKey) { _, addr -> + val ev = addr.event ?: return@filterIntoSet false + ev.dTag().startsWith(prefix) && ev.isNip82SoftwareRelease() + }.mapNotNull { it.event as? ReleaseArtifactSetEvent } } /** @@ -270,15 +271,10 @@ private fun nip82ReleaseNotesFor(app: SoftwareApplicationEvent): Set { */ private fun Note.asNip82ReleaseFor(prefix: String): ReleaseArtifactSetEvent? { val ev = event as? ReleaseArtifactSetEvent ?: return null - return ev.takeIf { it.isNip82SoftwareRelease() && it.dTag().startsWith(prefix) } + return ev.takeIf { it.dTag().startsWith(prefix) && it.isNip82SoftwareRelease() } } -fun findAllNip82Releases(app: SoftwareApplicationEvent): List { - val prefix = "${app.dTag()}@" - return nip82ReleaseNotesFor(app) - .mapNotNull { it.asNip82ReleaseFor(prefix) } - .sortedByDescending { it.createdAt } -} +fun findAllNip82Releases(app: SoftwareApplicationEvent): List = nip82ReleasesFor(app).sortedByDescending { it.createdAt } @Composable fun AppIcon( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt index f389c7a9c1..24dace858c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/create/NewCalendarCollectionViewModel.kt @@ -62,7 +62,7 @@ class NewCalendarCollectionViewModel : ViewModel() { private var dTag: String? = null /** The full original event in edit mode; needed to publish a NIP-09 deletion. */ - private var loadedEvent: com.vitorpamplona.quartz.nip52Calendar.calendar.CalendarCollectionEvent? = null + private var loadedEvent: CalendarCollectionEvent? = null val selectedAddresses = mutableStateListOf
() val availableAppointments = mutableStateOf>(emptyList()) diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/appfunctions/AmethystAppFunctions.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/appfunctions/AmethystAppFunctions.kt index f14ec732b7..c865137a76 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/appfunctions/AmethystAppFunctions.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/appfunctions/AmethystAppFunctions.kt @@ -62,6 +62,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -1787,7 +1788,7 @@ class AmethystAppFunctions { lnAddress: String, sats: Long, comment: String, - zapRequest: com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent, + zapRequest: ZapRequestEvent, ): String { // Compute the LNURL-pay endpoint so we can ask the privacy-aware // HttpClient builder for the right OkHttpClient for that host. diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/ops/CashuWalletOps.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/ops/CashuWalletOps.kt index e26b1471d1..8262ec6188 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/ops/CashuWalletOps.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/cashu/ops/CashuWalletOps.kt @@ -57,6 +57,7 @@ import com.vitorpamplona.quartz.nip60Cashu.wallet.CashuWalletEvent import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent import com.vitorpamplona.quartz.nip61Nutzaps.info.tags.NutzapMintTag import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent +import com.vitorpamplona.quartz.nip61Nutzaps.redemption.notifySender import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent import com.vitorpamplona.quartz.utils.Log @@ -914,7 +915,10 @@ class CashuWalletOps( TokenReference(nutzap.id, null, TokenReference.MARKER_REDEEMED), ), signer = signer, - ) + ) { + // NIP-61: tag the nutzap sender so their client learns it was redeemed. + notifySender(EventHintBundle(nutzap)) + } val historyEvent = signer.sign(historyTemplate) publish(historyEvent) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModel.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModel.kt index faf5d970f1..e642ed3aa5 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModel.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/NestViewModel.kt @@ -45,6 +45,7 @@ import com.vitorpamplona.nestsclient.connectReconnectingNestsSpeaker import com.vitorpamplona.nestsclient.transport.WebTransportFactory import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.collections.immutable.ImmutableSet @@ -629,7 +630,7 @@ class NestViewModel( * same window so the visual cadence matches the React button. */ fun onZapEvent( - event: com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent, + event: ZapReceiptEvent, nowSec: Long, windowSec: Long = REACTION_WINDOW_SEC, ) { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index a25cac2394..ce53aa2bde 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -166,6 +166,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket +import com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent @@ -1926,7 +1927,7 @@ fun MainContent( // Process into chatroomList based on event type when (event) { - is com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent -> { + is EncryptedDmEvent -> { iAccount.chatroomList.addMessage( event.chatroomKey(iAccount.pubKey), note, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt index d72846949e..140f96d844 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt @@ -39,6 +39,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.fetchAll import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException @@ -310,7 +311,7 @@ class DesktopRelaySubscriptionsCoordinator( ), // Zap receipts (kind 9735) targeting these notes Filter( - kinds = listOf(com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent.KIND), + kinds = listOf(ZapReceiptEvent.KIND), tags = mapOf("e" to noteIds), ), // Reposts (kind 6) targeting these notes diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt index 257baa799b..eb45da9db6 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/DesktopRichText.kt @@ -84,6 +84,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NPub +import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent import java.awt.Toolkit import java.awt.datatransfer.StringSelection import java.net.URI @@ -376,7 +377,7 @@ private fun RenderBechSegment( .NAddress .parse(segment.segmentText) } - if (naddr != null && naddr.kind == com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent.KIND) { + if (naddr != null && naddr.kind == StarterPackEvent.KIND) { val followPacks = com.vitorpamplona.amethyst.desktop.ui.deck.LocalFollowPacksState.current val relayManager = com.vitorpamplona.amethyst.desktop.ui.deck.LocalRelayManager.current if (localCache != null && relayManager != null && followPacks != null) { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/Nip82ReleaseDetector.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/Nip82ReleaseDetector.kt index 088ad5934a..6ed0e63cf5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/Nip82ReleaseDetector.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/Nip82ReleaseDetector.kt @@ -44,4 +44,9 @@ fun Event.isNip82SoftwareRelease() = * Reinterprets `this` as a [ReleaseArtifactSetEvent]. Use [isNip82SoftwareRelease] * to gate the call. */ +@Deprecated( + "EventFactory already builds kind 30063 as ReleaseArtifactSetEvent, which exposes the NIP-82 fields. " + + "Re-wrapping an event of another kind yields one whose kind no longer matches its id and signature.", + ReplaceWith("this as ReleaseArtifactSetEvent", "com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent"), +) fun Event.asSoftwareRelease() = this as? ReleaseArtifactSetEvent ?: ReleaseArtifactSetEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/SoftwareReleaseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/SoftwareReleaseEvent.kt index 86712d1400..872b86630e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/SoftwareReleaseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/release/SoftwareReleaseEvent.kt @@ -20,7 +20,11 @@ */ package com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release +import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAssetEvent +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent +import com.vitorpamplona.quartz.utils.TimeUtils /** * NIP-82 software releases share kind 30063 with NIP-51 release artifact sets, and @@ -32,3 +36,31 @@ import com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSet ReplaceWith("ReleaseArtifactSetEvent", "com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent"), ) typealias SoftwareReleaseEvent = ReleaseArtifactSetEvent + +/** The old `SoftwareReleaseEvent.buildDTag`. */ +@Deprecated( + "Use ReleaseArtifactSetEvent.buildSoftwareReleaseDTag.", + ReplaceWith("ReleaseArtifactSetEvent.buildSoftwareReleaseDTag(appId, version)", "com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent"), +) +fun ReleaseArtifactSetEvent.Companion.buildDTag( + appId: String, + version: String, +) = buildSoftwareReleaseDTag(appId, version) + +/** The old `SoftwareReleaseEvent.build`: a NIP-82 release, not the NIP-51 set builder. */ +@Deprecated( + "Use ReleaseArtifactSetEvent.buildSoftwareRelease.", + ReplaceWith( + "ReleaseArtifactSetEvent.buildSoftwareRelease(appId, version, channel, assets, releaseNotes, createdAt, initializer)", + "com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent", + ), +) +fun ReleaseArtifactSetEvent.Companion.build( + appId: String, + version: String, + channel: String, + assets: List>, + releaseNotes: String = "", + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, +) = buildSoftwareRelease(appId, version, channel, assets, releaseNotes, createdAt, initializer) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt index 0e8a9db9ab..8926ed25cb 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/releaseArtifactSet/ReleaseArtifactSetEvent.kt @@ -82,8 +82,9 @@ class ReleaseArtifactSetEvent( } // Only NIP-82 releases carry release notes in `content`. A NIP-51 set may keep - // encrypted private items there, which must never reach the index. - private fun searchableReleaseNotes() = if (isNip82SoftwareRelease()) content.ifBlank { null } else null + // encrypted private items there, which must never reach the index. The blank + // check runs first so empty sets skip the two tag scans on every search. + private fun searchableReleaseNotes() = if (content.isNotBlank() && isNip82SoftwareRelease()) content else null override fun eventHints() = tags.mapNotNull(EventBookmark::parseAsHint) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip60Cashu/history/TokenReference.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip60Cashu/history/TokenReference.kt index fe16e631f2..ca73026eca 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip60Cashu/history/TokenReference.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip60Cashu/history/TokenReference.kt @@ -43,9 +43,11 @@ data class TokenReference( const val MARKER_REDEEMED = "redeemed" fun parseFromTag(tag: Array): TokenReference? { - if (tag.size < 4 || tag[0] != "e") return null + // Same id rule as ETag.parse, so redeemedReferences() and the NIP-61 + // redeemedNutzaps() agree on which tags are valid. + if (tag.size < 4 || tag[0] != "e" || tag[1].length != 64) return null val marker = tag[3] - if (marker !in listOf(MARKER_CREATED, MARKER_DESTROYED, MARKER_REDEEMED)) return null + if (marker != MARKER_CREATED && marker != MARKER_DESTROYED && marker != MARKER_REDEEMED) return null return TokenReference( eventId = tag[1], relay = tag.getOrNull(2)?.ifEmpty { null }, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/redemption/NutzapRedemptionEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/redemption/NutzapRedemptionEvent.kt index 1382f57b12..0b2897a1cf 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/redemption/NutzapRedemptionEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/redemption/NutzapRedemptionEvent.kt @@ -44,6 +44,22 @@ fun CashuSpendingHistoryEvent.Companion.buildNutzapRedemption( initializer() } +/** The old `NutzapRedemptionEvent.build`. */ +@Deprecated( + "Use CashuSpendingHistoryEvent.buildNutzapRedemption.", + ReplaceWith( + "CashuSpendingHistoryEvent.buildNutzapRedemption(nutzap, encryptedContent, createdAt, initializer)", + "com.vitorpamplona.quartz.nip60Cashu.history.CashuSpendingHistoryEvent", + "com.vitorpamplona.quartz.nip61Nutzaps.redemption.buildNutzapRedemption", + ), +) +fun CashuSpendingHistoryEvent.Companion.build( + nutzap: EventHintBundle, + encryptedContent: String, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, +) = buildNutzapRedemption(nutzap, encryptedContent, createdAt, initializer) + /** * NIP-61 redemptions are NIP-60 spending history events (kind 7376). This used to be a * second class for the same kind, which `EventFactory` could never instantiate because diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/token/TokenEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/token/TokenEvent.kt index 0e72608c50..e48f186e62 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/token/TokenEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip61Nutzaps/token/TokenEvent.kt @@ -20,7 +20,10 @@ */ package com.vitorpamplona.quartz.nip61Nutzaps.token +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenEvent +import com.vitorpamplona.quartz.utils.TimeUtils /** * NIP-61 does not define its own kind 7375: redeemed nutzap proofs are stored as @@ -32,3 +35,16 @@ import com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenEvent ReplaceWith("CashuTokenEvent", "com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenEvent"), ) typealias TokenEvent = CashuTokenEvent + +/** The old `TokenEvent.build` took content that was already encrypted. */ +@Deprecated( + "Use CashuTokenEvent.build(tokenContent, signer), which encrypts the token content.", + ReplaceWith("CashuTokenEvent.build(tokenContent, signer)", "com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenEvent"), +) +fun CashuTokenEvent.Companion.build( + encryptedContent: String, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, +) = eventTemplate(KIND, encryptedContent, createdAt) { + initializer() +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/SoftwareApplicationEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/SoftwareApplicationEventTest.kt index 6f5166d193..39bd9b45f4 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/SoftwareApplicationEventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/nip82SoftwareApps/SoftwareApplicationEventTest.kt @@ -32,7 +32,6 @@ import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.SoftwareAss import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.apkCertificateHash import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.platform import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.versionCode -import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.asSoftwareRelease import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.isNip82SoftwareRelease import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.shared.Platform import com.vitorpamplona.quartz.nip01Core.core.Event @@ -137,13 +136,12 @@ class SoftwareApplicationEventTest { } @Test - fun asSoftwareRelease_exposesParsedFields() { - val event = - Event( + fun releaseArtifactSet_exposesParsedNip82Fields() { + val release = + ReleaseArtifactSetEvent( id = "0".repeat(64), pubKey = "1".repeat(64), createdAt = 0, - kind = ReleaseArtifactSetEvent.KIND, tags = arrayOf( arrayOf("d", "com.example.app@1.0.0"), @@ -154,7 +152,6 @@ class SoftwareApplicationEventTest { content = "Initial release", sig = "", ) - val release = event.asSoftwareRelease() assertEquals("com.example.app", release.appId()) assertEquals("1.0.0", release.version()) assertEquals("main", release.channel()) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/DeprecatedMergedEventApiTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/DeprecatedMergedEventApiTest.kt new file mode 100644 index 0000000000..740e463ab3 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/DeprecatedMergedEventApiTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +@file:Suppress("DEPRECATION") + +package com.vitorpamplona.quartz.utils + +import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.SoftwareReleaseEvent +import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.build +import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release.buildDTag +import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle +import com.vitorpamplona.quartz.nip60Cashu.history.CashuSpendingHistoryEvent +import com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenEvent +import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent +import com.vitorpamplona.quartz.nip61Nutzaps.redemption.NutzapRedemptionEvent +import com.vitorpamplona.quartz.nip61Nutzaps.redemption.build +import com.vitorpamplona.quartz.nip61Nutzaps.token.TokenEvent +import com.vitorpamplona.quartz.nip61Nutzaps.token.build +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * The classes merged into another class keep their old companion calls compiling + * through the deprecated typealiases. If any of these stop resolving, external + * Quartz users break on upgrade. + */ +class DeprecatedMergedEventApiTest { + private val nutzap = NutzapEvent("a".repeat(64), "1".repeat(64), 1, emptyArray(), "", "b".repeat(128)) + + @Test + fun tokenEventBuildStillTakesEncryptedContent() { + val template = TokenEvent.build("encrypted") + assertEquals(CashuTokenEvent.KIND, template.kind) + assertEquals("encrypted", template.content) + } + + @Test + fun nutzapRedemptionBuildStillAddsRedeemedAndSenderTags() { + val template = NutzapRedemptionEvent.build(EventHintBundle(nutzap), "encrypted") + assertEquals(CashuSpendingHistoryEvent.KIND, template.kind) + assertEquals(listOf("e", "p"), template.tags.map { it[0] }) + } + + @Test + fun softwareReleaseBuildStillBuildsNip82Release() { + assertEquals("app@1.0", SoftwareReleaseEvent.buildDTag("app", "1.0")) + val template = SoftwareReleaseEvent.build("app", "1.0", "main", emptyList()) + assertEquals("app@1.0", template.tags.first { it[0] == "d" }[1]) + assertEquals("1.0", template.tags.first { it[0] == "version" }[1]) + } +}