Commit Graph
17710 Commits
Author SHA1 Message Date
Claude 79fee9492e Merge remote-tracking branch 'origin/main' into claude/nip-2421-pr-review-6znvdd
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt
2026-07-24 23:50:40 +00:00
Vitor PamplonaandGitHub b4e8719ee2 Merge pull request #3701 from vitorpamplona/claude/nip47-spec-compliance-1c9ook
Add NWC payment notifications and deep-link pairing
2026-07-24 19:43:01 -04:00
Claude 7d9597b91f feat(bolt12): gate BOLT12 zaps on wallet pay support, fall back to lightning
Phase 3 capability gating. NwcSignerState caches the default wallet's advertised
NIP-47 methods; Account refetches them via nwc#2 get_info whenever the default
wallet changes. A zap now prefers the BOLT12 pay rail only when the wallet
advertises `pay` (Account.defaultWalletSupportsBolt12Pay) — otherwise the
recipient falls back to lightning through the existing partition, so a wallet
without pay support degrades gracefully instead of erroring. The profile
"pay with wallet" action is gated on the same signal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-24 23:21:06 +00:00
Claude 40820db10f refactor(wallet): move NWC notification subscription into the always-on account subscription layer
The bespoke applicationIOScope watcher that opened its own relay
subscription for NIP-47 wallet notifications is replaced by an EOSE
manager registered in AccountFilterAssembler.group — the same always-on
scheme as the account's zap/notification inbox subscriptions. It is now
owned by the single AccountFilterAssemblerSubscription in LoggedInPage,
kept warm in the background by NotificationRelayService, and torn down on
logout — matching zap-receipt lifecycle exactly (and not gated on OS
notification permission).

- NwcNotificationsEoseManager (PerUserEoseManager): one filter per
  connected wallet's own relay (kind 23197/23196, #p = per-wallet client
  pubkey), re-invalidating when the wallet set changes, `since`-floored at
  watch start, deduped by a seen set. Because these events are ephemeral,
  encrypted, and never land in LocalCache, onEvent decrypts them via
  NwcSignerState.handleIncomingNotification.
- NwcSignerState.handleIncomingNotification decrypts with the matching
  wallet's connection secret, drops zap-carrying payments, and publishes
  non-zap payments to a new incomingNonZapPayments SharedFlow — a clean
  seam a future in-app Notifications-tab consumer can also drain.
- NwcPaymentNotificationWatcher is now just the Context-bound bridge that
  drains that flow into an OS tray notification (no relay work, no client
  dependency).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDAAS4ktFbWtRnEVXQsjfs
2026-07-24 23:17:15 +00:00
Claude ec4928fc0d Merge remote-tracking branch 'origin/main' into claude/nip-2421-pr-review-6znvdd
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
2026-07-24 23:15:54 +00:00
Vitor PamplonaandGitHub 09e389373c Merge pull request #3703 from vitorpamplona/claude/blossom-file-import-xwcrzn
Add Blossom blob import flow to pull files from other servers
2026-07-24 19:01:04 -04:00
Claude 16c6acd78a fix(blossom): auto-dismiss the finished sync/import banner
The app-level "sync all" / import progress banner switched to "Sync
complete" when the sweep finished but then lingered until the user
tapped X. Auto-dismiss it a few seconds after completion, keeping the X
for dismissing early. Keyed on the running flag so a new sweep cancels
the pending dismiss and tapping X re-keys it to a no-op.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E5G515Grhc4t7ACoza9eyN
2026-07-24 22:51:46 +00:00
Claude eced3c4027 fix(bolt12): honor NONZAP zaps as a receiptless BOLT12 payment
A NONZAP (pay-without-receipt) zap must not publish a public 9736. sendBolt12Zap
now settles the offer over NWC without binding an intent or emitting a receipt
when the zap type is NONZAP, matching bolt11 NONZAP privacy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-24 22:50:03 +00:00
Claude 26272a6c3b feat(bolt12): send BOLT12 zaps over NWC, preferred when offered
Integrates BOLT12 zap-sending into the zap pipeline (nwc#2 `pay` returns the
payer proof). Account.sendBolt12Zap signs a 9737 intent, pays the offer over
NWC with the intent-bound payer_note, and — only if the returned proof passes
Bolt12ZapValidator — self-consumes and publishes the 9736; otherwise reports
"paid, no receipt" (fail-safe against a wallet that misroutes the note).

ZapPaymentHandler.zap now resolves each recipient's kind:10058 offer and
partitions recipients into a BOLT12 lane (offer present + NWC wallet configured)
and the existing lightning lane, sharing split weight across both so mixed
splits stay proportional. Anonymous/public follows the account zap type. Adds
Bolt12ZapBuilderTest proving the send-side assembly round-trips to a
validator-accepted, crypto-verified zap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-24 22:48:36 +00:00
Claude 895bcc3381 refactor(blossom): harden import flow after audit
Follow-up fixes from an audit of the import feature:

- Cancel an in-flight scan when the source selection changes
  (toggle/add/remove/enable-all). Without this a scan started against
  the old selection could land afterwards and offer blobs sourced from a
  server the user just de-selected — which importSelected() would then
  mirror from.
- Sign the BUD-02 list token once per scan and reuse it across every
  source and target. The token carries no `server` scope tag, so it's
  valid everywhere; per-server signing was a round-trip storm with
  remote NIP-46 signers.
- BlossomMirrorQueue.start() now returns whether it actually started a
  sweep. importSelected() keys the Started/Busy result off that instead
  of a separate isRunning check, closing a TOCTOU where the import would
  report "started" but the queue silently dropped the work.
- The import screen's empty-state now collects the kind-10063 server
  list reactively, so the "add servers first" ↔ picker switch recomposes
  when the list arrives from a relay after the screen opens.
- init() re-points at the current account each call (matching the
  sibling BlobManager VM) while still seeding the source list only once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E5G515Grhc4t7ACoza9eyN
2026-07-24 22:47:52 +00:00
Vitor PamplonaandGitHub eddffdbd3e Merge pull request #3702 from vitorpamplona/claude/ots-notes-lifecycle-ok3mz3
Anchor OTS attestations to target notes, replace verification cache
2026-07-24 18:44:29 -04:00
Claude fef1d15f12 fix(ots): never persist the non-terminal Verifying sentinel on the note
verifyOts wrote otsVerification = Verifying before the suspending blockchain
verifyState() call and only overwrote it with the real verdict afterwards. That
verification runs inside LoadOts's cancellable LaunchedEffect, so a row scrolling
off-screen cancels the coroutine at the network suspension point — the verdict
write never happens and the note is left stuck at Verifying. cacheVerifyOts
treats Verifying as terminal, so earliestOtsVerifiedTime then returns null and the
confirmed-timestamp pill silently vanishes for the note's lifetime. The old
VerificationStateCache had the same write but its LRU eventually evicted the stuck
entry; anchoring the verdict on the long-lived note removed that recovery.

Store only terminal verdicts (Verified / Error / NetworkError). A cancelled or
in-flight verification leaves the field null and simply retries on the next read;
the cost is at worst two concurrent first-time verifications, already benign.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014D5fenZbAhCbDiwv7Rtpvj
2026-07-24 22:42:51 +00:00
Claude 9789ff61f4 fix(nip47): parse space-separated encryption/notification tags; drop accumulating notification subscription
Two issues found in an audit of the NWC changes:

1. (correctness, high) NIP-44 negotiation never triggered against real
   wallets. The info event carries schemes in a single space-separated
   tag value (["encryption", "nip44_v2 nip04"]), but encryptionSchemes()
   returned tag.drop(1) = ["nip44_v2 nip04"], so the nip44_v2 membership
   check never matched and every request fell back to NIP-04. Split each
   tag value on whitespace in encryptionSchemes()/notificationTypes() so
   both the spec's space-separated form and a multi-element tag normalize
   to individual tokens. Adds NwcInfoEvent tests for the wire format.

2. (performance) NwcPaymentNotificationWatcher subscribed via
   subscribeAsFlow, which accumulates every event into an ever-growing
   list and re-emits the whole list per event — wrong for a lifetime
   subscription (unbounded retention + O(n) rescan per event). Replace
   with a raw client.subscribe listener (callbackFlow) that emits each
   event once; reconnect re-delivery is still de-duped by the seen set.

Also documents why the watcher keys the account flow on pubkey.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDAAS4ktFbWtRnEVXQsjfs
2026-07-24 22:42:12 +00:00
Vitor PamplonaandGitHub 8e2846b3f5 Merge pull request #3700 from vitorpamplona/claude/chat-picture-sending-consistency-qit0pz
Add image attachment support to minichat threads
2026-07-24 18:39:30 -04:00
Claude c7415f1559 docs(bolt12): record nwc#2 resolution — Phase 2 unblocked
Maintainer confirmed payer_note maps to invreq_payer_note for BOLT12 and
payer_proof is returned for successful BOLT12 payments. Notes the
validate-before-publish fail-safe so a non-conforming wallet never yields an
invalid receipt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-24 22:29:30 +00:00
Claude a4329912aa refactor(wallet): share a TTL'd NWC info-event cache across features
Replace the single-purpose per-wallet "supports nip44" boolean with a
shared NwcInfoCache that stores each wallet's full kind 13194 info event
(capabilities + encryption schemes + notification support), keyed by
wallet pubkey and owned by Account.

- Entries expire after 2 days so a wallet that changes its advertised
  capabilities is eventually re-checked. Reads never block: the payment
  path reads the cached value and nudges a background refresh when the
  entry is missing or stale (self-healing without holding up the tx);
  failed fetches are not cached, so a transient error retries next use.
- NwcSignerState derives the NIP-44 preference from the cache.
- NwcPaymentNotificationWatcher now consults supportsNotifications() and
  skips opening a relay subscription for wallets that advertise none
  (fail-open when the info event is unknown).

Adds NwcInfoCacheTest covering caching, TTL expiry, no-cache-on-failure,
and definitive-missing-info caching (injectable clock).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDAAS4ktFbWtRnEVXQsjfs
2026-07-24 22:19:17 +00:00
Claude a65d2b9342 refactor(ots): anchor OTS attestations on the Note lifecycle, drop the verification cache
NIP-03 OpenTimestamps attestations (kind 1040) were stored loosely in the main
note cache and found via a full-cache scan, with their blockchain verdicts held
in a separate, id-keyed VerificationStateCache LRU. Nothing tied either to the
lifecycle of the note being timestamped, so a deleted/pruned note leaked its
attestations, and consume(OtsEvent) invalidated the attestation's own
(observer-less) flow instead of the target's — so a live-arriving proof never
pinged the target's UI.

Mirror the recent edits→Note migration:

- Note gains a `timestamps` child collection (like `edits`/`reactions`), wired
  into clearChildLinks/removeNote, so an attestation survives exactly as long as
  its target and is collected when the target is pruned or deleted.
- consume(OtsEvent) anchors the proof on its target via the `e` tag and
  invalidates the target's `ots` flow; unlinkAndRemove detaches it symmetrically.
- Each attestation memoizes its own verdict in `Note.otsVerification`, so the
  result shares the note's lifecycle. This replaces VerificationStateCache
  (deleted) and the full-cache scan: the OTS pill now folds `note.timestamps`
  via the new Note.earliestOtsVerifiedTime / cacheVerifyOts helpers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014D5fenZbAhCbDiwv7Rtpvj
2026-07-24 22:18:27 +00:00
Claude 6ce61f0dc8 Merge remote-tracking branch 'origin/main' into claude/chat-picture-sending-consistency-qit0pz
# Conflicts:
#	commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/actions/ConcordActions.kt
2026-07-24 22:05:36 +00:00
Claude f4ba3b3581 Merge remote-tracking branch 'origin/main' into claude/blossom-file-import-xwcrzn
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomBlobManagerScreen.kt
2026-07-24 21:59:51 +00:00
Vitor PamplonaandGitHub 89cbba42a5 Merge pull request #3699 from vitorpamplona/claude/blossom-files-gallery-re25b4
Redesign Blossom blob manager UI with gallery grid and full-screen viewer
2026-07-24 17:49:15 -04:00
Claude d58bca4177 feat(bolt12): pay a recipient's offer in-app over NWC
Adds a "pay with connected wallet" action to the BOLT12 offer dialog on a
profile: when the account has a NIP-47 wallet configured, an amount sheet
collects sats and settles the offer over the wallet via the nwc#2 `pay`
method (BIP321 bitcoin:?lno=). Falls back to the external bitcoin: intent
otherwise. Outcome is surfaced as a toast. This is a plain payment, not a
NIP-XX zap (no receipt) — that's the deferred Phase 2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-24 21:45:56 +00:00
Claude 7d3f7f7ca4 feat(nwc): add pay/receive methods for BOLT12 (nostr-wallet-connect/nwc#2)
Adds the generalized `pay` (BIP321 payment instruction, incl. BOLT12 `lno=`)
and `receive` NIP-47 methods, plus the UNSUPPORTED_PAYMENT_INSTRUCTION /
UNSUPPORTED_NETWORK error codes. The `pay` result carries `payer_proof`
(lnp1…) — the proof a kind:9736 BOLT12 zap needs. Wired through both
serialization backends (kotlinx + Jackson) with round-trip tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-24 21:38:32 +00:00
Claude 6f40d997c6 feat(minichat): allow sending pictures in thread replies
Every chat composer in Amethyst already had a picture/media attach button
(SelectFromGallery) except the minichat "thread" screen — the kind-1111
reply composer opened from the "N replies" chip — which was text-only. This
brings it to parity with every other chat.

- quartz: ChannelChat.imageReply() — a kind-1111 thread reply carrying
  encrypted image imeta(s), combining reply()'s NIP-22 pointers with
  imageMessage()'s ciphertext-URL/imeta handling (+ round-trip test).
- commons: ConcordActions.buildChannelImageReply().
- Account.sendMinichatReply() now accepts imetas and routes per backend:
  Concord sends an encrypted image reply; NIP-28/NIP-29 public chats append
  the URL to the content and carry a plaintext imeta on the comment; Buzz
  appends the URL to the stream message content.
- Extract toConcordImeta()/toPlainImetas() into a shared UploadImetas.kt so
  the minichat and Concord composers build imeta the same way.
- MinichatScreen: add the SelectFromGallery leading icon + ChatFileUpload
  dialog, encrypting only when the backend is end-to-end (Concord).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8FD5xm8nyEKzd8dk9VfT1
2026-07-24 21:36:05 +00:00
Claude cd82d7ffff feat(wallet): prefer NIP-44 for NWC and notify non-zap payments
NIP-44 encryption preference: NwcSignerState now fetches each wallet's
kind 13194 info event (via the relay client, injected by Account),
caches whether it advertises `nip44_v2`, and sends pay/RPC requests
with NIP-44 when supported — satisfying NIP-47's "client should always
prefer nip44 if supported by the wallet service". Falls back to NIP-04
(the legacy default) when unknown or unsupported; response decryption
already auto-detects the scheme.

Non-zap payment notifications: NwcPaymentNotificationWatcher keeps a
standing subscription to each connected wallet's NIP-47 notification
stream (kind 23197/23196) on the wallet relay, decrypts payment_received
events, and posts a tray notification on a new Payments Received channel.
Payments carrying a NIP-57 zap request are skipped, since those already
surface through the kind-9735 ZapNotification path — so only plain,
non-zap incoming payments are announced.

Deep-link pairing: the "Connect wallet via app" button now builds its
`nostrnwc://connect` URI through the tested quartz Nip47DeepLink helper
instead of a hardcoded percent-encoded string.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDAAS4ktFbWtRnEVXQsjfs
2026-07-24 21:34:55 +00:00
Claude c2f6aa9992 feat(nip47): add NWC-07 deep-link helper and NIP-44 request opt-in
Add Nip47DeepLink for the NWC-07 same-device pairing convention:
build/parse the `nostrnwc://connect` request (client -> wallet) and
the callback URI that returns the `nostr+walletconnect://` pairing code
(wallet -> client). All params are URI-encoded per the spec.

Also thread `useNip44` through LnZapPaymentRequestEvent.create so
pay_invoice requests can opt into NIP-44, matching createRequest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDAAS4ktFbWtRnEVXQsjfs
2026-07-24 21:34:32 +00:00
Claude ccd991c847 feat: video thumbnails + full-screen zoomable viewer for Blossom files
Videos in the gallery now show a decoded first frame (Coil
VideoFrameDecoder) with a play badge instead of a generic glyph; the same
preview is reused in the detail header, with a type-glyph fallback for
blobs Coil can't decode (e.g. HLS playlists).

Tapping an image or video tile now opens a full-screen viewer: images are
zoomable/pannable (engawapg zoomable, matching ZoomableImageDialog) and
videos play inline. Its top bar carries back, a new share action (system
share sheet for the blob URL), and a button that reveals the file's
storage matrix and sync/copy/open/share/report/delete actions in a bottom
drawer. Non-visual blobs (PDFs, arbitrary files) still open straight to
that action sheet. The actions list is now a shared BlobActionsContent so
the sheet and the viewer drawer stay identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDLTQZM6yX13EwaYFNWTz7
2026-07-24 21:26:35 +00:00
Claude 84a85ffaf7 feat(blossom): import files from other Blossom servers
On the "My Blossom Files" screen, replace the top-bar sync icon with a
3-dot overflow menu offering "Refresh" and "Import files…".

The new Import flow lets the user pull files they've uploaded to other
Blossom servers into their own. They pick source servers to check —
seeded from the recommended bootstrap list (minus servers already in
their own list, with an enable-all toggle) and/or hand-typed addresses.
A scan fans a BUD-02 GET /list/<pubkey> across the enabled sources,
works out which of those blobs are missing from the user's own
kind-10063 servers, and hands the gaps to the existing app-level
BlossomMirrorQueue so each server fetches them via BUD-04 mirror —
reusing the same floating progress banner as the on-screen sync.

- BlossomImportViewModel: source list management, scan, gap detection,
  mirror hand-off.
- BlossomImportScreen: server picker, custom-URL field, scan + results.
- New Route.ImportBlossomBlobs wired into AppNavigation.
- Strings + plurals for the import UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E5G515Grhc4t7ACoza9eyN
2026-07-24 21:22:04 +00:00
Vitor PamplonaandGitHub f8f3631a15 Merge pull request #3698 from vitorpamplona/claude/chat-updates-concord-75jx1g
Anchor message edits to their targets instead of workspace state
2026-07-24 17:08:00 -04:00
Claude 802652dd4f docs(bolt12): scope NWC BOLT12 pay/receive (nwc#2) integration
Captures the design for paying BOLT12 offers over NIP-47 and — via the pay
result's payer_proof — sending real kind:9736 zaps. Maps the reusable NIP-47
plumbing, breaks the work into phases, and flags the linchpin risk: nwc#2 does
not guarantee payer_note lands in the BOLT12 invreq_payer_note that NIP-2421's
zap binding depends on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-24 21:01:20 +00:00
Claude 8c5b71c3a5 Merge remote-tracking branch 'origin/main' into claude/chat-updates-concord-75jx1g
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageActionSheet.kt
2026-07-24 20:54:48 +00:00
Claude 3fb44dba60 feat: gallery view for My Blossom Files with per-file detail sheet
Replace the single-column list of large blob cards with an adaptive
thumbnail grid so many files no longer mean an endless scroll. Each tile
shows an image preview (or a type glyph) plus a corner badge summarizing
how many of the user's servers hold it (green check when on all, amber
cloud with a present/total count otherwise).

Tapping a tile opens a bottom sheet with the file's details: hash,
type/size, the per-server storage matrix ("Stored on"), the sync
(mirror-to-missing) button, and the copy/open/report/delete actions that
previously lived behind the card's overflow menu.

The ViewModel is unchanged — only the presentation layer moved from
list-of-cards to grid-plus-detail-sheet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDLTQZM6yX13EwaYFNWTz7
2026-07-24 20:38:43 +00:00
Claude 893a270c65 fix(edits): unlink deleted edits from their message; one collector per row
Audit of the three edit paths (feed 1010 / Buzz 40003 / Concord 3302) found:

1. Bug (feed regression): a deleted edit kept overlaying its message. Edits
   anchor on the target's Note.edits with no `replyTo` back-link, and
   removeNote didn't cover `edits`, so unlinkAndRemove never dropped them — the
   old cache-scan resolver dropped deleted edits for free, Note.edits did not.
   Fix: removeNote now also removeEdit()s, and unlinkAndRemove resolves the
   edit's `e`-tag target and unlinks it there (editedTargetIdOf covers all
   three kinds). New test: deleting an edit un-overlays and unlinks it.

2. Perf: every chat row ran two edits-flow collectors (observeConcordEdit +
   observeBuzzEdit). A message is only ever one kind, so they're merged into a
   single observeChatEdit that resolves latestConcordEdit() ?: latestBuzzEdit()
   — one collector per row, dispatched by the winning edit's event type.

3. Nits: latestBuzzEdit now tie-breaks by idHex (deterministic on same-second
   edits, matching Concord); dropped a redundant takeIf in latestConcordEdit.

The author check stays at read time on purpose: an edit can be consumed before
its target loads (author unknown), so an attach-time gate would wrongly drop
early-arriving legit edits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013HdLnAa4Pa1pFV9FTYVTB6
2026-07-24 20:18:53 +00:00
Claude 6d111626ee fix(bolt12): don't let an unverified proof grief a verified zap total
The payment-hash dedup kept the LOWER amount and OR'd cryptoVerified across
entries. Because a payer proof publishes its proof_preimage, once a BOLT12 zap
is public anyone can replay its payment hash in a compressed (unverifiable)
proof with a 1-msat amount; the merge would keep that amount AND inherit the
verified flag, driving the counted total to ~zero and mislabeling a fabricated
amount as verified.

Per the NIP, dedup by invoice_payment_hash applies among *validated* proofs.
Only a crypto-verified proof has a signature-bound amount, so a verified entry
now always wins over an unverified duplicate; the lower-amount rule applies only
between entries of the same verification status. Adds a two-order regression
test for the replay-griefing case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-24 20:16:38 +00:00
Vitor PamplonaandGitHub 5d72a0415c Merge pull request #3696 from vitorpamplona/claude/geode-release-process-q1dp17
Add geode standalone Nostr relay with release pipeline
2026-07-24 16:08:14 -04:00
Claude f9ad60c423 refactor: move edit-overlay resolvers off LocalCache onto Note
findLatestModificationForNote (and the Buzz resolver) were pure Note.edits
filters with no LocalCache state — they only lived there for historical
reasons (back when resolving edits meant scanning the whole cache). Now that
every edit is a hard-referenced child of its message, resolution is a cheap
in-memory fold that belongs on the note.

New NoteEditOverlays.kt collects all three as Note extensions, so every edit
kind resolves the same way and none touches LocalCache:
- Note.textNoteModifications() (1010, author-only + NIP-40, version list)
- Note.latestBuzzEdit() (40003, author-only, newest by created_at)
- Note.latestConcordEdit() (3302, author-only, newest by CORD-02 send time)

Callers updated: observeEdits, observeNoteModifications, observeBuzzEdit,
observeConcordEdit, and the Buzz test. observeConcordEdit also drops its
early-return-before-produceState guards (a conditional-hook hazard) since the
resolver returns null for a non-Concord note anyway. LocalCache no longer
carries any edit-filtering logic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013HdLnAa4Pa1pFV9FTYVTB6
2026-07-24 19:59:54 +00:00
Claude 70d38b205e fix(bolt12): hand offers off as bitcoin:?lno= (BIP21), not lightning:
A BOLT12 offer (lno1…) is not a lightning: payload — that scheme is for BOLT11
lnbc… invoices. Per BIP21/BIP321 an offer travels as the lno parameter of a
bitcoin URI (bitcoin:?lno=lno1…), with the on-chain address optional so a
Lightning-only offer stands alone. The bech32 offer value is URL-safe, so no
percent-encoding is needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-24 19:53:38 +00:00
Claude 3a906e8b6b feat(bolt12): pay a recipient's BOLT12 offer via wallet intent
Adds a profile action that reads the recipient's kind:10058 offer list and,
for each published BOLT12 offer, hands it to an installed wallet via the
lightning: scheme (payViaBolt12Intent, sibling to the BOLT11 payViaIntent).
This is the first payment step — a plain wallet handoff, not a NIP-XX zap, so
it produces no Nostr receipt. NWC payment instructions are left for later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-24 19:45:31 +00:00
Claude 999bb14032 feat(bolt12): add editor to publish own kind:10058 BOLT12 offer list
Adds a Settings entry (mirroring the Payment Targets editor) that lets the
logged-in user add/remove reusable BOLT12 offers (lno1…) and publishes them
as a replaceable kind:10058 offer list. Offers are validated against
Bolt12Bech32 before being accepted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-24 19:43:07 +00:00
Claude 9fa1d769f9 refactor: drop cachedModificationEventsForNote, now a redundant alias
It existed to serve observeEdits a cheap synchronous value (an LRU read)
distinct from the expensive IO-only findLatestModificationForNote cache scan.
Since edits fold from Note.edits, findLatestModificationForNote is itself
cheap and thread-safe, and cachedModificationEventsForNote had become a plain
alias for it. observeEdits now calls findLatestModificationForNote directly —
the same function observeNoteModifications already uses — and the LocalCache +
AccountViewModel aliases are removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013HdLnAa4Pa1pFV9FTYVTB6
2026-07-24 19:38:11 +00:00
Claude e8739f5e83 ci: run geode tests in a dedicated isolated job
The first cut appended :geode:test to the build-desktop matrix command.
That was wrong twice over: geode is JVM-only, so it ran 3× across the
ubuntu/macos/windows matrix, and — because org.gradle.parallel=true —
its default suite's CPU-heavy throughput benchmarks (a 1M-event mirror
sync, WireReqFloor, NegentropyServerReconcile) ran concurrently with the
timing-sensitive quartz relay-client tests, flaking
NostrClientReqBypassingRelayLimitsTest.denseSecondBeyondCapIsSteppedPastWithoutStalling.

Move :geode:test into its own test-geode job (needs: lint, ubuntu, JVM
21) so it runs once and its benchmark load can't starve another module's
timing assertions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCdJwdhGtmLZ12ViS56S3k
2026-07-24 19:37:09 +00:00
Vitor PamplonaandGitHub 99af7f58a3 Merge pull request #3695 from davotoula/fix/exoplayer-ownership-leak
Stop the decoder-budget counter from leaking and disabling the warm player cache
2026-07-24 15:36:07 -04:00
Claude c5d89ff7a0 feat(amethyst): cache + keep-live the BOLT12 offer list (kind 10058)
Downloads and keeps kind 10058 fresh the way the per-user relay/payment lists do,
so a recipient's BOLT12 offer is available for discovery before a zap.

- LocalCache consumes 10058 as an addressable note (consumeBaseReplaceable),
  keyed by Address(10058, pubkey, "").
- User gains a pinned bolt12OfferListNote + bolt12OfferList()/bolt12Offers()
  accessors — this is how a payer reads a recipient's offers.
- Bolt12OfferListState: the logged-in user's own offer list as live account state
  (a StateFlow<List<String>> of offers), persisted across restarts and published
  via saveOffers() — cloned from NipA3PaymentTargetsState. Wired into Account
  (bolt12OfferList + saveBolt12Offers), AccountSettings (backup + updater), and
  LocalPreferences (persist/restore).
- Subscriptions: kind 10058 added to FilterUserMetadataForKey (fetches OTHER
  users' offers — the key edit for zap recipients) and to the account's
  FilterAccountInfoAndListsFromKey (fetches your own at login).

Editor UI and the payment intent follow next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-24 19:28:43 +00:00
Claude 9075fb39b8 feat(quartz): add BOLT12 offer list (NIP-2421 kind 10058)
The NIP was updated to publish a recipient's BOLT12 offer(s) in a dedicated
replaceable event (kind 10058, `bolt12_offer`) instead of a kind:0 field — this
is what ties an offer to a Nostr identity (the author's signature) and gives
BOLT12 zaps the send-side addressability that lightning zaps get from lud16.

- Bolt12OfferListEvent (kind 10058): a BaseReplaceableEvent holding one or more
  `["offer","lno1..."]` tags (reusing OfferTag), with offers()/firstOffer()
  accessors and create/updateOffers factories (mirrors ChatMessageRelayListEvent).
- Registered in EventFactory + a KindNames display entry.
- Tests: offers round-trip + factory typing, malformed-offer-tag filtering, and
  updateOffers replacing the offer set while keeping other tags.

Discovery: a payer fetches the recipient's latest 10058 and picks an offer. The
app-side caching, subscription, editor, and payment intent follow in later commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-24 19:18:27 +00:00
Claude 0a30231196 feat(geode): add a release & distribution pipeline
geode was runnable only via ./gradlew :geode:run and was absent from CI.
Give it the same release process as the amy CLI (it's the same kind of
application-plugin JVM module), plus the pieces a long-running server
daemon needs that a one-shot CLI does not.

- Main.kt: add terminal --version/-V and --help/-h flags so a packaged
  binary has a fast, exit-0 command (Homebrew test block, package smoke
  checks, Docker healthcheck).
- build.gradle.kts: jlinkRuntime + geodeImage (portable flat app-image
  with a bundled JRE, plus config.example.toml + geode.service under
  share/) + jpackageDeb/jpackageRpm, mirroring cli/. No Compose to
  exclude — geode depends only on :quartz.
- Dockerfile + .dockerignore: multi-stage image (gradle installDist ->
  temurin JRE), the primary channel for relay operators.
- packaging/: systemd unit, macOS hardened-runtime entitlements, and a
  reference Homebrew formula.
- scripts/asset-name.sh: geode_asset_name/collect_geode_assets under the
  canonical geode-<version>-<family>-<arch>.<ext> scheme.
- create-release.yml: build-geode matrix (tarball + deb/rpm + no-JRE jvm
  bundle, with a serve+NIP-11 smoke test of the jlink image) and a
  docker-geode job pushing ghcr.io/<owner>/geode:<version> (+ :latest).
- bump-homebrew-geode-formula.yml: auto-sync the reference formula on
  stable releases.
- build.yml: run :geode:test in CI (it ran in no workflow before).
- README.md + plans/2026-07-24-geode-release.md: operator docs + design.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCdJwdhGtmLZ12ViS56S3k
2026-07-24 17:56:50 +00:00
Claude 8dcee46cdf fix(buzz): only the author's own kind-40003 edit may rewrite a message
The Buzz edit overlay applied the newest kind-40003 by created_at regardless
of who signed it, so a 40003 signed by anyone — targeting someone else's
message — would rewrite that message in every reader's UI. The send side
already gates Edit to your own messages, but the apply side re-checked
nothing and effectively trusted the relay to reject cross-author edits.

Enforce author-only on apply, matching feed (1010) and Concord (3302) edits:
observeBuzzEdit now resolves through LocalCache.findLatestBuzzEditForNote,
which keeps only edits whose author is the original message's author (newest
by created_at — Buzz's own last-write-wins rule otherwise). There is no Buzz
feature that edits another user's message; moderation is delete/hide.

Added a test: a verified forged edit by a different author lands in the store
but never overrides the message, while the real author's later edit does.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013HdLnAa4Pa1pFV9FTYVTB6
2026-07-24 17:54:23 +00:00
Claude 98d8f88c0a refactor: unify all message edits onto Note.edits
All three edit kinds now anchor on the message they edit via the same
Note.edits collection, instead of each maintaining its own store:

- Feed edits (kind 1010): consume(TextNoteModificationEvent) calls
  editedNote.addEdit(note); findLatestModificationForNote folds note.edits
  (author-only, NIP-40 expiry) instead of scanning the whole cache. Drops
  the O(all-notes) scan and the 20-entry modificationCache LRU;
  cachedModificationEventsForNote is now synchronous (no Loading state).
- Buzz edits (kind 40003): consume(StreamMessageEditEvent) calls
  target.addEdit(note); observeBuzzEdit reads note.edits (newest by
  created_at, no author gate — Buzz's own rule). Removes the channel-keyed
  BuzzWorkspaceState edit store, its editUpdates/editFor/effectiveContentFor/
  addEdit and the pruneEdits reaping (edits now prune with their message).
- Concord edits (kind 3302): already on note.edits.

Each reader keeps its own semantics by filtering note.edits on its event
type; the shared field only unifies storage + lifecycle, so an edit lives
exactly as long as the message it edits. Buzz edit tests rewritten against
note.edits (6/6 green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013HdLnAa4Pa1pFV9FTYVTB6
2026-07-24 17:39:03 +00:00
davotoula 874209ab39 Code review:
- don't double-return the player if register() throws
- guarantee pool teardown even if a session retire throws
- simplify pool ownership code, one funnel and paired counters
2026-07-24 19:18:34 +02:00
davotoula 0bd08bf013 Manual testing fixes:
- fix(playback): remove pool from livePools after its own teardown
- fix(playback): floor decoder decrements and report teardown drift
- fix(playback): own the player across the acquire-to-register window
- fix(playback): give every pooled player exactly one owner
2026-07-24 19:18:34 +02:00
davotoula bb41a867a4 feat(playback): add SessionRegistry owning session reachability
test(playback): pin SessionRegistry's identity-vs-equality drop guard
2026-07-24 19:18:34 +02:00
Vitor PamplonaandGitHub 94210d202d Merge pull request #3694 from vitorpamplona/claude/remove-module-warnings-s7v2q0
Refactor persistent collections API and fix deprecation warnings
2026-07-24 13:03:07 -04:00