Commit Graph
2297 Commits
Author SHA1 Message Date
Claude 7d7f2f275f refactor(quartz): dedicated NutzapEvent.buildToUser for profile nutzaps
Restores the non-null zappedEvent on NutzapEvent.build and adds a
separate buildToUser builder (p tag only, no e/k tags) for nutzaps that
target a profile instead of an event — mirroring NIP-57's profile zap
convention. CashuWalletOps.sendNutzap dispatches between the two.

https://claude.ai/code/session_01UERRsbDoRPz46Qx5HCXgAa
2026-06-11 19:48:56 +00:00
Claude f2702f9299 feat(profile): unified Send Payment screen for lightning, clink, on-chain and cashu zaps
Replaces the click-to-expand payment cards on the profile page with a
dedicated Send Payment screen that collects amount, optional message and
zap type, pays on the spot through the selected rail, and shows the
invoice-request + payment progress in the screen itself before closing.

- New Route.SendPayment(userHex, method, lnAddressOverride) with a
  stateless SendPaymentContent (previews for editing, fixed-price clink,
  in-progress, success and failure states).
- Rails offered per profile: Lightning (lud16/lud06 or a lightning
  payment target), CLINK offer (kind-0 / NIP-05, with expired-or-moved
  redirect), on-chain NIP-BC (fee tier selector), and NIP-61 cashu
  nutzaps gated on a shared funded mint.
- Lightning rail keeps the Public/Private/Anonymous zap types and adds
  the Non-Zap (plain payment) option; clink is a direct payment; cashu
  and on-chain receipts are inherent to their protocols and noted as such.
- Paying from this screen skips the extra in-app wallet confirmation
  dialog: the explicit amount + Pay tap is the confirmation.
- Profile LN-address row, CLINK chip, lightning payment-target chips and
  the wallet button's pay action now navigate to the new screen; other
  target types keep their external payto/URI behavior.
- NutzapEvent.build / CashuWalletState.sendNutzap now accept a null
  zapped event so nutzaps can target a profile (p-tag only), and
  AccountViewModel gains sendNutzapToUser + a zapType override on
  sendSats.

https://claude.ai/code/session_01UERRsbDoRPz46Qx5HCXgAa
2026-06-11 19:28:02 +00:00
Claude 67ea7c5c95 Merge remote-tracking branch 'origin/main' into claude/sweet-shannon-smjotq 2026-06-11 19:01:36 +00:00
Claude 438f37a1ad Merge remote-tracking branch 'origin/main' into claude/kind-lamport-dwtzh8 2026-06-11 19:00:39 +00:00
Claude d242eb62aa Merge remote-tracking branch 'origin/claude/trusting-mayer-6o0yd5' into claude/trusting-mayer-6o0yd5 2026-06-11 18:18:57 +00:00
Claude 990c5afe99 Merge remote-tracking branch 'origin/main' into claude/trusting-mayer-6o0yd5
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt
2026-06-11 18:15:04 +00:00
Vitor PamplonaandClaude Opus 4.8 d1bd5734cd fix(relay): rebuild sockets opened on the wrong transport
connectAndSyncFiltersIfDisconnected() bailed whenever a socket already
existed, so a still-connecting socket built for the wrong transport (e.g.
a relay whose Tor classification changed since the dial started) could
never be preempted — it blocked until the hung dial timed out. The
connected-relay path in RelayPool.reconnectIfNeedsTo already rebuilds
ready sockets via needsToReconnect(); this covers the connecting state it
cannot see (isConnectionStarted() true but isConnected() false).

Now: if a socket exists but reports needsReconnect() (transport/proxy
mismatch against the current builder decision), drop it and redial on the
correct transport; otherwise leave it. Disconnected relays still honor
their reconnect backoff.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 13:34:49 -04:00
Claude c6b09c4b53 fix: anonymous profile zaps were encrypted as private zaps; nutzap chips reply on long-press
The user-only LnZapRequestEvent.create overload marked ANONYMOUS requests
with a blank-valued anon tag, which the signer treats as an unsigned
private zap: the message was encrypted to the recipient under the
throwaway key instead of staying public. Use the valueless anon tag, as
the event-targeted overload already does. Adds a regression test.

Also carries the nutzap note into the notification gallery chips so the
long-press reply-to-zap gesture works for NIP-61 nutzaps too — no extra
tagging needed there since nutzaps are signed by the sender.

https://claude.ai/code/session_01LM3KTECMMAdNBHZfs1dANa
2026-06-10 23:41:26 +00:00
Claude 2eb6510eec fix: stop refetching the full MLS kind:445 backlog on every restart
The Marmot subscription since, the processed-event dedup set, and the
application ratchet position (group state persists only at commits) are
all in-memory only. On restart, relays therefore redeliver the group's
entire kind:445 history and the rewound ratchet re-decrypts old
application messages as if they had just arrived — wasted decryption
work and, when a replay beats the disk restore, duplicate entries
appended to the persisted plaintext message log.

Two defenses:

- MarmotManager.restoreAll() now seeds each restored group's
  subscription since from the newest persisted decrypted message, minus
  a one-day overlap window for late/out-of-order publishes. Seeding
  happens before syncWithGroupManager registers default entries, so
  even the first filter set sent to relays carries it. The CLI is
  unaffected: it builds group filters from its own persisted since.

- MarmotMessageStore appends are now explicitly idempotent (contract
  was previously ambiguous and both real stores appended blindly):
  the Android and CLI file stores skip an entry that is already in the
  group's log, so replays inside the overlap window cannot grow it.

Covered by MarmotManagerRestoreTest in commons jvmTest — placed there
rather than androidHostTest because CI only runs :commons:jvmTest (the
androidHostTest task currently fails on android.util.Log stubs even
for the pre-existing Marmot test).
2026-06-10 23:03:48 +00:00
Claude e4ce7b887d feat: support replying to zaps from the notification screen
Zap receipts (kind 9735) are signed by the recipient's lightning provider,
not by the person who zapped, so both the reply tagging and the rendering
around replies-to-zaps need the sender resolved from the embedded kind 9734
zap request:

- Long-press on a zap chip in the notification galleries (MultiSetCard and
  ZapUserSetCard) opens the NIP-22 comment composer targeting the zap
  receipt, reusing the existing generic-comment fallback in routeReplyTo.
- CommentPostViewModel now p-tags the zap request author when replying to a
  zap so the zapper actually gets notified (the receipt's own author tags
  point at the custodian). Requests carrying an anon tag (anonymous or
  private zaps) are skipped: the embedded key is ephemeral and tagging the
  decrypted sender of a private zap would publicly expose them.
- The notifying chip row shows the zapper and removing the chip is
  respected, including across draft reload.
- FirstUserInfoRow and the compact reply-to label now display the zap
  sender (decrypted for private zaps, locally only) instead of the wallet
  service when the note or the replied-to parent is a zap receipt.
- ZapAmountCommentNotification carries the receipt note so chips can act on
  the zap itself.
- Adds LnZapRequestEvent.hasAnonTag() with tests covering public, anonymous
  and private zap requests.

https://claude.ai/code/session_01LM3KTECMMAdNBHZfs1dANa
2026-06-10 22:55:25 +00:00
Claude 7e7898bf77 refactor(clink): audit follow-ups — consistent error detail, non-null priceType, budget guard
From the audit of this session's changes:

- Error surfacing: the budget (WalletScreen) and offer/invoice card
  (InvoicePaymentDispatcher) paths now use DebitResponse.failureDetail() like the
  zap path, so a GFY code-5/code-4 surfaces its range/retry_after instead of just
  the bare error string.
- NOffer.priceType is now non-null: decode already defaults an absent TLV 3 to
  SPONTANEOUS, so the nullable type was misleading and the '?: SPONTANEOUS'
  fallbacks in ClinkOfferPreview were dead. Drops them and the now-redundant
  always-emit-TLV3 test (covered by the spontaneous round-trip).
- WalletViewModel.requestDebitBudget catches the budget-validation
  IllegalArgumentException so a malformed frequency dismisses the dialog instead
  of hanging the spinner.
- Document why ClinkDebitPayer signs with the persistent account key (stable
  identity for budgets) while ClinkOfferPayer uses an ephemeral key.

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 18:55:57 +00:00
davotoula 572f4005e1 test: guard kind-range vs class-hierarchy invariant in EventFactory
Sweeps every typed kind: addressable kinds (30000..39999) must read
their d tag, plain replaceables (10000..19999, 0, 3) must ignore stray
ones — the invariant the kind-34235/34236 fix restores.
2026-06-10 20:33:22 +02:00
davotoula d90574c4e9 refactor: rename ReplaceableVideoEvent to AddressableVideoEvent 2026-06-10 20:32:57 +02:00
Claude 61387ba12c docs(clink): record interop review + spec-conformance pass results
https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 17:40:29 +00:00
Claude f4e0bcf73d fix(clink): spec-conformance hardening (k1 length, frequency, description, GFY detail)
Follow-ups from the line-by-line spec audit, scoped to the consume-only client:

- NDebit.parse rejects a TLV-3 session id that isn't exactly 32 bytes (64 hex),
  per clink-debits: a wrong-length k1 is a malformed session pointer.
- DebitClient.requestBudget validates frequency.unit is one of day/week/month
  (DebitFrequency.VALID_UNITS) instead of sending a unit a node service will GFY.
- OfferClient caps the invoice description at 100 chars per clink-offers.
- DebitResponse.failureDetail() composes the GFY error with its actionable extra
  (allowed range for code 5, retry_after for code 4); the debit zap path now
  surfaces that instead of the bare error string.

Adds regression tests for each (malformed-k1 rejection, invalid-unit throw,
description truncation, failureDetail range/retry_after).

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 16:44:00 +00:00
Claude c78ba27c86 test(clink): correct provenance of the default-offer interop vector
The vector is the canonical @shocknet/clink-sdk example — both its MIT README
usage snippet and clink-demo's public-domain DEFAULT_NOFFER are the same string.
Confirmed the published npm tarball ships only build output (no test vectors), so
this is the one real codec vector the ecosystem exposes.

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 16:15:25 +00:00
Claude eb7b3bad07 test(clink): golden wire-shape fixtures from the public-domain specs
Adds ClinkWireShapeTest: the literal decrypted JSON payload bodies documented in
shocknet/CLINK/specs/clink-{offers,debits,manage}.md (public domain) must
deserialize into our DTOs with the right fields. Covers the encrypted-content
half the bech32 pointer vectors don't: offer request + success/error codes 1-5
(incl. code-3 latest, code-5 range) + receipts; debit direct/budget requests,
success, and GFY 1-6 (incl. delta, retry_after, range); manage nested
offer.fields requests and responses — including the single-object 'details'
coercing to a list, which exercises the Manage list/single interop fix.

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 15:46:51 +00:00
Claude 5e465410b2 test(clink): add clink-demo DEFAULT_NOFFER interop vector
The clinkme.dev demo (shocknet/clink-demo, public domain) hard-codes a live
default noffer. Adds it as an 8th cross-impl vector — a real-world spontaneous,
relay-bearing, no-price offer with a 64-char-hex offer-id — decoded and
round-tripped through our parser.

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 15:26:03 +00:00
davotoula 638486ea1f Compute legacy NIP-71 video addresses with their d tag:
Extend BaseAddressableEvent instead so dTag() reads the real `d` tag.
2026-06-10 17:20:14 +02:00
Claude f9ed2e0ab7 feat(clink): close ecosystem interop gaps (manage list, TLV3, NIP-05, receipts, payer privacy)
Interop review against the shocknet/CLINK ecosystem (Lightning.Pub, clink-sdk,
ShockWallet, Zeus, Stacker News, bridgelet, clinkme.dev) surfaced five fixes:

1. Manage `details` single-object responses now parse. Lightning.Pub returns a
   bare OfferData object for create/update/get and an array only for list; enable
   Jackson ACCEPT_SINGLE_VALUE_AS_ARRAY so both shapes coerce into the list field.
2. NOffer.encode() always emits the price-type TLV (3), even for spontaneous
   offers — the reference SDK and bridgelet decoders throw on a missing TLV 3, so
   an absent field made our pointers undecodable by every JS consumer. Decode now
   defaults an absent/unknown price-type to SPONTANEOUS, per the spec.
3. Nip05Parser.parseClinkOffer accepts bridgelet's flat top-level
   `"clink_offer":"noffer1…"` string in addition to the spec's per-name map.
4. Offer payment receipts: OfferEvent.createReceipt/decryptReceipt +
   OfferClient.parseReceipt + OfferReceipt.isOk() make the post-settlement receipt
   (the SDK's onReceipt) a parseable primitive instead of a dead DTO.
5. ClinkOfferPayer signs offer requests with an ephemeral key, like the SDK / Zeus
   / Stacker News, so paying an offer no longer reveals the user's Nostr identity
   to the service. Debits keep the persistent account key (budgets need a stable
   app identity).

Adds regression tests for each: always-emit TLV3, flat-string NIP-05 discovery,
and a receipt round-trip.

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 15:16:44 +00:00
Claude 33b3eb5b6f docs(clink): record final implementation state, audit results, spec-vs-SDK notes
Append a 'Final implementation state' section to the CLINK plan capturing
what shipped across Phases 0-3 + the receive side + CLI, the audit findings
and their fixes, the three-level verification matrix, and the critical
spec-vs-SDK gotchas (offer 'latest' at GFY code 3 and ndebit k1 at TLV-3 are
spec-defined and must not be removed). Flip the doc status to implemented.

https://claude.ai/code/session_01NM2TyJtosLdY5ycjyabSRS
2026-06-10 07:18:02 +00:00
Claude 2bd18eb50d test(clink): regression tests for the audit fixes
Locks in the protocol-layer fixes that were previously only compile-checked:
- offerLargePriceRoundTripIsUnsigned: a price > Int.MAX_VALUE round-trips as a
  positive Long (guards the unsigned-decode fix).
- cannotDecryptAuthoredEventMissingRecipient: an authored event with no p tag
  can't be decrypted by anyone (guards the no-self-fallback conversationPeer).
- manageCreateRequestSerializesNested + manageFailureResponseParsesField: the
  Manage request nests under offer.fields, payer_data is a string list, and the
  failure response carries field (guards the 21003 shape fix).

All CLINK tests pass.
2026-06-10 05:10:54 +00:00
Claude 488d459984 harden(clink): no self-decrypt fallback + #p on response filters
Two robustness fixes from the audit:
- OfferEvent/DebitEvent/ManageEvent: replace talkingWith() (which fell back to
  self when an authored event lacked its p tag, deriving a NIP-44 key with
  myself) with conversationPeer(), which returns null when I'm neither the
  author nor the addressed recipient; decryptContent then fails cleanly with
  UnauthorizedDecryptionException. canDecrypt() is now exactly 'a valid peer
  exists'.
- OfferClient/DebitClient/ManageClient responseFilter now also requires
  #p == my pubkey, so a service reply that e-tags my request but is addressed
  to a different payer no longer matches my subscription.

Valid request/response round-trips are unchanged (CLINK tests pass).
2026-06-10 04:32:51 +00:00
Claude 3968790db1 fix(clink): audit fixes — unsigned offer price, Manage shape, NIP-05 cache
From a spec/SDK audit (verified against the CLINK spec, not just SDK 1.5.5):
- NOffer.price: decode as UNSIGNED 4-byte big-endian (now Long) — the SDK reads
  price via parseInt(hex); reading it signed turned prices >= 2^31 sats negative
  and broke encode/decode idempotency for high-bit prices.
- Manage (21003) messages corrected to the nested spec shape: request nests offer
  data under offer{id,fields}, payer_data is a string list (not a map), and the
  response uses details + field (was offer/offers). Documented the single-object
  details limitation (Manage is consume-unused).
- DisplayClinkOffer: cache NIP-05 .well-known clink_offer lookups (incl. negative
  results) so profile visits / kind-0 refreshes don't refetch nostr.json.

Deliberately NOT changed: the offer 'latest' (code 3) field and ndebit k1 at
TLV-3 — both are SPEC-defined; the SDK 1.5.5 merely lags, as the code comments
already noted. CLINK tests pass; app compiles.
2026-06-10 04:20:47 +00:00
Claude 084c7be23a feat(clink): debit spending budgets (one-time + recurring)
Exposes the CLINK Debits budget capability (requestBudget) the spec describes:
- ClinkDebitPayer.requestBudget publishes the kind-21002 budget request and
  awaits the reply; the publish/await machinery is factored out of payInvoice
  into a shared sendAndAwait helper.
- DebitFrequency gains UNIT_DAY/WEEK/MONTH constants.
- WalletViewModel.requestDebitBudget resolves the debit pointer and runs it.
- A 'Budget' action on CLINK debit rows opens ClinkBudgetDialog (amount +
  one-time/daily/weekly/monthly cadence); the result is surfaced as a toast.

:amethyst compiles. The 21002 budget round-trip is untested end-to-end.
2026-06-10 02:37:46 +00:00
Claude 7bac8cfd46 refactor(clink): clink_version as a shared ClinkVersionTag class
Models the protocol-version tag the way other tags are modeled, instead of a
loose helper on the Clink object:
- New ClinkVersionTag (TAG_NAME/CURRENT/assemble/parse) under clink/tags, with
  a clinkVersion() TagArrayBuilder DSL extension, reused by all three events.
- OfferEvent/DebitEvent/ManageEvent read version() via ClinkVersionTag::parse
  and build via clinkVersion() in their templates.
- Retires the now-empty Clink object (its KDoc moved to the tag class).

Behavior-preserving: assemble() emits the identical ["clink_version", "1"]
tag in the same position. All CLINK tests pass.
2026-06-10 01:41:04 +00:00
Claude 03b091d3ec refactor(clink): model event tags via PTag/ETag classes + builder DSL
Brings OfferEvent/DebitEvent/ManageEvent (21001-3) in line with the codebase
tag conventions, replacing raw inline tags:
- Build via eventTemplate(KIND, content) { pTag(...); eTag/add; alt(...) } and
  signer.sign(template), instead of hand-rolled arrayOf("p"/"e", ...) + sign().
- Accessors use PTag.parseKey / ETag.parseId instead of matching "p"/"e" literals.

Behavior-preserving: PTag.assemble(x, null) yields the identical ["p", x] bytes
and tag order is unchanged, so signed events are byte-identical. All CLINK tests
pass (ClinkEventTest, ClinkClientServerTest, pointer/interop).

Note: these are NIP-44-encrypted request/response events, so create*() stays a
suspend factory that encrypts then signs the template — matching NIP-47; a pure
pre-signing template isn't possible without the signer.
2026-06-10 01:26:06 +00:00
Claude c1a0e707a0 refactor(clink): clink_offer metadata via ClinkOfferTag + DSL builder
Brings the kind-0 clink_offer field in line with the sibling fields' structure
instead of a raw string constant written to content only:
- New ClinkOfferTag (TAG_NAME/assemble/parse) under nip01Core/metadata/tags.
- clinkOffer() TagArrayBuilder DSL extension in TagArrayBuilderExt.
- MetadataEvent uses ClinkOfferTag.TAG_NAME and dual-writes it as a kind-0 tag
  in updateOrDeleteTagNames (NIP-1770 pattern), like lud16/nip05; drops the
  ad-hoc CLINK_OFFER_PROPERTY constant.

UpdateMetadataTest now also asserts the tag is emitted. quartz tests pass.
2026-06-10 01:10:49 +00:00
Claude 6b9184bf50 feat(clink): read + surface a profile's noffer (kind-0 + NIP-05)
Completes the receive side: a payable CLINK Offer card now appears on a
profile that advertises one, preferring the kind-0 clink_offer and falling
back to the NIP-05 .well-known clink_offer.
- Nip05Parser.parseClinkOffer + INip05Client.loadClinkOffer fetch/parse the
  well-known clink_offer (keyed by local name, mirroring the names map; exact
  shape isn't a finalized spec so a mismatch yields null). JVM-tested.
- DrawAdditionalInfo.DisplayClinkOffer resolves kind-0 first, else fetches
  NIP-05 on IO, parses the noffer, and renders ClinkOfferPreview zapping the
  profile.

quartz tests pass; :amethyst compiles. Network fetch + card render untested
end-to-end.
2026-06-09 23:39:36 +00:00
Claude 50b4ed8c1e feat(clink): kind-0 clink_offer metadata field
Adds the CLINK Offers discovery pointer to profile metadata, mirroring the
NIP-05 `clink_offer` key:
- UserMetadata.clinkOffer (@SerialName clink_offer) + clinkOffer() accessor,
  with trim/blank cleanup alongside the other fields.
- MetadataEvent.createNew/updateFromPast gain a clinkOffer param written into
  kind-0 content via the new CLINK_OFFER_PROPERTY key.

Covered by UpdateMetadataTest (write + parse round-trip) on JVM.
2026-06-09 23:22:36 +00:00
Claude ef7658ae09 test(clink): add cross-impl interop vectors from @shocknet/clink-sdk
Adds ClinkInteropTest with bech32 pointer strings generated by the
reference TypeScript SDK (clink-sdk 1.5.5) for noffer/ndebit/nmanage.
Asserts our parser decodes the SDK's bytes into the expected fields and
that re-encoding round-trips. TLV is order-independent on decode, so
interop is functional (not byte-identical: we emit fields ascending,
the SDK descending); the reverse direction (SDK decoding our output)
was verified out-of-band against decodeBech32.
2026-06-09 20:39:45 +00:00
Claude c619338204 feat(clink): add CLINK client and server facades
Adds the high-level request/response orchestration over the CLINK
pointers and event kinds (experimental/clink):
- OfferClient / DebitClient / ManageClient: build the kind-21001/2/3
  request from a decoded pointer, expose the relays to publish on, the
  response filter (kind + author + #e=requestId), and the response parser
- ClinkServer: per-kind request filters (#p=service), 30s freshness
  check, plus K1Tracker for single-use debit session enforcement

Filter construction, freshness window and k1 single-use covered by
ClinkClientServerTest on JVM; request-building encryption round-trips
will be added under androidDeviceTest (lazysodium constraint).
2026-06-09 20:24:48 +00:00
Claude 8fa06f525f feat(clink): add CLINK request/response event kinds and DTOs
Adds the three CLINK message kinds to quartz (experimental/clink):
- OfferEvent (21001), DebitEvent (21002), ManageEvent (21003), each
  carrying both request and response over one kind, NIP-44 encrypted,
  with p + clink_version tags and an e tag on responses
- Request/response DTOs per spec (offers, debits, manage) plus shared
  SatRange/GfyDelta and GFY/offer error-code constants
- Registers all three kinds in EventFactory

Pure-logic + JSON (de)serialization covered by ClinkEventTest on JVM;
the NIP-44 encrypt/decrypt round-trip will live in androidDeviceTest
(lazysodium is unavailable in JVM unit tests).
2026-06-09 20:19:17 +00:00
Claude 41b1b63482 feat(clink): add CLINK bech32 pointer types and parser
Implements the noffer/ndebit/nmanage pointers (CLINK Offers/Debits/Manage)
as standard-bech32 TLV codes, with a dedicated ClinkPointerParser kept
separate from NIP-19. Wire format (HRPs, TLV indices, single-byte priceType,
4-byte big-endian price) verified against @shocknet/clink-sdk 1.5.5.

Adds round-trip + dispatch + reject tests in commonTest.
2026-06-09 20:04:00 +00:00
Claude 5f41907149 docs(clink): add CLINK protocol implementation plan
Plan to implement CLINK (Offers 21001 / Debits 21002 / Manage 21003) on
Quartz (client + server) and Amethyst (consume-only), reusing NIP-44,
bech32/TLV, the NWC encrypted-event pattern, and ZapPaymentHandler.
Pointers parsed by a dedicated ClinkPointerParser (separate from NIP-19).
2026-06-09 19:44:21 +00:00
Vitor PamplonaandClaude Opus 4.8 96ff5316dc fix(dm): realign the per-relay download window when DMs are pruned
Memory pruning drops DM messages out of the cache but left the per-relay
paging cursors untouched, so a relay still claimed to have delivered the
dropped band (reachedUntil deep, or done) and the demand-driven loader
never re-requested it — a silent hole until app restart.

- Prune NIP-17 too: pruneMessagesToTheLatestOnly now reaps both NIP-04
  (PrivateDmEvent) and NIP-17 (WrappedEvent rumors) on one merged top-N
  cut, so a conversation is cut at a single time point (no NIP-04-without
  -NIP-17 holes). NIP-17 is the actual memory-pressure driver.
- HostStub carries the host's createdAt, so a decrypted rumor self-
  describes its outer gift-wrap time (the time the cursor pages by; the
  rumor's own time is the message time, not the wrap time).
- RelayLoadingCursors.rewindTo() pulls a relay's reached cursor up past
  the pruned band, clears done, and un-arms it (demand-driven re-fetch);
  advance() now resumes from the rewound reached point instead of the
  floor.
- LocalCache.pruneOldMessages accumulates the newest pruned created_at
  per relay (outer-wrap time for gift wraps, event time for NIP-04),
  filtered below each cursor's floor, then rewinds giftWrapHistory +
  rooms-list nip04History (account-wide) and the per-conversation
  nip04History.

The gift-wrap window is account-global, so pruning one room rewinds the
shared sweep; the interference is bounded (already-held wraps short-
circuit in consumeRegularEvent, re-fetch is demand-gated).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:03:50 -04:00
Claude 1e76705c87 Merge remote-tracking branch 'origin/main' into claude/relay-message-pagination-LMqSQ
# Conflicts:
#	commons/src/commonMain/composeResources/values/strings.xml
2026-06-08 18:15:56 +00:00
Vitor PamplonaandGitHub f05500792c Merge pull request #3144 from davotoula/fix/resilient-profile-metadata
Resilient profile metadata (birthday)
2026-06-07 17:36:11 -04:00
davotoula 0107808ef6 Code review:
- Expose a nullable descriptor
- Log the JSON element kind instead of the raw, network-sourced value.
- drop birthday happy-path tests duplicated by UpdateMetadataTest
2026-06-07 23:18:57 +02:00
davotoula 39531b85fb fix(metadata): tolerate non-spec birthday so it can't drop the profile 2026-06-07 23:05:11 +02:00
davotoula fba4b933b0 Code review:
- Make birdex_species_preview_more a <plurals> keyed on the remaining count
- Bound the species preview with maxLines=2
- Hoist the joined-names remember out of the conditional (stable slot).
- Drop the unused accountViewModel parameter
- BirdexEvent.speciesCount() derives from speciesNames().size instead of re-scanning tags
- remember() the joined species-name string so it is not rebuilt on every recomposition.
2026-06-07 22:37:05 +02:00
davotoula 98ff13b83f feat(birdstar): render Birdex species collections (kind 12473) 2026-06-07 22:18:29 +02:00
nrobi144 8b9875d9cc fix(quartz): NIP-46 bunker double-resume + retry id-reuse races
Two correctness bugs in `RemoteSignerManager` (NIP-46) and its NIP-55
sibling `IntentRequestManager`:

1. **Double-resume crash** — `awaitingRequests.get(id)?.resume(value)`
   was non-atomic. Multi-relay delivery, bunker echo/retry, and
   late-after-timeout responses could call `resume` twice for the same
   continuation, throwing `IllegalStateException: Already resumed` on a
   `Dispatchers.Default` worker.
2. **Retry id-reuse → wrong data** (NIP-46 only) —
   `launchWaitAndParse` built the request and event once, then re-used
   the same `request.id` across retry attempts. A late response from
   attempt N could resume attempt N+1's continuation with stale data.

Replace the cached-`Continuation` map with the in-house Channel-per-request
correlation pattern already used in `quartz/.../accessories/NostrClientPublishExt.kt`
(`LargeCache<id, Channel<Response>(capacity=1)>` + atomic `remove` +
`trySend` + `withTimeoutOrNull { receive() }`). Each retry attempt now
builds a fresh request with a new id; the builder is still called only
once. `finally`-block cleanup removes the cache entry on every path,
incidentally fixing a slow leak on the success path.

Adds three regression tests:
- duplicate responses → no crash + single resume (fails on \`main\`
  with \`IllegalStateException\`)
- late response after timeout → silently discarded
- late attempt-1 response does not corrupt attempt-2 result (fails on
  \`main\`: the two attempts share an id)

Design + review notes: \`quartz/plans/2026-06-03-fix-nip46-bunker-double-resume-plan.md\`
2026-06-07 14:36:29 +03:00
davotoula 7aa04e773b Code review:
- read fundraiser value tags via shared helpers
2026-06-06 12:50:29 +02:00
davotoula be12abcb21 feat(agora): render Agora fundraiser campaigns (kind 33863)
Agora (a crowdfunding client on the Ditto stack) publishes fundraising
campaigns as kind 33863 — an app-specific addressable kind with no NIP.
Amethyst had no parser or renderer, so it hit the "Event Not Supported"
path and was dropped; reposts of one rendered as a permanently blank card.

Add first-class support, modelled on NIP-99 Classifieds (title/image/body)
plus NIP-75 zap goals (goal/deadline/progress).
2026-06-06 12:50:02 +02:00
Vitor PamplonaandClaude Opus 4.8 26c0ae7f69 refactor(dm): move the paging orchestrators from quartz to commons/relayClient
Per commons/ARCHITECTURE.md, quartz is protocol/NIPs/crypto/relay framing while
commons owns the relay-subscription client and StateFlow state holders. The
paging *orchestrators* are exactly that — StateFlow-backed, subscription-loading
state — so they belong in commons, not quartz:

- BackwardRelayPager, PerRelayLoadTracker, WindowLoadTracker (+ trackingListener)
  -> commons/relayClient/paging (jvmAndroid source set, same as before).
- BackwardRelayPagerTest -> commons jvmTest.

The pure protocol-paging primitives stay in quartz commonMain:
- RelayLoadingCursors (the until+limit cursor mechanics) and RelayPagingProgress.

They had no upward deps, so the move is downhill (commons -> quartz): the
orchestrators now import RelayLoadingCursors / RelayPagingProgress from quartz.
Consumers (the six DM managers/assemblers + WindowLoadTrackerIdleTest) repoint
their imports to the commons package. The quartz geode wire test keeps testing
the relay contract; its lone BackwardRelayPager KDoc link is demoted to a
backtick (no longer reachable from quartz).

No behaviour change. DM suite green (26/26); quartz + commons compile on iOS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:52:58 -04:00
Vitor PamplonaandClaude Opus 4.8 1751f41a68 docs(dm): keep the rest of the quartz paging package module-neutral
Same principle as RelayLoadingCursors: BackwardRelayPager, PerRelayLoadTracker,
WindowLoadTracker and RelayPagingProgress are reusable quartz classes, so their
docs shouldn't lean on Amethyst's Chatroom / ChatroomList / feeds / loading card
/ on-screen markers / sentinels / "decrypted into rooms" / invalidateFilters().
Reworded to generic library terms ("the caller", "the bound scope", "a
demand-driven loader", "a per-relay progress display", "the owning object").

Comment-only. (The DMPagination log tag stays — it's an established log key,
not doc prose.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:22:45 -04:00
Vitor PamplonaandClaude Opus 4.8 3fde69c3a9 docs(dm): keep RelayLoadingCursors KDoc free of downstream-module concepts
It's a reusable quartz class, so it shouldn't document itself in terms of
Amethyst's Chatroom / ChatroomList / feeds / on-screen markers / sentinels /
invalidateFilters(). Reword generically: "the caller holds one instance per
scope on whatever object owns it", "a demand-driven loader", "the loaded-back-to
point", "the owner re-issues the relay's REQ". BackwardRelayPager demoted to a
backtick mention (it's jvmAndroid, so the KDoc link can't resolve from
commonMain anyway).

Comment-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:18:23 -04:00
Vitor PamplonaandClaude Opus 4.8 d836c06772 docs(dm): tighten the RelayLoadingCursors KDoc
Same content, ~⅓ shorter: fold the per-relay/on-demand intro into the
asked-vs-delivered framing, compress the time-window rationale and the
thread-safety note, keep the two-cursor explanation and the short-page caveat.

Comment-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:14:19 -04:00
Vitor PamplonaandClaude Opus 4.8 50676bcc64 docs(dm): fix stale KDoc links on RelayLoadingCursors
After the class moved to commonMain and was renamed, two doc nits remained:
- [BackwardRelayPager] is a jvmAndroid type, so the KDoc link can't resolve
  from commonMain — demote it to a plain mention.
- the bare [done] links pointed at the private RelayCursor.done, not a member
  of this class — repoint them to the public [isDone].
Also reword "Not internally synchronized" (it leans on the thread-safe
LargeCache + serialized per-relay callbacks) to not read as a contradiction.

Comment-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:11:51 -04:00