Quartz now lives in github.com/vitorpamplona/quartz, split into quartz-core, quartz-social, quartz-relay-client and the other quartz-* libraries, with history kept. Amethyst depends on the `quartz` umbrella artifact at a released version (1.20.0) instead of the :quartz project. - Remove the quartz/ module, jitpack.yml and the Quartz CI, release and pre-push steps; tools/unicode-nfkc moved with it. - Point every module at libs.quartz. - Move the device-test fixtures the benchmark and relayBench still read to testdata/. - NostrConnectLoginUseCase parses bunker messages with BunkerJson, which now owns NIP-46 JSON. - Repoint docs and skills at the Quartz repository; the nostr-expert scripts read a Quartz checkout (QUARTZ_REPO, default ../quartz). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ZR5ZBfhFzRiXubJc5Evyv
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 (the Quartz repository) 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/, mls/, …)
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, a registration in its Quartz module's
EventKindProviderand 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) // utils/RandomInstance.kt — secure random, not kotlin.random
RandomInstance.randomChars() // 16-char subscription id
sha256(bytes) // utils/sha256/Sha256.kt
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).