diff --git a/.git-hooks/pre-push b/.git-hooks/pre-push
index 903d42dc60..2c0dbdc27f 100755
--- a/.git-hooks/pre-push
+++ b/.git-hooks/pre-push
@@ -12,7 +12,7 @@ echo "$JAVA_HOME"
echo "$(java -version)"
echo "Running test... "
-./gradlew test
+./gradlew test --quiet
status=$?
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 61e76d85f9..8387a5298d 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -54,7 +54,7 @@ jobs:
name: FDroid Debug APK
path: amethyst/build/outputs/apk/fdroid/debug/amethyst-fdroid-universal-debug.apk
- - name: Build APK (gradle)
+ - name: Build Benchmark APK (gradle)
run: ./gradlew assembleBenchmark
- name: Upload Play APK Benchmark
diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml
index 6332634f02..0c808a742b 100644
--- a/amethyst/src/main/res/values-hi-rIN/strings.xml
+++ b/amethyst/src/main/res/values-hi-rIN/strings.xml
@@ -1068,6 +1068,13 @@
संख्या (साट्स)
विवरण (विकल्पात्मक)
चालान बनाएँ
+ चालान बनाया जा रहा है…
+ चालान अनुकृति
+ अभी कोई लेनदेन नहीं
+ आवहन चालू…
+ प्राप्त
+ भेजा गया
+ नवीकरण
सुरक्षार्थ छलनियाँ
अनुचरित आयात करें
नया पत्र प्रकाशन
@@ -1104,8 +1111,8 @@
पुनःप्रसारक सूची चयनकर्ता
मतदान
मतदान अक्षम करें
- बिटकोयिन चालान
- बिटकोयिन चालान निरस्त करें
+ द्व्यंकरूप्य चालान
+ द्व्यंकरूप्य चालान निरस्त करें
वस्तु बिक्री निरस्त करें
ज्सापोपार्जन योजना
ज्सापोपार्जन योजना निरस्त करें
@@ -1210,7 +1217,7 @@
अनुकृति :
ओ॰टी॰एस : %1$s
समयांकन प्रमाण
- प्रमाण उपलब्ध है कि इस पत्र पर हस्ताक्षर किया गया %1$s के कुछ पहले। प्रमाण अंकित किया गया बिटकोयिन खण्डश्रृंखला में उस समय उस दिन पर।
+ प्रमाण उपलब्ध है कि इस पत्र पर हस्ताक्षर किया गया %1$s के कुछ पहले। प्रमाण अंकित किया गया द्व्यंकरूप्य खण्डश्रृंखला में उस समय उस दिन पर।
पत्र का सम्पादन करें
पत्र शोधन के लिए प्रस्ताव
परिवर्तनों का साराम्श
@@ -1445,8 +1452,8 @@
अभिलेख शीर्षक
परिचय चित्रालय
अभिलेख सेवासंगणक
- अंकीय तथ्याभिलेख
- अंकीय अभिलेख शीर्षक
+ द्व्यंकीय अभिलेख
+ द्व्यंकीय अभिलेख शीर्षक
चिकित्सा अभिलेख
अनुचरण पोटलियाँ
पुनःप्रकाशन (१६)
@@ -1527,4 +1534,28 @@
अनुचरित सूची आयात
प्रयोक्ता चुनें अनुचरण करने के लिए
प्रयोक्ता परिचय जिससे आयात करना है
+ खोज, एनपुब॰१…, alice@example.com
+ एनपुब॰ एन॰परिचय निप॰०५ षोडशांक तथा नामरूप्य का आलम्बन करता है (.bit, d/, id/)
+ अनुचरण सूची देखें
+ पारितोषिक
+ %1$d लेखाएँ प्राप्त
+ %1$d चयनित
+ नामरूप्य द्वारा सुलझा गया
+ अब %1$d लेखाएँ अनुचरित
+ आपकी सूचनावली उपलब्ध है।
+ छोडें
+ अन्य खोजें
+ %1$d लेखाओं का अनुचरण करें
+ अधिक आयात
+ चलते रहें
+ अभी के लिए छोडें
+ %1$s का सुलझन चालू…
+ अनुचरण सूची प्राप्त की जा रही है…
+ कोई अनुचरित नहीं
+ %1$d लेखाओं का अनुचरण…
+ "किसी मित्र अथवा समूह नेता का परिचय प्रविष्ट करें। उनके एनपुब॰ अथवा निप॰०५ पता अथवा नामरूप्य नाम जैसे कि alice@example.com अथवा id/alice का उपयोग आप कर सकते हैं खण्डश्रृंखला सत्यापित विभेदकों के लिए।"
+ सभी चुनें
+ %1$d%% समय निरन्तर उपलब्ध
+ नामरूप्य स्थापना विकल्प
+ द्व्यंकरूप्य समन्वेषक (ओटीएस॰)
diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml
index cdaa9bb882..8615b4bfb1 100644
--- a/amethyst/src/main/res/values-hu-rHU/strings.xml
+++ b/amethyst/src/main/res/values-hu-rHU/strings.xml
@@ -418,7 +418,7 @@
Áthelyezés a nyilvános könyvjelzőkbe
Áthelyezés a privát könyvjelzőkbe
Wallet Connect szolgáltatás
- Hitelesíti, hogy a Nostr Secret az alkalmazásból való kilépés nélkül fizessen a Zap-et. Tartsa biztonságban a titkot, és lehetőség szerint használjon privát átjátszót
+ Lehetővé teszi az Amethyst számára, hogy az alkalmazásból kilépés nélkül fizessen. Tartsa biztonságos helyen a titkot!
Wallet Connect nyilvános kulcs
Wallet Connect átjátszó
Wallet Connect titok
@@ -1052,6 +1052,29 @@
Globális
Rövidek
Sakk
+ Pénztárca
+ Egyenleg
+ Küldés
+ Fogadás
+ Tranzakciók
+ Nincs pénztárca összekapcsolva
+ A pénztárca használatához állítson be egy Nostr Wallet Connect (NWC) kapcsolatot a zap-beállításokban.
+ Pénztárca beállítása
+ satoshik
+ Egy BOLT-11 számla beillesztése
+ Fizetés
+ Sikeres fizetés
+ Fizetés küldése…
+ Összeg (satoshiban)
+ Leírás (nem kötelező)
+ Számla létrehozása
+ Számla létrehozása…
+ Számla másolása
+ Még nincsenek tranzakciók
+ Betöltés…
+ Fogadott
+ Elküldött
+ Frissítés
Biztonsági szűrők
Követettek importálása
Új bejegyzés
diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/domain/nip46/NostrConnectLoginUseCase.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/domain/nip46/NostrConnectLoginUseCase.kt
index 674a38219f..6725ef332f 100644
--- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/domain/nip46/NostrConnectLoginUseCase.kt
+++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/domain/nip46/NostrConnectLoginUseCase.kt
@@ -213,6 +213,6 @@ object NostrConnectLoginUseCase {
private fun generateSecret(): String {
val bytes = ByteArray(32)
SecureRandom().nextBytes(bytes)
- return bytes.joinToString("") { "%02x".format(it) }
+ return bytes.toHexKey()
}
}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CommandKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CommandKSerializer.kt
new file mode 100644
index 0000000000..4d3eba2998
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CommandKSerializer.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.nip01Core.kotlinSerialization
+
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.AuthCmd
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CloseCmd
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CountCmd
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd
+import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonEncoder
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.buildJsonArray
+import kotlinx.serialization.json.jsonArray
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+
+object CommandKSerializer : KSerializer {
+ override val descriptor: SerialDescriptor =
+ buildClassSerialDescriptor("Command")
+
+ override fun serialize(
+ encoder: Encoder,
+ value: Command,
+ ) {
+ val jsonEncoder = encoder as JsonEncoder
+ val element =
+ buildJsonArray {
+ add(JsonPrimitive(value.label()))
+ when (value) {
+ is ReqCmd -> {
+ add(JsonPrimitive(value.subId))
+ for (filter in value.filters) {
+ add(FilterKSerializer.serializeToElement(filter))
+ }
+ }
+
+ is EventCmd -> {
+ add(EventKSerializer.serializeToElement(value.event))
+ }
+
+ is CloseCmd -> {
+ add(JsonPrimitive(value.subId))
+ }
+
+ is AuthCmd -> {
+ add(EventKSerializer.serializeToElement(value.event))
+ }
+
+ is CountCmd -> {
+ add(JsonPrimitive(value.queryId))
+ for (filter in value.filters) {
+ add(FilterKSerializer.serializeToElement(filter))
+ }
+ }
+ }
+ }
+ jsonEncoder.encodeJsonElement(element)
+ }
+
+ override fun deserialize(decoder: Decoder): Command {
+ val jsonDecoder = decoder as JsonDecoder
+ val array = jsonDecoder.decodeJsonElement().jsonArray
+ val type = array[0].jsonPrimitive.content
+
+ return when (type) {
+ ReqCmd.LABEL -> {
+ val subId = array[1].jsonPrimitive.content
+ val filters =
+ (2 until array.size).map { i ->
+ FilterKSerializer.deserializeFromElement(array[i].jsonObject)
+ }
+ ReqCmd(subId, filters)
+ }
+
+ CountCmd.LABEL -> {
+ val queryId = array[1].jsonPrimitive.content
+ val filters =
+ (2 until array.size).map { i ->
+ FilterKSerializer.deserializeFromElement(array[i].jsonObject)
+ }
+ CountCmd(queryId, filters)
+ }
+
+ EventCmd.LABEL -> {
+ EventCmd(EventKSerializer.deserializeFromElement(array[1].jsonObject))
+ }
+
+ CloseCmd.LABEL -> {
+ CloseCmd(array[1].jsonPrimitive.content)
+ }
+
+ AuthCmd.LABEL -> {
+ AuthCmd(EventKSerializer.deserializeFromElement(array[1].jsonObject) as RelayAuthEvent)
+ }
+
+ else -> {
+ throw IllegalArgumentException("Message $type is not supported")
+ }
+ }
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CountResultKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CountResultKSerializer.kt
new file mode 100644
index 0000000000..ed0ff95aea
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CountResultKSerializer.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.nip01Core.kotlinSerialization
+
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountResult
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.descriptors.element
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonEncoder
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.boolean
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.int
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.put
+
+object CountResultKSerializer : KSerializer {
+ override val descriptor: SerialDescriptor =
+ buildClassSerialDescriptor("CountResult") {
+ element("count")
+ element("pubkey")
+ }
+
+ override fun serialize(
+ encoder: Encoder,
+ value: CountResult,
+ ) {
+ val jsonEncoder = encoder as JsonEncoder
+ jsonEncoder.encodeJsonElement(serializeToElement(value))
+ }
+
+ fun serializeToElement(value: CountResult): JsonObject =
+ buildJsonObject {
+ put("count", value.count)
+ // Matches Jackson's CountResultSerializer which writes "pubkey" for approximate
+ put("pubkey", value.approximate)
+ }
+
+ override fun deserialize(decoder: Decoder): CountResult {
+ val jsonDecoder = decoder as JsonDecoder
+ return deserializeFromElement(jsonDecoder.decodeJsonElement().jsonObject)
+ }
+
+ fun deserializeFromElement(jsonObject: JsonObject): CountResult =
+ CountResult(
+ count = jsonObject["count"]!!.jsonPrimitive.int,
+ approximate = jsonObject["approximate"]?.jsonPrimitive?.boolean ?: false,
+ )
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventKSerializer.kt
new file mode 100644
index 0000000000..4eba5b3973
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventKSerializer.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.nip01Core.kotlinSerialization
+
+import com.vitorpamplona.quartz.nip01Core.core.Event
+import com.vitorpamplona.quartz.nip01Core.core.HexKey
+import com.vitorpamplona.quartz.nip01Core.core.Kind
+import com.vitorpamplona.quartz.nip01Core.core.TagArray
+import com.vitorpamplona.quartz.utils.EventFactory
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.descriptors.element
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonEncoder
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.int
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.long
+import kotlinx.serialization.json.put
+
+object EventKSerializer : KSerializer {
+ private val emptyTagArray = emptyArray>()
+
+ override val descriptor: SerialDescriptor =
+ buildClassSerialDescriptor("Event") {
+ element("id")
+ element("pubkey")
+ element("created_at")
+ element("kind")
+ element("tags", TagArrayKSerializer.descriptor)
+ element("content")
+ element("sig")
+ }
+
+ override fun serialize(
+ encoder: Encoder,
+ value: Event,
+ ) {
+ val jsonEncoder = encoder as JsonEncoder
+ jsonEncoder.encodeJsonElement(serializeToElement(value))
+ }
+
+ fun serializeToElement(event: Event): JsonObject =
+ buildJsonObject {
+ put("id", event.id)
+ put("pubkey", event.pubKey)
+ put("created_at", event.createdAt)
+ put("kind", event.kind)
+ put("tags", TagArrayKSerializer.serializeToElement(event.tags))
+ put("content", event.content)
+ put("sig", event.sig)
+ }
+
+ override fun deserialize(decoder: Decoder): Event {
+ val jsonDecoder = decoder as JsonDecoder
+ return deserializeFromElement(jsonDecoder.decodeJsonElement().jsonObject)
+ }
+
+ fun deserializeFromElement(jsonObject: JsonObject): Event {
+ var id: HexKey = ""
+ var pubKey: HexKey = ""
+ var createdAt: Long = 0
+ var kind: Kind = 0
+ var tags: TagArray = emptyTagArray
+ var content = ""
+ var sig: HexKey = ""
+
+ for ((key, value) in jsonObject) {
+ when (key) {
+ "id" -> id = value.jsonPrimitive.content
+ "pubkey" -> pubKey = value.jsonPrimitive.content
+ "created_at" -> createdAt = value.jsonPrimitive.long
+ "kind" -> kind = value.jsonPrimitive.int
+ "tags" -> tags = TagArrayKSerializer.deserializeFromElement(value)
+ "content" -> content = value.jsonPrimitive.content
+ "sig" -> sig = value.jsonPrimitive.content
+ }
+ }
+
+ if (pubKey.isEmpty()) {
+ throw IllegalArgumentException("Event not found")
+ }
+
+ return EventFactory.create(id, pubKey, createdAt, kind, tags, content, sig)
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventTemplateKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventTemplateKSerializer.kt
new file mode 100644
index 0000000000..58ca13df3a
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventTemplateKSerializer.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.nip01Core.kotlinSerialization
+
+import com.vitorpamplona.quartz.nip01Core.core.Event
+import com.vitorpamplona.quartz.nip01Core.core.TagArray
+import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.descriptors.element
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonEncoder
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.int
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.long
+import kotlinx.serialization.json.put
+
+object EventTemplateKSerializer : KSerializer> {
+ private val emptyTagArray = emptyArray>()
+
+ override val descriptor: SerialDescriptor =
+ buildClassSerialDescriptor("EventTemplate") {
+ element("created_at")
+ element("kind")
+ element("tags", TagArrayKSerializer.descriptor)
+ element("content")
+ }
+
+ override fun serialize(
+ encoder: Encoder,
+ value: EventTemplate,
+ ) {
+ val jsonEncoder = encoder as JsonEncoder
+ val element =
+ buildJsonObject {
+ put("created_at", value.createdAt)
+ put("kind", value.kind)
+ put("tags", TagArrayKSerializer.serializeToElement(value.tags))
+ put("content", value.content)
+ }
+ jsonEncoder.encodeJsonElement(element)
+ }
+
+ override fun deserialize(decoder: Decoder): EventTemplate {
+ val jsonDecoder = decoder as JsonDecoder
+ val jsonObject = jsonDecoder.decodeJsonElement().jsonObject
+
+ var createdAt = 0L
+ var kind = 0
+ var tags: TagArray = emptyTagArray
+ var content = ""
+
+ for ((key, value) in jsonObject) {
+ when (key) {
+ "created_at" -> createdAt = value.jsonPrimitive.long
+ "kind" -> kind = value.jsonPrimitive.int
+ "tags" -> tags = TagArrayKSerializer.deserializeFromElement(value)
+ "content" -> content = value.jsonPrimitive.content
+ }
+ }
+
+ return EventTemplate(createdAt, kind, tags, content)
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/FilterKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/FilterKSerializer.kt
new file mode 100644
index 0000000000..0ce754e0f2
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/FilterKSerializer.kt
@@ -0,0 +1,139 @@
+/*
+ * 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.nip01Core.kotlinSerialization
+
+import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonEncoder
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.buildJsonArray
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.intOrNull
+import kotlinx.serialization.json.jsonArray
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.longOrNull
+import kotlinx.serialization.json.put
+
+object FilterKSerializer : KSerializer {
+ override val descriptor: SerialDescriptor =
+ buildClassSerialDescriptor("Filter")
+
+ override fun serialize(
+ encoder: Encoder,
+ value: Filter,
+ ) {
+ val jsonEncoder = encoder as JsonEncoder
+ jsonEncoder.encodeJsonElement(serializeToElement(value))
+ }
+
+ fun serializeToElement(filter: Filter): JsonObject =
+ buildJsonObject {
+ filter.kinds?.let { kinds ->
+ put(
+ "kinds",
+ buildJsonArray {
+ for (k in kinds) add(JsonPrimitive(k))
+ },
+ )
+ }
+ filter.ids?.let { ids ->
+ put(
+ "ids",
+ buildJsonArray {
+ for (id in ids) add(JsonPrimitive(id))
+ },
+ )
+ }
+ filter.authors?.let { authors ->
+ put(
+ "authors",
+ buildJsonArray {
+ for (a in authors) add(JsonPrimitive(a))
+ },
+ )
+ }
+ filter.tags?.let { tags ->
+ for ((key, values) in tags) {
+ put(
+ "#$key",
+ buildJsonArray {
+ for (v in values) add(JsonPrimitive(v))
+ },
+ )
+ }
+ }
+ filter.tagsAll?.let { tagsAll ->
+ for ((key, values) in tagsAll) {
+ put(
+ "&$key",
+ buildJsonArray {
+ for (v in values) add(JsonPrimitive(v))
+ },
+ )
+ }
+ }
+ filter.since?.let { put("since", it) }
+ filter.until?.let { put("until", it) }
+ filter.limit?.let { put("limit", it) }
+ filter.search?.let { put("search", it) }
+ }
+
+ override fun deserialize(decoder: Decoder): Filter {
+ val jsonDecoder = decoder as JsonDecoder
+ return deserializeFromElement(jsonDecoder.decodeJsonElement().jsonObject)
+ }
+
+ fun deserializeFromElement(jsonObject: JsonObject): Filter {
+ val tags = mutableMapOf>()
+ val tagsAll = mutableMapOf>()
+
+ for ((key, value) in jsonObject) {
+ when {
+ key.startsWith("#") -> {
+ tags[key.substring(1)] = value.jsonArray.mapNotNull { it.jsonPrimitive.content }
+ }
+
+ key.startsWith("&") -> {
+ tagsAll[key.substring(1)] = value.jsonArray.mapNotNull { it.jsonPrimitive.content }
+ }
+ }
+ }
+
+ return Filter(
+ ids = jsonObject["ids"]?.jsonArray?.mapNotNull { it.jsonPrimitive.content },
+ authors = jsonObject["authors"]?.jsonArray?.mapNotNull { it.jsonPrimitive.content },
+ kinds = jsonObject["kinds"]?.jsonArray?.mapNotNull { it.jsonPrimitive.intOrNull },
+ tags = tags.ifEmpty { null },
+ tagsAll = tagsAll.ifEmpty { null },
+ since = jsonObject["since"]?.jsonPrimitive?.longOrNull,
+ until = jsonObject["until"]?.jsonPrimitive?.longOrNull,
+ limit = jsonObject["limit"]?.jsonPrimitive?.intOrNull,
+ search = jsonObject["search"]?.jsonPrimitive?.content,
+ )
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt
new file mode 100644
index 0000000000..d5b79dbf47
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt
@@ -0,0 +1,136 @@
+/*
+ * 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.nip01Core.kotlinSerialization
+
+import com.vitorpamplona.quartz.nip01Core.core.Event
+import com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable
+import com.vitorpamplona.quartz.nip01Core.core.TagArray
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command
+import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
+import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerMessage
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
+import com.vitorpamplona.quartz.nip46RemoteSigner.kotlinSerialization.BunkerMessageKSerializer
+import com.vitorpamplona.quartz.nip46RemoteSigner.kotlinSerialization.BunkerRequestKSerializer
+import com.vitorpamplona.quartz.nip46RemoteSigner.kotlinSerialization.BunkerResponseKSerializer
+import com.vitorpamplona.quartz.nip47WalletConnect.Notification
+import com.vitorpamplona.quartz.nip47WalletConnect.Request
+import com.vitorpamplona.quartz.nip47WalletConnect.Response
+import com.vitorpamplona.quartz.nip47WalletConnect.kotlinSerialization.Nip47NotificationKSerializer
+import com.vitorpamplona.quartz.nip47WalletConnect.kotlinSerialization.Nip47RequestKSerializer
+import com.vitorpamplona.quartz.nip47WalletConnect.kotlinSerialization.Nip47ResponseKSerializer
+import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
+import com.vitorpamplona.quartz.nip59Giftwrap.rumors.kotlinSerialization.RumorKSerializer
+import kotlinx.serialization.json.Json
+
+class KotlinSerializationMapper {
+ companion object {
+ val json =
+ Json {
+ ignoreUnknownKeys = true
+ isLenient = true
+ encodeDefaults = false
+ }
+
+ fun fromJson(jsonStr: String): Event = json.decodeFromString(EventKSerializer, jsonStr)
+
+ fun fromJsonToMessage(jsonStr: String): Message = json.decodeFromString(MessageKSerializer, jsonStr)
+
+ fun fromJsonToCommand(jsonStr: String): Command = json.decodeFromString(CommandKSerializer, jsonStr)
+
+ fun fromJsonToTagArray(jsonStr: String): TagArray = json.decodeFromString(TagArrayKSerializer, jsonStr)
+
+ fun fromJsonToRumor(jsonStr: String): Rumor = json.decodeFromString(RumorKSerializer, jsonStr)
+
+ fun fromJsonToEventTemplate(jsonStr: String): EventTemplate = json.decodeFromString(EventTemplateKSerializer, jsonStr)
+
+ fun toJson(event: Event): String = json.encodeToString(EventKSerializer, event)
+
+ fun toJson(tags: TagArray): String = json.encodeToString(TagArrayKSerializer, tags)
+
+ fun toJson(value: OptimizedSerializable): String =
+ when (value) {
+ is Event -> {
+ json.encodeToString(EventKSerializer, value)
+ }
+
+ is Filter -> {
+ json.encodeToString(FilterKSerializer, value)
+ }
+
+ is Rumor -> {
+ json.encodeToString(RumorKSerializer, value)
+ }
+
+ is EventTemplate<*> -> {
+ @Suppress("UNCHECKED_CAST")
+ json.encodeToString(EventTemplateKSerializer, value as EventTemplate)
+ }
+
+ is Message -> {
+ json.encodeToString(MessageKSerializer, value)
+ }
+
+ is Command -> {
+ json.encodeToString(CommandKSerializer, value)
+ }
+
+ is BunkerRequest -> {
+ json.encodeToString(BunkerRequestKSerializer, value)
+ }
+
+ is BunkerResponse -> {
+ json.encodeToString(BunkerResponseKSerializer, value)
+ }
+
+ is BunkerMessage -> {
+ json.encodeToString(BunkerMessageKSerializer, value)
+ }
+
+ else -> {
+ throw IllegalArgumentException("Unsupported type: ${value::class}")
+ }
+ }
+
+ inline fun fromJsonTo(jsonStr: String): T {
+ val result: Any =
+ when (T::class) {
+ Event::class -> fromJson(jsonStr)
+ Filter::class -> json.decodeFromString(FilterKSerializer, jsonStr)
+ Rumor::class -> fromJsonToRumor(jsonStr)
+ EventTemplate::class -> fromJsonToEventTemplate(jsonStr)
+ Message::class -> fromJsonToMessage(jsonStr)
+ Command::class -> fromJsonToCommand(jsonStr)
+ BunkerRequest::class -> json.decodeFromString(BunkerRequestKSerializer, jsonStr)
+ BunkerResponse::class -> json.decodeFromString(BunkerResponseKSerializer, jsonStr)
+ BunkerMessage::class -> json.decodeFromString(BunkerMessageKSerializer, jsonStr)
+ Response::class -> json.decodeFromString(Nip47ResponseKSerializer, jsonStr)
+ Request::class -> json.decodeFromString(Nip47RequestKSerializer, jsonStr)
+ Notification::class -> json.decodeFromString(Nip47NotificationKSerializer, jsonStr)
+ else -> throw IllegalArgumentException("Unsupported type: ${T::class}")
+ }
+ @Suppress("UNCHECKED_CAST")
+ return result as T
+ }
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/MessageKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/MessageKSerializer.kt
new file mode 100644
index 0000000000..964b77d35a
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/MessageKSerializer.kt
@@ -0,0 +1,152 @@
+/*
+ * 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.nip01Core.kotlinSerialization
+
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.AuthMessage
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ClosedMessage
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountMessage
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EoseMessage
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NoticeMessage
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NotifyMessage
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonEncoder
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.boolean
+import kotlinx.serialization.json.buildJsonArray
+import kotlinx.serialization.json.jsonArray
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+
+object MessageKSerializer : KSerializer {
+ override val descriptor: SerialDescriptor =
+ buildClassSerialDescriptor("Message")
+
+ override fun serialize(
+ encoder: Encoder,
+ value: Message,
+ ) {
+ val jsonEncoder = encoder as JsonEncoder
+ val element =
+ buildJsonArray {
+ add(JsonPrimitive(value.label()))
+ when (value) {
+ is EventMessage -> {
+ add(JsonPrimitive(value.subId))
+ add(EventKSerializer.serializeToElement(value.event))
+ }
+
+ is NoticeMessage -> {
+ add(JsonPrimitive(value.message))
+ }
+
+ is OkMessage -> {
+ add(JsonPrimitive(value.eventId))
+ // Jackson writes success as a string, not boolean
+ add(JsonPrimitive(value.success.toString()))
+ if (value.message.isNotBlank()) {
+ add(JsonPrimitive(value.message))
+ }
+ }
+
+ is AuthMessage -> {
+ add(JsonPrimitive(value.challenge))
+ }
+
+ is NotifyMessage -> {
+ add(JsonPrimitive(value.message))
+ }
+
+ is ClosedMessage -> {
+ add(JsonPrimitive(value.subId))
+ add(JsonPrimitive(value.message))
+ }
+
+ is CountMessage -> {
+ add(CountResultKSerializer.serializeToElement(value.result))
+ }
+ }
+ }
+ jsonEncoder.encodeJsonElement(element)
+ }
+
+ override fun deserialize(decoder: Decoder): Message {
+ val jsonDecoder = decoder as JsonDecoder
+ val array = jsonDecoder.decodeJsonElement().jsonArray
+ val type = array[0].jsonPrimitive.content
+
+ return when (type) {
+ EventMessage.LABEL -> {
+ val subId = array[1].jsonPrimitive.content
+ val event = EventKSerializer.deserializeFromElement(array[2].jsonObject)
+ EventMessage(subId, event)
+ }
+
+ EoseMessage.LABEL -> {
+ EoseMessage(array[1].jsonPrimitive.content)
+ }
+
+ NoticeMessage.LABEL -> {
+ NoticeMessage(array[1].jsonPrimitive.content)
+ }
+
+ OkMessage.LABEL -> {
+ OkMessage(
+ eventId = array[1].jsonPrimitive.content,
+ success = array[2].jsonPrimitive.boolean,
+ message = if (array.size > 3) array[3].jsonPrimitive.content else "",
+ )
+ }
+
+ AuthMessage.LABEL -> {
+ AuthMessage(array[1].jsonPrimitive.content)
+ }
+
+ NotifyMessage.LABEL -> {
+ NotifyMessage(array[1].jsonPrimitive.content)
+ }
+
+ ClosedMessage.LABEL -> {
+ ClosedMessage(
+ subId = array[1].jsonPrimitive.content,
+ message = if (array.size > 2) array[2].jsonPrimitive.content else "",
+ )
+ }
+
+ CountMessage.LABEL -> {
+ val queryId = array[1].jsonPrimitive.content
+ val result = CountResultKSerializer.deserializeFromElement(array[2].jsonObject)
+ CountMessage(queryId, result)
+ }
+
+ else -> {
+ throw IllegalArgumentException("Message $type is not supported")
+ }
+ }
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/TagArrayKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/TagArrayKSerializer.kt
new file mode 100644
index 0000000000..c9ebbe5eaf
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/TagArrayKSerializer.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.nip01Core.kotlinSerialization
+
+import com.vitorpamplona.quartz.nip01Core.core.TagArray
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.builtins.ListSerializer
+import kotlinx.serialization.builtins.serializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonEncoder
+import kotlinx.serialization.json.JsonNull
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.buildJsonArray
+import kotlinx.serialization.json.jsonArray
+import kotlinx.serialization.json.jsonPrimitive
+
+object TagArrayKSerializer : KSerializer {
+ override val descriptor: SerialDescriptor =
+ ListSerializer(ListSerializer(String.serializer())).descriptor
+
+ override fun serialize(
+ encoder: Encoder,
+ value: TagArray,
+ ) {
+ val jsonEncoder = encoder as JsonEncoder
+ val element =
+ buildJsonArray {
+ for (tag in value) {
+ add(
+ buildJsonArray {
+ for (s in tag) {
+ add(JsonPrimitive(s))
+ }
+ },
+ )
+ }
+ }
+ jsonEncoder.encodeJsonElement(element)
+ }
+
+ override fun deserialize(decoder: Decoder): TagArray {
+ val jsonDecoder = decoder as JsonDecoder
+ return deserializeFromElement(jsonDecoder.decodeJsonElement())
+ }
+
+ fun deserializeFromElement(element: JsonElement): TagArray {
+ val array = element.jsonArray
+ val outerList = ArrayList>(array.size)
+ for (inner in array) {
+ val innerArray = inner.jsonArray
+ val innerList = ArrayList(innerArray.size.coerceAtLeast(5))
+ for (s in innerArray) {
+ if (s is JsonNull) {
+ innerList.add("")
+ } else {
+ innerList.add(s.jsonPrimitive.content)
+ }
+ }
+ outerList.add(innerList.toTypedArray())
+ }
+ return outerList.toTypedArray()
+ }
+
+ fun serializeToElement(value: TagArray): JsonArray =
+ buildJsonArray {
+ for (tag in value) {
+ add(
+ buildJsonArray {
+ for (s in tag) {
+ add(JsonPrimitive(s))
+ }
+ },
+ )
+ }
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerMessageKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerMessageKSerializer.kt
new file mode 100644
index 0000000000..025e586cc4
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerMessageKSerializer.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.nip46RemoteSigner.kotlinSerialization
+
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerMessage
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonEncoder
+import kotlinx.serialization.json.jsonArray
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+
+object BunkerMessageKSerializer : KSerializer {
+ override val descriptor: SerialDescriptor =
+ buildClassSerialDescriptor("BunkerMessage")
+
+ override fun serialize(
+ encoder: Encoder,
+ value: BunkerMessage,
+ ) {
+ val jsonEncoder = encoder as JsonEncoder
+ when (value) {
+ is BunkerRequest -> BunkerRequestKSerializer.serialize(jsonEncoder, value)
+ is BunkerResponse -> BunkerResponseKSerializer.serialize(jsonEncoder, value)
+ else -> throw IllegalArgumentException("Unknown BunkerMessage type")
+ }
+ }
+
+ override fun deserialize(decoder: Decoder): BunkerMessage {
+ val jsonDecoder = decoder as JsonDecoder
+ val jsonObject = jsonDecoder.decodeJsonElement().jsonObject
+ val isRequest = jsonObject.containsKey("method")
+
+ return if (isRequest) {
+ val id = jsonObject["id"]!!.jsonPrimitive.content
+ val method = jsonObject["method"]!!.jsonPrimitive.content
+ val params =
+ jsonObject["params"]?.jsonArray?.map { it.jsonPrimitive.content }?.toTypedArray()
+ ?: emptyArray()
+ dispatchBunkerRequest(id, method, params)
+ } else {
+ BunkerResponseKSerializer.deserializeFromElement(jsonObject)
+ }
+ }
+
+ private fun dispatchBunkerRequest(
+ id: String,
+ method: String,
+ params: Array,
+ ): BunkerRequest =
+ when (method) {
+ com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect.METHOD_NAME -> {
+ com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect
+ .parse(id, params)
+ }
+
+ com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetPublicKey.METHOD_NAME -> {
+ com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetPublicKey
+ .parse(id, params)
+ }
+
+ com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetRelays.METHOD_NAME -> {
+ com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetRelays
+ .parse(id, params)
+ }
+
+ com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Decrypt.METHOD_NAME -> {
+ com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Decrypt
+ .parse(id, params)
+ }
+
+ com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Encrypt.METHOD_NAME -> {
+ com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Encrypt
+ .parse(id, params)
+ }
+
+ com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Decrypt.METHOD_NAME -> {
+ com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Decrypt
+ .parse(id, params)
+ }
+
+ com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Encrypt.METHOD_NAME -> {
+ com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Encrypt
+ .parse(id, params)
+ }
+
+ com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestPing.METHOD_NAME -> {
+ com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestPing
+ .parse(id, params)
+ }
+
+ com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign.METHOD_NAME -> {
+ com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign
+ .parse(id, params)
+ }
+
+ else -> {
+ BunkerRequest(id, method, params)
+ }
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerRequestKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerRequestKSerializer.kt
new file mode 100644
index 0000000000..7797cf3371
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerRequestKSerializer.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.nip46RemoteSigner.kotlinSerialization
+
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetPublicKey
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetRelays
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Decrypt
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Encrypt
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Decrypt
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Encrypt
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestPing
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.descriptors.element
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonEncoder
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.buildJsonArray
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.jsonArray
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.put
+
+object BunkerRequestKSerializer : KSerializer {
+ override val descriptor: SerialDescriptor =
+ buildClassSerialDescriptor("BunkerRequest") {
+ element("id")
+ element("method")
+ element>("params")
+ }
+
+ override fun serialize(
+ encoder: Encoder,
+ value: BunkerRequest,
+ ) {
+ val jsonEncoder = encoder as JsonEncoder
+ val element =
+ buildJsonObject {
+ put("id", value.id)
+ put("method", value.method)
+ put(
+ "params",
+ buildJsonArray {
+ for (p in value.params) {
+ add(JsonPrimitive(p))
+ }
+ },
+ )
+ }
+ jsonEncoder.encodeJsonElement(element)
+ }
+
+ override fun deserialize(decoder: Decoder): BunkerRequest {
+ val jsonDecoder = decoder as JsonDecoder
+ val jsonObject = jsonDecoder.decodeJsonElement().jsonObject
+ val id = jsonObject["id"]!!.jsonPrimitive.content
+ val method = jsonObject["method"]!!.jsonPrimitive.content
+ val params =
+ jsonObject["params"]?.jsonArray?.map { it.jsonPrimitive.content }?.toTypedArray()
+ ?: emptyArray()
+
+ return when (method) {
+ BunkerRequestConnect.METHOD_NAME -> BunkerRequestConnect.parse(id, params)
+ BunkerRequestGetPublicKey.METHOD_NAME -> BunkerRequestGetPublicKey.parse(id, params)
+ BunkerRequestGetRelays.METHOD_NAME -> BunkerRequestGetRelays.parse(id, params)
+ BunkerRequestNip04Decrypt.METHOD_NAME -> BunkerRequestNip04Decrypt.parse(id, params)
+ BunkerRequestNip04Encrypt.METHOD_NAME -> BunkerRequestNip04Encrypt.parse(id, params)
+ BunkerRequestNip44Decrypt.METHOD_NAME -> BunkerRequestNip44Decrypt.parse(id, params)
+ BunkerRequestNip44Encrypt.METHOD_NAME -> BunkerRequestNip44Encrypt.parse(id, params)
+ BunkerRequestPing.METHOD_NAME -> BunkerRequestPing.parse(id, params)
+ BunkerRequestSign.METHOD_NAME -> BunkerRequestSign.parse(id, params)
+ else -> BunkerRequest(id, method, params)
+ }
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerResponseKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerResponseKSerializer.kt
new file mode 100644
index 0000000000..9787b10eb3
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerResponseKSerializer.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.nip46RemoteSigner.kotlinSerialization
+
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseAck
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEvent
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseGetRelays
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePong
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePublicKey
+import com.vitorpamplona.quartz.utils.Hex
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.descriptors.element
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonEncoder
+import kotlinx.serialization.json.JsonNull
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.put
+
+object BunkerResponseKSerializer : KSerializer {
+ override val descriptor: SerialDescriptor =
+ buildClassSerialDescriptor("BunkerResponse") {
+ element("id")
+ element("result")
+ element("error")
+ }
+
+ override fun serialize(
+ encoder: Encoder,
+ value: BunkerResponse,
+ ) {
+ val jsonEncoder = encoder as JsonEncoder
+ val element =
+ buildJsonObject {
+ put("id", value.id)
+ value.result?.let { put("result", it) }
+ value.error?.let { put("error", it) }
+ }
+ jsonEncoder.encodeJsonElement(element)
+ }
+
+ override fun deserialize(decoder: Decoder): BunkerResponse {
+ val jsonDecoder = decoder as JsonDecoder
+ return deserializeFromElement(jsonDecoder.decodeJsonElement().jsonObject)
+ }
+
+ fun deserializeFromElement(jsonObject: JsonObject): BunkerResponse {
+ val id = jsonObject["id"]!!.jsonPrimitive.content
+ val result = jsonObject["result"]?.let { if (it is JsonNull) null else it.jsonPrimitive.content }
+ val error = jsonObject["error"]?.let { if (it is JsonNull) null else it.jsonPrimitive.content }
+
+ if (error != null) {
+ return BunkerResponseError.parse(id, result, error)
+ }
+
+ if (result != null) {
+ when (result) {
+ BunkerResponseAck.RESULT -> {
+ return BunkerResponseAck.parse(id, result, error)
+ }
+
+ BunkerResponsePong.RESULT -> {
+ return BunkerResponsePong.parse(id, result, error)
+ }
+
+ else -> {
+ if (result.length == 64 && Hex.isHex(result)) {
+ return BunkerResponsePublicKey.parse(id, result)
+ }
+
+ if (result.isNotEmpty() && result[0] == '{') {
+ try {
+ return BunkerResponseEvent.parse(id, result)
+ } catch (_: Exception) {
+ }
+
+ try {
+ return BunkerResponseGetRelays.parse(id, result)
+ } catch (_: Exception) {
+ }
+ }
+
+ return BunkerResponse(id, result, error)
+ }
+ }
+ }
+
+ return BunkerResponse(id, result, error)
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47NotificationKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47NotificationKSerializer.kt
new file mode 100644
index 0000000000..811e46fe44
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47NotificationKSerializer.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.nip47WalletConnect.kotlinSerialization
+
+import com.vitorpamplona.quartz.nip47WalletConnect.HoldInvoiceAcceptedData
+import com.vitorpamplona.quartz.nip47WalletConnect.HoldInvoiceAcceptedNotification
+import com.vitorpamplona.quartz.nip47WalletConnect.Notification
+import com.vitorpamplona.quartz.nip47WalletConnect.NwcNotificationType
+import com.vitorpamplona.quartz.nip47WalletConnect.PaymentReceivedNotification
+import com.vitorpamplona.quartz.nip47WalletConnect.PaymentSentNotification
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonNull
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.longOrNull
+
+object Nip47NotificationKSerializer : KSerializer {
+ override val descriptor: SerialDescriptor =
+ buildClassSerialDescriptor("Nip47Notification")
+
+ override fun serialize(
+ encoder: Encoder,
+ value: Notification,
+ ): Unit = throw UnsupportedOperationException("NIP-47 Notification serialization not supported")
+
+ override fun deserialize(decoder: Decoder): Notification {
+ val jsonDecoder = decoder as JsonDecoder
+ val jsonObject = jsonDecoder.decodeJsonElement().jsonObject
+ val notificationType =
+ jsonObject["notification_type"]?.let { if (it is JsonNull) null else it.jsonPrimitive.content }
+
+ return when (notificationType) {
+ NwcNotificationType.PAYMENT_RECEIVED -> {
+ PaymentReceivedNotification(
+ notification =
+ Nip47ResponseKSerializer.parseTransaction(
+ jsonObject["notification"]?.jsonObject,
+ ),
+ )
+ }
+
+ NwcNotificationType.PAYMENT_SENT -> {
+ PaymentSentNotification(
+ notification =
+ Nip47ResponseKSerializer.parseTransaction(
+ jsonObject["notification"]?.jsonObject,
+ ),
+ )
+ }
+
+ NwcNotificationType.HOLD_INVOICE_ACCEPTED -> {
+ val notifObj = jsonObject["notification"]?.jsonObject
+ HoldInvoiceAcceptedNotification(
+ notification =
+ notifObj?.let {
+ HoldInvoiceAcceptedData(
+ type = it["type"]?.jsonPrimitive?.content,
+ invoice = it["invoice"]?.jsonPrimitive?.content,
+ payment_hash = it["payment_hash"]?.jsonPrimitive?.content,
+ amount = it["amount"]?.jsonPrimitive?.longOrNull,
+ created_at = it["created_at"]?.jsonPrimitive?.longOrNull,
+ expires_at = it["expires_at"]?.jsonPrimitive?.longOrNull,
+ settle_deadline = it["settle_deadline"]?.jsonPrimitive?.longOrNull,
+ )
+ },
+ )
+ }
+
+ else -> {
+ throw IllegalArgumentException("Unknown notification type: $notificationType")
+ }
+ }
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47RequestKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47RequestKSerializer.kt
new file mode 100644
index 0000000000..691627409c
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47RequestKSerializer.kt
@@ -0,0 +1,239 @@
+/*
+ * 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.nip47WalletConnect.kotlinSerialization
+
+import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceMethod
+import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceParams
+import com.vitorpamplona.quartz.nip47WalletConnect.CreateConnectionMethod
+import com.vitorpamplona.quartz.nip47WalletConnect.CreateConnectionParams
+import com.vitorpamplona.quartz.nip47WalletConnect.GetBalanceMethod
+import com.vitorpamplona.quartz.nip47WalletConnect.GetBudgetMethod
+import com.vitorpamplona.quartz.nip47WalletConnect.GetInfoMethod
+import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsMethod
+import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsParams
+import com.vitorpamplona.quartz.nip47WalletConnect.LookupInvoiceMethod
+import com.vitorpamplona.quartz.nip47WalletConnect.LookupInvoiceParams
+import com.vitorpamplona.quartz.nip47WalletConnect.MakeHoldInvoiceMethod
+import com.vitorpamplona.quartz.nip47WalletConnect.MakeHoldInvoiceParams
+import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceMethod
+import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceParams
+import com.vitorpamplona.quartz.nip47WalletConnect.NwcMethod
+import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod
+import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceParams
+import com.vitorpamplona.quartz.nip47WalletConnect.PayKeysendMethod
+import com.vitorpamplona.quartz.nip47WalletConnect.PayKeysendParams
+import com.vitorpamplona.quartz.nip47WalletConnect.Request
+import com.vitorpamplona.quartz.nip47WalletConnect.SettleHoldInvoiceMethod
+import com.vitorpamplona.quartz.nip47WalletConnect.SettleHoldInvoiceParams
+import com.vitorpamplona.quartz.nip47WalletConnect.SignMessageMethod
+import com.vitorpamplona.quartz.nip47WalletConnect.SignMessageParams
+import com.vitorpamplona.quartz.nip47WalletConnect.TlvRecord
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonNull
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.booleanOrNull
+import kotlinx.serialization.json.intOrNull
+import kotlinx.serialization.json.jsonArray
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.longOrNull
+
+object Nip47RequestKSerializer : KSerializer {
+ override val descriptor: SerialDescriptor =
+ buildClassSerialDescriptor("Nip47Request")
+
+ override fun serialize(
+ encoder: Encoder,
+ value: Request,
+ ): Unit = throw UnsupportedOperationException("NIP-47 Request serialization not supported")
+
+ override fun deserialize(decoder: Decoder): Request {
+ val jsonDecoder = decoder as JsonDecoder
+ val jsonObject = jsonDecoder.decodeJsonElement().jsonObject
+ val method = jsonObject["method"]?.let { if (it is JsonNull) null else it.jsonPrimitive.content }
+
+ return when (method) {
+ NwcMethod.PAY_INVOICE -> parsePayInvoice(jsonObject)
+ NwcMethod.PAY_KEYSEND -> parsePayKeysend(jsonObject)
+ NwcMethod.MAKE_INVOICE -> parseMakeInvoice(jsonObject)
+ NwcMethod.LOOKUP_INVOICE -> parseLookupInvoice(jsonObject)
+ NwcMethod.LIST_TRANSACTIONS -> parseListTransactions(jsonObject)
+ NwcMethod.GET_BALANCE -> GetBalanceMethod()
+ NwcMethod.GET_INFO -> GetInfoMethod()
+ NwcMethod.GET_BUDGET -> GetBudgetMethod()
+ NwcMethod.SIGN_MESSAGE -> parseSignMessage(jsonObject)
+ NwcMethod.CREATE_CONNECTION -> parseCreateConnection(jsonObject)
+ NwcMethod.MAKE_HOLD_INVOICE -> parseMakeHoldInvoice(jsonObject)
+ NwcMethod.CANCEL_HOLD_INVOICE -> parseCancelHoldInvoice(jsonObject)
+ NwcMethod.SETTLE_HOLD_INVOICE -> parseSettleHoldInvoice(jsonObject)
+ else -> throw IllegalArgumentException("Unknown NWC method: $method")
+ }
+ }
+
+ private fun parsePayInvoice(json: JsonObject): PayInvoiceMethod {
+ val params = json["params"]?.jsonObject
+ return PayInvoiceMethod(
+ params?.let {
+ PayInvoiceParams(
+ invoice = it["invoice"]?.jsonPrimitive?.content,
+ amount = it["amount"]?.jsonPrimitive?.longOrNull,
+ )
+ },
+ )
+ }
+
+ private fun parsePayKeysend(json: JsonObject): PayKeysendMethod {
+ val params = json["params"]?.jsonObject
+ return PayKeysendMethod(
+ params?.let {
+ PayKeysendParams(
+ amount = it["amount"]?.jsonPrimitive?.longOrNull,
+ pubkey = it["pubkey"]?.jsonPrimitive?.content,
+ preimage = it["preimage"]?.jsonPrimitive?.content,
+ tlv_records =
+ it["tlv_records"]?.jsonArray?.map { record ->
+ val obj = record.jsonObject
+ TlvRecord(
+ type = obj["type"]?.jsonPrimitive?.longOrNull,
+ value = obj["value"]?.jsonPrimitive?.content,
+ )
+ },
+ )
+ },
+ )
+ }
+
+ private fun parseMakeInvoice(json: JsonObject): MakeInvoiceMethod {
+ val params = json["params"]?.jsonObject
+ return MakeInvoiceMethod(
+ params?.let {
+ MakeInvoiceParams(
+ amount = it["amount"]?.jsonPrimitive?.longOrNull,
+ description = it["description"]?.jsonPrimitive?.content,
+ description_hash = it["description_hash"]?.jsonPrimitive?.content,
+ expiry = it["expiry"]?.jsonPrimitive?.longOrNull,
+ )
+ },
+ )
+ }
+
+ private fun parseLookupInvoice(json: JsonObject): LookupInvoiceMethod {
+ val params = json["params"]?.jsonObject
+ return LookupInvoiceMethod(
+ params?.let {
+ LookupInvoiceParams(
+ payment_hash = it["payment_hash"]?.jsonPrimitive?.content,
+ invoice = it["invoice"]?.jsonPrimitive?.content,
+ )
+ },
+ )
+ }
+
+ private fun parseListTransactions(json: JsonObject): ListTransactionsMethod {
+ val params = json["params"]?.jsonObject
+ return ListTransactionsMethod(
+ params?.let {
+ ListTransactionsParams(
+ from = it["from"]?.jsonPrimitive?.longOrNull,
+ until = it["until"]?.jsonPrimitive?.longOrNull,
+ limit = it["limit"]?.jsonPrimitive?.intOrNull,
+ offset = it["offset"]?.jsonPrimitive?.intOrNull,
+ unpaid = it["unpaid"]?.jsonPrimitive?.booleanOrNull,
+ unpaid_outgoing = it["unpaid_outgoing"]?.jsonPrimitive?.booleanOrNull,
+ unpaid_incoming = it["unpaid_incoming"]?.jsonPrimitive?.booleanOrNull,
+ type = it["type"]?.jsonPrimitive?.content,
+ )
+ },
+ )
+ }
+
+ private fun parseSignMessage(json: JsonObject): SignMessageMethod {
+ val params = json["params"]?.jsonObject
+ return SignMessageMethod(
+ params?.let {
+ SignMessageParams(
+ message = it["message"]?.jsonPrimitive?.content,
+ )
+ },
+ )
+ }
+
+ private fun parseCreateConnection(json: JsonObject): CreateConnectionMethod {
+ val params = json["params"]?.jsonObject
+ return CreateConnectionMethod(
+ params?.let {
+ CreateConnectionParams(
+ pubkey = it["pubkey"]?.jsonPrimitive?.content,
+ name = it["name"]?.jsonPrimitive?.content,
+ request_methods = it["request_methods"]?.jsonArray?.map { m -> m.jsonPrimitive.content },
+ notification_types = it["notification_types"]?.jsonArray?.map { n -> n.jsonPrimitive.content },
+ max_amount = it["max_amount"]?.jsonPrimitive?.longOrNull,
+ budget_renewal = it["budget_renewal"]?.jsonPrimitive?.content,
+ expires_at = it["expires_at"]?.jsonPrimitive?.longOrNull,
+ isolated = it["isolated"]?.jsonPrimitive?.booleanOrNull,
+ )
+ },
+ )
+ }
+
+ private fun parseMakeHoldInvoice(json: JsonObject): MakeHoldInvoiceMethod {
+ val params = json["params"]?.jsonObject
+ return MakeHoldInvoiceMethod(
+ params?.let {
+ MakeHoldInvoiceParams(
+ amount = it["amount"]?.jsonPrimitive?.longOrNull,
+ description = it["description"]?.jsonPrimitive?.content,
+ description_hash = it["description_hash"]?.jsonPrimitive?.content,
+ expiry = it["expiry"]?.jsonPrimitive?.longOrNull,
+ payment_hash = it["payment_hash"]?.jsonPrimitive?.content,
+ min_cltv_expiry_delta = it["min_cltv_expiry_delta"]?.jsonPrimitive?.intOrNull,
+ )
+ },
+ )
+ }
+
+ private fun parseCancelHoldInvoice(json: JsonObject): CancelHoldInvoiceMethod {
+ val params = json["params"]?.jsonObject
+ return CancelHoldInvoiceMethod(
+ params?.let {
+ CancelHoldInvoiceParams(
+ payment_hash = it["payment_hash"]?.jsonPrimitive?.content,
+ )
+ },
+ )
+ }
+
+ private fun parseSettleHoldInvoice(json: JsonObject): SettleHoldInvoiceMethod {
+ val params = json["params"]?.jsonObject
+ return SettleHoldInvoiceMethod(
+ params?.let {
+ SettleHoldInvoiceParams(
+ preimage = it["preimage"]?.jsonPrimitive?.content,
+ )
+ },
+ )
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt
new file mode 100644
index 0000000000..7c124de3cd
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt
@@ -0,0 +1,305 @@
+/*
+ * 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.nip47WalletConnect.kotlinSerialization
+
+import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceSuccessResponse
+import com.vitorpamplona.quartz.nip47WalletConnect.CreateConnectionSuccessResponse
+import com.vitorpamplona.quartz.nip47WalletConnect.GetBalanceSuccessResponse
+import com.vitorpamplona.quartz.nip47WalletConnect.GetBudgetSuccessResponse
+import com.vitorpamplona.quartz.nip47WalletConnect.GetInfoSuccessResponse
+import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsSuccessResponse
+import com.vitorpamplona.quartz.nip47WalletConnect.LookupInvoiceSuccessResponse
+import com.vitorpamplona.quartz.nip47WalletConnect.MakeHoldInvoiceSuccessResponse
+import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceSuccessResponse
+import com.vitorpamplona.quartz.nip47WalletConnect.NwcError
+import com.vitorpamplona.quartz.nip47WalletConnect.NwcErrorCode
+import com.vitorpamplona.quartz.nip47WalletConnect.NwcErrorResponse
+import com.vitorpamplona.quartz.nip47WalletConnect.NwcMethod
+import com.vitorpamplona.quartz.nip47WalletConnect.NwcTransaction
+import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse
+import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse
+import com.vitorpamplona.quartz.nip47WalletConnect.PayKeysendSuccessResponse
+import com.vitorpamplona.quartz.nip47WalletConnect.Response
+import com.vitorpamplona.quartz.nip47WalletConnect.SettleHoldInvoiceSuccessResponse
+import com.vitorpamplona.quartz.nip47WalletConnect.SignMessageSuccessResponse
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonNull
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.jsonArray
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.longOrNull
+
+object Nip47ResponseKSerializer : KSerializer {
+ override val descriptor: SerialDescriptor =
+ buildClassSerialDescriptor("Nip47Response")
+
+ override fun serialize(
+ encoder: Encoder,
+ value: Response,
+ ): Unit = throw UnsupportedOperationException("NIP-47 Response serialization not supported")
+
+ override fun deserialize(decoder: Decoder): Response {
+ val jsonDecoder = decoder as JsonDecoder
+ val jsonObject = jsonDecoder.decodeJsonElement().jsonObject
+ val resultType = jsonObject["result_type"]?.let { if (it is JsonNull) null else it.jsonPrimitive.content }
+ val hasError = jsonObject["error"]?.let { it !is JsonNull } ?: false
+ val hasResult = jsonObject["result"]?.let { it !is JsonNull } ?: false
+
+ if (hasError) {
+ return when (resultType) {
+ NwcMethod.PAY_INVOICE -> {
+ parsePayInvoiceError(jsonObject)
+ }
+
+ else -> {
+ val error = jsonObject["error"]?.jsonObject?.let { parseNwcError(it) }
+ NwcErrorResponse(resultType ?: "", error)
+ }
+ }
+ }
+
+ if (hasResult || resultType != null) {
+ return when (resultType) {
+ NwcMethod.PAY_INVOICE -> {
+ parsePayInvoiceSuccess(jsonObject)
+ }
+
+ NwcMethod.PAY_KEYSEND -> {
+ parsePayKeysendSuccess(jsonObject)
+ }
+
+ NwcMethod.MAKE_INVOICE -> {
+ MakeInvoiceSuccessResponse(parseTransaction(jsonObject["result"]?.jsonObject))
+ }
+
+ NwcMethod.LOOKUP_INVOICE -> {
+ LookupInvoiceSuccessResponse(parseTransaction(jsonObject["result"]?.jsonObject))
+ }
+
+ NwcMethod.LIST_TRANSACTIONS -> {
+ parseListTransactionsSuccess(jsonObject)
+ }
+
+ NwcMethod.GET_BALANCE -> {
+ parseGetBalanceSuccess(jsonObject)
+ }
+
+ NwcMethod.GET_INFO -> {
+ parseGetInfoSuccess(jsonObject)
+ }
+
+ NwcMethod.GET_BUDGET -> {
+ parseGetBudgetSuccess(jsonObject)
+ }
+
+ NwcMethod.SIGN_MESSAGE -> {
+ parseSignMessageSuccess(jsonObject)
+ }
+
+ NwcMethod.CREATE_CONNECTION -> {
+ parseCreateConnectionSuccess(jsonObject)
+ }
+
+ NwcMethod.MAKE_HOLD_INVOICE -> {
+ MakeHoldInvoiceSuccessResponse(parseTransaction(jsonObject["result"]?.jsonObject))
+ }
+
+ NwcMethod.CANCEL_HOLD_INVOICE -> {
+ CancelHoldInvoiceSuccessResponse()
+ }
+
+ NwcMethod.SETTLE_HOLD_INVOICE -> {
+ SettleHoldInvoiceSuccessResponse()
+ }
+
+ else -> {
+ // backward compatibility: guess by result content
+ val resultObj = jsonObject["result"]?.jsonObject
+ if (resultObj?.containsKey("preimage") == true) {
+ return parsePayInvoiceSuccess(jsonObject)
+ }
+ throw IllegalArgumentException("Unknown NWC response type: $resultType")
+ }
+ }
+ }
+
+ throw IllegalArgumentException("NWC response has neither result nor error")
+ }
+
+ private fun parseNwcError(obj: JsonObject): NwcError {
+ val code =
+ obj["code"]?.jsonPrimitive?.content?.let { codeName ->
+ try {
+ NwcErrorCode.valueOf(codeName)
+ } catch (_: Exception) {
+ null
+ }
+ }
+ return NwcError(code, obj["message"]?.jsonPrimitive?.content)
+ }
+
+ fun parseTransaction(obj: JsonObject?): NwcTransaction? {
+ if (obj == null) return null
+ return NwcTransaction(
+ type = obj["type"]?.jsonPrimitive?.content,
+ state = obj["state"]?.jsonPrimitive?.content,
+ invoice = obj["invoice"]?.jsonPrimitive?.content,
+ description = obj["description"]?.jsonPrimitive?.content,
+ description_hash = obj["description_hash"]?.jsonPrimitive?.content,
+ preimage = obj["preimage"]?.jsonPrimitive?.content,
+ payment_hash = obj["payment_hash"]?.jsonPrimitive?.content,
+ amount = obj["amount"]?.jsonPrimitive?.longOrNull,
+ fees_paid = obj["fees_paid"]?.jsonPrimitive?.longOrNull,
+ created_at = obj["created_at"]?.jsonPrimitive?.longOrNull,
+ expires_at = obj["expires_at"]?.jsonPrimitive?.longOrNull,
+ settled_at = obj["settled_at"]?.jsonPrimitive?.longOrNull,
+ settle_deadline = obj["settle_deadline"]?.jsonPrimitive?.longOrNull,
+ )
+ }
+
+ private fun parsePayInvoiceSuccess(json: JsonObject): PayInvoiceSuccessResponse {
+ val result = json["result"]?.jsonObject
+ return PayInvoiceSuccessResponse(
+ result?.let {
+ PayInvoiceSuccessResponse.PayInvoiceResultParams(
+ preimage = it["preimage"]?.jsonPrimitive?.content,
+ fees_paid = it["fees_paid"]?.jsonPrimitive?.longOrNull,
+ )
+ },
+ )
+ }
+
+ private fun parsePayInvoiceError(json: JsonObject): PayInvoiceErrorResponse {
+ val error = json["error"]?.jsonObject
+ return PayInvoiceErrorResponse(
+ error?.let {
+ PayInvoiceErrorResponse.PayInvoiceErrorParams(
+ code =
+ it["code"]?.jsonPrimitive?.content?.let { codeName ->
+ try {
+ NwcErrorCode.valueOf(codeName)
+ } catch (_: Exception) {
+ null
+ }
+ },
+ message = it["message"]?.jsonPrimitive?.content,
+ )
+ },
+ )
+ }
+
+ private fun parsePayKeysendSuccess(json: JsonObject): PayKeysendSuccessResponse {
+ val result = json["result"]?.jsonObject
+ return PayKeysendSuccessResponse(
+ result?.let {
+ PayKeysendSuccessResponse.PayKeysendResult(
+ preimage = it["preimage"]?.jsonPrimitive?.content,
+ fees_paid = it["fees_paid"]?.jsonPrimitive?.longOrNull,
+ )
+ },
+ )
+ }
+
+ private fun parseListTransactionsSuccess(json: JsonObject): ListTransactionsSuccessResponse {
+ val result = json["result"]?.jsonObject
+ return ListTransactionsSuccessResponse(
+ result?.let {
+ ListTransactionsSuccessResponse.ListTransactionsResult(
+ transactions = it["transactions"]?.jsonArray?.mapNotNull { t -> parseTransaction(t.jsonObject) },
+ total_count = it["total_count"]?.jsonPrimitive?.longOrNull,
+ )
+ },
+ )
+ }
+
+ private fun parseGetBalanceSuccess(json: JsonObject): GetBalanceSuccessResponse {
+ val result = json["result"]?.jsonObject
+ return GetBalanceSuccessResponse(
+ result?.let {
+ GetBalanceSuccessResponse.GetBalanceResult(
+ balance = it["balance"]?.jsonPrimitive?.longOrNull,
+ )
+ },
+ )
+ }
+
+ private fun parseGetInfoSuccess(json: JsonObject): GetInfoSuccessResponse {
+ val result = json["result"]?.jsonObject
+ return GetInfoSuccessResponse(
+ result?.let {
+ GetInfoSuccessResponse.GetInfoResult(
+ alias = it["alias"]?.jsonPrimitive?.content,
+ color = it["color"]?.jsonPrimitive?.content,
+ pubkey = it["pubkey"]?.jsonPrimitive?.content,
+ network = it["network"]?.jsonPrimitive?.content,
+ block_height = it["block_height"]?.jsonPrimitive?.longOrNull,
+ block_hash = it["block_hash"]?.jsonPrimitive?.content,
+ methods = it["methods"]?.jsonArray?.map { m -> m.jsonPrimitive.content },
+ notifications = it["notifications"]?.jsonArray?.map { n -> n.jsonPrimitive.content },
+ lud16 = it["lud16"]?.jsonPrimitive?.content,
+ )
+ },
+ )
+ }
+
+ private fun parseGetBudgetSuccess(json: JsonObject): GetBudgetSuccessResponse {
+ val result = json["result"]?.jsonObject
+ return GetBudgetSuccessResponse(
+ result?.let {
+ GetBudgetSuccessResponse.GetBudgetResult(
+ used_budget = it["used_budget"]?.jsonPrimitive?.longOrNull,
+ total_budget = it["total_budget"]?.jsonPrimitive?.longOrNull,
+ renews_at = it["renews_at"]?.jsonPrimitive?.longOrNull,
+ renewal_period = it["renewal_period"]?.jsonPrimitive?.content,
+ )
+ },
+ )
+ }
+
+ private fun parseSignMessageSuccess(json: JsonObject): SignMessageSuccessResponse {
+ val result = json["result"]?.jsonObject
+ return SignMessageSuccessResponse(
+ result?.let {
+ SignMessageSuccessResponse.SignMessageResult(
+ message = it["message"]?.jsonPrimitive?.content,
+ signature = it["signature"]?.jsonPrimitive?.content,
+ )
+ },
+ )
+ }
+
+ private fun parseCreateConnectionSuccess(json: JsonObject): CreateConnectionSuccessResponse {
+ val result = json["result"]?.jsonObject
+ return CreateConnectionSuccessResponse(
+ result?.let {
+ CreateConnectionSuccessResponse.CreateConnectionResult(
+ wallet_pubkey = it["wallet_pubkey"]?.jsonPrimitive?.content,
+ )
+ },
+ )
+ }
+}
diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/rumors/kotlinSerialization/RumorKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/rumors/kotlinSerialization/RumorKSerializer.kt
new file mode 100644
index 0000000000..f6fb5622a7
--- /dev/null
+++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/rumors/kotlinSerialization/RumorKSerializer.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.nip59Giftwrap.rumors.kotlinSerialization
+
+import com.vitorpamplona.quartz.nip01Core.core.HexKey
+import com.vitorpamplona.quartz.nip01Core.core.TagArray
+import com.vitorpamplona.quartz.nip01Core.kotlinSerialization.TagArrayKSerializer
+import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.descriptors.element
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonEncoder
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.int
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.long
+import kotlinx.serialization.json.put
+
+object RumorKSerializer : KSerializer {
+ override val descriptor: SerialDescriptor =
+ buildClassSerialDescriptor("Rumor") {
+ element("id")
+ element("pubkey")
+ element("created_at")
+ element("kind")
+ element("tags", TagArrayKSerializer.descriptor)
+ element("content")
+ }
+
+ override fun serialize(
+ encoder: Encoder,
+ value: Rumor,
+ ) {
+ val jsonEncoder = encoder as JsonEncoder
+ val element =
+ buildJsonObject {
+ value.id?.let { put("id", it) }
+ value.pubKey?.let { put("pubkey", it) }
+ value.createdAt?.let { put("created_at", it) }
+ value.kind?.let { put("kind", it) }
+ value.tags?.let { put("tags", TagArrayKSerializer.serializeToElement(it)) }
+ value.content?.let { put("content", it) }
+ }
+ jsonEncoder.encodeJsonElement(element)
+ }
+
+ override fun deserialize(decoder: Decoder): Rumor {
+ val jsonDecoder = decoder as JsonDecoder
+ val jsonObject = jsonDecoder.decodeJsonElement().jsonObject
+
+ var id: HexKey? = null
+ var pubKey: HexKey? = null
+ var createdAt: Long? = null
+ var kind: Int? = null
+ var tags: TagArray? = null
+ var content: String? = null
+
+ for ((key, value) in jsonObject) {
+ when (key) {
+ "id" -> id = value.jsonPrimitive.content
+ "pubkey" -> pubKey = value.jsonPrimitive.content
+ "created_at" -> createdAt = value.jsonPrimitive.long
+ "kind" -> kind = value.jsonPrimitive.int
+ "tags" -> tags = TagArrayKSerializer.deserializeFromElement(value)
+ "content" -> content = value.jsonPrimitive.content
+ }
+ }
+
+ return Rumor(id, pubKey, createdAt, kind, tags, content)
+ }
+}
diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEventTest.kt
index bc5af24320..271c2783b2 100644
--- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEventTest.kt
+++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEventTest.kt
@@ -21,6 +21,7 @@
package com.vitorpamplona.quartz.nip47WalletConnect
import com.vitorpamplona.quartz.nip01Core.core.HexKey
+import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import kotlinx.coroutines.test.runTest
@@ -44,7 +45,7 @@ class LnZapPaymentRequestEventTest {
val walletKeyPair = KeyPair()
val clientSigner = NostrSignerInternal(clientKeyPair)
val walletServicePubkey: HexKey =
- walletKeyPair.pubKey.joinToString("") { "%02x".format(it) }
+ walletKeyPair.pubKey.toHexKey()
val event =
LnZapPaymentRequestEvent.create(
@@ -68,7 +69,7 @@ class LnZapPaymentRequestEventTest {
val walletKeyPair = KeyPair()
val clientSigner = NostrSignerInternal(clientKeyPair)
val walletServicePubkey: HexKey =
- walletKeyPair.pubKey.joinToString("") { "%02x".format(it) }
+ walletKeyPair.pubKey.toHexKey()
val request = GetBalanceMethod.create()
val event =
@@ -92,7 +93,7 @@ class LnZapPaymentRequestEventTest {
val walletKeyPair = KeyPair()
val clientSigner = NostrSignerInternal(clientKeyPair)
val walletServicePubkey: HexKey =
- walletKeyPair.pubKey.joinToString("") { "%02x".format(it) }
+ walletKeyPair.pubKey.toHexKey()
val request = GetBalanceMethod.create()
val event =
@@ -116,7 +117,7 @@ class LnZapPaymentRequestEventTest {
val clientSigner = NostrSignerInternal(clientKeyPair)
val walletSigner = NostrSignerInternal(walletKeyPair)
val walletServicePubkey: HexKey =
- walletKeyPair.pubKey.joinToString("") { "%02x".format(it) }
+ walletKeyPair.pubKey.toHexKey()
val event =
LnZapPaymentRequestEvent.create(
@@ -139,7 +140,7 @@ class LnZapPaymentRequestEventTest {
val clientSigner = NostrSignerInternal(clientKeyPair)
val walletSigner = NostrSignerInternal(walletKeyPair)
val walletServicePubkey: HexKey =
- walletKeyPair.pubKey.joinToString("") { "%02x".format(it) }
+ walletKeyPair.pubKey.toHexKey()
val request = MakeInvoiceMethod.create(5000L, "test payment")
val event =
@@ -163,7 +164,7 @@ class LnZapPaymentRequestEventTest {
val clientSigner = NostrSignerInternal(clientKeyPair)
val walletSigner = NostrSignerInternal(walletKeyPair)
val walletServicePubkey: HexKey =
- walletKeyPair.pubKey.joinToString("") { "%02x".format(it) }
+ walletKeyPair.pubKey.toHexKey()
val request = GetInfoMethod.create()
val event =
@@ -191,7 +192,7 @@ class LnZapPaymentRequestEventTest {
val walletSigner = NostrSignerInternal(walletKeyPair)
val otherSigner = NostrSignerInternal(otherKeyPair)
val walletServicePubkey: HexKey =
- walletKeyPair.pubKey.joinToString("") { "%02x".format(it) }
+ walletKeyPair.pubKey.toHexKey()
val event =
LnZapPaymentRequestEvent.create(
diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NotificationTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NotificationTest.kt
index b06d3710ca..39fd0f56b8 100644
--- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NotificationTest.kt
+++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NotificationTest.kt
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip47WalletConnect
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
import kotlin.test.Test
import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
import kotlin.test.assertIs
import kotlin.test.assertNotNull
import kotlin.test.assertNull
@@ -35,16 +36,16 @@ class NotificationTest {
val notification = OptimizedJsonMapper.fromJsonTo(json)
assertIs(notification)
assertNotNull(notification.notification)
- assertEquals("incoming", notification.notification?.type)
- assertEquals("lnbc50n1...", notification.notification?.invoice)
- assertEquals("coffee", notification.notification?.description)
- assertEquals("abc", notification.notification?.preimage)
- assertEquals("hash123", notification.notification?.payment_hash)
- assertEquals(5000L, notification.notification?.amount)
- assertEquals(10L, notification.notification?.fees_paid)
- assertEquals(1693876497L, notification.notification?.created_at)
- assertEquals(1694876497L, notification.notification?.expires_at)
- assertEquals(1694876500L, notification.notification?.settled_at)
+ assertEquals("incoming", notification.notification.type)
+ assertEquals("lnbc50n1...", notification.notification.invoice)
+ assertEquals("coffee", notification.notification.description)
+ assertEquals("abc", notification.notification.preimage)
+ assertEquals("hash123", notification.notification.payment_hash)
+ assertEquals(5000L, notification.notification.amount)
+ assertEquals(10L, notification.notification.fees_paid)
+ assertEquals(1693876497L, notification.notification.created_at)
+ assertEquals(1694876497L, notification.notification.expires_at)
+ assertEquals(1694876500L, notification.notification.settled_at)
}
@Test
@@ -54,11 +55,11 @@ class NotificationTest {
val notification = OptimizedJsonMapper.fromJsonTo(json)
assertIs(notification)
assertNotNull(notification.notification)
- assertEquals("outgoing", notification.notification?.type)
- assertEquals("lnbc100n1...", notification.notification?.invoice)
- assertEquals("def456", notification.notification?.preimage)
- assertEquals(10000L, notification.notification?.amount)
- assertEquals(50L, notification.notification?.fees_paid)
+ assertEquals("outgoing", notification.notification.type)
+ assertEquals("lnbc100n1...", notification.notification.invoice)
+ assertEquals("def456", notification.notification.preimage)
+ assertEquals(10000L, notification.notification.amount)
+ assertEquals(50L, notification.notification.fees_paid)
}
@Test
@@ -68,13 +69,13 @@ class NotificationTest {
val notification = OptimizedJsonMapper.fromJsonTo(json)
assertIs(notification)
assertNotNull(notification.notification)
- assertEquals("incoming", notification.notification?.type)
- assertEquals("lnbc200n1...", notification.notification?.invoice)
- assertEquals("hash789", notification.notification?.payment_hash)
- assertEquals(20000L, notification.notification?.amount)
- assertEquals(800000L, notification.notification?.settle_deadline)
- assertEquals(1693876497L, notification.notification?.created_at)
- assertEquals(1694876497L, notification.notification?.expires_at)
+ assertEquals("incoming", notification.notification.type)
+ assertEquals("lnbc200n1...", notification.notification.invoice)
+ assertEquals("hash789", notification.notification.payment_hash)
+ assertEquals(20000L, notification.notification.amount)
+ assertEquals(800000L, notification.notification.settle_deadline)
+ assertEquals(1693876497L, notification.notification.created_at)
+ assertEquals(1694876497L, notification.notification.expires_at)
}
@Test
@@ -89,9 +90,11 @@ class NotificationTest {
}
@Test
+ @Throws(IllegalArgumentException::class)
fun testUnknownNotificationTypeReturnsNull() {
val json = """{"notification_type":"unknown_type","notification":{}}"""
- val notification = OptimizedJsonMapper.fromJsonTo(json)
- assertNull(notification)
+ assertFailsWith {
+ OptimizedJsonMapper.fromJsonTo(json)
+ }
}
}
diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcInfoEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcInfoEventTest.kt
index 3a8b15ca8c..82d55cc645 100644
--- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcInfoEventTest.kt
+++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcInfoEventTest.kt
@@ -28,7 +28,7 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue
class NwcInfoEventTest {
- private val signer = DeterministicSigner("nsec1w4uucmeyyng0kegm7486r23sv4majkmvqsj6eypprq0xttxss55s5mgg9t".nsecToKeyPair())
+ private val signer = DeterministicSigner("nsec10g0wheggqn9dawlc0yuv6adnat6n09anr7eyykevw2dm8xa5fffs0wsdsr".nsecToKeyPair())
@Test
fun testBuildInfoEvent() {
diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/RequestTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/RequestTest.kt
index 21816b6473..5b43e7bf9f 100644
--- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/RequestTest.kt
+++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/RequestTest.kt
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip47WalletConnect
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
import kotlin.test.Test
import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
import kotlin.test.assertIs
import kotlin.test.assertNotNull
import kotlin.test.assertNull
@@ -396,7 +397,8 @@ class RequestTest {
@Test
fun testUnknownMethodReturnsNull() {
val json = """{"method":"unknown_method","params":{}}"""
- val request = OptimizedJsonMapper.fromJsonTo(json)
- assertNull(request)
+ assertFailsWith {
+ OptimizedJsonMapper.fromJsonTo(json)
+ }
}
}
diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.ios.kt
index d77753912b..05e0bbbc0b 100644
--- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.ios.kt
+++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.ios.kt
@@ -20,29 +20,66 @@
*/
package com.vitorpamplona.quartz.nip01Core.core
+import com.vitorpamplona.quartz.nip01Core.kotlinSerialization.KotlinSerializationMapper
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
+import kotlinx.serialization.SerializationException
actual object OptimizedJsonMapper {
- actual fun fromJson(json: String): Event = TODO("Not yet implemented")
+ actual fun fromJson(json: String): Event =
+ try {
+ KotlinSerializationMapper.fromJson(json)
+ } catch (e: SerializationException) {
+ throw IllegalArgumentException(e.message, e)
+ }
- actual fun toJson(event: Event): String = TODO("Not yet implemented")
+ actual fun toJson(event: Event): String = KotlinSerializationMapper.toJson(event)
- actual fun fromJsonToMessage(json: String): Message = TODO("Not yet implemented")
+ actual fun fromJsonToMessage(json: String): Message =
+ try {
+ KotlinSerializationMapper.fromJsonToMessage(json)
+ } catch (e: SerializationException) {
+ throw IllegalArgumentException(e.message, e)
+ }
- actual fun fromJsonToCommand(json: String): Command = TODO("Not yet implemented")
+ actual fun fromJsonToCommand(json: String): Command =
+ try {
+ KotlinSerializationMapper.fromJsonToCommand(json)
+ } catch (e: SerializationException) {
+ throw IllegalArgumentException(e.message, e)
+ }
- actual fun fromJsonToTagArray(json: String): Array> = TODO("Not yet implemented")
+ actual fun fromJsonToTagArray(json: String): Array> =
+ try {
+ KotlinSerializationMapper.fromJsonToTagArray(json)
+ } catch (e: SerializationException) {
+ throw IllegalArgumentException(e.message, e)
+ }
- actual fun fromJsonToEventTemplate(json: String): EventTemplate = TODO("Not yet implemented")
+ actual fun fromJsonToEventTemplate(json: String): EventTemplate =
+ try {
+ KotlinSerializationMapper.fromJsonToEventTemplate(json)
+ } catch (e: SerializationException) {
+ throw IllegalArgumentException(e.message, e)
+ }
- actual fun fromJsonToRumor(json: String): Rumor = TODO("Not yet implemented")
+ actual fun fromJsonToRumor(json: String): Rumor =
+ try {
+ KotlinSerializationMapper.fromJsonToRumor(json)
+ } catch (e: SerializationException) {
+ throw IllegalArgumentException(e.message, e)
+ }
- actual fun toJson(tags: Array>): String = TODO("Not yet implemented")
+ actual fun toJson(tags: Array>): String = KotlinSerializationMapper.toJson(tags)
- actual inline fun fromJsonTo(json: String): T = TODO("Not yet implemented")
+ actual inline fun fromJsonTo(json: String): T =
+ try {
+ KotlinSerializationMapper.fromJsonTo(json)
+ } catch (e: SerializationException) {
+ throw IllegalArgumentException(e.message, e)
+ }
- actual fun toJson(value: OptimizedSerializable): String = TODO("Not yet implemented")
+ actual fun toJson(value: OptimizedSerializable): String = KotlinSerializationMapper.toJson(value)
}
diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.jvmAndroid.kt
index 65af774218..5a501c070b 100644
--- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.jvmAndroid.kt
+++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.jvmAndroid.kt
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.quartz.nip01Core.core
+import com.fasterxml.jackson.databind.RuntimeJsonMappingException
import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command
@@ -78,6 +79,10 @@ actual object OptimizedJsonMapper {
JacksonMapper.fromJsonTo(json)
} catch (e: com.fasterxml.jackson.core.JsonParseException) {
throw IllegalArgumentException(e.message, e)
+ } catch (e: com.fasterxml.jackson.core.JsonProcessingException) {
+ throw IllegalArgumentException(e.message, e)
+ } catch (e: RuntimeJsonMappingException) {
+ throw IllegalArgumentException(e.message, e)
}
actual fun toJson(value: OptimizedSerializable): String = JacksonMapper.toJson(value)
diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/namecoin/ElectrumXClient.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/namecoin/ElectrumXClient.kt
index c56dd72b1b..b0ba1d6b7d 100644
--- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/namecoin/ElectrumXClient.kt
+++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/namecoin/ElectrumXClient.kt
@@ -20,6 +20,7 @@
*/
package com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin
+import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -277,7 +278,7 @@ class ElectrumXClient(
*/
private fun electrumScriptHash(script: ByteArray): String {
val digest = MessageDigest.getInstance("SHA-256").digest(script)
- return digest.reversedArray().joinToString("") { "%02x".format(it) }
+ return digest.reversedArray().toHexKey()
}
/**
diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt
new file mode 100644
index 0000000000..959b4de530
--- /dev/null
+++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt
@@ -0,0 +1,740 @@
+/*
+ * 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.nip01Core.kotlinSerialization
+
+import com.vitorpamplona.quartz.nip01Core.core.Event
+import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.AuthMessage
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ClosedMessage
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountMessage
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountResult
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EoseMessage
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NoticeMessage
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NotifyMessage
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CloseCmd
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd
+import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd
+import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
+import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest
+import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
+import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent
+import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
+import kotlin.test.Test
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+class KotlinSerializationMapperTest {
+ val tags =
+ arrayOf(
+ arrayOf("title", "Retro Computer Fans"),
+ arrayOf("d", "xmbspe8rddsq"),
+ arrayOf("image", "https://blog.johnnovak.net/2022/04/15/achieving-period-correct-graphics-in-personal-computer-emulators-part-1-the-amiga/img/dream-setup.jpg"),
+ arrayOf("p", "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da"),
+ arrayOf("p", "9a9a4aa0e43e57873380ab22e8a3df12f3c4cf5bb3a804c6e3fed0069a6e2740"),
+ arrayOf("p", "4f5dd82517b11088ce00f23d99f06fe8f3e2e45ecf47bc9c2f90f34d5c6f7382"),
+ arrayOf("p", "ac92102a2ecb873c488e0125354ef5a97075a16198668c360eda050007ed42cd"),
+ arrayOf("p", "47f54409a4620eb35208a3bc1b53555bf3d0656b246bf0471a93208e20672f6f"),
+ arrayOf("p", "2624911545afb7a2b440cf10f5c69308afa33aae26fca664d8c94623dc0f1baf"),
+ arrayOf("p", "6641f26f5c59f7010dbe3e42e4593398e27c087497cb7d20e0e7633a17e48a94"),
+ arrayOf("description", "Retro computer fans and enthusiasts "),
+ )
+
+ val followCard =
+ FollowListEvent(
+ id = "eca31634fce7c9068b56fa8db9f387da70bdcceb3986a77ca1a9844f3128eb5f",
+ pubKey = "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da",
+ createdAt = 1761736286,
+ tags = tags,
+ content = "",
+ sig = "3aa388edafad151e81cb0228fe04e115dbbcaa851c666bfe3c8740b6cd99575f0fc3ba2d47acda86f7626564a05e9dbc05ef452a7bd0ac00f828dbad0e1bae6c",
+ )
+
+ val followCardRumor =
+ Rumor(
+ id = followCard.id,
+ pubKey = followCard.pubKey,
+ createdAt = followCard.createdAt,
+ kind = followCard.kind,
+ tags = followCard.tags,
+ content = followCard.content,
+ )
+
+ val followCardTemplate =
+ EventTemplate(
+ createdAt = followCard.createdAt,
+ kind = followCard.kind,
+ tags = followCard.tags,
+ content = followCard.content,
+ )
+
+ // =========================================================================
+ // TagArray Tests
+ // =========================================================================
+
+ @Test
+ fun serializeTagArray_matchesJackson() {
+ val jacksonJson = JacksonMapper.toJson(tags)
+ val kotlinJson = KotlinSerializationMapper.toJson(tags)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun deserializeTagArray_matchesJackson() {
+ val json = JacksonMapper.toJson(tags)
+ val deserialized = KotlinSerializationMapper.fromJsonToTagArray(json)
+ tags.forEachIndexed { index, tag ->
+ assertContentEquals(tag, deserialized[index])
+ }
+ }
+
+ @Test
+ fun tagArrayRoundTrip() {
+ val json = KotlinSerializationMapper.toJson(tags)
+ val deserialized = KotlinSerializationMapper.fromJsonToTagArray(json)
+ assertEquals(tags.size, deserialized.size)
+ tags.forEachIndexed { index, tag ->
+ assertContentEquals(tag, deserialized[index])
+ }
+ }
+
+ @Test
+ fun tagArrayWithNullValues() {
+ val json = """[["key",null,"value"]]"""
+ val deserialized = KotlinSerializationMapper.fromJsonToTagArray(json)
+ assertEquals(1, deserialized.size)
+ assertEquals("key", deserialized[0][0])
+ assertEquals("", deserialized[0][1]) // null -> ""
+ assertEquals("value", deserialized[0][2])
+ }
+
+ @Test
+ fun emptyTagArray() {
+ val json = "[]"
+ val deserialized = KotlinSerializationMapper.fromJsonToTagArray(json)
+ assertEquals(0, deserialized.size)
+ }
+
+ // =========================================================================
+ // Event Tests
+ // =========================================================================
+
+ @Test
+ fun serializeEvent_matchesJackson() {
+ val jacksonJson = JacksonMapper.toJson(followCard)
+ val kotlinJson = KotlinSerializationMapper.toJson(followCard)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun deserializeEvent_matchesJackson() {
+ val json = JacksonMapper.toJson(followCard)
+ val deserialized = KotlinSerializationMapper.fromJson(json)
+
+ assertEquals(followCard.id, deserialized.id)
+ assertEquals(followCard.pubKey, deserialized.pubKey)
+ assertEquals(followCard.createdAt, deserialized.createdAt)
+ assertEquals(followCard.kind, deserialized.kind)
+ assertEquals(followCard.content, deserialized.content)
+ assertEquals(followCard.sig, deserialized.sig)
+ tags.forEachIndexed { index, tag ->
+ assertContentEquals(tag, deserialized.tags[index])
+ }
+ }
+
+ @Test
+ fun eventRoundTrip() {
+ val json = KotlinSerializationMapper.toJson(followCard)
+ val deserialized = KotlinSerializationMapper.fromJson(json)
+
+ assertEquals(followCard.id, deserialized.id)
+ assertEquals(followCard.kind, deserialized.kind)
+ assertEquals(followCard.createdAt, deserialized.createdAt)
+ assertEquals(followCard.pubKey, deserialized.pubKey)
+ assertEquals(followCard.content, deserialized.content)
+ assertEquals(followCard.sig, deserialized.sig)
+ }
+
+ @Test
+ fun deserializeEventWithUnknownFields() {
+ val json =
+ """{"id":"abc123","pubkey":"def456","created_at":12345,"kind":1,"tags":[],"content":"test","sig":"sig123","unknown_field":"ignored"}"""
+ // Should not throw with unknown fields, should be ignored
+ val deserialized = KotlinSerializationMapper.fromJson(json)
+ assertEquals("abc123", deserialized.id)
+ assertEquals("def456", deserialized.pubKey)
+ assertEquals("test", deserialized.content)
+ }
+
+ @Test
+ fun deserializeEventWithSpecialCharactersInContent() {
+ val content = "Hello \"world\" \n\ttab\\backslash"
+ val event =
+ FollowListEvent(
+ id = "abc",
+ pubKey = "def",
+ createdAt = 1000,
+ tags = emptyArray(),
+ content = content,
+ sig = "sig",
+ )
+ val json = KotlinSerializationMapper.toJson(event)
+ val deserialized = KotlinSerializationMapper.fromJson(json)
+ assertEquals(content, deserialized.content)
+ }
+
+ @Test
+ fun crossDeserializationEvent() {
+ // Serialize with Jackson, deserialize with Kotlin Serialization
+ val jacksonJson = JacksonMapper.toJson(followCard)
+ val kotlinDeserialized = KotlinSerializationMapper.fromJson(jacksonJson)
+
+ assertEquals(followCard.id, kotlinDeserialized.id)
+ assertEquals(followCard.pubKey, kotlinDeserialized.pubKey)
+ assertEquals(followCard.kind, kotlinDeserialized.kind)
+
+ // Serialize with Kotlin Serialization, deserialize with Jackson
+ val kotlinJson = KotlinSerializationMapper.toJson(followCard)
+ val jacksonDeserialized = JacksonMapper.fromJson(kotlinJson)
+
+ assertEquals(followCard.id, jacksonDeserialized.id)
+ assertEquals(followCard.pubKey, jacksonDeserialized.pubKey)
+ assertEquals(followCard.kind, jacksonDeserialized.kind)
+ }
+
+ // =========================================================================
+ // Rumor Tests
+ // =========================================================================
+
+ @Test
+ fun serializeRumor_matchesJackson() {
+ val jacksonJson = JacksonMapper.toJson(followCardRumor)
+ val kotlinJson = KotlinSerializationMapper.toJson(followCardRumor)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun deserializeRumor_matchesJackson() {
+ val json = JacksonMapper.toJson(followCardRumor)
+ val deserialized = KotlinSerializationMapper.fromJsonToRumor(json)
+
+ assertEquals(followCardRumor.id, deserialized.id)
+ assertEquals(followCardRumor.pubKey, deserialized.pubKey)
+ assertEquals(followCardRumor.createdAt, deserialized.createdAt)
+ assertEquals(followCardRumor.kind, deserialized.kind)
+ assertEquals(followCardRumor.content, deserialized.content)
+ }
+
+ @Test
+ fun rumorRoundTrip() {
+ val json = KotlinSerializationMapper.toJson(followCardRumor)
+ val deserialized = KotlinSerializationMapper.fromJsonToRumor(json)
+
+ assertEquals(followCardRumor.id, deserialized.id)
+ assertEquals(followCardRumor.kind, deserialized.kind)
+ }
+
+ @Test
+ fun rumorWithNullFields() {
+ val rumor = Rumor(null, null, null, null, null, null)
+ val json = KotlinSerializationMapper.toJson(rumor)
+ assertEquals("{}", json)
+
+ val deserialized = KotlinSerializationMapper.fromJsonToRumor(json)
+ assertNull(deserialized.id)
+ assertNull(deserialized.pubKey)
+ assertNull(deserialized.createdAt)
+ assertNull(deserialized.kind)
+ assertNull(deserialized.tags)
+ assertNull(deserialized.content)
+ }
+
+ @Test
+ fun rumorPartialFields() {
+ val rumor = Rumor("abc", null, 1000, 1, null, "content")
+ val json = KotlinSerializationMapper.toJson(rumor)
+ val deserialized = KotlinSerializationMapper.fromJsonToRumor(json)
+
+ assertEquals("abc", deserialized.id)
+ assertNull(deserialized.pubKey)
+ assertEquals(1000L, deserialized.createdAt)
+ assertEquals(1, deserialized.kind)
+ assertNull(deserialized.tags)
+ assertEquals("content", deserialized.content)
+ }
+
+ // =========================================================================
+ // EventTemplate Tests
+ // =========================================================================
+
+ @Test
+ fun serializeTemplate_matchesJackson() {
+ val jacksonJson = JacksonMapper.toJson(followCardTemplate)
+ val kotlinJson = KotlinSerializationMapper.toJson(followCardTemplate)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun deserializeTemplate_matchesJackson() {
+ val json = JacksonMapper.toJson(followCardTemplate)
+ val deserialized = KotlinSerializationMapper.fromJsonToEventTemplate(json)
+
+ assertEquals(followCardTemplate.kind, deserialized.kind)
+ assertEquals(followCardTemplate.createdAt, deserialized.createdAt)
+ assertEquals(followCardTemplate.content, deserialized.content)
+ tags.forEachIndexed { index, tag ->
+ assertContentEquals(tag, deserialized.tags[index])
+ }
+ }
+
+ @Test
+ fun templateRoundTrip() {
+ val json = KotlinSerializationMapper.toJson(followCardTemplate)
+ val deserialized = KotlinSerializationMapper.fromJsonToEventTemplate(json)
+
+ assertEquals(followCardTemplate.kind, deserialized.kind)
+ assertEquals(followCardTemplate.createdAt, deserialized.createdAt)
+ assertEquals(followCardTemplate.content, deserialized.content)
+ }
+
+ @Test
+ fun templateWithDifferentFieldOrder() {
+ // Fields in different order than expected
+ val json = """{"kind":1,"content":"test","created_at":1234,"tags":[]}"""
+ val deserialized = KotlinSerializationMapper.fromJsonToEventTemplate(json)
+ assertEquals(1, deserialized.kind)
+ assertEquals("test", deserialized.content)
+ assertEquals(1234L, deserialized.createdAt)
+ }
+
+ // =========================================================================
+ // Filter Tests
+ // =========================================================================
+
+ @Test
+ fun emptyFilter_matchesJackson() {
+ val filter = Filter()
+ val jacksonJson = JacksonMapper.toJson(filter)
+ val kotlinJson = KotlinSerializationMapper.toJson(filter)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun filterWithAllFields_matchesJackson() {
+ val filter =
+ Filter(
+ ids = listOf("abc123" + "0".repeat(58)),
+ authors = listOf("def456" + "0".repeat(58)),
+ kinds = listOf(1, 2, 3),
+ tags = mapOf("p" to listOf("3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da")),
+ tagsAll = mapOf("p" to listOf("3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da")),
+ since = 1000L,
+ until = 2000L,
+ limit = 50,
+ search = "hello",
+ )
+ val jacksonJson = JacksonMapper.toJson(filter)
+ val kotlinJson = KotlinSerializationMapper.toJson(filter)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun filterRoundTrip() {
+ val expectedTagValue = "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da"
+ val filter =
+ Filter(
+ tags = mapOf("p" to listOf(expectedTagValue)),
+ tagsAll = mapOf("p" to listOf(expectedTagValue)),
+ )
+ val json = KotlinSerializationMapper.toJson(filter)
+ val deserialized = KotlinSerializationMapper.fromJsonTo(json)
+
+ assertEquals(true, deserialized.tags?.keys?.contains("p"))
+ assertEquals(listOf(expectedTagValue), deserialized.tags?.get("p"))
+ assertEquals(true, deserialized.tagsAll?.keys?.contains("p"))
+ assertEquals(listOf(expectedTagValue), deserialized.tagsAll?.get("p"))
+ }
+
+ @Test
+ fun deserializeEmptyFilter() {
+ val json = Filter().toJson()
+ val deserialized = KotlinSerializationMapper.fromJsonTo(json)
+ assertNull(deserialized.ids)
+ }
+
+ @Test
+ fun crossDeserializationFilter() {
+ val expectedTagValue = "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da"
+ val filter =
+ Filter(
+ tags = mapOf("p" to listOf(expectedTagValue)),
+ tagsAll = mapOf("p" to listOf(expectedTagValue)),
+ )
+
+ // Jackson serialized -> Kotlin deserialized
+ val jacksonJson = JacksonMapper.toJson(filter)
+ val kotlinDeserialized = KotlinSerializationMapper.fromJsonTo(jacksonJson)
+ assertEquals(listOf(expectedTagValue), kotlinDeserialized.tags?.get("p"))
+ assertEquals(listOf(expectedTagValue), kotlinDeserialized.tagsAll?.get("p"))
+
+ // Kotlin serialized -> Jackson deserialized
+ val kotlinJson = KotlinSerializationMapper.toJson(filter)
+ val jacksonDeserialized = JacksonMapper.fromJsonTo(kotlinJson)
+ assertEquals(listOf(expectedTagValue), jacksonDeserialized.tags?.get("p"))
+ assertEquals(listOf(expectedTagValue), jacksonDeserialized.tagsAll?.get("p"))
+ }
+
+ // =========================================================================
+ // Message Tests
+ // =========================================================================
+
+ @Test
+ fun serializeEventMessage_matchesJackson() {
+ val msg = EventMessage("sub1", followCard)
+ val jacksonJson = JacksonMapper.toJson(msg)
+ val kotlinJson = KotlinSerializationMapper.toJson(msg)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun deserializeEventMessage() {
+ val msg = EventMessage("sub1", followCard)
+ val json = KotlinSerializationMapper.toJson(msg)
+ val deserialized = KotlinSerializationMapper.fromJsonToMessage(json)
+
+ assertTrue(deserialized is EventMessage)
+ assertEquals("sub1", deserialized.subId)
+ assertEquals(followCard.id, deserialized.event.id)
+ }
+
+ @Test
+ fun serializeNoticeMessage_matchesJackson() {
+ val msg = NoticeMessage("something went wrong")
+ val jacksonJson = JacksonMapper.toJson(msg)
+ val kotlinJson = KotlinSerializationMapper.toJson(msg)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun serializeOkMessage_matchesJackson() {
+ val msg = OkMessage("abc123", true, "success")
+ val jacksonJson = JacksonMapper.toJson(msg)
+ val kotlinJson = KotlinSerializationMapper.toJson(msg)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun deserializeOkMessage() {
+ val msg = OkMessage("abc123", false, "rate limited")
+ val json = KotlinSerializationMapper.toJson(msg)
+ val deserialized = KotlinSerializationMapper.fromJsonToMessage(json)
+
+ assertTrue(deserialized is OkMessage)
+ assertEquals("abc123", deserialized.eventId)
+ assertEquals(false, deserialized.success)
+ assertEquals("rate limited", deserialized.message)
+ }
+
+ @Test
+ fun serializeAuthMessage_matchesJackson() {
+ val msg = AuthMessage("challenge123")
+ val jacksonJson = JacksonMapper.toJson(msg)
+ val kotlinJson = KotlinSerializationMapper.toJson(msg)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun serializeNotifyMessage_matchesJackson() {
+ val msg = NotifyMessage("notification text")
+ val jacksonJson = JacksonMapper.toJson(msg)
+ val kotlinJson = KotlinSerializationMapper.toJson(msg)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun serializeClosedMessage_matchesJackson() {
+ val msg = ClosedMessage("sub1", "subscription closed")
+ val jacksonJson = JacksonMapper.toJson(msg)
+ val kotlinJson = KotlinSerializationMapper.toJson(msg)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun deserializeEoseMessage() {
+ val json = """["EOSE","sub123"]"""
+ val deserialized = KotlinSerializationMapper.fromJsonToMessage(json)
+ assertTrue(deserialized is EoseMessage)
+ assertEquals("sub123", (deserialized).subId)
+ }
+
+ @Test
+ fun serializeCountMessage_matchesJackson() {
+ val msg = CountMessage("q1", CountResult(42, false))
+ val jacksonJson = JacksonMapper.toJson(msg)
+ val kotlinJson = KotlinSerializationMapper.toJson(msg)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun crossDeserializationMessages() {
+ val messages =
+ listOf(
+ NoticeMessage("test"),
+ AuthMessage("challenge"),
+ NotifyMessage("notify"),
+ ClosedMessage("sub1", "reason"),
+ )
+
+ for (msg in messages) {
+ val jacksonJson = JacksonMapper.toJson(msg)
+ val kotlinDeserialized = KotlinSerializationMapper.fromJsonToMessage(jacksonJson)
+ assertEquals(msg.label(), kotlinDeserialized.label())
+
+ val kotlinJson = KotlinSerializationMapper.toJson(msg)
+ val jacksonDeserialized = JacksonMapper.fromJsonToMessage(kotlinJson)
+ assertEquals(msg.label(), jacksonDeserialized.label())
+ }
+ }
+
+ // =========================================================================
+ // Command Tests
+ // =========================================================================
+
+ @Test
+ fun serializeReqCmd_matchesJackson() {
+ val filter =
+ Filter(
+ kinds = listOf(1),
+ limit = 10,
+ )
+ val cmd = ReqCmd("sub1", listOf(filter))
+ val jacksonJson = JacksonMapper.toJson(cmd)
+ val kotlinJson = KotlinSerializationMapper.toJson(cmd)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun deserializeReqCmd() {
+ val filter = Filter(kinds = listOf(1), limit = 10)
+ val cmd = ReqCmd("sub1", listOf(filter))
+ val json = KotlinSerializationMapper.toJson(cmd)
+ val deserialized = KotlinSerializationMapper.fromJsonToCommand(json)
+
+ assertTrue(deserialized is ReqCmd)
+ assertEquals("sub1", deserialized.subId)
+ assertEquals(1, deserialized.filters.size)
+ assertEquals(listOf(1), deserialized.filters[0].kinds)
+ assertEquals(10, deserialized.filters[0].limit)
+ }
+
+ @Test
+ fun serializeEventCmd_matchesJackson() {
+ val cmd = EventCmd(followCard)
+ val jacksonJson = JacksonMapper.toJson(cmd)
+ val kotlinJson = KotlinSerializationMapper.toJson(cmd)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun serializeCloseCmd_matchesJackson() {
+ val cmd = CloseCmd("sub1")
+ val jacksonJson = JacksonMapper.toJson(cmd)
+ val kotlinJson = KotlinSerializationMapper.toJson(cmd)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun crossDeserializationCommands() {
+ val cmd = CloseCmd("sub1")
+
+ val jacksonJson = JacksonMapper.toJson(cmd)
+ val kotlinDeserialized = KotlinSerializationMapper.fromJsonToCommand(jacksonJson)
+ assertTrue(kotlinDeserialized is CloseCmd)
+ assertEquals("sub1", (kotlinDeserialized).subId)
+
+ val kotlinJson = KotlinSerializationMapper.toJson(cmd)
+ val jacksonDeserialized = JacksonMapper.fromJsonToCommand(kotlinJson)
+ assertTrue(jacksonDeserialized is CloseCmd)
+ assertEquals("sub1", (jacksonDeserialized).subId)
+ }
+
+ // =========================================================================
+ // BunkerRequest Tests
+ // =========================================================================
+
+ @Test
+ fun serializeBunkerRequest_matchesJackson() {
+ val req = BunkerRequest("id1", "connect", arrayOf("pubkey", "secret"))
+ val jacksonJson = JacksonMapper.toJson(req)
+ val kotlinJson = KotlinSerializationMapper.toJson(req)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun deserializeBunkerRequest() {
+ val json = """{"id":"id1","method":"ping","params":[]}"""
+ val deserialized = KotlinSerializationMapper.fromJsonTo(json)
+ assertEquals("id1", deserialized.id)
+ assertEquals("ping", deserialized.method)
+ }
+
+ @Test
+ fun crossDeserializationBunkerRequest() {
+ val req = BunkerRequest("id1", "sign_event", arrayOf("{\"created_at\":1234,\"kind\":1,\"tags\":[],\"content\":\"This is an unsigned event.\"}"))
+ val jacksonJson = JacksonMapper.toJson(req)
+ val kotlinDeserialized = KotlinSerializationMapper.fromJsonTo(jacksonJson)
+ assertEquals(req.id, kotlinDeserialized.id)
+ assertEquals(req.method, kotlinDeserialized.method)
+ assertContentEquals(req.params, kotlinDeserialized.params)
+
+ val kotlinJson = KotlinSerializationMapper.toJson(req)
+ val jacksonDeserialized = JacksonMapper.fromJsonTo(kotlinJson)
+ assertEquals(req.id, jacksonDeserialized.id)
+ assertEquals(req.method, jacksonDeserialized.method)
+ assertContentEquals(req.params, jacksonDeserialized.params)
+ }
+
+ // =========================================================================
+ // BunkerResponse Tests
+ // =========================================================================
+
+ @Test
+ fun serializeBunkerResponse_matchesJackson() {
+ val resp = BunkerResponse("id1", "ok", null)
+ val jacksonJson = JacksonMapper.toJson(resp)
+ val kotlinJson = KotlinSerializationMapper.toJson(resp)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun serializeBunkerResponseWithError_matchesJackson() {
+ val resp = BunkerResponse("id1", null, "something went wrong")
+ val jacksonJson = JacksonMapper.toJson(resp)
+ val kotlinJson = KotlinSerializationMapper.toJson(resp)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun deserializeBunkerResponse() {
+ val json = """{"id":"id1","result":"pong"}"""
+ val deserialized = KotlinSerializationMapper.fromJsonTo(json)
+ assertEquals("id1", deserialized.id)
+ assertNotNull(deserialized.result)
+ }
+
+ // =========================================================================
+ // OptimizedSerializable toJson dispatch Tests
+ // =========================================================================
+
+ @Test
+ fun toJsonDispatchForFilter() {
+ val filter = Filter(kinds = listOf(1))
+ val jacksonJson = JacksonMapper.toJson(filter)
+ val kotlinJson = KotlinSerializationMapper.toJson(filter)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun toJsonDispatchForRumor() {
+ val jacksonJson = JacksonMapper.toJson(followCardRumor)
+ val kotlinJson = KotlinSerializationMapper.toJson(followCardRumor)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ @Test
+ fun toJsonDispatchForEventTemplate() {
+ val jacksonJson = JacksonMapper.toJson(followCardTemplate)
+ val kotlinJson = KotlinSerializationMapper.toJson(followCardTemplate)
+ assertEquals(jacksonJson, kotlinJson)
+ }
+
+ // =========================================================================
+ // Edge Cases
+ // =========================================================================
+
+ @Test
+ fun emptyContentEvent() {
+ val event =
+ FollowListEvent(
+ id = "a".repeat(64),
+ pubKey = "b".repeat(64),
+ createdAt = 0,
+ tags = emptyArray(),
+ content = "",
+ sig = "c".repeat(64),
+ )
+ val json = KotlinSerializationMapper.toJson(event)
+ val deserialized = KotlinSerializationMapper.fromJson(json)
+ assertEquals("", deserialized.content)
+ assertEquals(0, deserialized.tags.size)
+ }
+
+ @Test
+ fun largeTagArray() {
+ val largeTags = Array(100) { i -> arrayOf("p", "key$i") }
+ val json = KotlinSerializationMapper.toJson(largeTags)
+ val deserialized = KotlinSerializationMapper.fromJsonToTagArray(json)
+ assertEquals(100, deserialized.size)
+ assertEquals("key99", deserialized[99][1])
+ }
+
+ @Test
+ fun eventWithUnicodeContent() {
+ val content = "Hello \uD83D\uDE00 world \u00E9\u00E8\u00EA"
+ val event =
+ FollowListEvent(
+ id = "a".repeat(64),
+ pubKey = "b".repeat(64),
+ createdAt = 1000,
+ tags = emptyArray(),
+ content = content,
+ sig = "c".repeat(64),
+ )
+ val json = KotlinSerializationMapper.toJson(event)
+ val deserialized = KotlinSerializationMapper.fromJson(json)
+ assertEquals(content, deserialized.content)
+ }
+
+ @Test
+ fun filterWithMultipleTagTypes() {
+ val filter =
+ Filter(
+ tags =
+ mapOf(
+ "p" to listOf("pubkey1", "pubkey2"),
+ "e" to listOf("eventid1"),
+ "t" to listOf("nostr", "bitcoin"),
+ ),
+ )
+ val json = KotlinSerializationMapper.toJson(filter)
+ val deserialized = KotlinSerializationMapper.fromJsonTo(json)
+
+ assertEquals(3, deserialized.tags?.size)
+ assertEquals(listOf("pubkey1", "pubkey2"), deserialized.tags?.get("p"))
+ assertEquals(listOf("eventid1"), deserialized.tags?.get("e"))
+ assertEquals(listOf("nostr", "bitcoin"), deserialized.tags?.get("t"))
+ }
+}
diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientManualSubTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientManualSubTest.kt
index bff422a78b..338a832e8b 100644
--- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientManualSubTest.kt
+++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientManualSubTest.kt
@@ -81,7 +81,7 @@ class NostrClientManualSubTest : BaseNostrClientTest() {
client.openReqSubscription(mySubId, filters, listener)
- withTimeoutOrNull(30000) {
+ withTimeoutOrNull(10000) {
while (events.size < 101) {
val event = resultChannel.receive()
events.add(event)
diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSendAndWaitTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSendAndWaitTest.kt
index 5f289a4c61..e942fe1345 100644
--- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSendAndWaitTest.kt
+++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSendAndWaitTest.kt
@@ -48,7 +48,7 @@ class NostrClientSendAndWaitTest : BaseNostrClientTest() {
val resultDamus =
client.sendAndWaitForResponse(
event = event,
- relayList = setOf("wss://relay.damus.io".normalizeRelayUrl()),
+ relayList = setOf("wss://nostr.bitcoiner.social".normalizeRelayUrl()),
)
val resultNos =
diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionAsFlowTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionAsFlowTest.kt
index f7faca43c4..e28b887f2b 100644
--- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionAsFlowTest.kt
+++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionAsFlowTest.kt
@@ -54,7 +54,7 @@ class NostrClientSubscriptionAsFlowTest : BaseNostrClientTest() {
val flow =
client.reqAsFlow(
- relay = "wss://relay.damus.io",
+ relay = "wss://nos.lol",
filter =
Filter(
kinds = listOf(MetadataEvent.KIND),
diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionUntilEoseAsFlowTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionUntilEoseAsFlowTest.kt
index ddcb9fde94..913bf5eff9 100644
--- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionUntilEoseAsFlowTest.kt
+++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionUntilEoseAsFlowTest.kt
@@ -54,7 +54,7 @@ class NostrClientSubscriptionUntilEoseAsFlowTest : BaseNostrClientTest() {
val flow =
client.reqUntilEoseAsFlow(
- relay = "wss://relay.damus.io",
+ relay = "wss://nos.lol",
filter =
Filter(
kinds = listOf(MetadataEvent.KIND),