Skip to content
vitorpamplonaPublic

About

Nostr library for Kotlin Multiplatform

Resources

Stars

5 stars

Watchers

0 watching

Forks

Repository files navigation

Quartz

A Nostr library for Kotlin Multiplatform: JVM, Android, iOS, macOS and Linux. Quartz has the event classes, tags and builders for the NIPs, signing (secp256k1) and NIP-04/NIP-44 encryption, a relay client with subscriptions, paging, publish-and-confirm, NIP-45 counts and NIP-77 sync, a SQLite event store with NIP-50 search, and the server side to run a relay on. It also carries whole protocol families built on Nostr: Marmot's events (MLS over Nostr), ContextVM, Concord, OpenAgents and Buzz. The MLS engines live in their own libraries on top of Quartz: marmot-kmp (Marmot groups) and cordn-kmp (cordn).

It is the protocol layer of Amethyst, its desktop app, the amy CLI and the geode relay.

Install

dependencies {
    implementation("com.vitorpamplona.quartz:quartz:<version>")
}
Requirement
JVM Java 17
Android minSdk 26
Apple iosArm64, iosSimulatorArm64, macosArm64
Linux linuxX64

Snapshots (JitPack)

Tagged releases go to Maven Central. To test an unreleased fix straight from main or a branch, use JitPack, which builds the library on demand from any git ref:

repositories {
    maven { url = uri("https://jitpack.io") }
}

dependencies {
    // version can be a tag, a commit hash, or "<branch>-SNAPSHOT"
    implementation("com.github.vitorpamplona.quartz:quartz:main-SNAPSHOT")
}

Prefer a Maven Central release for anything shipping to production; JitPack snapshots are not guaranteed stable.

Guides

  • Client architecture: wiring the store, cache, client and signer into an app.
  • Client API: transports, fetching, subscribing and publishing.
  • Relay: running a relay on Quartz's server code.
  • One-shot relay operations: fetchAll, fetchFirst, fetchAllPages, publishAndConfirm, count, negentropyReconcile.
  • Agent skill: install it in your project so a coding agent reaches for Quartz's helpers instead of re-implementing them.

Usage

Manage logged in users with the KeyPair class

val keyPair = KeyPair() // creates a random key
val keyPair = KeyPair("hex...".hexToByteArray())
val keyPair = KeyPair("nsec1...".bechToBytes())
val keyPair = KeyPair(Nip06().privateKeyFromMnemonic("<mnemonic>"))
val readOnly = KeyPair(pubKey = "hex...".hexToByteArray())
val readOnly = KeyPair(pubKey = "npub1...".bechToBytes())

Create signers that can be Internal, when you have the private key or a read-only public key, or External, when it is controlled by Amber in NIP-55.

Use either the NostrSignerInternal or NostrSignerExternal class:

val signer = NostrSignerInternal(keyPair)
val amberSigner = NostrSignerExternal(
    pubKey = keyPair.pubKey.toHexKey(),
    packageName = signerPackageName, // Amber package name
    contentResolver = appContext.contentResolver,
)

Create a single NostrClient for the entire application and control which relays it will access by registering subscriptions and sending events. The pool will automatically change based on filters + outbox events.

You will need a coroutine scope to process events and if you are using OKHttp, we offer a basic wrapper to create the socket connections themselves.

val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
val rootClient = OkHttpClient.Builder().build()
val socketBuilder = BasicOkHttpWebSocket.Builder { url -> rootClient }

val client = NostrClient(socketBuilder, appScope)

If you want to auth, given a logged-in signer:

val authCoordinator = RelayAuthenticator(client, appScope) { authTemplate ->
    listOf(
        // for each signed-in user, return an event
        signer.sign(authTemplate)
    )
}

To make a request subscription simply do:

val metadataSub = client.req(
    relay = "wss://nos.lol",
    filter = Filter(
        kinds = listOf(MetadataEvent.KIND),
        authors = listOf(signer.pubkey)
    )
) { event ->
    /* consume event */
}

The client will add the relay to the pool, connect to it and start receiving events. The metadataSub will be active until you call metadataSub.close(). If the client disconnects and reconnects, the sub will be active again.

To manage subscriptions that change over time, the simplest approach is to build mutable subscriptions with a filters lambda that you can change at will.

val metadataSub = client.req(
    filters = {
        // Let's say you have a list of users that need to be rendered
        val users = pubkeysSeeingInTheScreen()
        // And a cache repository with their outbox relays
        val outboxRelays = outboxRelays(users)

        val filters = listOf(
            Filter(
                kinds = listOf(MetadataEvent.KIND),
                authors = users
            )
        )

        outboxRelays.associateWith { filters }
    }
) { event ->
    /* consume event */
}

In that way, you can simply call metadataSub.updateFilter() when you need to update subscriptions to all relays. Or call metadataSub.close() to stop the sub without deleting it.

When your app goes to the background, you can use NostrClient's connect and disconnect methods to stop all communication to relays. Add the connect to your onResume and disconnect to onPause methods.

Dependencies

Crypto primitives come from crypto-kmp, secp256k1 from secp256k1-kmp and NIP-77 from negentropy-kmp. On the JVM and Android, Jackson parses events and OkHttp is the bundled WebSocket transport.

Developer Setup

./gradlew installGitHook            # pre-commit (spotless) and pre-push (checks + tests) hooks
./gradlew spotlessApply
./gradlew :quartz:jvmTest
./gradlew :quartz:verifyKmpPurity :quartz:compileCommonMainKotlinMetadata :quartz:compileAppleMainKotlinMetadata
./gradlew :quartz:linuxX64Test      # the whole suite on Linux Native
./gradlew :quartz:iosSimulatorArm64Test   # macOS only

To work on crypto-kmp alongside Quartz, point Gradle at a local checkout: ./gradlew -PcryptoKmpDir=../crypto-kmp :quartz:jvmTest. To try a Quartz change in Amethyst, build Amethyst with -PquartzDir=../quartz.

AGENTS.md has the project rules, for people and coding agents alike.

Versioning

Releases follow semantic versioning: a breaking change to the public API bumps the major version. Each release is tagged v<version> and listed on the releases page. Until 1.19.0 Quartz was released from the Amethyst repo with the app's version number.

License

MIT, see LICENSE.

About

Nostr library for Kotlin Multiplatform

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages