fix: drill relay attribution into Marmot kind-445 inner notes

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEiq1NMK3q12KGQ2yYhPEp
This commit is contained in:
Claude
2026-07-10 23:01:45 +00:00
parent 1cffd8db18
commit f151cb74fc
3 changed files with 31 additions and 1 deletions
@@ -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
@@ -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)
@@ -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<Array<String>>,
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.