mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
- 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
8.6 KiB
8.6 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 (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:
// 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
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.