refactor(concord): community-list, control, invite-bundle events own their kind literals

Removes the CONTROL/COMMUNITY_LIST/INVITE_BUNDLE constants from ConcordKinds
now that each has a dedicated Event class. Callers reference
ControlEditionEvent.KIND, ConcordCommunityListEvent.KIND, and
ConcordInviteBundleEvent.KIND directly, matching the nip88Polls per-kind
event convention.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
This commit is contained in:
Claude
2026-07-11 14:56:28 +00:00
parent ae6983a7a8
commit 127d959c8d
11 changed files with 26 additions and 31 deletions
@@ -67,7 +67,7 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.ui.note.njumpLink
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.concord.events.ConcordKinds
import com.vitorpamplona.quartz.concord.cord05Invites.bundle.ConcordInviteBundleEvent
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser
@@ -224,7 +224,7 @@ private fun DisplayAddress(
// needs the 16-byte unlock token that only lives in the full invite link's #fragment —
// a naddr alone can't be joined. Show an informative label instead of the generic
// (and here always-empty) addressable-note card.
if (nip19.kind == ConcordKinds.INVITE_BUNDLE) {
if (nip19.kind == ConcordInviteBundleEvent.KIND) {
Text(
text = stringRes(R.string.concord_invite_naddr_label) + (additionalChars ?: ""),
color = MaterialTheme.colorScheme.primary,
@@ -31,6 +31,7 @@ import com.vitorpamplona.quartz.concord.cord05Invites.ConcordInviteBundle
import com.vitorpamplona.quartz.concord.cord05Invites.ConcordInviteLink
import com.vitorpamplona.quartz.concord.cord05Invites.MintedInviteLink
import com.vitorpamplona.quartz.concord.cord05Invites.ParsedInviteLink
import com.vitorpamplona.quartz.concord.cord05Invites.bundle.ConcordInviteBundleEvent
import com.vitorpamplona.quartz.concord.crypto.ConcordKeyDerivation
import com.vitorpamplona.quartz.concord.crypto.GroupKey
import com.vitorpamplona.quartz.concord.envelope.ConcordStreamEnvelope
@@ -86,7 +87,7 @@ object ConcordActions {
fun planeFilterFor(planePubKeysHex: List<HexKey>): Filter = Filter(kinds = listOf(ConcordKinds.WRAP), authors = planePubKeysHex)
/** The public invite bundle for a link signer. */
fun bundleFilter(linkSignerPubKeyHex: HexKey): Filter = Filter(kinds = listOf(ConcordKinds.INVITE_BUNDLE), authors = listOf(linkSignerPubKeyHex))
fun bundleFilter(linkSignerPubKeyHex: HexKey): Filter = Filter(kinds = listOf(ConcordInviteBundleEvent.KIND), authors = listOf(linkSignerPubKeyHex))
/** Pending direct invites addressed to the given member (indexed by k=3313). */
fun directInvitesFilter(memberPubKeyHex: HexKey): Filter = Filter(kinds = listOf(ConcordKinds.WRAP), tags = mapOf("p" to listOf(memberPubKeyHex), "k" to listOf(ConcordKinds.DIRECT_INVITE.toString())))
@@ -21,7 +21,6 @@
package com.vitorpamplona.quartz.concord.cord02Community
import com.vitorpamplona.quartz.concord.cord04Roles.ConcordJson
import com.vitorpamplona.quartz.concord.events.ConcordKinds
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import kotlinx.serialization.Serializable
@@ -64,7 +63,7 @@ class ConcordCommunityListEntry(
/**
* The member's private, self-encrypted list of joined Concord communities
* (kind [ConcordKinds.COMMUNITY_LIST] = 13302, CORD-05) — the NIP-51 analog that
* (kind [ConcordCommunityListEvent.KIND] = 13302, CORD-05) — the NIP-51 analog that
* lets a client return to the groups the user signed up for. Replaceable and
* NIP-44-encrypted to the member's own key, so relays store only ciphertext.
*
@@ -79,7 +78,7 @@ object ConcordCommunityList {
createdAt: Long,
): Event {
val content = signer.nip44Encrypt(encode(entries), signer.pubKey)
return signer.sign(createdAt, ConcordKinds.COMMUNITY_LIST, emptyArray(), content)
return signer.sign(createdAt, ConcordCommunityListEvent.KIND, emptyArray(), content)
}
/** Serializes [entries] to the plaintext JSON that gets NIP-44 self-encrypted. */
@@ -98,7 +97,7 @@ object ConcordCommunityList {
event: Event,
signer: NostrSigner,
): List<ConcordCommunityListEntry> {
if (event.kind != ConcordKinds.COMMUNITY_LIST) return emptyList()
if (event.kind != ConcordCommunityListEvent.KIND) return emptyList()
return try {
decode(signer.nip44Decrypt(event.content, signer.pubKey))
} catch (_: Exception) {
@@ -21,7 +21,6 @@
package com.vitorpamplona.quartz.concord.cord02Community
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.concord.events.ConcordKinds
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
@@ -30,7 +29,7 @@ import com.vitorpamplona.quartz.utils.TimeUtils
/**
* The member's private, self-encrypted list of joined Concord communities (kind
* [ConcordKinds.COMMUNITY_LIST] = 13302, CORD-05). A replaceable event whose
* 13302, CORD-05). A replaceable event whose
* `content` is the NIP-44 self-encryption of the [ConcordCommunityListEntry] JSON
* — including each community's secrets (`community_root`, salt, epoch,
* private-channel keys), so a single event both syncs membership across devices
@@ -60,7 +59,7 @@ class ConcordCommunityListEvent(
}
companion object {
const val KIND = ConcordKinds.COMMUNITY_LIST
const val KIND = 13302
const val ALT = "Private list of joined Concord communities"
/** The replaceable coordinate for a member's list: `(13302, pubkey, "")`. */
@@ -22,7 +22,6 @@ package com.vitorpamplona.quartz.concord.cord04Roles.control
import com.vitorpamplona.quartz.concord.cord04Roles.AuthorityCitation
import com.vitorpamplona.quartz.concord.cord04Roles.ControlEntityKind
import com.vitorpamplona.quartz.concord.events.ConcordKinds
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
@@ -60,7 +59,7 @@ class ControlEditionEvent(
fun authorityCitation() = tags.vac()
companion object {
const val KIND = ConcordKinds.CONTROL
const val KIND = 3308
/**
* Builds the edition template for [entityKind]/[entityId] at [version].
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.quartz.concord.cord05Invites
import com.vitorpamplona.quartz.concord.events.ConcordKinds
import com.vitorpamplona.quartz.concord.cord05Invites.bundle.ConcordInviteBundleEvent
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
@@ -164,7 +164,7 @@ object ConcordInviteLink {
token: ByteArray,
relays: List<String>? = null,
): String {
val naddr = NAddress.create(ConcordKinds.INVITE_BUNDLE, linkSignerPubKey, "", null)
val naddr = NAddress.create(ConcordInviteBundleEvent.KIND, linkSignerPubKey, "", null)
val trimmed = base.trimEnd('/')
return "$trimmed/invite/$naddr#${encodeFragment(token, relays)}"
}
@@ -183,7 +183,7 @@ object ConcordInviteLink {
if (marker < 0) return null
val naddr = url.substring(marker + "/invite/".length, hash)
val parsed = NAddress.parse(naddr) ?: return null
if (parsed.kind != ConcordKinds.INVITE_BUNDLE) return null
if (parsed.kind != ConcordInviteBundleEvent.KIND) return null
return ParsedInviteLink(naddr, parsed.author, parsed.kind, fragment)
}
}
@@ -23,7 +23,6 @@ package com.vitorpamplona.quartz.concord.cord05Invites.bundle
import com.vitorpamplona.quartz.concord.cord04Roles.ControlEntityKind
import com.vitorpamplona.quartz.concord.cord04Roles.control.tags.VskTag
import com.vitorpamplona.quartz.concord.cord04Roles.control.vsk
import com.vitorpamplona.quartz.concord.events.ConcordKinds
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
@@ -57,7 +56,7 @@ class ConcordInviteBundleEvent(
fun versionedSubKind() = tags.vsk()
companion object {
const val KIND = ConcordKinds.INVITE_BUNDLE
const val KIND = 33301
/** Builds the addressable bundle template carrying the already-encrypted [encryptedInvite]. */
fun build(
@@ -55,12 +55,10 @@ object ConcordKinds {
// Person-addressed rumor (CORD-05)
const val DIRECT_INVITE = 3313
// Control / rekey rumors (CORD-02/04/06)
const val CONTROL = 3308
// Rekey rumor (CORD-06). Control 3308 now lives on ControlEditionEvent.KIND.
const val REKEY = 3303
// Bare bookkeeping events
const val INVITE_BUNDLE = 33301 // addressable, CORD-05
const val COMMUNITY_LIST = 13302 // private self-list, CORD-05
// Bare bookkeeping events. Community-list 13302 (ConcordCommunityListEvent.KIND) and
// invite-bundle 33301 (ConcordInviteBundleEvent.KIND) now own their literals.
const val INVITE_LIST = 13303 // private self-list, CORD-05
}
@@ -20,7 +20,7 @@
*/
package com.vitorpamplona.quartz.concord.cord02Community
import com.vitorpamplona.quartz.concord.events.ConcordKinds
import com.vitorpamplona.quartz.concord.cord02Community.ConcordCommunityListEvent
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import kotlinx.coroutines.test.runTest
@@ -53,7 +53,7 @@ class ConcordCommunityListTest {
val entries = listOf(entry("11".repeat(32), "Gamers"), entry("22".repeat(32), "Nostrichs"))
val event = ConcordCommunityList.build(signer, entries, createdAt = 1_700_000_000L)
assertEquals(ConcordKinds.COMMUNITY_LIST, event.kind)
assertEquals(ConcordCommunityListEvent.KIND, event.kind)
assertFalse(event.content.contains("Gamers")) // encrypted on the wire
val parsed = ConcordCommunityList.parse(event, signer)
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.quartz.concord.cord04Roles
import com.vitorpamplona.quartz.concord.cord04Roles.control.ControlEditionEvent
import com.vitorpamplona.quartz.concord.crypto.EditionHash
import com.vitorpamplona.quartz.concord.events.ConcordKinds
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
@@ -69,7 +69,7 @@ class ControlEditionTest {
arrayOf("ep", prev.toHexKey()),
arrayOf("vac", grantId.toHexKey(), "2", grantHash.toHexKey()),
)
val rumor = RumorAssembler.assembleRumor<Event>(author, 1_700_000_000L, ConcordKinds.CONTROL, tags, content)
val rumor = RumorAssembler.assembleRumor<Event>(author, 1_700_000_000L, ControlEditionEvent.KIND, tags, content)
val ed = ControlEdition.fromRumor(rumor)
assertNotNull(ed)
@@ -92,7 +92,7 @@ class ControlEditionTest {
// missing eid
assertNull(
ControlEdition.fromRumor(
RumorAssembler.assembleRumor<Event>(author, 1L, ConcordKinds.CONTROL, arrayOf(arrayOf("vsk", "0"), arrayOf("ev", "0")), "{}"),
RumorAssembler.assembleRumor<Event>(author, 1L, ControlEditionEvent.KIND, arrayOf(arrayOf("vsk", "0"), arrayOf("ev", "0")), "{}"),
),
)
// unknown vsk (bit 7 retired)
@@ -101,7 +101,7 @@ class ControlEditionTest {
RumorAssembler.assembleRumor<Event>(
author,
1L,
ConcordKinds.CONTROL,
ControlEditionEvent.KIND,
arrayOf(arrayOf("vsk", "7"), arrayOf("eid", eid.toHexKey()), arrayOf("ev", "0")),
"{}",
),
@@ -112,7 +112,7 @@ class ControlEditionTest {
@Test
fun genesisHasNullPrevWhenEpAbsent() {
val tags = arrayOf(arrayOf("vsk", "2"), arrayOf("eid", eid.toHexKey()), arrayOf("ev", "0"))
val ed = ControlEdition.fromRumor(RumorAssembler.assembleRumor<Event>(author, 1L, ConcordKinds.CONTROL, tags, """{"name":"general"}"""))
val ed = ControlEdition.fromRumor(RumorAssembler.assembleRumor<Event>(author, 1L, ControlEditionEvent.KIND, tags, """{"name":"general"}"""))
assertNotNull(ed)
assertNull(ed.prevHash)
}
@@ -20,8 +20,8 @@
*/
package com.vitorpamplona.quartz.concord.cord05Invites
import com.vitorpamplona.quartz.concord.cord05Invites.bundle.ConcordInviteBundleEvent
import com.vitorpamplona.quartz.concord.crypto.ConcordKeyDerivation
import com.vitorpamplona.quartz.concord.events.ConcordKinds
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import kotlin.io.encoding.Base64
@@ -83,7 +83,7 @@ class ConcordInviteLinkTest {
val parsed = ConcordInviteLink.parseUrl(url)
assertNotNull(parsed)
assertEquals(signer, parsed.linkSignerPubKey)
assertEquals(ConcordKinds.INVITE_BUNDLE, parsed.kind)
assertEquals(ConcordInviteBundleEvent.KIND, parsed.kind)
assertContentEquals(token, parsed.fragment.token)
}