Merge pull request #1854 from vitorpamplona/claude/kotlin-serialization-mapper-4WYOP

Add Kotlin Serialization support for Nostr protocol types
This commit is contained in:
Vitor Pamplona
2026-03-15 16:04:21 -04:00
committed by GitHub
17 changed files with 2660 additions and 10 deletions
@@ -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.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<Command> {
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")
}
}
}
@@ -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<CountResult> {
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("CountResult") {
element<Int>("count")
element<Boolean>("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,
)
}
@@ -0,0 +1,111 @@
/*
* 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.JsonPrimitive
import kotlinx.serialization.json.buildJsonArray
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<Event> {
private val emptyTagArray = emptyArray<Array<String>>()
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("Event") {
element<String>("id")
element<String>("pubkey")
element<Long>("created_at")
element<Int>("kind")
element("tags", TagArrayKSerializer.descriptor)
element<String>("content")
element<String>("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: String = ""
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)
}
}
@@ -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<EventTemplate<Event>> {
private val emptyTagArray = emptyArray<Array<String>>()
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("EventTemplate") {
element<Long>("created_at")
element<Int>("kind")
element("tags", TagArrayKSerializer.descriptor)
element<String>("content")
}
override fun serialize(
encoder: Encoder,
value: EventTemplate<Event>,
) {
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<Event> {
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)
}
}
@@ -0,0 +1,141 @@
/*
* 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.JsonElement
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.int
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import kotlinx.serialization.json.longOrNull
import kotlinx.serialization.json.put
object FilterKSerializer : KSerializer<Filter> {
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<String, List<String>>()
val tagsAll = mutableMapOf<String, List<String>>()
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,
)
}
}
@@ -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.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<Event> = 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<Event>)
}
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 <reified T : OptimizedSerializable> 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
}
}
}
@@ -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.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<Message> {
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")
}
}
}
@@ -0,0 +1,101 @@
/*
* 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<TagArray> {
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<String>>(array.size)
for (inner in array) {
val innerArray = inner.jsonArray
val innerList = ArrayList<String>(innerArray.size.coerceAtLeast(5))
for (s in innerArray) {
if (s is JsonNull) {
innerList.add("")
} else {
innerList.add(s.jsonPrimitive.content)
}
}
@Suppress("UNCHECKED_CAST")
outerList.add(innerList.toArray(arrayOfNulls<String>(innerList.size)) as Array<String>)
}
@Suppress("UNCHECKED_CAST")
return outerList.toArray(arrayOfNulls<Array<String>>(outerList.size)) as TagArray
}
fun serializeToElement(value: TagArray): JsonArray =
buildJsonArray {
for (tag in value) {
add(
buildJsonArray {
for (s in tag) {
add(JsonPrimitive(s))
}
},
)
}
}
}
@@ -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.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<BunkerMessage> {
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<String>,
): BunkerRequest {
return 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)
}
}
}
@@ -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<BunkerRequest> {
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("BunkerRequest") {
element<String>("id")
element<String>("method")
element<List<String>>("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)
}
}
}
@@ -0,0 +1,110 @@
/*
* 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<BunkerResponse> {
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("BunkerResponse") {
element<String>("id")
element<String?>("result")
element<String?>("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)
}
}
@@ -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.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<Notification> {
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("Nip47Notification")
override fun serialize(
encoder: Encoder,
value: Notification,
) {
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")
}
}
}
@@ -0,0 +1,241 @@
/*
* 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<Request> {
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("Nip47Request")
override fun serialize(
encoder: Encoder,
value: Request,
) {
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,
)
},
)
}
}
@@ -0,0 +1,263 @@
/*
* 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<Response> {
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("Nip47Response")
override fun serialize(
encoder: Encoder,
value: Response,
) {
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?.map { 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,
)
},
)
}
}
@@ -0,0 +1,95 @@
/*
* 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.kotlinSerialization.TagArrayKSerializer
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArray
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<Rumor> {
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("Rumor") {
element<String>("id")
element<String>("pubkey")
element<Long>("created_at")
element<Int>("kind")
element("tags", TagArrayKSerializer.descriptor)
element<String>("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)
}
}
@@ -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<Array<String>> = TODO("Not yet implemented")
actual fun fromJsonToTagArray(json: String): Array<Array<String>> =
try {
KotlinSerializationMapper.fromJsonToTagArray(json)
} catch (e: SerializationException) {
throw IllegalArgumentException(e.message, e)
}
actual fun fromJsonToEventTemplate(json: String): EventTemplate<Event> = TODO("Not yet implemented")
actual fun fromJsonToEventTemplate(json: String): EventTemplate<Event> =
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<Array<String>>): String = TODO("Not yet implemented")
actual fun toJson(tags: Array<Array<String>>): String = KotlinSerializationMapper.toJson(tags)
actual inline fun <reified T : OptimizedSerializable> fromJsonTo(json: String): T = TODO("Not yet implemented")
actual inline fun <reified T : OptimizedSerializable> fromJsonTo(json: String): T =
try {
KotlinSerializationMapper.fromJsonTo<T>(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)
}
@@ -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<Event>(
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<Filter>(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<Filter>(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<Filter>(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<Filter>(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 as EoseMessage).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 as CloseCmd).subId)
val kotlinJson = KotlinSerializationMapper.toJson(cmd)
val jacksonDeserialized = JacksonMapper.fromJsonToCommand(kotlinJson)
assertTrue(jacksonDeserialized is CloseCmd)
assertEquals("sub1", (jacksonDeserialized as CloseCmd).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<BunkerRequest>(json)
assertEquals("id1", deserialized.id)
assertEquals("ping", deserialized.method)
}
@Test
fun crossDeserializationBunkerRequest() {
val req = BunkerRequest("id1", "sign_event", arrayOf("eventJson"))
val jacksonJson = JacksonMapper.toJson(req)
val kotlinDeserialized = KotlinSerializationMapper.fromJsonTo<BunkerRequest>(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<BunkerRequest>(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<BunkerResponse>(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<Filter>(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"))
}
}