# Conflicts: # commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/cordn/CordnBackup.kt # commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/marmot/MarmotManager.kt # gradle/libs.versions.toml # marmotBench/src/main/kotlin/com/vitorpamplona/marmotbench/PrimitiveBenchmarks.kt # quartz/build.gradle.kts # quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/cordn/appEncryptedMedia/CordnMediaEncryption.kt # quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/cordn/spec03Payloads/SealedPayload.kt # quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/zapstore/identityProof/CertificateSignature.kt # quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/zapstore/identityProof/IdentityProofEvent.kt # quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/MarmotInboundProcessor.kt # quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/appComponents/CurrentProfileGroupFactory.kt # quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/appComponents/EncryptedMediaV2.kt # quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/appComponents/agentTextStream/AgentTextStreamCrypto.kt # quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip00KeyPackages/KeyPackageRotationManager.kt # quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip04EncryptedMedia/Mip04MediaEncryption.kt # quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/protocolCore/MarmotConvergenceEngine.kt # quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/protocolCore/MarmotPublishGate.kt # quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/protocolCore/MlsCandidateStateEngine.kt # quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/mls/crypto/Hpke.kt # quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/mls/crypto/MlsCryptoProvider.kt # quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/mls/crypto/X25519.kt # quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/mls/group/MlsGroup.kt # quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/mls/group/MlsGroupState.kt # quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip44Encryption/SharedKeyCache.kt # quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/zapstore/identityProof/IdentityProofEventTest.kt # quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/marmot/MarmotGroupImageTest.kt # quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/marmot/protocolCore/MlsCandidateGraphTest.kt # quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/mls/interop/WelcomeInteropTest.kt # quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/experimental/zapstore/identityProof/CertificateSignature.jvmAndroid.kt # quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/cordn/interop/TsMlsFixtures.kt # quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/experimental/zapstore/identityProof/IdentityProofVerificationTest.kt # quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/marmot/appComponents/CurrentProfileWelcomeTest.kt # quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/mls/HpkeTest.kt # quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/mls/MlsConformanceTest.kt # quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/mls/MlsGroupTest.kt # quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/mls/X25519Test.kt
7.2 KiB
name: nostr-expert description: Quartz Nostr code: event classes, kinds, tags, TagArrayBuilder, EventFactory, NIP-19 bech32, NIP-44, hex/time/hash utils, resolving npub/nprofile/NIP-05 to a pubkey, looking up a NIP spec. Use before writing protocol code in quartz/.
Nostr in Quartz
Quartz (quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/) has one package per
NIP — about 95 nipXX<Name>/ directories (hex ids too: nip5aStaticWebsites,
nipB7Blossom, nipXX… for unnumbered drafts) — plus experimental/ and the non-NIP
protocol families (marmot/, concord/, cordn/, contextvm/, buzz/, …)
listed in AGENTS.md › Project Overview. references/nip-catalog.md maps every package
to its event classes and kinds.
Look up a NIP
- The spec is the source of truth:
https://github.com/nostr-protocol/nips/blob/master/NN.md(zero-pad:01.md; hex ids as named:B7.md). .claude/skills/nostr-expert/scripts/nip-lookup.sh NNlists the package's files (NNdecimal or two-char hex; a word does a text search acrossnip*packages).grep -rln "NIP-NN\|nipNN" quartz/src/for code outside the package.- Keep what the NIP requires apart from what Quartz already does and from your advice.
Building events
Every event class has const val KIND and a companion build(...) returning an unsigned
EventTemplate<T>; a NostrSigner signs it (see auth-signers). Copy the shape of a
sibling class in the same NIP; references/event-hierarchy.md covers the base classes.
// nip01Core/signers/EventTemplate.kt
inline fun <T : Event> eventTemplate(kind: Int, description: String, createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<T>.() -> Unit = {}) = EventTemplate<T>(createdAt, kind, tagArray(initializer), description)
// usage — nip10Notes/TextNoteEvent.kt
val template = TextNoteEvent.build("gm", replyingTo = parentHintBundle) { hashtag("nostr") }
val signed: TextNoteEvent = signer.sign(template)
- Edit an existing event's tags with
eventUpdate(base) { … }/event.update { … }. - Tags: each tag is a
XxxTagcodec +TagArrayBuilderExt.ktwriter +TagArrayExt.ktreader in its NIP package. Grep for the builder before writingadd(arrayOf(...)). Seereferences/tag-patterns.md. - Kind ranges (
nip01Core/core/Kind.kt):isRegular()is> 0 && < 10_000 && != 3(kind 0 and 3 are replaceable),isReplaceable()is 0, 3 and 10000–19999,isEphemeral()20000–29999,isAddressable()30000–39999. - A new kind needs a class, an
EventFactorybranch and aLocalCachedispatch branch, or it is silently dropped:references/event-factory.md.
Hex (nip01Core/core/HexKey.kt, utils/Hex.kt)
Pubkeys, ids and signatures are lower-case hex HexKey (= String). Never hand-roll a
byte loop or pull in a hex library.
bytes.toHexKey() // ByteArray -> hex
hex.hexToByteArray() // throws on bad input
input.hexToByteArrayOrNull() // null on invalid hex
Hex.isHex(input); Hex.isHex64(input) // isHex64: fast path for a 32-byte key/id
hex.isValid() // 64 chars + valid hex
Hex.isEqual(hex, bytes) // compare without decoding
PUBKEY_LENGTH / EVENT_ID_LENGTH (both 64) live in nip01Core.core.
Time, randomness, hashing
TimeUtils.now() // utils/TimeUtils.kt — Unix SECONDS for created_at
TimeUtils.oneHourAgo() // relative bounds (…Ago / …FromNow), also seconds
RandomInstance.bytes(32) // com.vitorpamplona.crypto.random (crypto-kmp) — secure random, not kotlin.random
RandomInstance.randomChars() // 16-char subscription id
sha256(bytes) // com.vitorpamplona.crypto.sha2.sha256 (crypto-kmp)
EventHasher.hashId(pubKey, createdAt, kind, tags, content) // nip01Core/crypto/
EventHasher.hashIdCheck(id, pubKey, createdAt, kind, tags, content) // verify untrusted ids
Use EventHasher, not sha256 over your own JSON — the NIP-01 serialization is exact.
Verification: event.verify() / checkSignature() in nip01Core/crypto/EventExt.kt.
Encryption
App code encrypts through the signer — signer.nip44Encrypt(plaintext, toPublicKey),
signer.nip44Decrypt(ciphertext, fromPublicKey), or signer.decrypt(...) (picks NIP-04
vs NIP-44) — so remote and external signers work. Nip44.encrypt/decrypt
(nip44Encryption/Nip44.kt) is for code that holds the raw key. NIP-04 is legacy.
See references/crypto-and-encryption.md.
NIP-19 bech32
pubkeyBytes.toNpub(); privKeyBytes.toNsec(); idBytes.toNote() // nip19Bech32/ByteArrayExt.kt
NEvent.create(idHex, authorHex, kind, relays); NProfile.create(pubkeyHex, relays)
event.toNIP19(relayHint) // naddr or nevent
when (val e = Nip19Parser.uriToRoute(input)?.entity) { is NPub -> e.hex; is NAddress -> e.aTag(); … }
There is no Nip19.npubEncode and entities have no toBech32(). See
references/nip19-bech32.md.
Resolving user input to a pubkey
Before writing if (isHex) … else if (npub) … else if ("@" in s) fetchWellKnown(),
stop — it exists.
import com.vitorpamplona.quartz.nip05DnsIdentifiers.resolveUserHexOrNull
// hex | npub1… | nprofile1… | nsec1… | name@domain.tld -> HexKey? (null if unrecognized or lookup fails)
val pubkey = resolveUserHexOrNull(userInput, nip05Client)
- Tries the synchronous hex/bech32 path first (
decodePublicKeyAsHexOrNull); only NIP-05-shaped input hits the network.suspend; re-throws onlyCancellationException. Passnip05Client = nulloffline. - Client:
Nip05Client(fetcher = OkHttpNip05Fetcher { _ -> okHttp })(as incli/.../commands/LoginCommand.kt). The fetcher runs on IO and refuses redirects per NIP-05 — don't re-implement.well-known/nostr.json. - Verify a claimed identifier:
nip05Client.verify(Nip05Id.parse(id)!!, pubkey).
Full API (Nip05Id, Nip05Parser, KeyInfoSet, Namecoin .bit) and the anti-pattern:
references/nip05-identifiers.md.
Relays
Relay operations (fetch, page, publish-and-confirm, count, negentropy) are
INostrClient extensions — see AGENTS.md › Feature Workflow. Compose-scoped
subscriptions are the relay-client skill.
References
references/nip-catalog.md— every package → event classes and kinds (generated byscripts/nip-catalog.py; regenerate, don't hand-edit)references/event-hierarchy.md—Event/BaseReplaceableEvent/BaseAddressableEvent, kind ranges, hint interfacesreferences/tag-patterns.md— tag codec layout,TagArrayBuilder, core tags, readersreferences/event-factory.md—EventFactoryand the three registrations a new kind needsreferences/nip19-bech32.md—Nip19Parser, entities, encodersreferences/nip05-identifiers.md—resolveUserHexOrNull,Nip05Client, Namecoinreferences/crypto-and-encryption.md—EventHasher,Nip01Crypto, NIP-44references/large-cache.md—LargeCache, the store underLocalCachescripts/nip-lookup.sh— files of one NIP package, or a text search
Related skills: auth-signers (signing), event-store-semantics (stores, NIP-50
search), nip85-trusted-assertions, relay-client, kotlin-multiplatform
(expect/actual in Quartz).