Files
amethyst/.claude/skills/nostr-expert/references/event-hierarchy.md
T
Claude 6d2c37c346 refactor(quartz): rename event classes to match their NIP names
Old class names drifted from the NIP wording (or described the wrong
kind). Every old name stays available as a @Deprecated typealias with a
ReplaceWith, so external Quartz users get an IDE quick-fix instead of a
break. Wire format is unchanged: parsing goes by kind.

Renames:
- FollowListEvent (39089) -> StarterPackEvent
- LnZapPaymentRequestEvent/ResponseEvent (NIP-47) -> NwcRequestEvent/NwcResponseEvent
- VideoHorizontalEvent/VideoVerticalEvent -> AddressableNormalVideoEvent/AddressableShortVideoEvent
- CalendarEvent (31924) -> CalendarCollectionEvent
- HashtagListEvent -> InterestListEvent
- ContactCardEvent (30382) -> UserAssertionEvent
- LnZapEvent/LnZapRequestEvent/LnZapEventInterface -> ZapReceiptEvent/ZapRequestEvent/ZapReceiptEventInterface
- PrivateDmEvent -> EncryptedDmEvent, DeletionEvent -> DeletionRequestEvent
- LongTextNoteEvent -> LongFormContentEvent, WikiNoteEvent -> WikiArticleEvent
- PeopleListEvent -> FollowSetEvent, LabeledBookmarkListEvent -> BookmarkSetEvent
- RelayFeedsListEvent -> FavoriteRelayListEvent, EmojiPackSelectionEvent -> EmojiListEvent
- ChannelListEvent -> PublicChatListEvent, ChatMessageRelayListEvent -> DmRelayListEvent
- SealedRumorEvent -> SealEvent, FileHeaderEvent -> FileMetadataEvent
- GoalEvent -> ZapGoalEvent, StatusEvent -> UserStatusEvent
- NIP90*Event -> Dvm*Event (all NIP-90 events)
- NIP-29 group events get the Group prefix to separate them from NIP-43:
  PutUser, RemoveUser, EditMetadata, DeleteEvent, CreateInvite,
  UpdatePinList, JoinRequest, LeaveRequest; SupportedRolesEvent -> GroupRolesEvent

Merged duplicate classes for the same kind:
- nip61 TokenEvent (7375) was unreachable in EventFactory; now an alias of CashuTokenEvent.
- nip61 NutzapRedemptionEvent (7376) was unreachable; CashuSpendingHistoryEvent now
  carries its hint providers and redeemedNutzaps(), plus
  CashuSpendingHistoryEvent.buildNutzapRedemption.
- NIP-82 SoftwareReleaseEvent (30063) was never built by EventFactory;
  ReleaseArtifactSetEvent now exposes appId/version/channel/assets/releaseNotes
  and buildSoftwareRelease, and the app no longer re-parses the event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VvJ8XpeYqBe8bYT3bJ7UD
2026-09-26 20:19:55 +00:00

7.7 KiB

Event Hierarchy & Structure

Core Hierarchy

IEvent (empty interface)
  └── Event (@Immutable base class)
      ├── BaseAddressableEvent (replaceable + addressable, has d-tag)
      │   ├── BaseReplaceableEvent (kinds 10000-20000, FIXED_D_TAG = "")
      │   └── [Specific addressable events - 30000-40000]
      └── [Specific event implementations - all other kinds]

Event Base Class

Location: /quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Event.kt

@Immutable
open class Event(
    val id: HexKey,              // SHA-256 hash of serialized event
    val pubKey: HexKey,          // Author's public key (32 bytes hex)
    val createdAt: Long,         // Unix timestamp
    val kind: Kind,              // Event kind (Int typealias)
    val tags: TagArray,          // Array of tag arrays
    val content: String,         // Event content
    val sig: HexKey,             // schnorr signature (64 bytes hex)
) : IEvent, OptimizedSerializable

Kind Classification

typealias Kind = Int

fun Kind.isEphemeral() = this in 20000..29999
fun Kind.isReplaceable() = this == 0 || this == 3 || this in 10000..19999
fun Kind.isAddressable() = this in 30000..39999
fun Kind.isRegular() = this in 1000..9999

Common Event Types

Text Note (kind 1)

class TextNoteEvent(...) : BaseThreadedEvent(...),
    EventHintProvider, AddressHintProvider, PubKeyHintProvider, SearchableEvent

// Threading support via markers: reply, root, mention
fun replyTo(): List<Note>  // Direct reply targets
fun root(): Note?          // Root of thread

Metadata (kind 0)

class MetadataEvent(...) : BaseAddressableEvent(...)

// Replaceable: newest version overwrites old
// d-tag automatically set to "" for kind 0
fun name(): String?
fun displayName(): String?
fun picture(): String?
fun about(): String?
fun lnAddress(): String?

Reaction (kind 7)

class ReactionEvent(...) : Event(...)

companion object {
    const val LIKE = "+"
    const val DISLIKE = "-"

    fun like(reactedTo: EventHintBundle<Event>, ...)
    fun dislike(reactedTo: EventHintBundle<Event>, ...)
}

Zap Request/Receipt (kinds 9734, 9735)

class ZapRequestEvent(...) : Event(...)
    // Created by client, sent to Lightning Address

class ZapReceiptEvent(...) : Event(...)
    // Receipt from LSP, contains bolt11 + embedded zap request
    val zapRequest: ZapRequestEvent? by lazy { containedPost() }
    val amount: BigDecimal? by lazy { /* parse from bolt11 */ }

Long-Form Content (kind 30023)

class LongFormContentEvent(...) : BaseAddressableEvent(...)
    // Blog posts, articles
    // Addressable via kind:pubkey:d-tag

Lists (kinds 10000-30004)

sealed class FollowSetEvent : BaseAddressableEvent {
    object MuteList : FollowSetEvent(10000)
    object PinList : FollowSetEvent(10001)
    object BookmarkList : FollowSetEvent(10003)
    // ... 18 list types total
}

Event Interfaces

Hint Providers

Events can implement interfaces to optimize relay queries:

interface EventHintProvider {
    fun taggedEventIds(): Set<HexKey>
    fun taggedEventRelays(): Map<HexKey, Set<NormalizedRelayUrl>>
}

interface PubKeyHintProvider {
    fun taggedPubKeys(): Set<HexKey>
    fun taggedPubKeyRelays(): Map<HexKey, Set<NormalizedRelayUrl>>
}

interface AddressHintProvider {
    fun taggedAddresses(): Set<Address>
    fun taggedAddressRelays(): Map<Address, Set<NormalizedRelayUrl>>
}

interface SearchableEvent {
    fun subject(): String?
    fun isContentEncoded(): Boolean
}

Event Building Pattern

DSL Builder

TextNoteEvent.build(
    note = "Hello Nostr",
    replyingTo = eventBundle,
    createdAt = TimeUtils.now()
) {
    pTag(pubKey, relayHint)              // Tag person
    eTag(eventId, relayHint, "reply")    // Tag event with marker
    hashtag("nostr")                     // Add hashtag
    alt("A short note")                  // Alt text
}

Event Template (Low-level)

suspend fun eventTemplate(
    kind: Kind,
    content: String,
    createdAt: Long,
    initializer: TagArrayBuilder.() -> Unit
): EventTemplate {
    val tags = TagArrayBuilder().apply(initializer).build()
    return EventTemplate(kind, tags, content, createdAt)
}

// Sign with signer
val template = eventTemplate(1, "Hello", now()) { pTag(pubkey) }
val signedEvent = signer.sign(template)

Addressable vs Regular Events

Feature Regular Event Addressable Event
Identifier Event ID (SHA-256 hash) Address (kind:pubkey:d-tag)
Replaceability Immutable Newest replaces old
d-tag Optional Required
Lookup By event ID By address
Example Text note (kind 1) Metadata (kind 0), Long-form (kind 30023)
// Regular event address
note = LocalCache.getNoteIfExists(eventId)

// Addressable event address
address = Address(kind = 30023, pubkey = authorHex, dTag = "my-article")
note = LocalCache.getAddressableNoteIfExists(address)

Event Validation

// Verify event ID matches computed hash
fun Event.verifyId(): Boolean =
    EventHasher.hashIdCheck(id, pubKey, createdAt, kind, tags, content)

// Verify signature
fun Event.verifySignature(): Boolean =
    Nip01.verify(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))

// Complete verification
fun Event.checkSignature() {
    if (!verifyId()) throw Exception("ID mismatch")
    if (!verifySignature()) throw Exception("Bad signature!")
}

Event Serialization

// To JSON (for transmission/signing)
fun Event.toJson(): String = OptimizedJsonMapper.toJson(this)

// From JSON
fun Event.fromJson(json: String): Event = OptimizedJsonMapper.fromJson(json)

// Event ID generation (SHA-256 of canonical JSON)
fun EventHasher.hashId(
    pubKey: HexKey,
    createdAt: Long,
    kind: Kind,
    tags: TagArray,
    content: String
): HexKey {
    val serialized = """[0,"$pubKey",$createdAt,$kind,${tags.toJson()},"$content"]"""
    return sha256(serialized.encodeToByteArray()).toHexKey()
}

Event Lifecycle in LocalCache

Event received from relay
    ↓
LocalCache.consume(event, relay, wasVerified)
    ↓
getOrCreateNote(event.id) or getOrCreateAddressableNote(address)
    ↓
justVerify(event) → checkSignature()
    ↓
note.loadEvent(event, author, replyTo)
    ↓
Update indices (replies, reactions, boosts)
    ↓
refreshNewNoteObservers(note) → emit to SharedFlow
    ↓
UI updates

Common Event Patterns

Reply Threading

// Root event (top of thread)
val rootEvent = TextNoteEvent.build("Thread root") { }

// Reply to root
val reply1 = TextNoteEvent.build("First reply", replyingTo = rootEvent) {
    // Automatically adds:
    // ["e", <root_id>, <relay>, "root"]
    // ["e", <root_id>, <relay>, "reply"]
}

// Reply to reply (nested)
val reply2 = TextNoteEvent.build("Nested reply", replyingTo = reply1) {
    // Automatically adds:
    // ["e", <root_id>, <relay>, "root"]
    // ["e", <reply1_id>, <relay>, "reply"]
}

Replaceable Events

// Metadata update (kind 0) - newest wins
val metadata1 = MetadataEvent.createNew(name = "Alice", picture = "url1")
Thread.sleep(1000)
val metadata2 = MetadataEvent.createNew(name = "Alice Updated", picture = "url2")

// LocalCache keeps only metadata2 (higher createdAt)

Event Deletion

// Delete events
val deletion = DeletionRequestEvent.create(
    deleteEvents = listOf(eventId1, eventId2),
    reason = "Spam",
    signer = signer
)

// LocalCache marks events as deleted, but doesn't remove (for verification)

63+ Event Classes

Full list at /quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip*/ - one class per event type across 60+ NIP implementations.