Files
amethyst/.claude/skills/nostr-expert/references/event-hierarchy.md
T
2025-12-30 15:45:02 +02: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 LnZapRequestEvent(...) : Event(...)
    // Created by client, sent to Lightning Address

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

Long-Form Content (kind 30023)

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

Lists (kinds 10000-30004)

sealed class PeopleListEvent : BaseAddressableEvent {
    object MuteList : PeopleListEvent(10000)
    object PinList : PeopleListEvent(10001)
    object BookmarkList : PeopleListEvent(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 = DeletionEvent.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.