From 37a4f891783ab6ccac6adde4b79e6ce36ec87774 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Apr 2026 03:45:12 +0000 Subject: [PATCH] =?UTF-8?q?feat(cli):=20drop=20relays.json=20=E2=80=94=20k?= =?UTF-8?q?ind:10002/10050/10051=20in=20the=20store=20ARE=20the=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The local relay configuration was redundant once the event store became the source of truth: every account already publishes signed kind:10002 (NIP-65), kind:10050 (DM inbox), and kind:10051 (KeyPackage relays) events, and Context now reads each set straight out of the local store. Removed: - data class RelayConfig (nip65/inbox/keyPackage buckets) - DataDir.relaysFile / loadRelays() / saveRelays() - Context.relays parameter — Context.open() no longer reads JSON - CreateCommand.defaultRelayConfig() helper - CreateCommand's saveRelays() call (redundant: ctx.publish persists the bootstrap events into the store anyway) Context now serves the four relay accessors from the store with sane fallbacks: outboxRelays() = relaysOf(self)?.writeRelaysNorm() ?: DefaultNIP65RelaySet inboxRelays() = dmInboxOf(self)?.relays() ?: DefaultDMRelayList.toSet() keyPackageRelays() = keyPackageRelaysOf(self)?.relays() ?: outboxRelays() anyRelays() = union of the three bootstrapRelays() = anyRelays() ∪ DefaultNIP65RelaySet ∪ DefaultDMRelayList RelayCommands rewritten: - `relay add URL --type T` — read existing relay-list event from the store, append URL, build + sign + ingest a new event of the matching kind. The replaceable slot mechanism replaces the old winner atomically. - `relay list` — dump URLs from the latest event in each bucket. - `relay publish-lists` — broadcast whichever events are present in the store; errors out cleanly when none exist (suggests `relay add` or `create`). No migration: existing data dirs that still have `relays.json` will ignore it. Run `amy relay add` or `amy create` to repopulate. Per discussion this is a clean break, not a backwards-compat shim. README + DEVELOPMENT updated to reflect the new on-disk layout (no more `relays.json`; events-store/ is the relay configuration). --- cli/DEVELOPMENT.md | 4 +- cli/README.md | 23 ++- .../com/vitorpamplona/amethyst/cli/Config.kt | 52 ------ .../com/vitorpamplona/amethyst/cli/Context.kt | 36 +++- .../amethyst/cli/commands/CreateCommand.kt | 35 ++-- .../amethyst/cli/commands/RelayCommands.kt | 161 +++++++++++++----- 6 files changed, 177 insertions(+), 134 deletions(-) diff --git a/cli/DEVELOPMENT.md b/cli/DEVELOPMENT.md index 63f04477b..21dea1aef 100644 --- a/cli/DEVELOPMENT.md +++ b/cli/DEVELOPMENT.md @@ -43,7 +43,7 @@ cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/ ├── Main.kt # argv → subcommand dispatch ├── Args.kt # tiny flag parser (no framework) ├── Json.kt # single-line stdout + error printer -├── Config.kt # Identity, RelayConfig, RunState, DataDir +├── Config.kt # Identity, RunState, DataDir ├── Context.kt # per-run wiring: signer + NostrClient + │ # MarmotManager + publish/drain/sync helpers ├── stores/FileStores.kt # File-backed MLS / KP / message stores @@ -196,7 +196,7 @@ Amy-specific layer still needs its own coverage: | Argument parsing (`Args`, flag forms, `--data-dir=…` vs `--data-dir …`) | Plain JVM unit tests in `cli/src/test/kotlin/`. | | Error / exit-code contract (bad args → 2, await timeout → 124, runtime → 1) | Table-driven tests invoking `main(argv)` with captured stdout/stderr. | | JSON output shape (each command's keys and types) | Snapshot tests: run a command against a throwaway data-dir, assert the JSON matches a golden file. | -| File layout on disk (`identity.json`, `relays.json`, `marmot/groups/*.mls`, `marmot/keypackages.bundle`) | Structural assertions after a command sequence. | +| File layout on disk (`identity.json`, `events-store/…`, `marmot/groups/*.mls`, `marmot/keypackages.bundle`) | Structural assertions after a command sequence. | | Round-trip between two data-dirs on a local relay | End-to-end shell harnesses under `cli/tests/`. Each harness spins up a local `nostr-rs-relay`, bootstraps two or more fresh identities in their own `--data-dir`s, and drives a scenario via `amy` (+ `wn` for Marmot interop against whitenoise-rs). Today there are two suites: `cli/tests/marmot/` (13 MLS scenarios vs whitenoise-rs) and `cli/tests/dm/` (NIP-17 DM round-trips between two `amy` clients). | | Interop with other clients | Covered by `cli/tests/marmot/marmot-interop-headless.sh` (drives Amy against whitenoise-rs `wn`/`wnd`). Add new scenarios there or start a new sibling under `cli/tests/`. | diff --git a/cli/README.md b/cli/README.md index 48a872fa2..4944509bb 100644 --- a/cli/README.md +++ b/cli/README.md @@ -260,8 +260,13 @@ events. ``` / ├── identity.json # nsec/npub/hex — the account -├── relays.json # nip65 / inbox / key_package buckets ├── state.json # sync cursors (giftWrapSince, groupSince) +├── events-store/ # FsEventStore — every observed Nostr event +│ ├── events///… # canonical kind:0 / 3 / 10002 / 10050 / 10051 / 1 / 5 / 1059 / … +│ ├── replaceable//… # one slot per (kind, pubkey) for kind:0/3/10000-19999 +│ ├── addressable/… # one slot per (kind, pubkey, d-tag) for kind:30000-39999 +│ ├── idx/ # hardlink indexes (kind / author / owner / tag / fts / expires_at) +│ └── tombstones/ # NIP-09 / NIP-62 enforcement └── marmot/ ├── keypackages.bundle # MLS KeyPackage bundles (NostrSignerInternal) └── groups/ @@ -272,6 +277,13 @@ events. All files are plain JSON or framed binary — human-inspectable, easy to diff across two data-dirs in a test run. +The local relay configuration (kind:10002 / 10050 / 10051) is **not** a +separate file — it lives in `events-store/` as signed events. +`amy relay add` builds + signs + ingests a new relay-list event; +`amy relay list` reads URLs straight out of the latest event for each +kind; `amy relay publish-lists` broadcasts those events to upstream +relays. There is no `relays.json`. + --- ## Troubleshooting @@ -281,9 +293,10 @@ diff across two data-dirs in a test run. - **`not_member`** — the group GID is unknown to this data-dir. Run `marmot group list` to confirm, or `marmot await group --name …` to wait for an invite to arrive. -- **Hang on a network verb** — Amy connects to the relays in - `relays.json`; verify with `amy relay list`. Every network-bound - operation has a timeout — use `--timeout` for `await`, or wrap the - whole command in `timeout(1)` if you're scripting. +- **Hang on a network verb** — Amy connects to the relays advertised + in your local kind:10002 / 10050 / 10051 events; inspect with + `amy relay list`. Every network-bound operation has a timeout — use + `--timeout` for `await`, or wrap the whole command in `timeout(1)` + if you're scripting. - **Nothing seems to publish** — inspect stderr; each publish prints per-relay `OK` / `REJECT` via the `[cli] …` traces. diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Config.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Config.kt index e54d80b1e..e260013c2 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Config.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Config.kt @@ -24,8 +24,6 @@ import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.nip19Bech32.toNsec @@ -81,49 +79,6 @@ data class Identity( } } -/** - * On-disk relay configuration, bucketed by purpose (mirrors `wn relays add --type`). - * - * - `nip65`: advertised read/write (kind:10002) - * - `inbox`: DM inbox / gift-wrap delivery (kind:10050) - * - `keyPackage`: where this account's KeyPackages (kind:30443) live - */ -data class RelayConfig( - val nip65: MutableList = mutableListOf(), - val inbox: MutableList = mutableListOf(), - val keyPackage: MutableList = mutableListOf(), -) { - fun all(): Set = (nip65 + inbox + keyPackage).toSet() - - fun add( - type: String, - url: String, - ): Boolean { - val list = - when (type) { - "nip65" -> nip65 - "inbox" -> inbox - "key_package", "keyPackage" -> keyPackage - else -> throw IllegalArgumentException("unknown relay type: $type") - } - if (list.contains(url)) return false - list.add(url) - return true - } - - fun normalized(kind: String): Set { - val src = - when (kind) { - "nip65" -> nip65 - "inbox" -> inbox - "key_package" -> keyPackage - "all" -> all().toList() - else -> throw IllegalArgumentException("unknown relay selector: $kind") - } - return src.mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) }.toSet() - } -} - /** Opaque per-run state (subscription cursors, etc). Stored alongside identity. */ data class RunState( var giftWrapSince: Long? = null, @@ -138,7 +93,6 @@ class DataDir( val root: File, ) { val identityFile = File(root, "identity.json") - val relaysFile = File(root, "relays.json") val stateFile = File(root, "state.json") val marmotDir = File(root, "marmot") val groupsDir = File(marmotDir, "groups") @@ -158,12 +112,6 @@ class DataDir( identityFile.writeText(Json.mapper.writeValueAsString(id)) } - fun loadRelays(): RelayConfig = if (relaysFile.exists()) Json.mapper.readValue(relaysFile.readText()) else RelayConfig() - - fun saveRelays(r: RelayConfig) { - relaysFile.writeText(Json.mapper.writeValueAsString(r)) - } - fun loadRunState(): RunState = if (stateFile.exists()) Json.mapper.readValue(stateFile.readText()) else RunState() fun saveRunState(s: RunState) { diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Context.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Context.kt index ca6b1d8fd..f0643d25c 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Context.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/Context.kt @@ -89,7 +89,6 @@ import okhttp3.OkHttpClient class Context( val dataDir: DataDir, val identity: Identity, - val relays: RelayConfig, val state: RunState, ) : AutoCloseable { val signer = NostrSignerInternal(identity.keyPair()) @@ -156,13 +155,39 @@ class Context( .resolveUserHexOrNull(input, nip05Client) ?: throw IllegalArgumentException("Could not resolve user: '$input' (accepts npub, nprofile, 64-hex, or name@domain.tld)") - fun outboxRelays(): Set = relays.normalized("nip65") + /** + * Outbox / NIP-65 write relays for this account. Read from the + * local kind:10002 (after `amy create` or `amy relay publish-lists` + * has written one); falls back to [DefaultNIP65RelaySet] when no + * advertised list exists yet. + * + * Returns *write*-marked URLs only — the semantic is "where I + * publish from", which mirrors `User.outboxRelays()` in the + * Android app. + */ + fun outboxRelays(): Set = + relaysOf(identity.pubKeyHex)?.writeRelaysNorm()?.takeIf { it.isNotEmpty() }?.toSet() + ?: DefaultNIP65RelaySet - fun inboxRelays(): Set = relays.normalized("inbox") + /** + * DM inbox relays (NIP-17 kind:10050) for this account. Falls back + * to [DefaultDMRelayList] when no kind:10050 has been seen. + */ + fun inboxRelays(): Set = + dmInboxOf(identity.pubKeyHex)?.relays()?.takeIf { it.isNotEmpty() }?.toSet() + ?: DefaultDMRelayList.toSet() - fun keyPackageRelays(): Set = relays.normalized("key_package") + /** + * KeyPackage relays (MIP-00 kind:10051) for this account. Falls + * back to [outboxRelays] when no kind:10051 has been seen — same + * fallback the Android app uses for KeyPackage discovery. + */ + fun keyPackageRelays(): Set = + keyPackageRelaysOf(identity.pubKeyHex)?.relays()?.takeIf { it.isNotEmpty() }?.toSet() + ?: outboxRelays() - fun anyRelays(): Set = relays.normalized("all") + /** Union of all three buckets. */ + fun anyRelays(): Set = outboxRelays() + inboxRelays() + keyPackageRelays() /** * Seed relays for "look up someone we know nothing about" queries — @@ -578,7 +603,6 @@ class Context( return Context( dataDir = dataDir, identity = identity, - relays = dataDir.loadRelays(), state = dataDir.loadRunState(), ) } diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/CreateCommand.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/CreateCommand.kt index b128d8999..56fb0826d 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/CreateCommand.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/CreateCommand.kt @@ -25,7 +25,6 @@ import com.vitorpamplona.amethyst.cli.Context import com.vitorpamplona.amethyst.cli.DataDir import com.vitorpamplona.amethyst.cli.Identity import com.vitorpamplona.amethyst.cli.Json -import com.vitorpamplona.amethyst.cli.RelayConfig import com.vitorpamplona.amethyst.commons.account.bootstrapAccountEvents import com.vitorpamplona.amethyst.commons.defaults.DefaultDMRelayList import com.vitorpamplona.amethyst.commons.defaults.DefaultNIP65List @@ -34,9 +33,14 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync /** * `amy create [--name NAME]` — provision a brand-new Nostr account with the - * same defaults Amethyst uses, publish the nine bootstrap events to the - * default NIP-65 relay set, and seed this data-dir's relay config so - * subsequent `amy marmot …` commands immediately target the right relays. + * same defaults Amethyst uses and publish the nine bootstrap events to the + * default NIP-65 relay set. + * + * The bootstrap events include kind:10002 / 10050 / 10051, which are + * persisted into [Context.store] as a side effect of [Context.publish]. + * From that point on, [Context.outboxRelays], [Context.inboxRelays], and + * [Context.keyPackageRelays] read straight from the local store — no + * separate `relays.json` is needed. * * The heavy lifting (which events to sign, with which defaults) lives in * `commons/.../AccountBootstrap.kt` so this command stays assembly-thin @@ -53,10 +57,9 @@ object CreateCommand { val args = Args(rest) val name = args.flag("name") - // 1. Mint identity + seed relay config. + // 1. Mint identity. val identity = Identity.create() dataDir.saveIdentity(identity) - dataDir.saveRelays(defaultRelayConfig()) // 2. Build the nine signed bootstrap events via the shared helper. val signer = NostrSignerSync(identity.keyPair()) @@ -64,6 +67,10 @@ object CreateCommand { // 3. Open a Context so we reuse publishAndConfirmDetailed + the // NostrClient plumbing instead of reinventing a second client. + // Each ctx.publish call also persists the event into the local + // store via verifyAndStore — so kind:10002 / 10050 / 10051 are + // immediately readable by outboxRelays() / inboxRelays() / + // keyPackageRelays() without a separate config file. val ctx = Context.open(dataDir) val accepted = mutableMapOf>() try { @@ -95,20 +102,4 @@ object CreateCommand { ) return 0 } - - /** - * Mirror of what Amethyst's in-app defaults would write on disk — NIP-65 - * outbox, DM inbox (kind:10050), and KeyPackage host relays (kind:10051). - * Keeping these in sync with the signed events above is load-bearing: - * `amy marmot key-package publish` later reads `relays.json` to decide - * where to publish, and if those diverge from the advertised kind:10051 - * nobody will find the KPs. - */ - private fun defaultRelayConfig(): RelayConfig { - val cfg = RelayConfig() - DefaultNIP65List.forEach { cfg.add("nip65", it.relayUrl.url) } - DefaultDMRelayList.forEach { cfg.add("inbox", it.url) } - DefaultNIP65RelaySet.forEach { cfg.add("key_package", it.url) } - return cfg - } } diff --git a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/RelayCommands.kt b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/RelayCommands.kt index 1cbe0ff56..d550e47e8 100644 --- a/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/RelayCommands.kt +++ b/cli/src/main/kotlin/com/vitorpamplona/amethyst/cli/commands/RelayCommands.kt @@ -25,11 +25,28 @@ import com.vitorpamplona.amethyst.cli.Context import com.vitorpamplona.amethyst.cli.DataDir import com.vitorpamplona.amethyst.cli.Json import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrlOrNull import com.vitorpamplona.quartz.nip17Dm.settings.ChatMessageRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayType +/** + * `amy relay ` — manage this account's relay sets. + * + * Source of truth is the local event store (`/events-store/`) + * via Context.relaysOf / dmInboxOf / keyPackageRelaysOf. There is no + * `relays.json` any more — the kind:10002 / 10050 / 10051 events ARE + * the relay configuration. + * + * - `relay add URL --type T` builds + signs + ingests a new relay- + * list event for the given bucket. No + * broadcast yet — call `publish-lists`. + * - `relay list` dumps the URLs from the local store. + * - `relay publish-lists` broadcasts the three events to every + * configured relay (union). + */ object RelayCommands { suspend fun dispatch( dataDir: DataDir, @@ -46,78 +63,128 @@ object RelayCommands { } } - private fun add( + private suspend fun add( dataDir: DataDir, args: Args, ): Int { - val url = args.positional(0, "url") + val rawUrl = args.positional(0, "url") val type = args.flag("type", "all") ?: "all" - val cfg = dataDir.loadRelays() - val addedTo = mutableListOf() + val normalized = + rawUrl.normalizeRelayUrlOrNull() + ?: return Json.error("bad_args", "invalid relay url: $rawUrl") + val targets = if (type == "all") listOf("nip65", "inbox", "key_package") else listOf(type) - for (t in targets) { - if (cfg.add(t, url)) addedTo.add(t) + val ctx = Context.open(dataDir) + try { + val addedTo = mutableListOf() + val alreadyPresent = mutableListOf() + for (t in targets) { + if (addToBucket(ctx, t, normalized)) addedTo.add(t) else alreadyPresent.add(t) + } + Json.writeLine( + mapOf( + "url" to rawUrl, + "added_to" to addedTo, + "already_present" to alreadyPresent, + ), + ) + return 0 + } finally { + ctx.close() + } + } + + /** + * Append [url] to the relay-list event for [type] (creating one if + * absent). Returns `true` when a new event was inserted, `false` + * when [url] was already present in the existing event. + */ + private suspend fun addToBucket( + ctx: Context, + type: String, + url: NormalizedRelayUrl, + ): Boolean { + val self = ctx.identity.pubKeyHex + return when (type) { + "nip65" -> { + val existing = ctx.relaysOf(self)?.relays().orEmpty() + if (existing.any { it.relayUrl.url == url.url }) return false + val combined = existing + AdvertisedRelayInfo(url, AdvertisedRelayType.BOTH) + val event = AdvertisedRelayListEvent.create(combined, ctx.signer) + ctx.verifyAndStore(event) + true + } + + "inbox" -> { + val existing = ctx.dmInboxOf(self)?.relays().orEmpty() + if (url in existing) return false + val event = ChatMessageRelayListEvent.create(existing + url, ctx.signer) + ctx.verifyAndStore(event) + true + } + + "key_package", "keyPackage" -> { + val existing = ctx.keyPackageRelaysOf(self)?.relays().orEmpty() + if (url in existing) return false + val event = KeyPackageRelayListEvent.create(existing + url, ctx.signer) + ctx.verifyAndStore(event) + true + } + + else -> { + throw IllegalArgumentException("unknown relay type: $type") + } } - dataDir.saveRelays(cfg) - Json.writeLine( - mapOf( - "url" to url, - "added_to" to addedTo, - "already_present" to (targets - addedTo.toSet()), - ), - ) - return 0 } private fun list(dataDir: DataDir): Int { - val cfg = dataDir.loadRelays() - Json.writeLine( - mapOf( - "nip65" to cfg.nip65, - "inbox" to cfg.inbox, - "key_package" to cfg.keyPackage, - ), - ) - return 0 + val ctx = Context.open(dataDir) + try { + val self = ctx.identity.pubKeyHex + Json.writeLine( + mapOf( + "nip65" to (ctx.relaysOf(self)?.relaysNorm()?.map { it.url } ?: emptyList()), + "inbox" to (ctx.dmInboxOf(self)?.relays()?.map { it.url } ?: emptyList()), + "key_package" to (ctx.keyPackageRelaysOf(self)?.relays()?.map { it.url } ?: emptyList()), + ), + ) + return 0 + } finally { + ctx.close() + } } private suspend fun publishLists(dataDir: DataDir): Int { val ctx = Context.open(dataDir) try { ctx.prepare() - val nip65Relays = ctx.relays.normalized("nip65").toList() - val inboxRelays = ctx.relays.normalized("inbox").toList() - // MIP-00: other clients discover our KeyPackages by querying the - // relays advertised in our kind:10051 event. If no key_package - // bucket is configured, fall back to the NIP-65 set so we always - // publish a non-empty list — an empty 10051 would make us - // undiscoverable by other Marmot clients. - val keyPackageRelays = - ctx.relays - .normalized("key_package") - .ifEmpty { ctx.outboxRelays() } - .toList() + val self = ctx.identity.pubKeyHex + val nip65Event = ctx.relaysOf(self) + val inboxEvent = ctx.dmInboxOf(self) + val keyPackageEvent = ctx.keyPackageRelaysOf(self) - val nip65Infos = nip65Relays.map { AdvertisedRelayInfo(it, AdvertisedRelayType.BOTH) } - val nip65Event = AdvertisedRelayListEvent.create(nip65Infos, ctx.signer) - val inboxEvent = ChatMessageRelayListEvent.create(inboxRelays, ctx.signer) - val keyPackageListEvent = KeyPackageRelayListEvent.create(keyPackageRelays, ctx.signer) + if (nip65Event == null && inboxEvent == null && keyPackageEvent == null) { + return Json.error( + "no_relays", + "no relay lists in the local store; run `amy relay add` first or `amy create` to bootstrap defaults", + ) + } val targets = ctx.anyRelays() - val nip65Result = ctx.publish(nip65Event, targets) - val inboxResult = ctx.publish(inboxEvent, targets) - val keyPackageListResult = ctx.publish(keyPackageListEvent, targets) + val nip65Result = nip65Event?.let { ctx.publish(it, targets) }.orEmpty() + val inboxResult = inboxEvent?.let { ctx.publish(it, targets) }.orEmpty() + val keyPackageResult = keyPackageEvent?.let { ctx.publish(it, targets) }.orEmpty() Json.writeLine( mapOf( - "nip65_event_id" to nip65Event.id, - "inbox_event_id" to inboxEvent.id, - "key_package_list_event_id" to keyPackageListEvent.id, + "nip65_event_id" to nip65Event?.id, + "inbox_event_id" to inboxEvent?.id, + "key_package_list_event_id" to keyPackageEvent?.id, "accepted_by" to mapOf( "nip65" to nip65Result.filterValues { it }.keys.map { it.url }, "inbox" to inboxResult.filterValues { it }.keys.map { it.url }, - "key_package_list" to keyPackageListResult.filterValues { it }.keys.map { it.url }, + "key_package_list" to keyPackageResult.filterValues { it }.keys.map { it.url }, ), ), )