Commit Graph
17742 Commits
Author SHA1 Message Date
Vitor PamplonaandGitHub c64d4eacd2 Merge pull request #3711 from davotoula/fix/flaky-large-cache-addressable-test
Fix GC-dependent flaky test in LargeCacheAddressableFilterTest
2026-07-25 00:47:54 -04:00
Vitor PamplonaandGitHub 9ffac69718 Merge pull request #3713 from vitorpamplona/claude/update-dependencies-kt62lx
Upgrade genaiPrompt to 1.0.0-beta4
2026-07-25 00:27:05 -04:00
Vitor PamplonaandGitHub 0cfbaf73ff Merge pull request #3712 from vitorpamplona/claude/nip29-relay-nav-behavior-tgddmf
Support RelayGroupChannelListScreen as bottom-nav tab
2026-07-25 00:26:35 -04:00
Claude 35914cf241 fix(bottom-bar): make a pinned NIP-29 relay a proper bottom-nav tab
A NIP-29 relay pinned to the bottom bar navigates to Route.RelayGroupServer
(RelayGroupChannelListScreen), but that screen always drew a back arrow and
never rendered a bottom bar — so tapping the pinned relay icon dropped the
bottom nav and showed a back arrow, unlike every other bottom-nav root.

Mirror the norm the analog Concord server screen already follows: read
nav.canPop() once, show the back arrow only when pushed (drawer / another
screen), and add an AppBottomBar keyed to the relay's own route. AppBottomBar
hides itself on a bottom-nav root, so the relay now behaves both ways — a
bottom-nav tab (bar visible, no arrow) when tapped from the bar, and a pushed
detail (arrow, no bar) when opened from the drawer or elsewhere.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BU7StQsshfjcaLDXTBtnB2
2026-07-25 04:24:16 +00:00
Claude 364d0d1fde chore(deps): bump ML Kit genai-prompt to 1.0.0-beta4
Update the on-device GenAI prompt dependency (play flavor only) from
1.0.0-beta3 to 1.0.0-beta4, the latest in-track release.

All other catalog entries are already at their latest stable versions.
appfunctions could not move to alpha10 because appfunctions-service
only publishes up to alpha09 and the three artifacts share one version.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZJ8ekaDDihWWhcJ6X2Fjz
2026-07-25 04:15:31 +00:00
davotoula f662b131c8 Code review:
- make the note list the fixture's source of truth
- trim the GC regression test
2026-07-25 06:10:08 +02:00
davotoula 94c50946e4 test(cache): add GC-forcing regression for LargeCacheAddressableFilterTest
fix(cache): keep LargeCacheAddressableFilterTest mocks strongly reachable

LargeSoftCache stores values as WeakReferences, so the cache alone does
not keep the mock AddressableNotes alive. Hold each note in a companion
strongRefs list for the lifetime of the test class, so a GC between
class-load and the read can no longer clear them.
2026-07-25 05:46:12 +02:00
Vitor PamplonaandGitHub 8c046df330 Merge pull request #3697 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-24 23:28:55 -04:00
Vitor PamplonaandGitHub 52d5bc93c9 Merge pull request #3709 from vitorpamplona/claude/nip01-multirelaypool-test-failure-dtl9qp
Fix race condition in NIP-01 compliance test with concurrent relays
2026-07-24 23:28:36 -04:00
Vitor PamplonaandGitHub 5244a4a643 Merge pull request #3707 from vitorpamplona/claude/message-screen-channel-rendering-f8zkvd
Extract chat room mark-as-read logic into reusable function
2026-07-24 23:28:09 -04:00
Vitor PamplonaandGitHub 90dc9a874e Merge pull request #3685 from vitorpamplona/claude/nip-2421-pr-review-6znvdd
Add NIP-XX BOLT12 zap support with validation and UI integration
2026-07-24 23:26:28 -04:00
vitorpamplonaandgithub-actions[bot] 72aac98ff1 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-25 03:01:55 +00:00
Claude 9caf330879 fix(bolt12): don't throw on an oversized tu64; cover proof_note path
Audit of the compressed-proof work found one real defect and one coverage gap.

Defect: a hostile BOLT12 proof/offer can carry a 9+ byte `invoice_amount`
(or any tu64 field) that parses as a valid TLV. `TlvStream.tu64` then called
the strict `Bolt12Values.tu64`, which throws `require(size <= 8)`. On the
`amy bolt12 verify` path (`Bolt12ZapActions.validate`, no surrounding catch)
that surfaced as an uncaught exception and abnormal exit instead of a clean
`Invalid`; the Android ingest path was already contained by LocalCache's broad
catch. Make the nullable stream accessor `TlvStream.tu64` return null for an
over-8-byte value so every amount read (invoice_amount, invreq_amount, offer
amount) degrades to a clean rejection. Regression-tested at the codec level.

Coverage: the writer's `proof_note` (1005) branch and the `with_note` vector's
note were never exercised. Add a `Bolt12PayerProof.proofNote()` reader and
thread the vector's note through the writer round-trip so 1005 is asserted.

The forged-proof, DoS, and reconstruction-accounting paths were reviewed and
found sound (the reconstructed root is only ever a BIP-340 message; the NIP
offer-binding gate still pins invoice_node_id to the offer's issuer).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-25 03:00:51 +00:00
Vitor PamplonaandGitHub 164e9975d7 Merge pull request #3708 from vitorpamplona/claude/blossom-mirror-api-support-q2h9t1
Add mirrorOrUpload fallback when Blossom /mirror endpoint unsupported
2026-07-24 22:59:01 -04:00
Claude cd221d5de1 feat: cap Messages room-label chips at ~half the row, unify Concord chip color
The type/label chips that sit beside a room name on the Messages screen — the
NIP-28 "Public Chat" pill (HeaderPill), the NIP-29 relay-host chip
(RelayNameChip), and the Concord community chip (ConcordCommunityPill) — could
grow with a long relay URL or community name and crowd the room name out.

Cap each at ChatLabelMaxWidth (140.dp, ~half a phone row) via widthIn(max); the
room name stays weighted so it keeps whatever the capped chip doesn't take, and
each chip's label truncates with a middle ellipsis (TextOverflow.MiddleEllipsis)
so the informative head and tail both survive. RelayNameChip switches from a
plain end ellipsis; ConcordCommunityPill drops its char-count truncation
(maxChars) for width-based truncation.

Also give the Concord chip the NIP-29 chip's highlighted look — secondaryContainer
background / onSecondaryContainer content (a gray on the dark theme) — so both
"which server/community does this room belong to" chips read the same.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQ9Cz2QjLMvemzVyJS1f5V
2026-07-25 02:57:56 +00:00
Claude 9bd339cba3 feat(blossom): fall back to upload when a sync/import target lacks /mirror
The File Sync / Import flow (and the mirror-on-upload fan-out) copy blobs
across the user's Blossom servers with BUD-04 `PUT /mirror`, but not every
server implements that endpoint. Blossom has no capability-discovery
mechanism, so a target without /mirror just answered 404/405/501 and the
whole copy was silently counted as failed.

Detect the "endpoint absent" statuses (404/405/501) as a typed
BlossomMirrorUnsupportedException — distinct from a mirror the server
understood but rejected (400/403/413/…) — and add BlossomClient.mirrorOrUpload,
which falls back to downloading the blob and re-uploading it (PUT /upload)
when mirror is unsupported. The downloaded bytes are verified against the
expected sha256 before re-upload, since a Blossom server is untrusted and
could substitute content, and the same t=upload auth is reused.

Wire every mirror path through mirrorOrUpload: the app-level
BlossomMirrorQueue (sync-all + import sweep), the blob manager's per-blob
mirror (including the paid-mirror retry), and UploadOrchestrator's
mirror-on-upload. Task now carries the descriptor content-type so the
fallback upload preserves the MIME.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168TWLTgrMxUR6yjjCCiBLS
2026-07-25 02:47:02 +00:00
Claude d32da653d3 fix(geode): make multi-relay compliance test thread-safe
multiRelayPoolReturnsContentFromEachRelay flaked with
"expected:<from-b> but was:<null>": the SubscriptionListener wrote the
per-relay results into a plain HashMap/HashSet, but each relay delivers
its EVENT/EOSE on its own InProcessWebSocket scope (Dispatchers.Default)
and PoolRequests dispatches the listener callbacks outside any lock. Two
relays therefore call `received[relay] = ...` concurrently, and a
HashMap.put racing a rehash can drop an entry, leaving a relay's value
null and failing the assertion.

Use ConcurrentHashMap and ConcurrentHashMap.newKeySet() for the shared
collections. Reproduced within 7 runs before the fix; 80 stress runs
clean after.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HeAjLDNBvGPjb5bfViU3ad
2026-07-25 02:42:36 +00:00
Claude 7535d791f3 feat(bolt12): verify compressed payer proofs via merkle reconstruction
Real BOLT12 wallets emit selective-disclosure payer proofs: `invreq_metadata`
is always withheld and other invoice fields may be elided for privacy, with
`proof_omitted_tlvs` / `proof_missing_hashes` / `proof_leaf_hashes` carrying
enough to rebuild the invoice signature's merkle root. The verifier previously
reported these as unsupported (cryptoVerified = false), so a zap paid through a
real wallet never counted locally.

Implement the lightning/bolts#1346 reader:

- Bolt12Merkle.reconstructRoot rebuilds the invoice root from the disclosed
  LnLeaf hashes + supplied nonce leaves (proof_leaf_hashes) + omitted-field
  markers + missing subtree hashes (consumed post-order DFS, smallest-to-largest).
  Add emitMissingHashes as the writer dual, unify both on one tree builder.
- Fix two latent interop bugs the vectors exposed: the nonce leaf hashes the
  record's type bytes (not the full encoded TLV), and the payer proof signs
  under fieldname `proof_signature` (not `signature`).
- Bolt12PayerProof gains marker/leaf/missing accessors and the invoice-field
  range predicate; the verifier reconstructs on every proof (type 0 is always
  the implied first omitted leaf) and drops the Unsupported result.
- Add Bolt12ProofBuilder to mint spec-compliant proofs (tests + future interop),
  and rewire Bolt12ProofFixture onto it.

Validated byte-for-byte against the draft's own conformance suite
(bolt12/payer-proof-test.json): all 5 valid vectors verify, all 23 invalid are
rejected, and the writer reproduces every vector's compression fields exactly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-25 02:35:16 +00:00
Claude 039beb0684 fix: mark all room types read in Messages "mark as read"
markAllChatNotesAsRead only enumerated public chats (IsInPublicChatChannel)
and DMs (ChatroomKeyable), so every newer room type fell through the when()
with no branch: NIP-29 relay groups, Concord communities, Marmot groups,
geohash chat, and ephemeral relay chat. Their unread dots on the Messages
screen could only be cleared by opening each room — "mark all as read" left
them lit. The collapsed per-server rows (RelayGroupServerRoomNote,
ConcordServerRoomNote) were skipped too.

Extract markRoomNoteAsRead(account, note), mirroring ChatroomEntry's type
dispatch so each row's last-read route is resolved the same way its unread
dot reads it: synthetic grouped rows first (fanning out to every joined
group on the relay / every channel in the community), then gatherer-attached
channels (Marmot, NIP-29, Concord, geohash), then the h-tag group fallback,
then the raw event type (public chat incl. ChannelCreateEvent, ephemeral,
DM, and drafts wrapping those). markAllChatNotesAsRead now just maps the
visible notes through it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQ9Cz2QjLMvemzVyJS1f5V
2026-07-25 02:29:34 +00:00
Claude 4fe60162e1 feat: smaller subtitle font in Messages screen rows
The message-preview second line on every Messages-screen row (channels,
groups, and DMs) rendered at the ambient bodyLarge (16sp), matching the
bold title above it. Drop it to bodyMedium (14sp) so the title and the
muted preview read as two tiers instead of one block of same-size text.

Covers both renderers all rows funnel through: ChannelName (public
chats, ephemeral/geohash chats, Marmot/NIP-29 groups, Concord) and
LastMessagePreview (NIP-17/NIP-04 DMs), including the
"event not found" fallback line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KQ9Cz2QjLMvemzVyJS1f5V
2026-07-25 02:20:09 +00:00
Vitor PamplonaandGitHub cdc6efd05f Merge pull request #3705 from vitorpamplona/fix/concord-control-plane-starvation
fix(concord): isolate the Control Plane sub so channels don't starve
2026-07-24 21:57:50 -04:00
Vitor PamplonaandGitHub bd1a6dbf2d Merge pull request #3706 from vitorpamplona/claude/bottom-nav-relay-community-nswtrf
Add pinnable relay servers and Concord channels to bottom bar
2026-07-24 21:57:11 -04:00
Vitor PamplonaandClaude Opus 4.8 844b0e9803 fix(concord): isolate the Control Plane sub so channels don't starve
A Concord community pinned to the bottom bar folded only a fraction of its
channels — Soapbox showed 1 of 12. The live subscription collapsed a
community's Control + Guestbook + rekey + every channel plane into ONE
kind-1059 filter per relay, and the channel list is folded from the Control
Plane. On an AUTH-gated relay that caps a REQ per filter (measured ~100
events/filter on relay.dreamith.to), the chatty Guestbook plane crowded the
channel-defining control editions out of the cap, so only a fraction of the
channels folded. On the strict relay.ditto.pub the collapsed multi-author
filter is refused wholesale until every author is authenticated.

- Split the Control Plane into its OWN filter, apart from the Guestbook /
  rekey / channel planes (ConcordSubscriptionPlanner.controlIsolatedFilters),
  so it gets an isolated per-filter budget. Both filters still ride the same
  per-relay REQ (no extra socket).
- Add a COMPLETE-mode Control-Plane sweep (Account.syncConcordControlPlanes):
  re-fetch the whole plane with no `since`, paging past the per-filter cap via
  fetchAllPagesFromPool, so a forward cursor can never hide an edition and the
  cap can never truncate the fold. Mounted account-wide, fired on load +
  membership/held-epoch change + relay reconnect (not a wall-clock poll — the
  persistent live subscription keeps a connected relay complete).

Mirrors Armada's plane-sweep design (one filter per plane scope, COMPLETE-mode
control). Verified on-device: Soapbox now folds all 12 channels.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 21:47:11 -04:00
Vitor PamplonaandGitHub 122321cd94 Merge pull request #3704 from vitorpamplona/claude/buzz-community-star-add-buttons-knx2ry
Support Buzz stream messages in group chat and improve chat previews
2026-07-24 21:46:58 -04:00
Claude f62ecb7ad2 fix(nwc): kotlinx pay/receive parsers mishandle explicit JSON null
Cross-backend defects in the kotlinx (native/iOS) NWC-321 parsers, found by the
audit and empirically reproduced. Jackson (JVM/Android) writes null-valued keys,
so a native peer parsing that output hit two bugs:

- parsePay/parseReceive crashed on `metadata: null` — `?.jsonObject` doesn't
  short-circuit on JsonNull (a non-null element). Use `as? JsonObject`.
- parsePaySuccess/parseReceiveSuccess (and parsePay's string fields) read an
  explicit JSON null as the literal string "null" via `?.jsonPrimitive?.content`.
  Use `contentOrNull`.

Only affects the kotlinx path (Android/JVM use Jackson), but violates the KMP
mapper-interchangeability contract. Adds Nip47KotlinSerializationNullTest hitting
the kotlinx serializers directly so it's covered regardless of platform actual.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-25 01:27:49 +00:00
Claude 42e69fd424 fix(bolt12): audit fixes — verify crash, decode hardening, send robustness
- amy bolt12 verify: the id-only query with a <Bolt12ZapEvent> type crashed
  with ClassCastException when the id pointed at a non-9736 event. Constrain
  the filter to kind 9736 and cast defensively (query<Event>() as?), returning
  a clean not_found instead.
- Bolt12ZapActions.decodeOffer/decodeProof: a parseable bech32 with an
  over-8-byte amount TLV threw in tu64 on field read instead of honoring the
  null contract; guarded the field extraction in runCatching. Adds a
  malformed-amount regression test.
- Account.sendBolt12Zap: wrap the NWC response callback in try/catch/finally so
  a post-payment receipt-assembly failure (e.g. a remote signer error) steps
  progress and surfaces "paid, no receipt" instead of vanishing as an uncaught
  coroutine exception.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-25 01:23:23 +00:00
Claude 107ff18366 fix(buzz): count all Buzz chat-timeline kinds as a room's last message
Widen the fix beyond the plain stream chat message: every Buzz kind the chat
feed renders as a row — stream messages (40002), system lines (40099), diffs
(40008), and the agent-job (43001-43006) and huddle (48100-48103) lifecycle
events — is `h`-scoped and attaches to the same RelayGroupChannel as a kind-9
via consumeBuzzTimelineEvent. So all of them must count as a room's newest
message and toward its unread dot; leaving them out left the Messages-list
preview stale whenever the newest thing in a channel was one of these.

- Add `Event.isBuzzChatTimelineContent()` (quartz buzz) enumerating exactly the
  kinds consumeBuzzTimelineEvent attaches / the chat renders — excluding edits
  (folded into their target), canvas, and forum kinds. `isGroupChatContent()`
  now ORs it in, so the initial scan, the live additive update, and the unread
  dot all agree.
- These kinds carry JSON/diff in `content`, so previewing raw `content` would
  dump `{"ephemeral_channel_id":…}`. Extract the in-chat labels into pure
  helpers (`buzzSystemMessageText`, `buzzActivityLabel`,
  `buzzTimelinePreviewSummary`) so the Messages-list preview shows the same
  human-readable summary the chat row shows ("🔊 huddle started", "topic
  changed", "⚙ job progress: …", "📄 <file>") instead of raw payload.
- Extend the regression test to cover stream/system/huddle counting and edit/
  reaction not counting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dtx8Shek4sSAXmHjnNMJF
2026-07-25 00:54:22 +00:00
Claude 9bb6237d8c docs(bolt12): note the nwc#2 ecosystem status (LND service exists, BOLT12 gap)
benthecarman/nostr-wallet-connect-lnd implements NWC-321 pay/receive (confirming
Phase 0), but is LND-backed so it rejects BOLT12 lno and returns no payer_proof.
The blocker for real BOLT12-zap testing is a CLN/LDK-backed NWC-321 service.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-25 00:51:38 +00:00
Claude 73859f4e04 feat(bottom-bar): pin a whole NIP-29 relay or a single Concord channel
The bottom-bar settings picker only exposed one level of each grouped chat
system: NIP-29 let you pin an individual group but not the host relay, while
Concord let you pin a community but not an individual channel. Since a NIP-29
relay is the analog of a Concord community (the container) and a Concord
channel is the analog of a NIP-29 group (the item), both systems now offer
both levels.

- Add BottomBarEntry.RelayServer(relayUrl) -> Route.RelayGroupServer (the
  relay's home page of all joined groups) and BottomBarEntry.ConcordChannel(
  communityId, channelId, relays) -> Route.Concord (a specific channel), with
  stable @SerialName discriminators and stableKeys.
- Resolve their live avatar/label/route: the relay via its cached NIP-11 doc,
  the channel via the community session's folded Control Plane (community icon
  + channel name).
- Regroup the picker by container: each relay/community is an addable "server"
  row with its groups/channels nested beneath it, so you can add the whole
  server or a single room in both systems.
- Bootstrap a pinned channel's community list (importConcordCommunities and the
  pinned-community preloader now also read ConcordChannel entries).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012v531djZmQBxVoyCm45BNY
2026-07-25 00:43:19 +00:00
Claude affc547b0e fix(buzz): update Messages-list preview + unread dot for Buzz stream channels
A Buzz stream-channel chat message is a kind-40002 StreamMessageV2Event, not a
NIP-C7 kind-9 ChatEvent. It is `h`-scoped and attaches to the same
RelayGroupChannel as a kind-9, but `isGroupChatContent()` only recognized
ChatEvent/PollEvent/ThreadEvent/CommentEvent, so the Messages-list "newest
message" logic (initial scan `newestChatNote` + the live additive
`filterRelevantRelayGroupMessages`) and the unread-dot check all skipped it.
Result: a Buzz channel's row never reflected its real chat and never updated
live as new messages arrived.

Include StreamMessageV2Event in `isGroupChatContent()` (the Buzz dialect of
NIP-29), fixing the Messages preview and unread dot in one place. Adds a
regression test asserting kind-40002 counts as group chat content while a
group-scoped reaction does not.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dtx8Shek4sSAXmHjnNMJF
2026-07-25 00:38:34 +00:00
Claude 57f95cf154 feat(cli): add amy bolt12 — decode, verify, offers, and two-step send
Adds a BOLT12 zap (NIP-XX) command group over a new shared commons
Bolt12ZapActions (assembly-only, mirrors ZapActions):

  bolt12 decode LNO1|LNP1        decode an offer or payer proof
  bolt12 verify EVENT-ID         validate a kind:9736 in the local store
  bolt12 offer get/set           read/publish a kind:10058 offer list
  bolt12 intent … / zap …        two-step out-of-band send (amy has no NWC
                                  rail): intent prints the payer_note; zap
                                  wraps the signed intent + settled proof
                                  into a validated kind:9736 and publishes

Keeps cli a thin assembly layer — all logic is quartz's Bolt12ZapBuilder/
Validator/codecs via commons Bolt12ZapActions. Adds Bolt12ZapActionsTest;
updates README + ROADMAP. Interop harness and NWC-fetched proofs remain TODO.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SpgpWLKzgD7vS9Fs4CXTR3
2026-07-25 00:22:42 +00:00
Claude 9ca79ec2c6 feat: pin icon for Buzz channels, fix Added padding, show "You:" in Messages
- Swap the Buzz community channel favorite from a Star to a PushPin icon
  (and rename the buzz_star/buzz_unstar strings to buzz_pin/buzz_unpin),
  since the action only pins a channel to the top of the list — it never
  publishes anything, unlike Add which imports into the kind-10009 list.
- Give the "Added" state in BuzzImportRow trailing padding so its label no
  longer jams against the row edge when the Add button flips to Added.
- Prefix a Messages-list DM preview with "You:" when the newest message was
  sent by the logged-in user, so a room shows who spoke last.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dtx8Shek4sSAXmHjnNMJF
2026-07-24 23:50:40 +00:00
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