refactor(clink): clink_offer metadata via ClinkOfferTag + DSL builder

Brings the kind-0 clink_offer field in line with the sibling fields' structure
instead of a raw string constant written to content only:
- New ClinkOfferTag (TAG_NAME/assemble/parse) under nip01Core/metadata/tags.
- clinkOffer() TagArrayBuilder DSL extension in TagArrayBuilderExt.
- MetadataEvent uses ClinkOfferTag.TAG_NAME and dual-writes it as a kind-0 tag
  in updateOrDeleteTagNames (NIP-1770 pattern), like lud16/nip05; drops the
  ad-hoc CLINK_OFFER_PROPERTY constant.

UpdateMetadataTest now also asserts the tag is emitted. quartz tests pass.
This commit is contained in:
Claude
2026-06-10 01:10:49 +00:00
parent 57850c3bcf
commit c1a0e707a0
4 changed files with 49 additions and 5 deletions
@@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.core.builder
import com.vitorpamplona.quartz.nip01Core.metadata.tags.AboutTag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.BannerTag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.ClinkOfferTag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.DisplayNameTag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.Lud06Tag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.Lud16Tag
@@ -88,9 +89,6 @@ class MetadataEvent(
const val KIND = 0
const val FIXED_D_TAG = ""
// CLINK Offers discovery key in kind-0 content (mirrors the NIP-05 `clink_offer` key).
const val CLINK_OFFER_PROPERTY = "clink_offer"
fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG)
fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null)
@@ -252,7 +250,7 @@ class MetadataEvent(
nip05?.let { addIfNotBlank(currentMetadata, Nip05Tag.TAG_NAME, it) }
lnAddress?.let { addIfNotBlank(currentMetadata, Lud16Tag.TAG_NAME, it) }
lnURL?.let { addIfNotBlank(currentMetadata, Lud06Tag.TAG_NAME, it) }
clinkOffer?.let { addIfNotBlank(currentMetadata, CLINK_OFFER_PROPERTY, it) }
clinkOffer?.let { addIfNotBlank(currentMetadata, ClinkOfferTag.TAG_NAME, it) }
}
// For https://github.com/nostr-protocol/nips/pull/1770
@@ -267,6 +265,7 @@ class MetadataEvent(
currentMetadata[Nip05Tag.TAG_NAME]?.let { nip05(it.text) } ?: run { remove(Nip05Tag.TAG_NAME) }
currentMetadata[Lud16Tag.TAG_NAME]?.let { lud16(it.text) } ?: run { remove(Lud16Tag.TAG_NAME) }
currentMetadata[Lud06Tag.TAG_NAME]?.let { lud06(it.text) } ?: run { remove(Lud06Tag.TAG_NAME) }
currentMetadata[ClinkOfferTag.TAG_NAME]?.let { clinkOffer(it.text) } ?: run { remove(ClinkOfferTag.TAG_NAME) }
}
private fun addIfNotBlank(
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip01Core.metadata
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.metadata.tags.AboutTag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.BannerTag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.ClinkOfferTag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.DisplayNameTag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.Lud06Tag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.Lud16Tag
@@ -51,3 +52,5 @@ fun TagArrayBuilder<MetadataEvent>.lud06(lud06: String) = addUnique(Lud06Tag.ass
fun TagArrayBuilder<MetadataEvent>.banner(banner: String) = addUnique(BannerTag.assemble(banner))
fun TagArrayBuilder<MetadataEvent>.pronouns(pronouns: String) = addUnique(PronounsTag.assemble(pronouns))
fun TagArrayBuilder<MetadataEvent>.clinkOffer(offer: String) = addUnique(ClinkOfferTag.assemble(offer))
@@ -0,0 +1,39 @@
/*
* 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.metadata.tags
import com.vitorpamplona.quartz.utils.ensure
/** CLINK Offers pointer (`noffer1…`) advertised in a kind-0 profile, mirroring the NIP-05 `clink_offer` key. */
class ClinkOfferTag {
companion object {
const val TAG_NAME = "clink_offer"
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(offer: String) = arrayOf(TAG_NAME, offer)
}
}
@@ -205,7 +205,10 @@ class UpdateMetadataTest {
// written into kind-0 content under the spec's `clink_offer` key
assertEquals(true, event.content.contains("\"clink_offer\":\"$noffer\""))
// and parses back out
// and dual-written as a kind-0 tag (NIP-1770 pattern), like the other fields
assertContentEquals(arrayOf("clink_offer", noffer), event.tags.firstOrNull { it.firstOrNull() == "clink_offer" })
// and parses back out of the content
val metadata = event.contactMetaData()
assertNotNull(metadata)
assertEquals(noffer, metadata.clinkOffer)