fix(cli): align amy cashu publish + nutzap relays with Amethyst

Two behavioral divergences from the Android wallet, found while auditing
for parity:

- Publish targets: Amethyst sends every cashu event via
  sendLiterallyEverywhere (all the user's relays). amy published only to
  outboxRelays(); switch to anyRelays() (outbox + inbox + keypackage), the
  CLI's closest analog, so the wallet lands on the same broad relay set.
- Nutzap relays on create: Amethyst always advertises the account's outbox
  relays in kind:10019 (so senders publish nutzaps where the user reads).
  amy defaulted to none; default to outboxRelays() unless --relay overrides.

Also align cashuSnapshot()'s store query with commons'
CashuWalletFilterAssembler exactly (six authored kinds by authors=[pk],
inbound nutzaps by #p) so amy projects the same event set the app
subscribes to. Verified the emitted kind:10019 now carries relay/mint/
pubkey tags identical to NutzapInfoEvent.build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
This commit is contained in:
Claude
2026-06-21 23:54:48 +00:00
parent 8bb537438f
commit 72a7f59d14
2 changed files with 14 additions and 4 deletions
@@ -204,7 +204,11 @@ class Context(
fun cashuOps(): CashuWalletOps =
CashuWalletOps(
signer = signer,
publish = { event -> publish(event, outboxRelays()) },
// Amethyst publishes cashu events via sendLiterallyEverywhere
// (all the user's relays). anyRelays() — outbox + inbox +
// keypackage — is the CLI's closest analog, so the wallet lands
// on the same broad relay set the app would use, not just outbox.
publish = { event -> publish(event, anyRelays()) },
okHttpClient = { okhttp },
secretFactory =
DeterministicSecretFactory(
@@ -228,6 +232,9 @@ class Context(
*/
suspend fun cashuSnapshot(): CashuWalletReader.WalletSnapshot {
val pk = identity.pubKeyHex
// Mirror commons' CashuWalletFilterAssembler exactly: authored wallet
// kinds by authors=[pk], inbound nutzaps by #p — so amy projects the
// same event set the Android app subscribes to.
val authored =
store.query<Event>(
Filter(
@@ -235,11 +242,10 @@ class Context(
kinds =
listOf(
CashuWalletEvent.KIND,
NutzapInfoEvent.KIND,
CashuTokenEvent.KIND,
CashuSpendingHistoryEvent.KIND,
CashuMintQuoteEvent.KIND,
NutzapEvent.KIND,
NutzapInfoEvent.KIND,
MintRecommendationEvent.KIND,
),
),
@@ -68,10 +68,14 @@ object CashuWalletCommands {
val mints = (args.csv("mint") + args.csv("mints") + args.positional.toList()).distinct()
if (mints.isEmpty()) return Output.error("bad_args", "at least one --mint URL is required")
val privkey = args.flag("privkey")
val nutzapRelays = args.csv("relay").mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) }
val explicitRelays = args.csv("relay").mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) }
Context.open(dataDir).use { ctx ->
ctx.prepare()
// Match Amethyst: kind:10019 advertises the account's outbox
// relays as nutzap-receiving relays unless the caller overrides
// with --relay, so senders publish nutzaps where we actually read.
val nutzapRelays = explicitRelays.ifEmpty { ctx.outboxRelays().toList() }
val created =
try {
ctx.cashuOps().publishWalletEvents(mints, privkey, nutzapRelays)