Commit Graph
2124 Commits
Author SHA1 Message Date
Vitor PamplonaandGitHub b37e62cd62 Merge pull request #3186 from nrobi144/feat/unhealthy-relay-review
feat(desktop): unhealthy-relay review banner + popup
2026-06-12 09:31:34 -04:00
nrobi144 a46a72a89f feat(desktop): unhealthy-relay review banner + popup
Surfaces relays unresponsive for 7+ days across the user's NIP-65 (10002),
DM (10050), and Search (10007) relay lists. A non-modal banner appears
above feed columns (and above the single-pane content) whenever the
classifier finds anything; tapping it opens an anchored Popup with one
row per unhealthy relay and per-row Remove / Open Dashboard / Snooze 7d
actions plus a banner-level "Snooze all 7d".

Quartz
- RelayStat gains best-effort lastConnectAt + lastIncomingAt timestamps
  (epoch seconds, 0 = never observed). RelayStats listener pushes them
  on onConnected / onIncomingMessage. Durable per-relay history lives
  outside quartz in the commons RelayHealthStore.

Commons (new commons/relays/health/ package)
- classifyRelayHealth() pure function with the v1 gates:
  * first-run grace (don't flag for 7d after firstScanAt)
  * offline grace (don't flag if no relay anywhere has responded)
  * Tor-mode skip (relay timing is intentionally lossy through Tor)
  * per-relay snooze (snoozedUntil > now)
  * 10006 (blocked) excluded from detection but still part of the
    multi-list Remove action
- RelayHealthStore (account-scoped, supervised scope, 5s debounced
  persist, 60s ticker for snooze expiry).
- RelayHealthListener wires the quartz lifecycle into the store.
- RelayHealthPersistence interface (no expect/actual — single impl per
  platform via injection).
- RelayListMutator interface + RelayRemovalResult sealed type.
- Shared UnhealthyRelayBanner (errorContainer @ 50% alpha) and
  UnhealthyRelayRow (static outlined tag chips, no ripple) composables.
- 8 classifier unit tests covering each gate + multi-list membership.

Desktop wiring
- PreferencesRelayHealthPersistence (java.util.prefs.Preferences, per
  account via 8-char pubkey prefix).
- DesktopRelayListMutator runs the 4 sign-and-broadcast jobs in
  parallel via async/awaitAll so a slow NIP-46 bunker doesn't multiply
  latency by 4.
- Banner placed in DeckColumnContainer + SinglePaneLayout, store +
  listener + per-account scan trigger wired in Main.kt's MainContent.

Scope: Desktop only for v1. Android wiring is intentionally not in
this PR — the commons module is platform-neutral and ready for Android
to follow whenever someone wants to pick it up.
2026-06-12 06:40:46 +03:00
Vitor PamplonaandGitHub d91988b26b Merge pull request #3185 from vitorpamplona/claude/focused-einstein-6jjqmj
Add unified profile payment screen with multi-rail support
2026-06-11 19:14:43 -04:00
Vitor PamplonaandGitHub 1e23b14ff2 Merge pull request #3184 from vitorpamplona/claude/beautiful-turing-j0czsm
Add NIP-101e fitness workout support (Kind 1301)
2026-06-11 18:26:36 -04:00
Claude 4361f95a17 feat: NIP-101e workout records (kind 1301) + Workouts feed screen
Quartz: new experimental/fitness/workout package shaped like nip88Polls —
WorkoutRecordEvent with per-tag classes (exercise, duration, distance,
elevation, calories, steps, heart rate, splits, strength sets/reps/weight,
source, workout_start_time), TagArrayBuilder/TagArray extensions, lax
RUNSTR-dialect parsing (unit defaults, HH:MM:SS or raw seconds), and
EventFactory + LocalCache registration. Covered by fixture tests.

Amethyst: new Workouts feed (drawer entry, route, follow-list top bar,
per-relay filter assemblers mirroring the Pictures feed) with a + FAB
opening a manual workout composer that publishes canonical kind-1301
events. Workout cards render stats chips and also display inside threads
via NoteCompose. Adds fitness Material Symbols glyphs and regenerates the
subset font.

https://claude.ai/code/session_01Kpx53UEeJqqR7CASzMu6GB
2026-06-11 21:48:32 +00:00
Claude 9999d92bca fix: register CLINK DTO serializers in KotlinSerializationMapper for native targets
All CLINK tests failed on iosSimulatorArm64 with IllegalArgumentException
because OptimizedJsonMapper on native dispatches through
KotlinSerializationMapper, whose fromJsonTo/toJson type lists did not
include the CLINK payload DTOs (Jackson handles them reflectively on
JVM/Android, which is why only iOS failed).

Adds hand-written kotlinx serializers for OfferRequest/OfferResponse/
OfferReceipt, DebitRequest/DebitResponse, and ManageRequest/ManageResponse,
mirroring Jackson behavior: ManageResponse.details coerces a lone object
into a one-element list (ACCEPT_SINGLE_VALUE_AS_ARRAY) and
OfferRequest.payer_data round-trips as a free-form JSON object.

Covered by a JVM test driving KotlinSerializationMapper directly and
cross-checking against Jackson, since the native path shares this code.

https://claude.ai/code/session_01SevV4fUCumKZ1UscSz85vS
2026-06-11 21:33:49 +00:00
Claude 02e0d9a4be feat(profile): pay bitcoin payment targets through the in-app on-chain wallet
Lightning payment targets already route into the Send Payment screen;
this extends the same treatment to bitcoin targets. Tapping a profile's
bitcoin payment-target chip (or its pay action in the wallet-button
dialog) now opens the Send Payment screen with the on-chain rail locked
to that announced address, paid directly from the user's NIP-BC Taproot
wallet — falling back to the external bitcoin: URI when the chain
backend is missing or the address isn't a payable native-segwit mainnet
address.

- quartz: SegwitAddress.scriptPubKeyFor/isPayableMainnetAddress;
  OnchainZapBuilder.buildToScripts core shared by the pubkey paths.
- commons: OnchainZapSender.sendToAddress — plain wallet send with the
  same fund-safety signing contract but no kind:8333 receipt (the
  destination isn't pubkey-derived, so none is possible); the signing
  block is now a single shared helper across send/sendSplit/sendToAddress
  and Success.receiptEventId is nullable for receipt-less sends.
- amethyst: Account.sendOnchainToAddress; Route.SendPayment gains
  btcAddressOverride; a shared inAppPaymentRouteFor() decides which
  payment targets the user's wallets can pay in-app (used by both the
  target chips and the payment-targets dialog).
- Send Payment screen: with an address override the on-chain rail shows
  the target address, hides the message field (no receipt to carry it),
  explains that no zap receipt is published, and dispatches the plain
  address send.

https://claude.ai/code/session_01UERRsbDoRPz46Qx5HCXgAa
2026-06-11 21:01:07 +00:00
Vitor PamplonaandGitHub 77a26ed131 Merge pull request #3180 from vitorpamplona/claude/dazzling-sagan-1c0ncz
Optimize string resource loading in WalletScreen
2026-06-11 16:40:07 -04:00
Claude 38023dac45 fix: resolve iOS test-name compile error and LocalContext lint error
- Rename Nip05Test backticked test name to drop parentheses, which are
  illegal identifier characters on Kotlin/Native (iosSimulatorArm64).
- Resolve CLINK budget toast strings at composition time via stringRes
  instead of context.getString inside the async callback, fixing the
  LocalContextGetResourceValueCall lint errors in WalletScreen.

https://claude.ai/code/session_01UgP8ErzBbQYkTDtkJx5nrt
2026-06-11 20:10:20 +00:00
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 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 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 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 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 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
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