docs: add quartz-integration skill for using Quartz in external KMP projects

Adds a comprehensive Claude skill covering Gradle setup, key management,
event creation/signing, relay client, subscriptions, NIP builders, and
platform-specific notes. Also updates the legacy quartz-kmp.md to redirect
to the new skill.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-02-26 19:37:44 -05:00
parent 5b156c4530
commit 23c3edbec8
3 changed files with 849 additions and 160 deletions
+688
View File
@@ -0,0 +1,688 @@
---
name: quartz-integration
description: Integration guide for using the Quartz Nostr KMP library in external projects. Use when: (1) adding Quartz as a Gradle dependency, (2) setting up NostrClient with WebSocket, (3) creating/signing/sending events, (4) building relay subscriptions with Filter, (5) handling keys with KeyPair/NostrSignerInternal, (6) using Bech32 encoding/decoding (NIP-19), (7) platform-specific setup (Android vs JVM/Desktop), (8) NIP-57 zaps, NIP-17 DMs, NIP-44 encryption in external projects.
---
# Quartz Integration Guide
Reference for integrating `com.vitorpamplona.quartz:quartz` into external Nostr KMP projects.
**Published artifact**: `com.vitorpamplona.quartz:quartz:1.05.1` (Maven Central)
**Targets**: JVM 21+, Android (minSdk 21+), iOS (XCFramework `quartz-kmpKit`)
**License**: MIT
---
## 1. Gradle Setup
### Version Catalog (`libs.versions.toml`)
```toml
[versions]
quartz = "1.05.1"
[libraries]
quartz = { module = "com.vitorpamplona.quartz:quartz", version.ref = "quartz" }
```
### `build.gradle.kts` (KMP project)
```kotlin
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.quartz)
}
}
}
```
### Android-only project
```kotlin
dependencies {
implementation("com.vitorpamplona.quartz:quartz:1.05.1")
}
```
### Transitive dependencies pulled in automatically
Quartz exposes these as `api` (you get them transitively):
| Dependency | Used for |
|-----------|----------|
| `fr.acinq.secp256k1:secp256k1-kmp-*` | Schnorr signing |
| `com.github.anthonynsimon:rfc3986-normalizer` | Relay URL normalization |
| `com.fasterxml.jackson.module:jackson-module-kotlin` | Event JSON parsing |
| `com.linkedin.urls:url-detector` | URL extraction from content |
For Android, add to `build.gradle.kts`:
```kotlin
android {
packaging {
resources.excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
```
---
## 2. Key Concepts
### Core Types
```kotlin
typealias HexKey = String // 64-char hex string (pubkey, event id, sig)
typealias Kind = Int // Event kind number
typealias TagArray = Array<Array<String>>
```
### Event Anatomy
```kotlin
@Immutable
open class Event(
val id: HexKey, // SHA-256 of canonical JSON (64 hex chars)
val pubKey: HexKey, // Author public key (64 hex chars)
val createdAt: Long, // Unix timestamp (seconds)
val kind: Kind, // Event type
val tags: TagArray, // [["e","eventid"], ["p","pubkey"], ...]
val content: String,
val sig: HexKey, // Schnorr signature (128 hex chars)
)
```
### Kind Classification
```kotlin
// Regular events — stored by relays forever
val isRegular = kind in 1..9999
// Replaceable events — relay keeps only latest per (pubkey, kind)
val isReplaceable = kind == 0 || kind == 3 || kind in 10000..19999
// Addressable events — relay keeps latest per (pubkey, kind, d-tag)
val isAddressable = kind in 30000..39999
// Ephemeral events — relays don't persist
val isEphemeral = kind in 20000..29999
```
---
## 3. Key Management
### Generate a new keypair
```kotlin
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
// Generate fresh random keys
val keyPair = KeyPair()
// From existing private key bytes
val keyPair = KeyPair(privKey = myPrivKeyBytes)
// Read-only (public key only, cannot sign)
val keyPair = KeyPair(pubKey = myPubKeyBytes)
// Access
val pubKeyHex: String = keyPair.pubKey.toHexKey()
val privKeyHex: String? = keyPair.privKey?.toHexKey()
```
### Convert between formats
```kotlin
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
// ByteArray → hex
val hex = byteArray.toHexKey()
// hex → ByteArray
val bytes = HexKey.decodeHex(hex)
// Bech32 import (npub, nsec)
val parsed = Nip19Parser.uriToRoute("npub1abc...")
// or
val parsed = Nip19Parser.uriToRoute("nsec1abc...")
```
---
## 4. Signing Events
### `NostrSignerInternal` (local key, JVM + Android)
```kotlin
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
val keyPair = KeyPair()
val signer = NostrSignerInternal(keyPair)
// Sign any EventTemplate
val template = TextNoteEvent.build("Hello Nostr!")
val signedEvent: TextNoteEvent = signer.sign(template)
```
### `NostrSignerSync` (synchronous, for testing)
```kotlin
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
val signerSync = NostrSignerSync(keyPair)
val event = signerSync.sign<TextNoteEvent>(
createdAt = TimeUtils.now(),
kind = 1,
tags = emptyArray(),
content = "Hello!"
)
```
### NostrSigner interface (for custom signers)
```kotlin
abstract class NostrSigner(val pubKey: HexKey) {
abstract fun isWriteable(): Boolean
abstract suspend fun <T : Event> sign(createdAt: Long, kind: Int, tags: Array<Array<String>>, content: String): T
abstract suspend fun nip04Encrypt(plaintext: String, toPublicKey: HexKey): String
abstract suspend fun nip04Decrypt(ciphertext: String, fromPublicKey: HexKey): String
abstract suspend fun nip44Encrypt(plaintext: String, toPublicKey: HexKey): String
abstract suspend fun nip44Decrypt(ciphertext: String, fromPublicKey: HexKey): String
abstract suspend fun deriveKey(nonce: HexKey): HexKey
abstract fun hasForegroundSupport(): Boolean
// Convenience: auto-detects NIP-04 vs NIP-44 by ciphertext format
suspend fun decrypt(encryptedContent: String, fromPublicKey: HexKey): String
}
```
---
## 5. Creating Events
### Using typed event builders (recommended)
```kotlin
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
// Kind 1 — Text note
val template = TextNoteEvent.build("Hello Nostr!")
val event: TextNoteEvent = signer.sign(template)
// Kind 1 — Reply
val replyTemplate = TextNoteEvent.build(
note = "Interesting thread!",
replyingTo = originalEventHintBundle
)
// Kind 7 — Reaction
val reactionTemplate = ReactionEvent.build(
content = "+", // "+" = like, "-" = dislike, emoji = custom
originalNote = targetEvent
)
```
### Using low-level `Event.build` DSL
```kotlin
import com.vitorpamplona.quartz.nip01Core.core.Event
val template = Event.build(
kind = 1,
content = "Hello world",
createdAt = TimeUtils.now()
) {
// TagArrayBuilder DSL
add(arrayOf("p", mentionedPubKey))
add(arrayOf("t", "nostr"))
add(arrayOf("subject", "Greeting"))
}
val event: Event = signer.sign(template)
```
### TagArrayBuilder DSL methods
```kotlin
// In the DSL lambda:
add(arrayOf("tagname", "value")) // append
addFirst(arrayOf("tagname", "value")) // prepend
addUnique(arrayOf("d", "my-slug")) // replace all tags with same name
addAll(listOf(arrayOf("t", "tag1"), ...)) // bulk add
remove("tagname") // remove all with this name
```
### Common tags
```kotlin
arrayOf("e", eventId) // event reference
arrayOf("e", eventId, relayHint, "reply") // threaded reply marker
arrayOf("p", pubKey) // person mention
arrayOf("p", pubKey, relayHint, "author") // with role
arrayOf("a", "30023:pubkey:slug") // addressable event
arrayOf("d", "unique-identifier") // addressable event id
arrayOf("t", "hashtag") // hashtag
arrayOf("subject", "Title") // NIP-14 subject
arrayOf("relay", "wss://relay.example") // relay hint
```
---
## 6. Relay Client Setup (JVM / Android)
The relay client requires an OkHttp WebSocket builder (available on JVM + Android).
### Minimal setup
```kotlin
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket
import okhttp3.OkHttpClient
// Build the WebSocket factory
val okHttpClient = OkHttpClient.Builder().build()
val wsBuilder = BasicOkHttpWebSocket.Builder { _ -> okHttpClient }
// Create client (manages its own CoroutineScope internally)
val nostrClient = NostrClient(websocketBuilder = wsBuilder)
nostrClient.connect()
```
### With custom OkHttpClient per relay
```kotlin
val wsBuilder = BasicOkHttpWebSocket.Builder { normalizedUrl ->
if (normalizedUrl.url.contains(".onion")) {
torEnabledOkHttpClient // Tor proxy for .onion relays
} else {
regularOkHttpClient
}
}
```
### With custom CoroutineScope
```kotlin
val appScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
val nostrClient = NostrClient(wsBuilder, scope = appScope)
```
---
## 7. Subscribing to Events
### Normalize relay URLs first
```kotlin
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
// Returns NormalizedRelayUrl (wrapper with validated wss:// URL)
val relayUrl = RelayUrlNormalizer.normalize("wss://relay.damus.io")
val relayUrlOrNull = RelayUrlNormalizer.normalizeOrNull("wss://relay.damus.io")
// Handles common fixes: https:// → wss://, strips whitespace, etc.
```
### Build a Filter
```kotlin
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
// Fetch a user's notes
val filter = Filter(
authors = listOf(pubKeyHex),
kinds = listOf(1),
limit = 50
)
// Since a timestamp
val filter = Filter(
kinds = listOf(1, 6),
since = System.currentTimeMillis() / 1000 - 3600 // last hour
)
// By event tags
val filter = Filter(
kinds = listOf(7),
tags = mapOf("e" to listOf(eventId)) // reactions to an event
)
// AND tag filter (NIP-91)
val filter = Filter(
kinds = listOf(1),
tagsAll = mapOf(
"t" to listOf("nostr"),
"p" to listOf(specificPubKey)
)
)
// Full-text search (NIP-50)
val filter = Filter(
kinds = listOf(1),
search = "bitcoin lightning"
)
```
### Open a subscription
```kotlin
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EoseMessage
val relay = RelayUrlNormalizer.normalize("wss://relay.damus.io")
val subId = "my-sub-${System.currentTimeMillis()}"
val filtersMap = mapOf(relay to listOf(filter))
nostrClient.openReqSubscription(
subId = subId,
filters = filtersMap,
listener = object : IRequestListener {
override fun onEvent(subId: String, event: Event, relay: IRelayClient) {
println("Got event: ${event.id}")
}
override fun onEOSE(subId: String, relay: IRelayClient) {
println("End of stored events from ${relay.url}")
}
}
)
// Close when done
nostrClient.close(subId)
```
### Global relay listener
```kotlin
nostrClient.subscribe(object : IRelayClientListener {
override fun onIncomingMessage(relay: IRelayClient, msgStr: String, msg: Message) {
when (msg) {
is EventMessage -> handleEvent(msg.subscriptionId, msg.event)
is EoseMessage -> handleEose(msg.subscriptionId)
else -> {}
}
}
override fun onConnected(relay: IRelayClient, pingMillis: Int, compressed: Boolean) {
println("Connected to ${relay.url} in ${pingMillis}ms")
}
override fun onDisconnected(relay: IRelayClient) {
println("Disconnected from ${relay.url}")
}
// other callbacks: onConnecting, onSent, onCannotConnect
})
```
---
## 8. Publishing Events
```kotlin
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
val relaySet = setOf(
RelayUrlNormalizer.normalize("wss://relay.damus.io"),
RelayUrlNormalizer.normalize("wss://nos.lol"),
)
// Sign the event
val template = TextNoteEvent.build("Hello Nostr!")
val event: TextNoteEvent = signer.sign(template)
// Send to relays (handles retry + reconnect automatically)
nostrClient.send(event, relaySet)
```
---
## 9. Event Serialization
```kotlin
import com.vitorpamplona.quartz.nip01Core.core.Event
// Serialize to JSON string
val json: String = event.toJson()
// Parse from JSON string
val event: Event = Event.fromJson(json)
// Null-safe parse
val event: Event? = Event.fromJsonOrNull(json)
// Specific typed parse (returns base Event, cast if needed)
val textNote = Event.fromJson(json) as? TextNoteEvent
```
---
## 10. Bech32 Encoding / Decoding (NIP-19)
```kotlin
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
// Decode any bech32 entity
val result = Nip19Parser.uriToRoute("npub1abc...")
// Returns: NPub | NSec | Note | NEvent | NProfile | NAddr | null
when (val r = Nip19Parser.uriToRoute(input)) {
is Nip19Parser.Return.NPub -> println("pubkey: ${r.hex}")
is Nip19Parser.Return.Note -> println("event id: ${r.hex}")
is Nip19Parser.Return.NEvent -> println("event: ${r.hex}, relays: ${r.relays}")
is Nip19Parser.Return.NProfile -> println("profile: ${r.hex}")
is Nip19Parser.Return.NAddr -> println("address: ${r.kind}:${r.pubKey}:${r.dTag}")
null -> println("not a valid bech32 entity")
else -> {}
}
// The parser also handles nostr: URI scheme
val result = Nip19Parser.uriToRoute("nostr:npub1abc...")
```
---
## 11. Encryption
### NIP-44 (modern, recommended)
```kotlin
// Via signer (preferred)
val encrypted = signer.nip44Encrypt(
plaintext = "Secret message",
toPublicKey = recipientPubKeyHex
)
val decrypted = signer.nip44Decrypt(
ciphertext = encrypted,
fromPublicKey = senderPubKeyHex
)
// Auto-detect format (NIP-04 or NIP-44)
val plaintext = signer.decrypt(encryptedContent, fromPublicKeyHex)
```
### NIP-04 (legacy, avoid for new code)
```kotlin
val encrypted = signer.nip04Encrypt(plaintext, recipientPubKeyHex)
val decrypted = signer.nip04Decrypt(ciphertext, senderPubKeyHex)
```
---
## 12. Common NIP Event Builders
### NIP-02 — Follow list (kind 3)
```kotlin
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
val template = ContactListEvent.build(
follows = listOf(
ContactListEvent.Contact(pubKey = alicePubKey, relayUrl = "wss://relay.damus.io", petname = "alice"),
ContactListEvent.Contact(pubKey = bobPubKey)
)
)
```
### NIP-25 — Reaction (kind 7)
```kotlin
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
val like = ReactionEvent.build("+", targetEvent)
val dislike = ReactionEvent.build("-", targetEvent)
val custom = ReactionEvent.build("🤙", targetEvent)
```
### NIP-57 — Zap request (kind 9734)
```kotlin
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
val template = LnZapRequestEvent.build(
message = "Great post!",
relays = listOf("wss://relay.damus.io"),
target = targetEvent,
zapType = LnZapRequestEvent.ZapType.PUBLIC
)
val zapRequest: LnZapRequestEvent = signer.sign(template)
```
### NIP-59 — Gift wrap / sealed DM (kind 1059 + 14)
```kotlin
import com.vitorpamplona.quartz.nip17Dm.NIP17Factory
// Creates sealed rumor + gift wrap pair
val (dmEvent, giftWrap) = NIP17Factory.create(
msg = "Private message",
fromSigner = senderSigner,
toUsers = listOf(recipientPubKey),
relayList = listOf("wss://relay.damus.io")
)
```
### NIP-23 — Long-form article (kind 30023)
```kotlin
import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent
val template = LongTextNoteEvent.build(
body = markdownContent,
title = "My Article",
image = "https://example.com/cover.jpg",
summary = "A brief summary",
slug = "my-article" // d-tag
)
```
---
## 13. Platform-Specific Notes
### JVM / Desktop
```kotlin
// jvmMain dependencies needed in consuming project:
// secp256k1-kmp-jni-jvm and lazysodium-java are transitive from quartz
// But you need JNA on the classpath for libsodium:
implementation("net.java.dev.jna:jna:5.18.1")
```
### Android
```kotlin
// androidMain dependencies (transitive from quartz):
// secp256k1-kmp-jni-android, lazysodium-android, jna (aar)
// No extra setup needed beyond the maven dependency.
// For NIP-55 (Android external signer apps):
import com.vitorpamplona.quartz.nip55AndroidSigner.ExternalSignerLauncher
```
### iOS
The library produces an XCFramework named `quartz-kmpKit`.
```bash
# Build XCFramework
./gradlew :quartz:assembleQuartz-kmpKitReleaseXCFramework
# Output: quartz/build/XCFrameworks/release/quartz-kmpKit.xcframework
```
In Xcode: drag & drop the `.xcframework` into your project, then use from Swift via Kotlin/Native interop.
---
## 14. Event Store (Android only)
SQLite-based storage with full NIP support (NIP-09, NIP-40, NIP-45, NIP-50, NIP-62):
```kotlin
import com.vitorpamplona.quartz.nip01Core.store.EventStore
import android.content.Context
val store = EventStore(context)
// Insert
store.insertOrReplace(event)
// Query
val events = store.query(
Filter(authors = listOf(pubKey), kinds = listOf(1), limit = 50)
)
// Count (NIP-45)
val count = store.count(Filter(kinds = listOf(1)))
// Full-text search (NIP-50)
val results = store.query(Filter(search = "bitcoin"))
```
---
## 15. Quick Reference
| Task | API | Package |
|------|-----|---------|
| Generate keys | `KeyPair()` | `nip01Core.crypto` |
| Create signer | `NostrSignerInternal(keyPair)` | `nip01Core.signers` |
| Build event | `TextNoteEvent.build(...)` or `Event.build(kind, content) { tags }` | `nip10Notes`, `nip01Core.core` |
| Sign event | `signer.sign(template)` | `nip01Core.signers` |
| Serialize | `event.toJson()` | `nip01Core.core` |
| Parse | `Event.fromJson(json)` | `nip01Core.core` |
| Normalize relay URL | `RelayUrlNormalizer.normalize("wss://...")` | `nip01Core.relay.normalizer` |
| Setup relay client | `NostrClient(BasicOkHttpWebSocket.Builder { okhttp })` | `nip01Core.relay.client` |
| Subscribe | `client.openReqSubscription(subId, mapOf(relay to filters), listener)` | `nip01Core.relay.client` |
| Publish | `client.send(event, setOf(relayUrl))` | `nip01Core.relay.client` |
| NIP-44 encrypt | `signer.nip44Encrypt(text, recipientPubKey)` | `nip01Core.signers` |
| Bech32 decode | `Nip19Parser.uriToRoute("npub1...")` | `nip19Bech32` |
| Bech32 encode | `Nip19Bech32.createNPub(pubKeyHex)` | `nip19Bech32` |
## Common Event Kinds
| Kind | Event Type | NIP | Quartz class |
|------|-----------|-----|-------------|
| 0 | User metadata | 01 | `MetadataEvent` |
| 1 | Text note | 10 | `TextNoteEvent` |
| 3 | Follow list | 02 | `ContactListEvent` |
| 4 | Legacy DM | 04 | `PrivateDmEvent` |
| 5 | Deletion | 09 | `DeletionEvent` |
| 6 | Repost | 18 | `RepostEvent` |
| 7 | Reaction | 25 | `ReactionEvent` |
| 14 | Chat message (sealed) | 17 | `NIP17GroupMessage` |
| 1059 | Gift wrap | 59 | `GiftWrapEvent` |
| 9734 | Zap request | 57 | `LnZapRequestEvent` |
| 9735 | Zap receipt | 57 | `LnZapEvent` |
| 10002 | Relay list | 65 | `AdvertisedRelayListEvent` |
| 30023 | Long-form content | 23 | `LongTextNoteEvent` |
## Related Skills
- **nostr-expert** — Internal Quartz patterns for Amethyst development
- **kotlin-multiplatform** — KMP source sets, expect/actual patterns
- **kotlin-coroutines** — Flow patterns for relay event streams
@@ -0,0 +1,143 @@
# Quartz Gradle Dependency Setup
## Current version
```
com.vitorpamplona.quartz:quartz:1.05.1
```
Check latest: https://central.sonatype.com/artifact/com.vitorpamplona.quartz/quartz
---
## KMP Project Setup
### `gradle/libs.versions.toml`
```toml
[versions]
quartz = "1.05.1"
[libraries]
quartz = { module = "com.vitorpamplona.quartz:quartz", version.ref = "quartz" }
```
### `build.gradle.kts` (library/app module)
```kotlin
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidKotlinMultiplatformLibrary) // or androidLibrary/androidApplication
}
kotlin {
jvm()
androidLibrary {
namespace = "com.example.myapp"
compileSdk = 35
minSdk = 21
}
// optional: iOS targets
sourceSets {
commonMain.dependencies {
implementation(libs.quartz)
}
// No platform-specific deps needed — Quartz provides them transitively
}
}
```
---
## Android-only Project
```kotlin
// build.gradle.kts (app module)
dependencies {
implementation("com.vitorpamplona.quartz:quartz:1.05.1")
}
```
---
## JVM (Desktop) standalone
```kotlin
// build.gradle.kts
plugins {
kotlin("jvm")
}
dependencies {
implementation("com.vitorpamplona.quartz:quartz:1.05.1")
// JNA needed for libsodium (NIP-44) on JVM
implementation("net.java.dev.jna:jna:5.18.1")
}
```
---
## Transitive Dependencies (what you get automatically)
### All platforms (`commonMain`)
- `org.jetbrains.kotlin:kotlin-stdlib`
- `org.jetbrains.kotlinx:kotlinx-coroutines-core`
- `org.jetbrains.kotlinx:kotlinx-collections-immutable`
- `org.jetbrains.kotlinx:kotlinx-serialization-json`
- `androidx.collection:collection` (LruCache)
- `androidx.compose.runtime:runtime-annotation` (@Immutable/@Stable)
- `fr.acinq.secp256k1:secp256k1-kmp` (Schnorr crypto — common)
### JVM + Android (`jvmAndroid`)
- `com.github.anthonynsimon:rfc3986-normalizer` (URL normalization)
- `com.fasterxml.jackson.module:jackson-module-kotlin` (JSON)
- `com.linkedin.urls:url-detector` (URL extraction)
- `com.squareup.okhttp3:okhttp` (WebSocket)
- `ru.gildor.coroutines:kotlin-coroutines-okhttp`
- `nl.bommber:kchesslib` (NIP-64 chess, version pinned to 1.0.0)
### JVM only
- `fr.acinq.secp256k1:secp256k1-kmp-jni-jvm`
- `com.goterl:lazysodium-java` (NIP-44 encryption)
- `net.java.dev.jna:jna`
### Android only
- `fr.acinq.secp256k1:secp256k1-kmp-jni-android`
- `com.goterl:lazysodium-android`
- `net.java.dev.jna:jna` (aar)
- `androidx.core:core-ktx`
---
## Packaging / ProGuard
Quartz ships consumer ProGuard rules automatically (`publish = true` in the library).
You don't need to add manual keep rules for Quartz classes in your app.
For Android release builds, these classes are preserved:
- All `com.vitorpamplona.quartz.**` event and model classes
- Jackson serialization annotations
---
## Common Build Errors
### `Duplicate class kotlin.collections.jdk8`
Add to `gradle.properties`:
```properties
android.useFullClasspathForDexingTransform=true
```
### `Could not find lazysodium-java` on JVM
Ensure JNA is on the classpath:
```kotlin
implementation("net.java.dev.jna:jna:5.18.1")
```
### iOS: `Framework not found quartz-kmpKit`
Build the XCFramework first:
```bash
./gradlew :quartz:assembleQuartz-kmpKitReleaseXCFramework
```
Then drag `quartz/build/XCFrameworks/release/quartz-kmpKit.xcframework` into Xcode.
+18 -160
View File
@@ -1,165 +1,23 @@
# Quartz KMP Conversion Skill
# Quartz KMP (Legacy Skill — Migration Complete)
When working with Quartz library conversion to Kotlin Multiplatform:
> The KMP migration of Quartz is **complete**. This file is kept for historical reference.
>
> For integrating Quartz into external projects, use the **`quartz-integration`** skill instead.
> For working with Quartz internals within Amethyst, use the **`nostr-expert`** skill.
## What was migrated
The Quartz library was successfully converted from Android-only to full KMP supporting:
- **commonMain** — All Nostr protocol logic, events, filters, tags
- **jvmAndroid** — OkHttp WebSocket, Jackson JSON, relay serializers
- **androidMain** — SQLite event store, NIP-55 Android signer
- **jvmMain** — Desktop JVM crypto (lazysodium-java, secp256k1-jni-jvm)
- **iosMain** — iOS targets (XCFramework `quartz-kmpKit`)
## Current artifact
## Current Structure (Android-only)
```
quartz/
├── build.gradle
└── src/
├── main/kotlin/ # All Nostr code here
├── test/
└── androidTest/
com.vitorpamplona.quartz:quartz:1.05.1
```
## Target Structure (KMP)
```
quartz/
├── build.gradle.kts
└── src/
├── commonMain/kotlin/ # Shared protocol code
├── commonTest/kotlin/ # Shared tests
├── androidMain/kotlin/ # Android crypto, storage
├── androidTest/kotlin/
├── jvmMain/kotlin/ # Desktop crypto, storage
└── jvmTest/kotlin/
```
## Platform Abstractions Required
### 1. Cryptography (expect/actual)
```kotlin
// commonMain
expect object Secp256k1 {
fun sign(data: ByteArray, privateKey: ByteArray): ByteArray
fun verify(data: ByteArray, signature: ByteArray, pubKey: ByteArray): Boolean
fun pubKeyCreate(privateKey: ByteArray): ByteArray
}
// androidMain - uses secp256k1-kmp-jni-android
actual object Secp256k1 {
actual fun sign(data: ByteArray, privateKey: ByteArray): ByteArray {
return fr.acinq.secp256k1.Secp256k1.sign(data, privateKey)
}
// ...
}
// jvmMain - uses secp256k1-kmp-jni-jvm
actual object Secp256k1 {
actual fun sign(data: ByteArray, privateKey: ByteArray): ByteArray {
return fr.acinq.secp256k1.Secp256k1.sign(data, privateKey)
}
// ...
}
```
### 2. NIP-44 Encryption (Sodium)
```kotlin
// commonMain
expect object Nip44 {
fun encrypt(plaintext: String, sharedSecret: ByteArray): String
fun decrypt(ciphertext: String, sharedSecret: ByteArray): String
}
// androidMain - lazysodium-android
// jvmMain - lazysodium-java or libsodium-jni
```
### 3. Secure Random
```kotlin
// commonMain
expect fun secureRandomBytes(size: Int): ByteArray
// androidMain
actual fun secureRandomBytes(size: Int): ByteArray {
return SecureRandom().let { random ->
ByteArray(size).also { random.nextBytes(it) }
}
}
// jvmMain
actual fun secureRandomBytes(size: Int): ByteArray {
return java.security.SecureRandom().let { random ->
ByteArray(size).also { random.nextBytes(it) }
}
}
```
## Build Configuration
```kotlin
// quartz/build.gradle.kts
plugins {
kotlin("multiplatform")
id("com.android.library")
}
kotlin {
androidTarget {
compilations.all {
kotlinOptions.jvmTarget = "17"
}
}
jvm("desktop") {
compilations.all {
kotlinOptions.jvmTarget = "17"
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.collections.immutable)
}
}
val androidMain by getting {
dependencies {
implementation(libs.secp256k1.kmp.jni.android)
implementation(libs.lazysodium.android)
}
}
val desktopMain by getting {
dependencies {
implementation(libs.secp256k1.kmp.jni.jvm)
// lazysodium-java or alternative
}
}
}
}
android {
namespace = "com.vitorpamplona.quartz"
compileSdk = 35
defaultConfig.minSdk = 26
}
```
## Migration Steps
1. **Convert build.gradle to build.gradle.kts** with KMP plugin
2. **Move pure Kotlin code** to `commonMain/`
3. **Identify platform dependencies** (crypto, JNA, Android APIs)
4. **Create expect declarations** for platform-specific APIs
5. **Implement actuals** in androidMain and jvmMain
6. **Update imports** in amethyst module
7. **Test on both platforms**
## Files to Move to commonMain
Most of Quartz can be shared:
- Event classes and parsing
- Filter definitions
- Relay message types
- NIP implementations (logic only)
- Utilities (hex encoding, bech32)
## Files Needing expect/actual
- `Secp256k1.kt` - Signature operations
- `Nip04.kt` - Legacy encryption (uses AES)
- `Nip44.kt` - Modern encryption (uses ChaCha)
- `KeyPair.kt` - Key generation
See `.claude/skills/quartz-integration/SKILL.md` for full integration guide.