refactor(clink): clink_version as a shared ClinkVersionTag class

Models the protocol-version tag the way other tags are modeled, instead of a
loose helper on the Clink object:
- New ClinkVersionTag (TAG_NAME/CURRENT/assemble/parse) under clink/tags, with
  a clinkVersion() TagArrayBuilder DSL extension, reused by all three events.
- OfferEvent/DebitEvent/ManageEvent read version() via ClinkVersionTag::parse
  and build via clinkVersion() in their templates.
- Retires the now-empty Clink object (its KDoc moved to the tag class).

Behavior-preserving: assemble() emits the identical ["clink_version", "1"]
tag in the same position. All CLINK tests pass.
This commit is contained in:
Claude
2026-06-10 01:41:04 +00:00
parent 03b091d3ec
commit 7bac8cfd46
6 changed files with 66 additions and 25 deletions
@@ -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)
},
)
@@ -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)
},
)
@@ -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)
},
)
@@ -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>): 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)
}
}
@@ -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<String> = arrayOf(VERSION_TAG_NAME, VERSION)
}
fun <T : Event> TagArrayBuilder<T>.clinkVersion(version: String = ClinkVersionTag.CURRENT) = addUnique(ClinkVersionTag.assemble(version))
@@ -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