mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
Merge pull request #3443 from vitorpamplona/claude/nip11-document-builder-gq7eeq
Add type-safe DSL builder for NIP-11 relay information documents
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
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.
|
||||
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, (9) running a relay on Quartz and serving/building its NIP-11 relay information document (application/nostr+json).
|
||||
---
|
||||
|
||||
# Quartz Integration Guide
|
||||
@@ -755,7 +755,100 @@ val results = store.query<Event>(Filter(search = "bitcoin"))
|
||||
|
||||
---
|
||||
|
||||
## 15. Quick Reference
|
||||
## 15. NIP-11 Relay Information Document
|
||||
|
||||
If you're standing up a relay on Quartz's relay-server code, serve your NIP-11
|
||||
document with the **type-safe builder** — don't hand-write the JSON string.
|
||||
|
||||
**Package:** `com.vitorpamplona.quartz.nip11RelayInfo`
|
||||
|
||||
```kotlin
|
||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
||||
import com.vitorpamplona.quartz.nip11RelayInfo.relayInformation
|
||||
|
||||
val info =
|
||||
relayInformation {
|
||||
name = "sot"
|
||||
description = "NIP-50 profile search ranked by Nostr web-of-trust"
|
||||
software = "https://github.com/vitorpamplona/sot"
|
||||
version = "0.1"
|
||||
supports(1, 11, 42, 50) // ints → spec-compliant [1,11,42,50] in the JSON
|
||||
}
|
||||
|
||||
val json = info.toJson() // null/empty fields are omitted
|
||||
```
|
||||
|
||||
Serve it at the relay root, branching on the `Accept` header (Ktor example):
|
||||
|
||||
```kotlin
|
||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
||||
import io.ktor.http.ContentType
|
||||
|
||||
get("/") {
|
||||
val accept = call.request.headers[HttpHeaders.Accept].orEmpty()
|
||||
if (accept.contains(Nip11RelayInformation.CONTENT_TYPE)) { // "application/nostr+json"
|
||||
call.respondText(json, ContentType.parse(Nip11RelayInformation.CONTENT_TYPE))
|
||||
} else {
|
||||
call.respondText("Open a WebSocket (NIP-01) or send Accept: ${Nip11RelayInformation.CONTENT_TYPE}")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Nested objects, lists, and enforced limits
|
||||
|
||||
```kotlin
|
||||
val info =
|
||||
relayInformation {
|
||||
name = "Paid Relay"
|
||||
supports(1, 11, 42)
|
||||
supportsExtensions("nip50-search") // supported_nip_extensions
|
||||
countries("US", "CA") // relay_countries; also languages(...), tags(...)
|
||||
nip50Features("profile_search") // the `nip50` field
|
||||
|
||||
// limitation { } — camelCase maps to NIP-11 snake_case fields
|
||||
limitation {
|
||||
maxSubscriptions = 20
|
||||
maxFilters = 10
|
||||
authRequired = true
|
||||
}
|
||||
|
||||
// fees { } — each helper is repeatable
|
||||
fees {
|
||||
admission(amount = 1000, unit = "msats")
|
||||
publication(amount = 100, unit = "msats", kinds = listOf(1, 30023))
|
||||
}
|
||||
|
||||
// retention(...) — call once per policy entry
|
||||
retention(kinds = listOf(0, 3), count = 1)
|
||||
}
|
||||
```
|
||||
|
||||
**Keep advertised limits in sync with enforced ones.** If you build a
|
||||
`RelayLimits` for the server's policy chain, hand the *same* object to the
|
||||
builder so what you publish can never drift from what you enforce:
|
||||
|
||||
```kotlin
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.server.policies.RelayLimits
|
||||
|
||||
val limits = RelayLimits(maxSubscriptions = 20, maxFilters = 10, maxLimit = 500, authRequired = true)
|
||||
|
||||
val info =
|
||||
relayInformation {
|
||||
name = "My Relay"
|
||||
supports(1, 11, 42, 45)
|
||||
limitation(limits) // == limits.toNip11Limitation()
|
||||
}
|
||||
```
|
||||
|
||||
To load an operator-supplied doc from disk or a string instead of building it,
|
||||
use `Nip11RelayInformation.fromJson(json)`.
|
||||
|
||||
> `geode` (Quartz's standalone relay) builds its default document exactly this
|
||||
> way — see `geode/.../RelayInfo.kt`.
|
||||
|
||||
---
|
||||
|
||||
## 16. Quick Reference
|
||||
|
||||
| Task | API | Package |
|
||||
|------|-----|---------|
|
||||
@@ -779,6 +872,8 @@ val results = store.query<Event>(Filter(search = "bitcoin"))
|
||||
| NIP-44 encrypt | `signer.nip44Encrypt(text, recipientPubKey)` | `nip01Core.signers` |
|
||||
| Bech32 decode | `Nip19Parser.uriToRoute("npub1...")` | `nip19Bech32` |
|
||||
| Bech32 encode | `Nip19Bech32.createNPub(pubKeyHex)` | `nip19Bech32` |
|
||||
| Build NIP-11 doc | `relayInformation { name = ...; supports(1, 11) }` | `nip11RelayInfo` |
|
||||
| Serialize NIP-11 doc | `info.toJson()` (media type `Nip11RelayInformation.CONTENT_TYPE`) | `nip11RelayInfo` |
|
||||
|
||||
## Common Event Kinds
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ package com.vitorpamplona.geode
|
||||
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation
|
||||
import com.vitorpamplona.quartz.nip11RelayInfo.relayInformation
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
@@ -68,13 +69,13 @@ data class RelayInfo(
|
||||
/** Pre-built default for `RelayEngine(url = ...)` — advertises the supported NIPs. */
|
||||
fun default(url: NormalizedRelayUrl): RelayInfo =
|
||||
RelayInfo(
|
||||
Nip11RelayInformation(
|
||||
name = NAME,
|
||||
description = DESCRIPTION,
|
||||
software = SOFTWARE,
|
||||
version = VERSION,
|
||||
supported_nips = SUPPORTED_NIPS,
|
||||
),
|
||||
relayInformation {
|
||||
name = NAME
|
||||
description = DESCRIPTION
|
||||
software = SOFTWARE
|
||||
version = VERSION
|
||||
supports(*SUPPORTED_NIPS.toTypedArray())
|
||||
},
|
||||
)
|
||||
|
||||
/** Loads a NIP-11 doc from a JSON file (e.g. a relay operator's config). */
|
||||
|
||||
+16
-2
@@ -31,6 +31,7 @@ import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonDecoder
|
||||
import kotlinx.serialization.json.JsonEncoder
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
@@ -87,8 +88,21 @@ object FlexibleIntListSerializer : KSerializer<List<String>?> {
|
||||
) {
|
||||
if (value == null) {
|
||||
encoder.encodeNull()
|
||||
} else {
|
||||
listSerializer.serialize(encoder, value)
|
||||
return
|
||||
}
|
||||
|
||||
require(encoder is JsonEncoder) { "This serializer can only be used with Json format" }
|
||||
|
||||
// NIP-11 expects `supported_nips` as an array of integers ([1, 11, 42]),
|
||||
// so emit numeric entries as JSON numbers. Non-numeric ids (rare, but the
|
||||
// model tolerates them) fall back to JSON strings so nothing is lost.
|
||||
val array =
|
||||
JsonArray(
|
||||
value.map { nip ->
|
||||
val asLong = nip.toLongOrNull()
|
||||
if (asLong != null) JsonPrimitive(asLong) else JsonPrimitive(nip)
|
||||
},
|
||||
)
|
||||
encoder.encodeJsonElement(array)
|
||||
}
|
||||
}
|
||||
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip11RelayInfo
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.server.policies.RelayLimits
|
||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation.RelayInformationFee
|
||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation.RelayInformationFees
|
||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation.RelayInformationLimitation
|
||||
import com.vitorpamplona.quartz.nip11RelayInfo.Nip11RelayInformation.RelayInformationRetentionData
|
||||
|
||||
/**
|
||||
* Scopes the NIP-11 DSL so an inner builder (e.g. `limitation { }`) can't
|
||||
* accidentally reach the outer builder's members.
|
||||
*/
|
||||
@DslMarker annotation class Nip11DslMarker
|
||||
|
||||
/**
|
||||
* Type-safe builder for a [Nip11RelayInformation] document, so a relay operator
|
||||
* never has to hand-write the JSON.
|
||||
*
|
||||
* Instead of the string that smells:
|
||||
* ```
|
||||
* private val NIP11 =
|
||||
* """{"name":"sot","description":"...","supported_nips":[1,11,42,50],"software":"...","version":"0.1"}"""
|
||||
* ```
|
||||
* write:
|
||||
* ```
|
||||
* val info = relayInformation {
|
||||
* name = "sot"
|
||||
* description = "NIP-50 profile search ranked by Nostr web-of-trust"
|
||||
* software = "https://github.com/vitorpamplona/sot"
|
||||
* version = "0.1"
|
||||
* supports(1, 11, 42, 50)
|
||||
* }
|
||||
* ```
|
||||
* and serve it with [Nip11RelayInformation.toJson] under
|
||||
* [Nip11RelayInformation.CONTENT_TYPE]. Null/empty fields are omitted, and
|
||||
* numeric NIP numbers serialize as the JSON integers the spec expects
|
||||
* (`[1,11,42,50]`, not `["1","11","42","50"]`).
|
||||
*/
|
||||
inline fun relayInformation(initializer: Nip11RelayInformationBuilder.() -> Unit): Nip11RelayInformation = Nip11RelayInformationBuilder().apply(initializer).build()
|
||||
|
||||
@Nip11DslMarker
|
||||
class Nip11RelayInformationBuilder {
|
||||
var id: String? = null
|
||||
var name: String? = null
|
||||
var description: String? = null
|
||||
var banner: String? = null
|
||||
var icon: String? = null
|
||||
var pubkey: HexKey? = null
|
||||
var self: HexKey? = null
|
||||
var contact: String? = null
|
||||
var software: String? = null
|
||||
var version: String? = null
|
||||
var postingPolicy: String? = null
|
||||
var privacyPolicy: String? = null
|
||||
var termsOfService: String? = null
|
||||
var paymentsUrl: String? = null
|
||||
|
||||
private val supportedNips = mutableListOf<String>()
|
||||
private val supportedNipExtensions = mutableListOf<String>()
|
||||
private val relayCountries = mutableListOf<String>()
|
||||
private val languageTags = mutableListOf<String>()
|
||||
private val tags = mutableListOf<String>()
|
||||
private val nip50Subfeatures = mutableListOf<String>()
|
||||
private val supportedGrasps = mutableListOf<String>()
|
||||
private val retention = mutableListOf<RelayInformationRetentionData>()
|
||||
|
||||
private var limitation: RelayInformationLimitation? = null
|
||||
private var fees: RelayInformationFees? = null
|
||||
|
||||
/** Advertise supported NIP numbers, e.g. `supports(1, 11, 42, 50)`. Repeatable. */
|
||||
fun supports(vararg nips: Int) = apply { nips.forEach { supportedNips.add(it.toString()) } }
|
||||
|
||||
/** Advertise supported NIPs by string id (for non-numeric extensions). Repeatable. */
|
||||
fun supports(vararg nips: String) = apply { supportedNips.addAll(nips) }
|
||||
|
||||
/** Advertise `supported_nip_extensions` (draft/experimental features). Repeatable. */
|
||||
fun supportsExtensions(vararg extensions: String) = apply { supportedNipExtensions.addAll(extensions) }
|
||||
|
||||
/** ISO-3166 country codes the relay serves under (`relay_countries`). Repeatable. */
|
||||
fun countries(vararg codes: String) = apply { relayCountries.addAll(codes) }
|
||||
|
||||
/** IETF language tags of content on the relay (`language_tags`). Repeatable. */
|
||||
fun languages(vararg codes: String) = apply { languageTags.addAll(codes) }
|
||||
|
||||
/** Free-form topic tags describing the relay's focus. Repeatable. */
|
||||
fun tags(vararg values: String) = apply { tags.addAll(values) }
|
||||
|
||||
/** NIP-50 search sub-features the relay implements (the `nip50` field). Repeatable. */
|
||||
fun nip50Features(vararg features: String) = apply { nip50Subfeatures.addAll(features) }
|
||||
|
||||
/** GRASP git-server capabilities the relay implements (`supported_grasps`). Repeatable. */
|
||||
fun grasps(vararg values: String) = apply { supportedGrasps.addAll(values) }
|
||||
|
||||
/** Declare the relay's `limitation` object via a nested DSL. */
|
||||
fun limitation(initializer: LimitationBuilder.() -> Unit) =
|
||||
apply {
|
||||
limitation = LimitationBuilder().apply(initializer).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Advertise the exact limits the relay actually enforces, keeping what is
|
||||
* published in sync with what is applied. See [RelayLimits.toNip11Limitation].
|
||||
*/
|
||||
fun limitation(limits: RelayLimits) = apply { limitation = limits.toNip11Limitation() }
|
||||
|
||||
/** Declare the relay's `fees` object via a nested DSL. */
|
||||
fun fees(initializer: FeesBuilder.() -> Unit) =
|
||||
apply {
|
||||
fees = FeesBuilder().apply(initializer).build()
|
||||
}
|
||||
|
||||
/** Add a data-retention policy entry (`retention`). Call multiple times for multiple entries. */
|
||||
fun retention(
|
||||
kinds: List<Int>? = null,
|
||||
time: Int? = null,
|
||||
count: Int? = null,
|
||||
) = apply {
|
||||
retention.add(RelayInformationRetentionData(kinds?.let { ArrayList(it) }, time, count))
|
||||
}
|
||||
|
||||
fun build(): Nip11RelayInformation =
|
||||
Nip11RelayInformation(
|
||||
id = id,
|
||||
name = name,
|
||||
description = description,
|
||||
banner = banner,
|
||||
icon = icon,
|
||||
pubkey = pubkey,
|
||||
self = self,
|
||||
contact = contact,
|
||||
supported_nips = supportedNips.ifEmpty { null }?.toList(),
|
||||
supported_nip_extensions = supportedNipExtensions.ifEmpty { null }?.toList(),
|
||||
software = software,
|
||||
version = version,
|
||||
limitation = limitation,
|
||||
relay_countries = relayCountries.ifEmpty { null }?.toList(),
|
||||
language_tags = languageTags.ifEmpty { null }?.toList(),
|
||||
tags = tags.ifEmpty { null }?.toList(),
|
||||
posting_policy = postingPolicy,
|
||||
privacy_policy = privacyPolicy,
|
||||
terms_of_service = termsOfService,
|
||||
payments_url = paymentsUrl,
|
||||
retention = retention.ifEmpty { null }?.toList(),
|
||||
fees = fees,
|
||||
nip50 = nip50Subfeatures.ifEmpty { null }?.toList(),
|
||||
supported_grasps = supportedGrasps.ifEmpty { null }?.toList(),
|
||||
)
|
||||
|
||||
@Nip11DslMarker
|
||||
class LimitationBuilder {
|
||||
var maxMessageLength: Int? = null
|
||||
var maxSubscriptions: Int? = null
|
||||
var maxFilters: Int? = null
|
||||
var maxLimit: Int? = null
|
||||
var defaultLimit: Int? = null
|
||||
var maxSubidLength: Int? = null
|
||||
var minPrefix: Int? = null
|
||||
var maxEventTags: Int? = null
|
||||
var maxContentLength: Int? = null
|
||||
var minPowDifficulty: Int? = null
|
||||
var authRequired: Boolean? = null
|
||||
var paymentRequired: Boolean? = null
|
||||
var restrictedWrites: Boolean? = null
|
||||
var createdAtLowerLimit: Int? = null
|
||||
var createdAtUpperLimit: Int? = null
|
||||
|
||||
fun build(): RelayInformationLimitation =
|
||||
RelayInformationLimitation(
|
||||
max_message_length = maxMessageLength,
|
||||
max_subscriptions = maxSubscriptions,
|
||||
max_filters = maxFilters,
|
||||
max_limit = maxLimit,
|
||||
default_limit = defaultLimit,
|
||||
max_subid_length = maxSubidLength,
|
||||
min_prefix = minPrefix,
|
||||
max_event_tags = maxEventTags,
|
||||
max_content_length = maxContentLength,
|
||||
min_pow_difficulty = minPowDifficulty,
|
||||
auth_required = authRequired,
|
||||
payment_required = paymentRequired,
|
||||
restricted_writes = restrictedWrites,
|
||||
created_at_lower_limit = createdAtLowerLimit,
|
||||
created_at_upper_limit = createdAtUpperLimit,
|
||||
)
|
||||
}
|
||||
|
||||
@Nip11DslMarker
|
||||
class FeesBuilder {
|
||||
private val admission = mutableListOf<RelayInformationFee>()
|
||||
private val subscription = mutableListOf<RelayInformationFee>()
|
||||
private val publication = mutableListOf<RelayInformationFee>()
|
||||
|
||||
/** A one-time fee to be allowed to write to the relay. Repeatable. */
|
||||
fun admission(
|
||||
amount: Int? = null,
|
||||
unit: String? = null,
|
||||
period: Int? = null,
|
||||
kinds: List<Int>? = null,
|
||||
) = apply { admission.add(RelayInformationFee(amount, unit, period, kinds)) }
|
||||
|
||||
/** A recurring fee to keep writing to the relay. Repeatable. */
|
||||
fun subscription(
|
||||
amount: Int? = null,
|
||||
unit: String? = null,
|
||||
period: Int? = null,
|
||||
kinds: List<Int>? = null,
|
||||
) = apply { subscription.add(RelayInformationFee(amount, unit, period, kinds)) }
|
||||
|
||||
/** A per-event fee to publish. Repeatable (e.g. one entry per kind group). */
|
||||
fun publication(
|
||||
amount: Int? = null,
|
||||
unit: String? = null,
|
||||
period: Int? = null,
|
||||
kinds: List<Int>? = null,
|
||||
) = apply { publication.add(RelayInformationFee(amount, unit, period, kinds)) }
|
||||
|
||||
fun build(): RelayInformationFees =
|
||||
RelayInformationFees(
|
||||
admission = admission.ifEmpty { null }?.toList(),
|
||||
subscription = subscription.ifEmpty { null }?.toList(),
|
||||
publication = publication.ifEmpty { null }?.toList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip11RelayInfo
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.server.policies.RelayLimits
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class Nip11RelayInformationBuilderTest {
|
||||
@Test
|
||||
fun buildsTheSotExampleWithoutHandWrittenJson() {
|
||||
val info =
|
||||
relayInformation {
|
||||
name = "sot"
|
||||
description = "NIP-50 profile search ranked by Nostr web-of-trust"
|
||||
software = "https://github.com/vitorpamplona/sot"
|
||||
version = "0.1"
|
||||
supports(1, 11, 42, 50)
|
||||
}
|
||||
|
||||
assertEquals("sot", info.name)
|
||||
assertEquals(listOf("1", "11", "42", "50"), info.supported_nips)
|
||||
|
||||
// Numeric NIPs serialize as JSON integers, exactly as the hand-written string had them.
|
||||
val json = info.toJson()
|
||||
assertTrue(json.contains("\"supported_nips\":[1,11,42,50]"), json)
|
||||
assertTrue(json.contains("\"name\":\"sot\""), json)
|
||||
// Unset fields stay out of the document.
|
||||
assertTrue(!json.contains("\"limitation\""), json)
|
||||
assertTrue(!json.contains("\"fees\""), json)
|
||||
|
||||
// Round-trips back to the same document.
|
||||
assertEquals(info, Nip11RelayInformation.fromJson(json))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun emptyBuilderProducesAllNullFields() {
|
||||
val info = relayInformation {}
|
||||
|
||||
assertNull(info.name)
|
||||
assertNull(info.supported_nips)
|
||||
assertNull(info.limitation)
|
||||
assertNull(info.fees)
|
||||
assertEquals("{}", info.toJson())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nestedLimitationDsl() {
|
||||
val info =
|
||||
relayInformation {
|
||||
name = "R"
|
||||
limitation {
|
||||
maxSubscriptions = 20
|
||||
maxFilters = 10
|
||||
maxLimit = 500
|
||||
authRequired = true
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(20, info.limitation?.max_subscriptions)
|
||||
assertEquals(10, info.limitation?.max_filters)
|
||||
assertEquals(500, info.limitation?.max_limit)
|
||||
assertEquals(true, info.limitation?.auth_required)
|
||||
// Untouched limitation fields remain null (and are omitted from JSON).
|
||||
assertNull(info.limitation?.max_message_length)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun limitationFromEnforcedRelayLimitsStaysInSync() {
|
||||
val limits = RelayLimits(maxSubscriptions = 20, maxFilters = 10, maxLimit = 500, authRequired = true)
|
||||
val info =
|
||||
relayInformation {
|
||||
name = "R"
|
||||
limitation(limits)
|
||||
}
|
||||
|
||||
assertEquals(limits.toNip11Limitation(), info.limitation)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun feesDslAccumulatesEntries() {
|
||||
val info =
|
||||
relayInformation {
|
||||
name = "Paid Relay"
|
||||
fees {
|
||||
admission(amount = 1000, unit = "msats")
|
||||
publication(amount = 100, unit = "msats", kinds = listOf(1, 30023))
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
1000,
|
||||
info.fees
|
||||
?.admission
|
||||
?.first()
|
||||
?.amount,
|
||||
)
|
||||
assertEquals(
|
||||
"msats",
|
||||
info.fees
|
||||
?.admission
|
||||
?.first()
|
||||
?.unit,
|
||||
)
|
||||
assertEquals(
|
||||
listOf(1, 30023),
|
||||
info.fees
|
||||
?.publication
|
||||
?.first()
|
||||
?.kinds,
|
||||
)
|
||||
assertNull(info.fees?.subscription)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun retentionEntriesAreRepeatable() {
|
||||
val info =
|
||||
relayInformation {
|
||||
name = "R"
|
||||
retention(kinds = listOf(0, 3), count = 1)
|
||||
retention(time = 3600)
|
||||
}
|
||||
|
||||
assertEquals(2, info.retention?.size)
|
||||
assertEquals(arrayListOf(0, 3), info.retention?.get(0)?.kinds)
|
||||
assertEquals(1, info.retention?.get(0)?.count)
|
||||
assertEquals(3600, info.retention?.get(1)?.time)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun listHelpersAreRepeatableAndCollapseWhenEmpty() {
|
||||
val info =
|
||||
relayInformation {
|
||||
name = "R"
|
||||
supports(1)
|
||||
supports(11, 42)
|
||||
supports("custom-ext")
|
||||
countries("US", "CA")
|
||||
languages("en")
|
||||
tags("search")
|
||||
}
|
||||
|
||||
assertEquals(listOf("1", "11", "42", "custom-ext"), info.supported_nips)
|
||||
assertEquals(listOf("US", "CA"), info.relay_countries)
|
||||
assertEquals(listOf("en"), info.language_tags)
|
||||
assertEquals(listOf("search"), info.tags)
|
||||
// Untouched lists collapse to null instead of empty arrays.
|
||||
assertNull(info.nip50)
|
||||
assertNull(info.supported_grasps)
|
||||
|
||||
// Mixed numeric + string ids: numbers stay numbers, the extension stays a string.
|
||||
assertTrue(info.toJson().contains("\"supported_nips\":[1,11,42,\"custom-ext\"]"), info.toJson())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user