diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/debits/DebitEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/debits/DebitEvent.kt index 61384743ec..f25458c108 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/debits/DebitEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/debits/DebitEvent.kt @@ -21,7 +21,8 @@ package com.vitorpamplona.quartz.experimental.clink.debits import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.experimental.clink.Clink +import com.vitorpamplona.quartz.experimental.clink.tags.ClinkVersionTag +import com.vitorpamplona.quartz.experimental.clink.tags.clinkVersion import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper @@ -58,7 +59,7 @@ class DebitEvent( fun isResponse() = requestId() != null - fun version() = tags.firstOrNull { it.size > 1 && it[0] == Clink.VERSION_TAG_NAME }?.get(1) + fun version() = tags.firstNotNullOfOrNull(ClinkVersionTag::parse) private fun talkingWith(oneSideHex: String): HexKey = if (pubKey == oneSideHex) recipientPubKey() ?: pubKey else pubKey @@ -88,7 +89,7 @@ class DebitEvent( return signer.sign( eventTemplate(KIND, encrypted, createdAt) { pTag(servicePubKey, null) - add(Clink.versionTag()) + clinkVersion() alt(ALT) }, ) @@ -107,7 +108,7 @@ class DebitEvent( eventTemplate(KIND, encrypted, createdAt) { pTag(requestorPubKey, null) add(ETag.assemble(requestEvent.id, null, null)) - add(Clink.versionTag()) + clinkVersion() alt(ALT) }, ) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/manage/ManageEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/manage/ManageEvent.kt index 48ab85d044..3ce4b6cc36 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/manage/ManageEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/manage/ManageEvent.kt @@ -21,7 +21,8 @@ package com.vitorpamplona.quartz.experimental.clink.manage import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.experimental.clink.Clink +import com.vitorpamplona.quartz.experimental.clink.tags.ClinkVersionTag +import com.vitorpamplona.quartz.experimental.clink.tags.clinkVersion import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper @@ -58,7 +59,7 @@ class ManageEvent( fun isResponse() = requestId() != null - fun version() = tags.firstOrNull { it.size > 1 && it[0] == Clink.VERSION_TAG_NAME }?.get(1) + fun version() = tags.firstNotNullOfOrNull(ClinkVersionTag::parse) private fun talkingWith(oneSideHex: String): HexKey = if (pubKey == oneSideHex) recipientPubKey() ?: pubKey else pubKey @@ -88,7 +89,7 @@ class ManageEvent( return signer.sign( eventTemplate(KIND, encrypted, createdAt) { pTag(serverPubKey, null) - add(Clink.versionTag()) + clinkVersion() alt(ALT) }, ) @@ -107,7 +108,7 @@ class ManageEvent( eventTemplate(KIND, encrypted, createdAt) { pTag(appPubKey, null) add(ETag.assemble(requestEvent.id, null, null)) - add(Clink.versionTag()) + clinkVersion() alt(ALT) }, ) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/offers/OfferEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/offers/OfferEvent.kt index 040e7ed9d1..9350c64980 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/offers/OfferEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/offers/OfferEvent.kt @@ -21,7 +21,8 @@ package com.vitorpamplona.quartz.experimental.clink.offers import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.experimental.clink.Clink +import com.vitorpamplona.quartz.experimental.clink.tags.ClinkVersionTag +import com.vitorpamplona.quartz.experimental.clink.tags.clinkVersion import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper @@ -60,7 +61,7 @@ class OfferEvent( fun isResponse() = requestId() != null - fun version() = tags.firstOrNull { it.size > 1 && it[0] == Clink.VERSION_TAG_NAME }?.get(1) + fun version() = tags.firstNotNullOfOrNull(ClinkVersionTag::parse) private fun talkingWith(oneSideHex: String): HexKey = if (pubKey == oneSideHex) recipientPubKey() ?: pubKey else pubKey @@ -90,7 +91,7 @@ class OfferEvent( return signer.sign( eventTemplate(KIND, encrypted, createdAt) { pTag(servicePubKey, null) - add(Clink.versionTag()) + clinkVersion() alt(ALT) }, ) @@ -109,7 +110,7 @@ class OfferEvent( eventTemplate(KIND, encrypted, createdAt) { pTag(payerPubKey, null) add(ETag.assemble(requestEvent.id, null, null)) - add(Clink.versionTag()) + clinkVersion() alt(ALT) }, ) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/tags/ClinkVersionTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/tags/ClinkVersionTag.kt new file mode 100644 index 0000000000..4d3c727cda --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/tags/ClinkVersionTag.kt @@ -0,0 +1,44 @@ +/* + * 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.experimental.clink.tags + +import com.vitorpamplona.quartz.utils.ensure + +/** + * Protocol-version tag shared by all three CLINK message kinds (21001-3). Every CLINK + * request and response carries a `["clink_version", "1"]` tag alongside its `p` tag, + * with the content NIP-44 encrypted between the two parties. + */ +class ClinkVersionTag { + companion object { + const val TAG_NAME = "clink_version" + const val CURRENT = "1" + + fun parse(tag: Array): String? { + ensure(tag.size > 1) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble(version: String = CURRENT) = arrayOf(TAG_NAME, version) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/Clink.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/tags/TagArrayBuilderExt.kt similarity index 71% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/Clink.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/tags/TagArrayBuilderExt.kt index 4fad7ab085..8c0d2da7b0 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/Clink.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/clink/tags/TagArrayBuilderExt.kt @@ -18,16 +18,9 @@ * 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.experimental.clink +package com.vitorpamplona.quartz.experimental.clink.tags -/** - * Constants shared by all three CLINK message kinds. Every CLINK request and - * response carries a `["clink_version", "1"]` tag and a `["p", recipient]` tag, - * and its content is a NIP-44 encrypted JSON payload. - */ -object Clink { - const val VERSION = "1" - const val VERSION_TAG_NAME = "clink_version" +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder - fun versionTag(): Array = arrayOf(VERSION_TAG_NAME, VERSION) -} +fun TagArrayBuilder.clinkVersion(version: String = ClinkVersionTag.CURRENT) = addUnique(ClinkVersionTag.assemble(version)) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/clink/ClinkEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/clink/ClinkEventTest.kt index ff26963486..39539aff19 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/clink/ClinkEventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/clink/ClinkEventTest.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.quartz.experimental.clink.debits.DebitResponse import com.vitorpamplona.quartz.experimental.clink.offers.OfferEvent import com.vitorpamplona.quartz.experimental.clink.offers.OfferRequest import com.vitorpamplona.quartz.experimental.clink.offers.OfferResponse +import com.vitorpamplona.quartz.experimental.clink.tags.ClinkVersionTag import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal @@ -55,7 +56,7 @@ class ClinkEventTest { buildList { add(arrayOf("p", recipient)) if (requestId != null) add(arrayOf("e", requestId)) - add(Clink.versionTag()) + add(ClinkVersionTag.assemble()) }.toTypedArray(), content = "encrypted-placeholder", sig = "b".repeat(128), @@ -69,7 +70,7 @@ class ClinkEventTest { assertFalse(request.isResponse()) assertNull(request.requestId()) assertEquals(service.pubKey, request.recipientPubKey()) - assertEquals(Clink.VERSION, request.version()) + assertEquals(ClinkVersionTag.CURRENT, request.version()) } @Test