From f151cb74fcc0fcd3b1c88543f2969d2e333acf0c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 23:01:45 +0000 Subject: [PATCH] fix: drill relay attribution into Marmot kind-445 inner notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit follow-up to the gift-wrap relay-icon fix: Marmot group chat rows render the decrypted inner note (kind 9/7) via the standard chat feed with RelayBadgesHorizontal, but nothing ever populated that note's relay list — OK acceptances and relay deliveries all landed on the kind-445 envelope, which has no link to its inner event. - GroupEvent now implements HasInnerEvent (same @Transient @Volatile innerEventId pattern as GiftWrapEvent/SealedRumorEvent), so LocalCache.addRelayToNoteAndInners drills 445 -> inner for both OK confirmations and duplicate EVENT deliveries. RouteMaker is unaffected: it gates on the concrete wrap types before casting to HasInnerEvent. - GroupEventHandler sets innerEventId at decrypt time and copies the envelope's accumulated relays down to the inner note (looked up by event.id, not the eventNote/publicNote params, which belong to the triggering event when replayed from retryPendingFor). - sendMarmotGroupMessage sets innerEventId on the freshly built envelope before consuming/publishing, so acceptances for sent group messages reach the rendered note as soon as the inner note is indexed. Also audited the remaining chat-rendered types: NIP-04 PrivateDmEvent, ChatMessageEvent rumors, NIP-C7 ChatEvent, ChannelMessage/Ephemeral/ LiveActivities messages all route through consumeRegularEvent (duplicates covered by the shared helper), and NIP-37 drafts render the wrap note itself with markAsSeen covering the version/addressable pair. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01YEiq1NMK3q12KGQ2yYhPEp --- .../com/vitorpamplona/amethyst/model/Account.kt | 4 ++++ .../screen/loggedIn/DecryptAndIndexProcessor.kt | 12 ++++++++++++ .../marmot/mip03GroupMessages/GroupEvent.kt | 16 +++++++++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 91234aadf7..7c67921322 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2632,6 +2632,10 @@ class Account( Log.d("MarmotDbg") { "sendMarmotGroupMessage: built outer kind:${outbound.signedEvent.kind} id=${outbound.signedEvent.id.take(8)}…" } + // Link the envelope to the inner message we just encrypted so relay + // OK acceptances drill down to the note the chat renders (see + // LocalCache.addRelayToNoteAndInners). + outbound.signedEvent.innerEventId = innerEvent.id cache.justConsumeMyOwnEvent(outbound.signedEvent) // Sending a message moves the group out of "New Requests" into // "Known" — do this eagerly before relay round-trip so the UI diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index e05a9627a1..7c5eb319e8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -617,6 +617,18 @@ class GroupEventHandler( } } + // Link the envelope to its inner note and copy over the + // relays that delivered/accepted the kind-445 so far, so + // the chat row shows relay icons. Later acceptances drill + // down on their own via LocalCache.addRelayToNoteAndInners + // once innerEventId is set. The outer note is looked up by + // event.id — NOT eventNote/publicNote, which belong to the + // *triggering* event when this runs from retryPendingFor. + event.innerEventId = innerEvent.id + cache.getNoteIfExists(event.id)?.let { outerNote -> + cache.copyRelaysFromTo(outerNote, innerEvent.id) + } + // Track the message in the Marmot group chatroom account.marmotGroupList.addMessage(result.groupId, innerNote) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip03GroupMessages/GroupEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip03GroupMessages/GroupEvent.kt index 396aacdcd7..de02441755 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip03GroupMessages/GroupEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mip03GroupMessages/GroupEvent.kt @@ -25,7 +25,9 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip59Giftwrap.HasInnerEvent import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.concurrent.Volatile /** * Marmot Group Event (MIP-03) — kind 445. @@ -59,7 +61,19 @@ class GroupEvent( tags: Array>, content: String, sig: HexKey, -) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { +) : Event(id, pubKey, createdAt, KIND, tags, content, sig), + HasInnerEvent { + // Set when the app layer learns the envelope ↔ inner mapping: on decrypt + // for inbound events, at build time for outbound ones. Lets relay + // attribution (OK acceptances, duplicate deliveries) drill from the + // kind-445 envelope down to the inner note the chat UI renders. + // `@Volatile`: written by the decrypt coroutine, read by relay socket + // threads. + @kotlinx.serialization.Transient + @kotlin.jvm.Transient + @Volatile + override var innerEventId: HexKey? = null + /** * Base64-encoded encrypted content: nonce(12 bytes) || ciphertext. * Decrypt with ChaCha20-Poly1305 using the MLS exporter-derived key.