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:
@@ -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
@@ -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.
|
||||
Reference in New Issue
Block a user