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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VvJ8XpeYqBe8bYT3bJ7UD
This commit is contained in:
Claude
2026-09-26 22:14:38 +00:00
parent f72388a559
commit e76566fba2
17 changed files with 187 additions and 37 deletions
@@ -95,14 +95,22 @@ class LongFormContentEvent(...) : BaseAddressableEvent(...)
// Addressable via kind:pubkey:d-tag // 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 ```kotlin
sealed class FollowSetEvent : BaseAddressableEvent { // Base for sets (kind 30000-39999); some replaceable lists use it too (MuteListEvent, InterestListEvent)
object MuteList : FollowSetEvent(10000) abstract class PrivateTagArrayEvent(...) : BaseAddressableEvent(...)
object PinList : FollowSetEvent(10001) // Replaceable lists (kind 10000-19999)
object BookmarkList : FollowSetEvent(10003) abstract class PrivateReplaceableTagArrayEvent(...) : BaseReplaceableEvent(...)
// ... 18 list types total
} 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 ## Event Interfaces
@@ -242,7 +242,7 @@ fun produceLatestReleaseVersion(app: SoftwareApplicationEvent): State<String?> {
return flow.collectAsStateWithLifecycle(initialValue = null) 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. */ /** Picks the newest NIP-82 release for [app] out of an already-narrowed [notes] collection. */
private fun latestNip82Release( private fun latestNip82Release(
@@ -256,12 +256,13 @@ private fun latestNip82Release(
} }
/** kind-30063 addressables authored by [app] whose `d` tag is `<app-id>@<version>`. */ /** kind-30063 addressables authored by [app] whose `d` tag is `<app-id>@<version>`. */
private fun nip82ReleaseNotesFor(app: SoftwareApplicationEvent): Set<Note> { private fun nip82ReleasesFor(app: SoftwareApplicationEvent): List<ReleaseArtifactSetEvent> {
val prefix = "${app.dTag()}@" val prefix = "${app.dTag()}@"
return LocalCache.addressables.filterIntoSet(ReleaseArtifactSetEvent.KIND, app.pubKey) { _, addr -> return LocalCache.addressables
val ev = addr.event ?: return@filterIntoSet false .filterIntoSet(ReleaseArtifactSetEvent.KIND, app.pubKey) { _, addr ->
ev.isNip82SoftwareRelease() && ev.dTag().startsWith(prefix) 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<Note> {
*/ */
private fun Note.asNip82ReleaseFor(prefix: String): ReleaseArtifactSetEvent? { private fun Note.asNip82ReleaseFor(prefix: String): ReleaseArtifactSetEvent? {
val ev = event as? ReleaseArtifactSetEvent ?: return null 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<ReleaseArtifactSetEvent> { fun findAllNip82Releases(app: SoftwareApplicationEvent): List<ReleaseArtifactSetEvent> = nip82ReleasesFor(app).sortedByDescending { it.createdAt }
val prefix = "${app.dTag()}@"
return nip82ReleaseNotesFor(app)
.mapNotNull { it.asNip82ReleaseFor(prefix) }
.sortedByDescending { it.createdAt }
}
@Composable @Composable
fun AppIcon( fun AppIcon(
@@ -62,7 +62,7 @@ class NewCalendarCollectionViewModel : ViewModel() {
private var dTag: String? = null private var dTag: String? = null
/** The full original event in edit mode; needed to publish a NIP-09 deletion. */ /** 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<Address>() val selectedAddresses = mutableStateListOf<Address>()
val availableAppointments = mutableStateOf<List<OwnedAppointmentSummary>>(emptyList()) val availableAppointments = mutableStateOf<List<OwnedAppointmentSummary>>(emptyList())
@@ -62,6 +62,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal import com.vitorpamplona.quartz.nip55AndroidSigner.client.NostrSignerExternal
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent
import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent
import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
@@ -1787,7 +1788,7 @@ class AmethystAppFunctions {
lnAddress: String, lnAddress: String,
sats: Long, sats: Long,
comment: String, comment: String,
zapRequest: com.vitorpamplona.quartz.nip57Zaps.ZapRequestEvent, zapRequest: ZapRequestEvent,
): String { ): String {
// Compute the LNURL-pay endpoint so we can ask the privacy-aware // Compute the LNURL-pay endpoint so we can ask the privacy-aware
// HttpClient builder for the right OkHttpClient for that host. // HttpClient builder for the right OkHttpClient for that host.
@@ -57,6 +57,7 @@ import com.vitorpamplona.quartz.nip60Cashu.wallet.CashuWalletEvent
import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent
import com.vitorpamplona.quartz.nip61Nutzaps.info.tags.NutzapMintTag import com.vitorpamplona.quartz.nip61Nutzaps.info.tags.NutzapMintTag
import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent 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.cashu.CashuMintEvent
import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent
import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.Log
@@ -914,7 +915,10 @@ class CashuWalletOps(
TokenReference(nutzap.id, null, TokenReference.MARKER_REDEEMED), TokenReference(nutzap.id, null, TokenReference.MARKER_REDEEMED),
), ),
signer = signer, signer = signer,
) ) {
// NIP-61: tag the nutzap sender so their client learns it was redeemed.
notifySender(EventHintBundle(nutzap))
}
val historyEvent = signer.sign(historyTemplate) val historyEvent = signer.sign(historyTemplate)
publish(historyEvent) publish(historyEvent)
@@ -45,6 +45,7 @@ import com.vitorpamplona.nestsclient.connectReconnectingNestsSpeaker
import com.vitorpamplona.nestsclient.transport.WebTransportFactory import com.vitorpamplona.nestsclient.transport.WebTransportFactory
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent import com.vitorpamplona.quartz.nipB1Bolt12Zaps.zap.Bolt12ZapEvent
import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.Log
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
@@ -629,7 +630,7 @@ class NestViewModel(
* same window so the visual cadence matches the React button. * same window so the visual cadence matches the React button.
*/ */
fun onZapEvent( fun onZapEvent(
event: com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent, event: ZapReceiptEvent,
nowSec: Long, nowSec: Long,
windowSec: Long = REACTION_WINDOW_SEC, windowSec: Long = REACTION_WINDOW_SEC,
) { ) {
@@ -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.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket 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.base.ChatroomKeyable
import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent import com.vitorpamplona.quartz.nip17Dm.settings.DmRelayListEvent
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
@@ -1926,7 +1927,7 @@ fun MainContent(
// Process into chatroomList based on event type // Process into chatroomList based on event type
when (event) { when (event) {
is com.vitorpamplona.quartz.nip04Dm.messages.EncryptedDmEvent -> { is EncryptedDmEvent -> {
iAccount.chatroomList.addMessage( iAccount.chatroomList.addMessage(
event.chatroomKey(iAccount.pubKey), event.chatroomKey(iAccount.pubKey),
note, note,
@@ -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.client.reqs.SubscriptionListener
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
@@ -310,7 +311,7 @@ class DesktopRelaySubscriptionsCoordinator(
), ),
// Zap receipts (kind 9735) targeting these notes // Zap receipts (kind 9735) targeting these notes
Filter( Filter(
kinds = listOf(com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent.KIND), kinds = listOf(ZapReceiptEvent.KIND),
tags = mapOf("e" to noteIds), tags = mapOf("e" to noteIds),
), ),
// Reposts (kind 6) targeting these notes // Reposts (kind 6) targeting these notes
@@ -84,6 +84,7 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
import com.vitorpamplona.quartz.nip19Bech32.entities.NNote import com.vitorpamplona.quartz.nip19Bech32.entities.NNote
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip51Lists.starterPack.StarterPackEvent
import java.awt.Toolkit import java.awt.Toolkit
import java.awt.datatransfer.StringSelection import java.awt.datatransfer.StringSelection
import java.net.URI import java.net.URI
@@ -376,7 +377,7 @@ private fun RenderBechSegment(
.NAddress .NAddress
.parse(segment.segmentText) .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 followPacks = com.vitorpamplona.amethyst.desktop.ui.deck.LocalFollowPacksState.current
val relayManager = com.vitorpamplona.amethyst.desktop.ui.deck.LocalRelayManager.current val relayManager = com.vitorpamplona.amethyst.desktop.ui.deck.LocalRelayManager.current
if (localCache != null && relayManager != null && followPacks != null) { if (localCache != null && relayManager != null && followPacks != null) {
@@ -44,4 +44,9 @@ fun Event.isNip82SoftwareRelease() =
* Reinterprets `this` as a [ReleaseArtifactSetEvent]. Use [isNip82SoftwareRelease] * Reinterprets `this` as a [ReleaseArtifactSetEvent]. Use [isNip82SoftwareRelease]
* to gate the call. * 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) fun Event.asSoftwareRelease() = this as? ReleaseArtifactSetEvent ?: ReleaseArtifactSetEvent(id, pubKey, createdAt, tags, content, sig)
@@ -20,7 +20,11 @@
*/ */
package com.vitorpamplona.quartz.experimental.nip82SoftwareApps.release 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.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent
import com.vitorpamplona.quartz.utils.TimeUtils
/** /**
* NIP-82 software releases share kind 30063 with NIP-51 release artifact sets, and * 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"), ReplaceWith("ReleaseArtifactSetEvent", "com.vitorpamplona.quartz.nip51Lists.releaseArtifactSet.ReleaseArtifactSetEvent"),
) )
typealias SoftwareReleaseEvent = 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<EventHintBundle<SoftwareAssetEvent>>,
releaseNotes: String = "",
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<ReleaseArtifactSetEvent>.() -> Unit = {},
) = buildSoftwareRelease(appId, version, channel, assets, releaseNotes, createdAt, initializer)
@@ -82,8 +82,9 @@ class ReleaseArtifactSetEvent(
} }
// Only NIP-82 releases carry release notes in `content`. A NIP-51 set may keep // 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. // encrypted private items there, which must never reach the index. The blank
private fun searchableReleaseNotes() = if (isNip82SoftwareRelease()) content.ifBlank { null } else null // 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) override fun eventHints() = tags.mapNotNull(EventBookmark::parseAsHint)
@@ -43,9 +43,11 @@ data class TokenReference(
const val MARKER_REDEEMED = "redeemed" const val MARKER_REDEEMED = "redeemed"
fun parseFromTag(tag: Array<String>): TokenReference? { fun parseFromTag(tag: Array<String>): 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] 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( return TokenReference(
eventId = tag[1], eventId = tag[1],
relay = tag.getOrNull(2)?.ifEmpty { null }, relay = tag.getOrNull(2)?.ifEmpty { null },
@@ -44,6 +44,22 @@ fun CashuSpendingHistoryEvent.Companion.buildNutzapRedemption(
initializer() 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<NutzapEvent>,
encryptedContent: String,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<CashuSpendingHistoryEvent>.() -> Unit = {},
) = buildNutzapRedemption(nutzap, encryptedContent, createdAt, initializer)
/** /**
* NIP-61 redemptions are NIP-60 spending history events (kind 7376). This used to be a * 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 * second class for the same kind, which `EventFactory` could never instantiate because
@@ -20,7 +20,10 @@
*/ */
package com.vitorpamplona.quartz.nip61Nutzaps.token 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.nip60Cashu.token.CashuTokenEvent
import com.vitorpamplona.quartz.utils.TimeUtils
/** /**
* NIP-61 does not define its own kind 7375: redeemed nutzap proofs are stored as * 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"), ReplaceWith("CashuTokenEvent", "com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenEvent"),
) )
typealias TokenEvent = 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<CashuTokenEvent>.() -> Unit = {},
) = eventTemplate(KIND, encryptedContent, createdAt) {
initializer()
}
@@ -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.apkCertificateHash
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.platform import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.platform
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.asset.versionCode 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.release.isNip82SoftwareRelease
import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.shared.Platform import com.vitorpamplona.quartz.experimental.nip82SoftwareApps.shared.Platform
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
@@ -137,13 +136,12 @@ class SoftwareApplicationEventTest {
} }
@Test @Test
fun asSoftwareRelease_exposesParsedFields() { fun releaseArtifactSet_exposesParsedNip82Fields() {
val event = val release =
Event( ReleaseArtifactSetEvent(
id = "0".repeat(64), id = "0".repeat(64),
pubKey = "1".repeat(64), pubKey = "1".repeat(64),
createdAt = 0, createdAt = 0,
kind = ReleaseArtifactSetEvent.KIND,
tags = tags =
arrayOf( arrayOf(
arrayOf("d", "com.example.app@1.0.0"), arrayOf("d", "com.example.app@1.0.0"),
@@ -154,7 +152,6 @@ class SoftwareApplicationEventTest {
content = "Initial release", content = "Initial release",
sig = "", sig = "",
) )
val release = event.asSoftwareRelease()
assertEquals("com.example.app", release.appId()) assertEquals("com.example.app", release.appId())
assertEquals("1.0.0", release.version()) assertEquals("1.0.0", release.version())
assertEquals("main", release.channel()) assertEquals("main", release.channel())
@@ -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])
}
}