From 03b091d3ec8eb063c17b874becbd70004de71fa9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 10 Jun 2026 01:26:06 +0000 Subject: [PATCH] refactor(clink): model event tags via PTag/ETag classes + builder DSL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings OfferEvent/DebitEvent/ManageEvent (21001-3) in line with the codebase tag conventions, replacing raw inline tags: - Build via eventTemplate(KIND, content) { pTag(...); eTag/add; alt(...) } and signer.sign(template), instead of hand-rolled arrayOf("p"/"e", ...) + sign(). - Accessors use PTag.parseKey / ETag.parseId instead of matching "p"/"e" literals. Behavior-preserving: PTag.assemble(x, null) yields the identical ["p", x] bytes and tag order is unchanged, so signed events are byte-identical. All CLINK tests pass (ClinkEventTest, ClinkClientServerTest, pointer/interop). Note: these are NIP-44-encrypted request/response events, so create*() stays a suspend factory that encrypts then signs the template — matching NIP-47; a pure pre-signing template isn't possible without the signer. --- .../experimental/clink/debits/DebitEvent.kt | 40 ++++++++++--------- .../experimental/clink/manage/ManageEvent.kt | 40 ++++++++++--------- .../experimental/clink/offers/OfferEvent.kt | 40 ++++++++++--------- 3 files changed, 66 insertions(+), 54 deletions(-) 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 5c833523a4..61384743ec 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 @@ -27,7 +27,11 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions -import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils /** @@ -48,9 +52,9 @@ class DebitEvent( ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { override fun isContentEncoded() = true - fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1) + fun recipientPubKey() = tags.firstNotNullOfOrNull(PTag::parseKey) - fun requestId() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1) + fun requestId() = tags.firstNotNullOfOrNull(ETag::parseId) fun isResponse() = requestId() != null @@ -80,14 +84,14 @@ class DebitEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), ): DebitEvent { - val tags = - arrayOf( - arrayOf("p", servicePubKey), - Clink.versionTag(), - AltTag.assemble(ALT), - ) val encrypted = signer.nip44Encrypt(OptimizedJsonMapper.toJson(request), servicePubKey) - return signer.sign(createdAt, KIND, tags, encrypted) + return signer.sign( + eventTemplate(KIND, encrypted, createdAt) { + pTag(servicePubKey, null) + add(Clink.versionTag()) + alt(ALT) + }, + ) } /** Builds a response event (service side) referencing the original [requestEvent]. */ @@ -98,15 +102,15 @@ class DebitEvent( createdAt: Long = TimeUtils.now(), ): DebitEvent { val requestorPubKey = requestEvent.pubKey - val tags = - arrayOf( - arrayOf("p", requestorPubKey), - arrayOf("e", requestEvent.id), - Clink.versionTag(), - AltTag.assemble(ALT), - ) val encrypted = signer.nip44Encrypt(OptimizedJsonMapper.toJson(response), requestorPubKey) - return signer.sign(createdAt, KIND, tags, encrypted) + return signer.sign( + eventTemplate(KIND, encrypted, createdAt) { + pTag(requestorPubKey, null) + add(ETag.assemble(requestEvent.id, null, null)) + add(Clink.versionTag()) + 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 ac51a82e86..48ab85d044 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 @@ -27,7 +27,11 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions -import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils /** @@ -48,9 +52,9 @@ class ManageEvent( ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { override fun isContentEncoded() = true - fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1) + fun recipientPubKey() = tags.firstNotNullOfOrNull(PTag::parseKey) - fun requestId() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1) + fun requestId() = tags.firstNotNullOfOrNull(ETag::parseId) fun isResponse() = requestId() != null @@ -80,14 +84,14 @@ class ManageEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), ): ManageEvent { - val tags = - arrayOf( - arrayOf("p", serverPubKey), - Clink.versionTag(), - AltTag.assemble(ALT), - ) val encrypted = signer.nip44Encrypt(OptimizedJsonMapper.toJson(request), serverPubKey) - return signer.sign(createdAt, KIND, tags, encrypted) + return signer.sign( + eventTemplate(KIND, encrypted, createdAt) { + pTag(serverPubKey, null) + add(Clink.versionTag()) + alt(ALT) + }, + ) } /** Builds a response event (server side) referencing the original [requestEvent]. */ @@ -98,15 +102,15 @@ class ManageEvent( createdAt: Long = TimeUtils.now(), ): ManageEvent { val appPubKey = requestEvent.pubKey - val tags = - arrayOf( - arrayOf("p", appPubKey), - arrayOf("e", requestEvent.id), - Clink.versionTag(), - AltTag.assemble(ALT), - ) val encrypted = signer.nip44Encrypt(OptimizedJsonMapper.toJson(response), appPubKey) - return signer.sign(createdAt, KIND, tags, encrypted) + return signer.sign( + eventTemplate(KIND, encrypted, createdAt) { + pTag(appPubKey, null) + add(ETag.assemble(requestEvent.id, null, null)) + add(Clink.versionTag()) + 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 8dd76b4499..040e7ed9d1 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 @@ -27,7 +27,11 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions -import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip01Core.tags.people.pTag +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.utils.TimeUtils /** @@ -49,10 +53,10 @@ class OfferEvent( override fun isContentEncoded() = true /** The `p` tag — the counterparty this message is addressed to. */ - fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1) + fun recipientPubKey() = tags.firstNotNullOfOrNull(PTag::parseKey) /** The `e` tag — present only on responses, referencing the request event id. */ - fun requestId() = tags.firstOrNull { it.size > 1 && it[0] == "e" }?.get(1) + fun requestId() = tags.firstNotNullOfOrNull(ETag::parseId) fun isResponse() = requestId() != null @@ -82,14 +86,14 @@ class OfferEvent( signer: NostrSigner, createdAt: Long = TimeUtils.now(), ): OfferEvent { - val tags = - arrayOf( - arrayOf("p", servicePubKey), - Clink.versionTag(), - AltTag.assemble(ALT), - ) val encrypted = signer.nip44Encrypt(OptimizedJsonMapper.toJson(request), servicePubKey) - return signer.sign(createdAt, KIND, tags, encrypted) + return signer.sign( + eventTemplate(KIND, encrypted, createdAt) { + pTag(servicePubKey, null) + add(Clink.versionTag()) + alt(ALT) + }, + ) } /** Builds a response event (service side) referencing the original [requestEvent]. */ @@ -100,15 +104,15 @@ class OfferEvent( createdAt: Long = TimeUtils.now(), ): OfferEvent { val payerPubKey = requestEvent.pubKey - val tags = - arrayOf( - arrayOf("p", payerPubKey), - arrayOf("e", requestEvent.id), - Clink.versionTag(), - AltTag.assemble(ALT), - ) val encrypted = signer.nip44Encrypt(OptimizedJsonMapper.toJson(response), payerPubKey) - return signer.sign(createdAt, KIND, tags, encrypted) + return signer.sign( + eventTemplate(KIND, encrypted, createdAt) { + pTag(payerPubKey, null) + add(ETag.assemble(requestEvent.id, null, null)) + add(Clink.versionTag()) + alt(ALT) + }, + ) } } }