Merge pull request #1943 from dadofsambonzuki/attestation-fix

Align attestation events with latest NIP format
This commit is contained in:
Vitor Pamplona
2026-03-25 07:49:07 -04:00
committed by GitHub
13 changed files with 19 additions and 180 deletions
@@ -84,7 +84,6 @@ import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.quartz.experimental.attestations.attestation.AttestationEvent
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.AttestationStatus
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.Validity
import com.vitorpamplona.quartz.experimental.attestations.proficiency.AttestorProficiencyEvent
import com.vitorpamplona.quartz.experimental.attestations.recommendation.AttestorRecommendationEvent
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
@@ -106,8 +105,7 @@ fun RenderAttestationPreview() {
arrayOf(
arrayOf("d", "af5aa898:fe108febb997:1773941524"),
arrayOf("e", "fe108febb99796c4091775e00aa1fc3ffc489ad22fdf1f8c559b2472815c09c7"),
arrayOf("s", "verified"),
arrayOf("v", "valid"),
arrayOf("s", "valid"),
arrayOf("client", "attestr.xyz"),
),
)
@@ -161,19 +159,17 @@ fun RenderAttestation(
accountViewModel: AccountViewModel,
nav: INav,
) {
val validity = remember(noteEvent) { noteEvent.validity() }
val status = remember(noteEvent) { noteEvent.status() }
val validFrom = remember(noteEvent) { noteEvent.validFrom() }
val validTo = remember(noteEvent) { noteEvent.validTo() }
val content = remember(noteEvent) { noteEvent.content.ifBlank { null } }
val statusColor = remember(status, validity) { attestationColor(status, validity) }
val statusIcon = remember(status, validity) { attestationIcon(status, validity) }
val statusLabel = attestationStatusLabel(status, validity)
val statusColor = remember(status) { attestationColor(status) }
val statusIcon = remember(status) { attestationIcon(status) }
val statusLabel = attestationStatusLabel(status)
val aboutAddress = remember(noteEvent) { noteEvent.assertionAddress() }
val aboutEvent = remember(noteEvent) { noteEvent.assertionEventId() }
val aboutPubkey = remember(noteEvent) { noteEvent.assertionPubkey() }
Column(
modifier =
@@ -257,13 +253,6 @@ fun RenderAttestation(
)
}
}
} else if (aboutPubkey != null) {
LoadUser(aboutPubkey, accountViewModel) {
if (it != null) {
Spacer(modifier = DoubleVertSpacer)
UserCompose(it, accountViewModel = accountViewModel, nav = nav)
}
}
}
}
@@ -592,47 +581,35 @@ fun RenderAttestorProficiency(
private fun attestationColor(
status: AttestationStatus?,
validity: Validity?,
): Color =
when {
validity == Validity.INVALID -> Color(0xFFB71C1C)
validity == Validity.VALID -> Color(0xFF2E7D32)
status == AttestationStatus.INVALID -> Color(0xFFB71C1C)
status == AttestationStatus.VALID -> Color(0xFF2E7D32)
status == AttestationStatus.REVOKED -> Color(0xFFB21CB7)
status == AttestationStatus.REJECTED -> Color(0xFFB71C1C)
status == AttestationStatus.VERIFIED -> Color(0xFF2E7D32)
status == AttestationStatus.VERIFYING -> Color(0xFF173CF5)
status == AttestationStatus.ACCEPTED -> Color(0xFF15C0C0)
else -> Color(0xFF757575)
}
private fun attestationIcon(
status: AttestationStatus?,
validity: Validity?,
): ImageVector =
when {
validity == Validity.INVALID -> Icons.Default.Close
validity == Validity.VALID -> Icons.Default.CheckCircle
status == AttestationStatus.INVALID -> Icons.Default.Close
status == AttestationStatus.VALID -> Icons.Default.CheckCircle
status == AttestationStatus.REVOKED -> Icons.Default.RemoveDone
status == AttestationStatus.REJECTED -> Icons.Default.Delete
status == AttestationStatus.VERIFIED -> Icons.Default.Approval
status == AttestationStatus.VERIFYING -> Icons.Default.HourglassTop
status == AttestationStatus.ACCEPTED -> Icons.Default.HourglassEmpty
else -> Icons.Default.ErrorOutline
}
@Composable
private fun attestationStatusLabel(
status: AttestationStatus?,
validity: Validity?,
): String =
when {
validity == Validity.INVALID -> stringRes(R.string.attestation_invalid)
validity == Validity.VALID -> stringRes(R.string.attestation_valid)
status == AttestationStatus.INVALID -> stringRes(R.string.attestation_invalid)
status == AttestationStatus.VALID -> stringRes(R.string.attestation_valid)
status == AttestationStatus.REVOKED -> stringRes(R.string.attestation_status_revoked)
status == AttestationStatus.REJECTED -> stringRes(R.string.attestation_status_rejected)
status == AttestationStatus.VERIFIED -> stringRes(R.string.attestation_status_verified)
status == AttestationStatus.VERIFYING -> stringRes(R.string.attestation_status_verifying)
status == AttestationStatus.ACCEPTED -> stringRes(R.string.attestation_status_accepted)
else -> stringRes(R.string.attestation)
}
@@ -22,7 +22,6 @@ package com.vitorpamplona.quartz.experimental.attestations.attestation
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.AttestationStatus
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.Validity
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
@@ -32,17 +31,14 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.serialization.json.JsonNull.content
@Immutable
class AttestationEvent(
@@ -54,8 +50,7 @@ class AttestationEvent(
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
EventHintProvider,
AddressHintProvider,
PubKeyHintProvider {
AddressHintProvider {
override fun eventHints(): List<EventIdHint> = tags.mapNotNull(ETag::parseAsHint)
override fun linkedEventIds(): List<HexKey> = tags.mapNotNull(ETag::parseId)
@@ -64,12 +59,6 @@ class AttestationEvent(
override fun linkedAddressIds(): List<String> = tags.mapNotNull(ATag::parseAddressId)
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
fun validity() = tags.validity()
fun status() = tags.status()
fun validFrom() = tags.validFrom()
@@ -94,10 +83,6 @@ class AttestationEvent(
fun assertionETag() = tags.firstNotNullOfOrNull(ETag::parse)
fun assertionPubkey() = tags.firstNotNullOfOrNull(PTag::parseKey)
fun assertionPTag() = tags.firstNotNullOfOrNull(PTag::parse)
companion object {
const val KIND = 31871
const val ALT_DESCRIPTION = "Attestation"
@@ -106,7 +91,6 @@ class AttestationEvent(
dTagId: String,
about: EventHintBundle<Event>,
content: String = "",
validity: Validity? = null,
status: AttestationStatus? = null,
validFrom: Long? = null,
validTo: Long? = null,
@@ -117,7 +101,6 @@ class AttestationEvent(
alt(ALT_DESCRIPTION)
dTag(dTagId)
about(about)
validity?.let { validity(it) }
status?.let { status(it) }
validFrom?.let { validFrom(it) }
validTo?.let { validTo(it) }
@@ -129,7 +112,6 @@ class AttestationEvent(
dTagId: String,
about: EventHintBundle<BaseReplaceableEvent>,
content: String = "",
validity: Validity? = null,
status: AttestationStatus? = null,
validFrom: Long? = null,
validTo: Long? = null,
@@ -140,7 +122,6 @@ class AttestationEvent(
alt(ALT_DESCRIPTION)
dTag(dTagId)
aboutReplaceable(about)
validity?.let { validity(it) }
status?.let { status(it) }
validFrom?.let { validFrom(it) }
validTo?.let { validTo(it) }
@@ -152,7 +133,6 @@ class AttestationEvent(
dTagId: String,
about: EventHintBundle<BaseAddressableEvent>,
content: String = "",
validity: Validity? = null,
status: AttestationStatus? = null,
validFrom: Long? = null,
validTo: Long? = null,
@@ -163,7 +143,6 @@ class AttestationEvent(
alt(ALT_DESCRIPTION)
dTag(dTagId)
aboutAddressable(about)
validity?.let { validity(it) }
status?.let { status(it) }
validFrom?.let { validFrom(it) }
validTo?.let { validTo(it) }
@@ -25,8 +25,6 @@ import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.Reque
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.StatusTag
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.ValidFromTag
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.ValidToTag
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.Validity
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.ValidityTag
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent
@@ -36,8 +34,6 @@ import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
fun TagArrayBuilder<AttestationEvent>.validity(validity: Validity) = addUnique(ValidityTag.assemble(validity))
fun TagArrayBuilder<AttestationEvent>.status(status: AttestationStatus) = addUnique(StatusTag.assemble(status))
fun TagArrayBuilder<AttestationEvent>.validFrom(timestamp: Long) = addUnique(ValidFromTag.assemble(timestamp))
@@ -24,11 +24,8 @@ import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.Reque
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.StatusTag
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.ValidFromTag
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.ValidToTag
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.ValidityTag
import com.vitorpamplona.quartz.nip01Core.core.TagArray
fun TagArray.validity() = firstNotNullOfOrNull(ValidityTag::parse)
fun TagArray.status() = firstNotNullOfOrNull(StatusTag::parse)
fun TagArray.validFrom() = firstNotNullOfOrNull(ValidFromTag::parse)
@@ -26,10 +26,9 @@ import com.vitorpamplona.quartz.utils.ensure
enum class AttestationStatus(
val code: String,
) {
ACCEPTED("accepted"),
REJECTED("rejected"),
VERIFYING("verifying"),
VERIFIED("verified"),
VALID("valid"),
INVALID("invalid"),
REVOKED("revoked"),
}
@@ -45,10 +44,9 @@ class StatusTag {
ensure(tag[1].isNotEmpty()) { return null }
return when (tag[1]) {
AttestationStatus.ACCEPTED.code -> AttestationStatus.ACCEPTED
AttestationStatus.REJECTED.code -> AttestationStatus.REJECTED
AttestationStatus.VERIFYING.code -> AttestationStatus.VERIFYING
AttestationStatus.VERIFIED.code -> AttestationStatus.VERIFIED
AttestationStatus.VALID.code -> AttestationStatus.VALID
AttestationStatus.INVALID.code -> AttestationStatus.INVALID
AttestationStatus.REVOKED.code -> AttestationStatus.REVOKED
else -> null
}
@@ -1,53 +0,0 @@
/*
* 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.attestations.attestation.tags
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.utils.ensure
enum class Validity(
val code: String,
) {
VALID("valid"),
INVALID("invalid"),
}
class ValidityTag {
companion object {
const val TAG_NAME = "v"
fun isTag(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
fun parse(tag: Array<String>): Validity? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return when (tag[1]) {
Validity.VALID.code -> Validity.VALID
Validity.INVALID.code -> Validity.INVALID
else -> null
}
}
fun assemble(validity: Validity) = arrayOf(TAG_NAME, validity.code)
}
}
@@ -40,7 +40,7 @@ class AttestorProficiencyEvent(
) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
fun kinds() = tags.kinds()
fun description() = tags.description()
fun description() = content.ifBlank { null }
companion object {
const val KIND = 11871
@@ -51,10 +51,9 @@ class AttestorProficiencyEvent(
description: String? = null,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<AttestorProficiencyEvent>.() -> Unit = {},
) = eventTemplate(KIND, "", createdAt) {
) = eventTemplate(KIND, description ?: "", createdAt) {
alt(ALT_DESCRIPTION)
kinds(kinds)
description?.let { desc(it) }
initializer()
}
}
@@ -20,11 +20,8 @@
*/
package com.vitorpamplona.quartz.experimental.attestations.proficiency
import com.vitorpamplona.quartz.experimental.attestations.recommendation.tags.DescriptionTag
import com.vitorpamplona.quartz.experimental.attestations.recommendation.tags.KindTag
import com.vitorpamplona.quartz.nip01Core.core.Kind
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
fun TagArrayBuilder<AttestorProficiencyEvent>.kinds(kinds: List<Kind>) = addAll(KindTag.assemble(kinds))
fun TagArrayBuilder<AttestorProficiencyEvent>.desc(description: String) = addUnique(DescriptionTag.assemble(description))
@@ -20,10 +20,7 @@
*/
package com.vitorpamplona.quartz.experimental.attestations.proficiency
import com.vitorpamplona.quartz.experimental.attestations.recommendation.tags.DescriptionTag
import com.vitorpamplona.quartz.experimental.attestations.recommendation.tags.KindTag
import com.vitorpamplona.quartz.nip01Core.core.TagArray
fun TagArray.kinds() = mapNotNull(KindTag::parse)
fun TagArray.description() = firstNotNullOfOrNull(DescriptionTag::parse)
@@ -41,7 +41,7 @@ class AttestorRecommendationEvent(
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
fun kinds() = tags.kinds()
fun description() = tags.description()
fun description() = content.ifBlank { null }
companion object {
const val KIND = 31873
@@ -53,11 +53,10 @@ class AttestorRecommendationEvent(
description: String? = null,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<AttestorRecommendationEvent>.() -> Unit = {},
) = eventTemplate(KIND, "", createdAt) {
) = eventTemplate(KIND, description ?: "", createdAt) {
alt(ALT_DESCRIPTION)
dTag(attestorPubKey)
kinds(kinds)
description?.let { desc(it) }
initializer()
}
}
@@ -20,11 +20,8 @@
*/
package com.vitorpamplona.quartz.experimental.attestations.recommendation
import com.vitorpamplona.quartz.experimental.attestations.recommendation.tags.DescriptionTag
import com.vitorpamplona.quartz.experimental.attestations.recommendation.tags.KindTag
import com.vitorpamplona.quartz.nip01Core.core.Kind
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
fun TagArrayBuilder<AttestorRecommendationEvent>.kinds(kinds: List<Kind>) = addAll(KindTag.assemble(kinds))
fun TagArrayBuilder<AttestorRecommendationEvent>.desc(description: String) = addUnique(DescriptionTag.assemble(description))
@@ -20,10 +20,7 @@
*/
package com.vitorpamplona.quartz.experimental.attestations.recommendation
import com.vitorpamplona.quartz.experimental.attestations.recommendation.tags.DescriptionTag
import com.vitorpamplona.quartz.experimental.attestations.recommendation.tags.KindTag
import com.vitorpamplona.quartz.nip01Core.core.TagArray
fun TagArray.kinds() = mapNotNull(KindTag::parse)
fun TagArray.description() = firstNotNullOfOrNull(DescriptionTag::parse)
@@ -1,41 +0,0 @@
/*
* 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.attestations.recommendation.tags
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.utils.ensure
class DescriptionTag {
companion object {
const val TAG_NAME = "desc"
fun isTag(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
fun parse(tag: Array<String>): String? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return tag[1]
}
fun assemble(description: String) = arrayOf(TAG_NAME, description)
}
}