Commit Graph
2344 Commits
Author SHA1 Message Date
Claude a2eae42c07 feat(napplets): app-store-style card + icon manifest tag; demote capabilities
Redesign the shared StaticWebsiteCard (used by the feed AND the napplets browse
screen) to look like an app entry instead of a manifest dump: square app icon
(with a colored monogram fallback), name, a NAPPLET/WEBSITE type label, a short
description, and an Open button. The technical details users don't care about —
declared capabilities, Blossom servers, source URL — move behind a tap-to-expand
"What it can access" disclosure; capabilities are still re-confirmed at the
consent prompt when actually used and remain fully manageable in the permissions
screen.

Add an `icon` tag (NIP-5A/5D) end-to-end:
- quartz: IconTag + siteIcon() accessor/builder, NappletManifest.icon(), and an
  icon param on all four site/napplet build() factories (+ round-trip test).
- amy: `--icon URL` on `nsite/napplet publish`, surfaced in the publish output.
- card: renders the icon via Coil, monogram fallback when absent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-22 17:13:41 +00:00
Claude 08947510d9 Merge remote-tracking branch 'origin/main' into claude/awesome-pasteur-xwiwad 2026-06-22 15:04:44 +00:00
Claude df15414424 Merge remote-tracking branch 'origin/main' into claude/awesome-pasteur-xwiwad
# Conflicts:
#	amethyst/src/main/res/values/strings.xml
2026-06-22 14:50:16 +00:00
Claude 3df9f831b8 feat(quartz): complete KindNames registry for every supported kind
Fill the KindNames registry from 145 to 280 entries so every event kind
quartz defines a class for has a canonical English label + NIP. Adds the
whole NIP-90 DVM request/response set, NIP-29 relay groups, NIP-60/61
Cashu wallet + nutzaps, NIP-43 relay members, WebRTC calls (NIP-AC),
marketplace (NIP-15), git PRs/state (NIP-34), CLINK, NIP-51 curation
sets, NIP-85 assertions, Marmot MLS events, and more.

For the handful of kind numbers shared by multiple classes, the registry
keeps one canonical entry (e.g. CashuToken over the deprecated nip61
TokenEvent, ExternalIdentities over GalleryList, ReleaseArtifactSet over
SoftwareRelease). Kind 1 stays "Notes" rather than the bounty value-add
helper that reuses it.

Also point `amy nip`'s Nostr fallback at quartz's canonical NipText kind
30817 (NipTextEvent) alongside the wiki and long-form kinds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 21:41:52 +00:00
Claude b6c065d421 feat(quartz,cli): add KindNames registry + amy kind
Introduce a canonical, i18n-free event-kind registry in quartz:
`com.vitorpamplona.quartz.kinds.KindNames` maps each of the 145 known
kinds to an English label + the defining NIP. The data is ported from
Amethyst's relay-view `kindDisplayName` mapping (the NIP derived from
each event class's package), so the "what is kind N" knowledge now lives
once in quartz instead of only in the Android UI.

`amy kind <N|NAME>` looks a kind up by number (label + NIP) or searches
labels by name — a thin wrapper over KindNames, dispatched statelessly
(no account/network).

i18n split: quartz holds the canonical English (quartz is intentionally
translation-free); localized front ends overlay their own strings and can
fall back to KindNames.nameFor() for kinds they don't translate. amy
prints the English directly.

Verified: kind 1/0/30023/1059/24133 labels+NIPs, name search ("podcast"
→ 4), unknown → known:false, text + JSON output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 21:00:54 +00:00
Claude 2c37ae642c feat(nip46): handle auth_url challenges in the remote-signer client
Adds NIP-46 `auth_url` (web-authorization) support to quartz's remote
signer, so amy (and Amethyst) can use auth-requiring bunkers like
nsec.app / nsecbunker:

- Both BunkerResponse deserializers (kotlinx + the JVM/Android Jackson
  one actually used by OptimizedJsonMapper) now special-case
  `{result:"auth_url", error:<url>}` BEFORE the generic error branch,
  which previously flattened it to a plain error and dropped the marker.
- RemoteSignerManager gained an `onAuthUrl` callback and a wait loop: on
  an auth_url response it surfaces the URL once and keeps waiting for the
  real response under the same request id (UNLIMITED channel so both are
  buffered). newResponse peeks the pending entry (get, not remove) so the
  follow-up isn't dropped; the waiter still removes it in finally.
- NostrSignerRemote forwards `onAuthUrl`; amy's Context prints the URL to
  stderr and the pending command keeps waiting.

Tests: a deserializer test (auth_url keeps result+error) and a manager
test (auth_url surfaced via callback, then the real response resumes the
request) — both green; existing duplicate/late-response manager tests
still pass after the get-vs-remove change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SapGdtAc1j7woifoCZ9fY
2026-06-21 20:19:01 +00:00
Claude 0d2ac83ebb feat(nsite): runtime hardening — blossom: fetch, sniffing, server fallback, blob cache
Closes the remaining nsite/napplet runtime gaps, all keeping content
integrity (every blob is sha256-verified) and the sandbox intact:

- resource.bytes blossom: scheme — blossom:<sha256> fetches from the
  user's kind:10063 Blossom servers and verifies the hash before
  returning. nostr: stays deferred (bytes semantics unspecified).
- Content-type byte-sniffing in the resolver: when a manifest path has
  no/unknown extension, sniff magic bytes (png/jpeg/webp/gif/pdf/wasm/...).
  Text/markup is never sniffed so HTML detection stays extension-driven.
  Unit-tested in quartz.
- kind:10063 server fallback: the launcher augments the manifest's servers
  with the author's published Blossom list (best-effort, when cached).
- Blob caching: the host OkHttp client caches blobs on disk with a forced
  immutable policy (content-addressed). The resolver re-verifies every
  served blob's sha256, so a stale/poisoned cache entry can't be served.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
2026-06-21 20:11:15 +00:00
Vitor PamplonaandGitHub fa11a94613 Merge pull request #3323 from vitorpamplona/claude/powr-workout-interop-r1efh0
Add POWR / NIP-101e strength workout interop (read-only)
2026-06-21 11:52:55 -04:00
Claude e2f2c4654e feat(fitness): resolve POWR exercise templates (kind 33401)
Render real exercise names in POWR workout cards instead of slug-derived
labels by fetching the referenced kind-33401 exercise templates.

- ExerciseTemplateEvent (kind 33401, addressable) with title/format/
  format_units/equipment/difficulty accessors; registered in EventFactory.
- WorkoutRecordEvent now implements AddressHintProvider: linkedAddressIds
  (deduped 33401 + 33402 coordinates) seed the gatherer so the card
  re-renders when a template arrives, and addressHints feed the relay-hint
  index with the relay.powr.build hints so the fetch reaches where POWR
  published the templates.
- WorkoutDisplay resolves each exercise via LoadAddressableNote +
  observeNoteEvent<ExerciseTemplateEvent>, showing the template title once
  fetched and the slug until then.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJwXz6CWez8r7trgHXT545
2026-06-20 19:24:25 +00:00
Claude 445a20b2f3 fix: preserve user-provided media descriptions for accessibility
The blanket NIP-31 alt removal also dropped genuine accessibility
descriptions (image descriptions for the blind) on a few media paths where
the user's caption was only stored in the event-level alt tag. Restore them
via the proper, non-deprecated fields:

- NIP-94 FileHeaderEvent (kind 1063) and NIP-17 encrypted file headers
  (kind 15): write the `alt` tag only when the user actually provided a
  caption (the NIP-94 accessibility description), never the old boilerplate
  fallback.
- MIP-04 encrypted group media (kind 9): route the caption into the imeta
  `alt` field via buildMip04IMetaTag instead of an event-level alt tag.

Re-adds the narrowly-scoped TagArrayBuilder.alt() / AltTag.assemble() write
helpers (documented as accessibility-only, not for deprecated NIP-31
boilerplate). Boilerplate alt tags on all other event kinds remain removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014xAESAz1H1VNjmQpMVqBXj
2026-06-20 19:19:16 +00:00
Claude b9ac370f4f feat(fitness): render POWR kind-1301 strength workouts
POWR and RUNSTR both publish kind 1301 but with incompatible tag schemas.
A POWR event previously rendered with the raw "33401:...:back-squat-bb"
coordinate as its activity label, no duration, and none of the set data.

Parse the POWR / NIP-101e dialect in quartz and render it in Amethyst:

- type tag for the activity (strength/circuit/emom/amrap), preferred over
  the RUNSTR exercise verb; coordinate-form exercise tags no longer leak as
  a verb.
- start/end session timestamps -> derived duration; completed flag.
- structured per-set exercise tags (kg weights, reps, rpe, set_type),
  grouped per exercise template with volume/top-weight aggregates.
- WorkoutDisplay now shows Exercises/Sets/Volume stats and a per-exercise
  breakdown (e.g. "Back Squat Bb -> 3 x 8 x 84 kg") in the viewer's unit.

Rendering interop only; Amethyst still publishes the RUNSTR-canonical form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJwXz6CWez8r7trgHXT545
2026-06-20 16:47:39 +00:00
Claude 18f736abf7 refactor: stop writing deprecated NIP-31 alt tags on events we create
The NIP-31 event-level "alt" tag is deprecated, so Amethyst no longer
emits it on any event it builds. Removed all `alt(...)` builder calls and
`AltTag.assemble(...)` insertions across every event kind in quartz (and
the few app-side builders), along with the now-unused `ALT`/`ALT_DESCRIPTION`
companion constants and the `TagArrayBuilder.alt()` / `AltTag.assemble()`
write helpers.

Reading alt tags from incoming events is kept (AltTag.parse/match,
TagArray.alt(), Event.alt()) for interop with clients that still send them,
and the imeta media accessibility `alt` field (NIP-92/94) is untouched.

Updated/removed tests that asserted alt-tag presence and refreshed the
deterministic event-id/sig golden masters in UpdateMetadataTest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014xAESAz1H1VNjmQpMVqBXj
2026-06-20 16:46:25 +00:00
Claude 96c5d9bcb6 feat: reply with kind 1111 to new Amethyst kind-1 thread roots
When replying to a note that is a kind 1 TextNoteEvent, is the root of a
new thread (no e-tags), and was itself posted from Amethyst (NIP-89
client tag), build a NIP-22 kind 1111 CommentEvent instead of a kind 1
reply. Forks keep using kind 1.

Applies across all kind-1 reply paths: the Android composer
(ShortNotePostViewModel), the notification quick-reply
(NotificationReplyReceiver), and the desktop composer (ComposeNoteDialog).

Adds Event.isClient / TagArray.isClient helpers (NIP-89, case-insensitive)
with unit coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7RyevA6jL1NuY7uev2agS
2026-06-20 15:00:04 +00:00
Claude da336897c2 fix: keep balanced closing delimiters and inline commas in detected URLs
The URL detector stripped a single trailing punctuation char unconditionally,
which dropped the closing ")" from legitimate URLs such as
https://en.wikipedia.org/wiki/Bitcoin_(disambiguation).

Make the trailing strip balance-aware: a trailing ")", "}" or "]" is kept when
the URL contains its matching opener (balanced), and only stripped when it is
unbalanced wrapping/sentence punctuation (e.g. "(see example.com)" or
"http://test.com)"). Commas without surrounding spaces were already kept inside
paths; this also adds "]" to the begin/end punctuation sets so an unbalanced
bracket is handled symmetrically with parens and braces.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AzZzVcMcuzjSdhD3xqCE87
2026-06-19 23:38:44 +00:00
Claude eac0daed9c refactor(blossom): centralize Blossom protocol strings in quartz
The Blossom auth-header encoding (`Nostr <base64-event>`), the `/upload`
endpoint path, and the `X-Reason` failure header were each re-derived in
both the commons JVM `BlossomClient`/`BlossomAuth` and the Android
`BlossomUploader`, using two different Base64 APIs. Move these
protocol-level facts into the quartz `nipB7Blossom` package, where the
rest of the Blossom protocol lives:

- `BlossomAuthorizationEvent.toAuthorizationHeader()` / `rawToken()` +
  `AUTH_HEADER_SCHEME`, mirroring NIP-98's
  `HTTPAuthorizationEvent.toAuthToken()` that Blossom auth reuses.
- new `BlossomServerUrl` with `upload()` / `blob()` endpoint builders and
  the `REASON_HEADER` constant.

Both transports now call these helpers instead of hand-building strings.
No behavior change for upload (existing desktop BlossomClientTest still
green); the Android delete URL now omits the trailing dot when no file
extension is known, matching BUD-02's `DELETE /<sha256>`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJgwV4Y99brVa97v7p3jJb
2026-06-19 22:40:28 +00:00
Vitor PamplonaandGitHub 977c94e4c6 Merge pull request #3296 from vitorpamplona/claude/napplet-protocol-n2ihd6
Add NIP-5A static-site resolver + NIP-5D napplet support
2026-06-19 18:26:36 -04:00
Claude 2648771429 feat(quartz): implement NIP-5D (Nostr Web Applets / napplets)
Adds the full NIP-5D napplet manifest layer, plus the NIP-5A aggregate-hash
infrastructure it depends on. Follows the nip88Polls package structure (event
class + tags/ + TagArrayExt + TagArrayBuilderExt).

NIP-5A shared infra (nip5aStaticWebsites):
- XTag — the aggregate-hash tag ["x", "<sha256>", "aggregate"].
- SiteAggregateHash — computes/verifies the NIP-5A aggregate hash: sort the
  per-path lines "<hash> <path>\n" lexicographically, concat as UTF-8, SHA-256.
  Pinned by a test against an independently computed sha256sum vector.
- siteAggregateHash() parse + builder extensions.

NIP-5D napplets (nip5dNapplets):
- NappletSnapshotEvent (5129, regular), RootNappletEvent (15129, replaceable),
  NamedNappletEvent (35129, addressable, d-tag) — all built on the NIP-5A
  path/server/title/description/source/x tag set.
- RequiresTag — ["requires", "<bare-nap-name>"] capability declarations.
- NappletManifest interface — uniform accessors (paths/servers/requires/title/
  …) plus computeAggregateHash()/verifyAggregate() shared across the three kinds.
  build() auto-stamps the x aggregate (required for snapshots, recommended for
  root/named).
- Registered all three kinds in EventFactory.

This covers the NIP-5D runtime verification contract end-to-end in quartz:
signature (core Event.verify), per-blob sha256 (StaticSiteResolver.verify), and
the aggregate x-tag (NappletManifest.verifyAggregate). Note the napplet kinds
5129/15129/35129 are distinct from the NIP-5A nsite kinds 5128/15128/35128, so
there is no collision.

Tests: SiteAggregateHashTest (vector + order-independence + tamper) and
NappletEventTest (build/parse round-trip for all three kinds, aggregate
verification, tamper detection, EventFactory routing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CdAJMbnHJfiMY7UcS99T6C
2026-06-19 22:06:49 +00:00
Claude 889b620b44 fix(quartz): never let signer timeouts crash from auto-signing launches
An ignored external NIP-55 signer prompt surfaces as
SignerExceptions.TimedOutException. Relay auth (NIP-42) signs replies in a
fire-and-forget scope.launch whose host scope (e.g. viewModelScope) carries no
CoroutineExceptionHandler, so an uncaught timeout there reached the platform
default handler and crashed the app ("Could not sign: User didn't accept or
reject in time.").

Guard the launch in RelayAuthenticator so signing failures are swallowed and
logged (re-throwing only CancellationException). Apply the same guard to
NostrSignerRemote's incoming-bunker-response launch, which decrypts untrusted
relay data on a handler-less scope. Add RelayAuthenticatorTimeoutTest covering
the swallowed-timeout and happy-path-still-sends-AUTH cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017RM8zAKJNE8aAQL5nUboso
2026-06-19 21:21:28 +00:00
Claude 1b2311e75f feat(cli): add amy nsite fetch to resolve + verify static sites / napplets
Wires the quartz NIP-5A resolver end-to-end so it can be exercised against
real manifests (interop / agents), without building the security-sensitive
WebView shell yet.

- commons BlossomClient: add download(url) — a Blossom GET returning raw bytes
  (null on non-2xx; connection failures propagate so callers try the next
  server). Does not verify the hash; that is the resolver's job.
- cli NsiteCommands: `amy nsite fetch AUTHOR [--d ID] [--path P] [--server …]
  [--relay …] [--out FILE] [--timeout SECS] [--max-inline-bytes N]`. Fetches
  the manifest (kind 15128 root, or 35128 named with --d) from relays, then
  resolves one path through StaticSiteResolver, downloading from the manifest's
  Blossom servers (plus any --server fallbacks) and accepting only the first
  blob whose sha256 matches the manifest pin. Emits the verified path's bytes
  (inlined for small text, or written to --out) with hash/server/content-type,
  or a structured not_found / path_not_found / unresolvable error.

Thin-assembly only: all resolution + verification stays in quartz, the byte
fetch in commons. Smoke-tested offline: bad-args, help, and a dead-relay run
that resolves cleanly to not_found in both text and --json modes.

Also converts the StaticSitePathLookup file-overview KDoc to a plain block
comment to satisfy ktlint no-consecutive-comments.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CdAJMbnHJfiMY7UcS99T6C
2026-06-19 20:46:32 +00:00
Claude b547b07747 feat(quartz): add NIP-5A static-site / napplet resolver
Adds a platform-agnostic resolver for NIP-5A static-website / napplet (NIP-5D)
manifests in quartz commonMain, under nip5aStaticWebsites/resolver/:

- StaticSitePathLookup: request-path normalization (query/fragment stripping,
  leading-slash insensitivity, root/dir -> index.html), slash-insensitive
  path lookup over a manifest's path tags, and a web-asset Content-Type guess.
- StaticSiteResolver: hash verification, Blossom candidate-URL assembly, and a
  suspend resolve() that downloads each listed server in order and accepts the
  first blob whose recomputed sha256 matches the manifest pin. HTTP is injected
  via a BlobFetcher typealias so quartz keeps no HTTP dependency.

The trust model is the point: the signed manifest is the authority, the Blossom
server is untrusted. A server that substitutes/corrupts a blob fails
verification and is skipped -- it can withhold content but never forge it.
Tests cover normalization, lookup, MIME guessing, and the security cases
(tampered server skipped -> falls through to honest server; all-tampered ->
Unresolvable; undeclared path -> PathNotInManifest without fetching).

Also adds quartz/plans/2026-06-19-napplet-nip5a-resolver.md documenting the
design and the open event-shape alignment questions (35128 vs 35129 manifest
kind, capability declaration vs NIP-89, aggregate build hash, server ordering)
to raise with the napplet author before the nsite/napplet event shape forks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CdAJMbnHJfiMY7UcS99T6C
2026-06-19 20:08:27 +00:00
Claude b8db7ae6b7 perf(cashu): dispatch token prefixes via DualCase startsWith
Replace startsWith(..., ignoreCase = true) — which case-folds on every
call — with the precomputed DualCase prefixes and the new
String.startsWith(DualCase) helper, so the cashuA/cashuB dispatch only
compares against already-cased strings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UMNKix4qEfiAPP9s2a4gB
2026-06-19 17:26:48 +00:00
Claude ebdf18139b feat(cashu): tolerate both base64 alphabets + redeem multi-mint tokens
- parseCashuA now decodes standard *and* url-safe base64. NUT-00 v3
  specifies base64-urlsafe, but legacy encoders (and older Amethyst
  builds) emitted standard base64; try standard first, fall back to
  url-safe so both round-trip.
- CashuWalletViewModel.redeemToken now redeems every mint/keyset group
  in a pasted token instead of only the first, validating all mints are
  in the wallet up front and summing the redeemed amounts.

Adds parser coverage for both base64 alphabets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UMNKix4qEfiAPP9s2a4gB
2026-06-19 16:01:22 +00:00
Claude 3515afa34c fix(cashu): consistent prefix casing + reuse one CBOR codec
Audit follow-ups on the cashu token codec move:

- CashuTokenB64Parser.parse() dispatches on the prefix case-insensitively
  (matching commons RichTextParser's case-insensitive cashuA/cashuB
  detection), but parseCashuA/parseCashuB stripped it with a case-sensitive
  removePrefix — so a mixed-case prefix passed dispatch and then fed its
  own prefix bytes into the Base64 decoder, failing to parse. Strip the
  fixed 6-char prefix with drop() so dispatch and stripping agree.
- hoist a single shared CashuV4Cbor instance instead of allocating a new
  Cbor on every encode (V4Encoder) and every cashuB parse.

Adds an acceptsMixedCasePrefix regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UMNKix4qEfiAPP9s2a4gB
2026-06-19 15:47:09 +00:00
Claude c5b1d0051a refactor(cashu): move out-of-band token parser + model to quartz
Follow-up to the V4Encoder move. Quartz could encode a cashuB string but
could not parse cashuA/cashuB back, and the parsing lived in amethyst even
though it is pure NUT-00 wire-format protocol. Worse, commons.RichTextParser
already detects cashuA/cashuB words while the parser sat up in the app, so
Desktop (its own rich-text viewer) could not parse a received token at all.

Consolidate the legacy out-of-band redeem stack onto quartz:

- new quartz CashuTokenB64Parser parses cashuA (standard-Base64 JSON, rewritten
  off Jackson onto kotlinx.serialization to satisfy quartz's no-Jackson rule)
  and cashuB (Base64URL CBOR, reusing the V4Token models), returning quartz
  types. It is the inverse of V4Encoder.
- move the CashuToken container model from commons to quartz, switching its
  proofs from the duplicate commons Proof (field C, amount Int) onto the
  canonical quartz CashuProof (field c, amount Long). The duplicate Proof
  type is deleted.
- delete amethyst V3Parser/V3Token/V4Parser; CashuParser/CachedCashuParser
  stay as thin amethyst adapters (off-main-thread guard + GenericLoadable +
  LruCache) over the quartz parser.
- this removes the manual Proof -> CashuProof conversion shims that
  MeltProcessor and CashuWalletViewModel previously carried.

Tests: full cashuA + cashuB vector coverage moves to quartz commonTest
(CashuTokenB64ParserTest, runs on JVM) plus an encode/parse round-trip; the
superseded amethyst CashuV4ParserTest is removed and CashuBTest stays as the
adapter integration test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UMNKix4qEfiAPP9s2a4gB
2026-06-19 15:32:48 +00:00
Claude 6a65680e61 refactor(cashu): move v4 (cashuB) token codec to quartz
The NUT-00 v4 `cashuB` wire-format encoder + CBOR models lived in
`amethyst/service/cashu/v4`, even though they are pure protocol code with
no app/UI dependencies. quartz had the proof/mint primitives but no
`cashuB` string codec at all, so this was a layering gap rather than a
duplicate.

Move `V4Encoder` and the `V4Token`/`V4T`/`V4Proof`/`V4DleqProof` wire
models into `quartz/nip60Cashu/token`, rewriting the JVM-only
`java.util.Base64` to the multiplatform `kotlin.io.encoding.Base64.UrlSafe`
so the codec lives in commonMain. This unblocks sharing CashuWalletOps /
CashuWalletState into commons (V4Encoder was one of the Android-only ties).

- add kotlinx-serialization-cbor (Apache-2.0) to quartz commonMain
- repoint CashuWalletOps (encoder) and V4Parser (V4Token) imports to quartz
- add V4EncoderTest round-trip coverage in quartz commonTest

amethyst's V4Parser stays put: it returns UI types (GenericLoadable +
commons CashuToken) and now just reuses the relocated quartz V4Token.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UMNKix4qEfiAPP9s2a4gB
2026-06-19 14:48:40 +00:00
Vitor PamplonaandGitHub 552cd5d7e1 Merge pull request #3277 from vitorpamplona/claude/modest-planck-o0fy3v
Roadstr: visual overhaul with map hero and category pills
2026-06-18 19:45:19 -04:00
Claude 2e32660d9b feat: roadstr-accurate road event rendering (kind 1315/1316)
Align Amethyst's road event cards with the roadstr reference clients
(https://github.com/jooray/roadstr) for both interoperability and a
richer presentation.

Interop:
- Match roadstr's exact emoji set: road_closure 🚫 (was ) and
  other ℹ️ (was 📍). The t codes and per-type TTLs already matched.
- Make the kind 1316 NIP-31 alt status-dependent ("Roadstr: event
  confirmed" / "Roadstr: event denied") per the spec, instead of a
  single "Roadstr: event confirmation".

Rendering:
- Colored teardrop map pin per category, using roadstr's exact color
  palette, with the category emoji on the head (new MapPinIcon).
- Freshness: fade the report pin to 0.6 under 25% of effective TTL and
  0.4 once effectively expired, matching roadstr's opacity rule.
- Subtitle meta line: "🕒 23m · expires in 1h" / "· Expired" on reports
  and "🕒 23m" on confirmations.
- Confirmations get a green  / red  status pin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017tYbcy4UGWxqQbcycyL7Yd
2026-06-18 22:38:07 +00:00
Claude af1e34a34d fix(quartz): clamp non-positive FTS reindex batch size
A batchSize <= 0 made the SQLite resumable reindex select no rows yet
never report done, so a caller's loop would spin forever. Clamp the page
size to at least one in both stores and add a regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BZqPFds2TPPUKkMmBngwys
2026-06-18 22:32:51 +00:00
Claude b419db4fe0 feat(quartz): make FTS reindex pausable/resumable for large stores
A full FTS rebuild can run for a long time on a big store, so add a
resumable, batched overload alongside the one-shot:

    reindexFullTextSearch(resumeFrom: String?, batchSize): FtsReindexProgress

Each call processes ~batchSize events in its own write transaction and
returns an opaque cursor + done flag. The caller loops until done and may
stop at any point — the cursor is durable across crash/app-restart, and
the writer lock is released between batches, so "pause" is just "don't
make the next call". The path is additive/refresh and keeps search usable
throughout (no up-front wipe); the one-shot variant remains for a
guaranteed-clean rebuild.

- SQLite: FullTextSearchModule.reindexBatch walks event_headers ordered
  by the monotonic row_id (a free, stable cursor), restricted to
  searchable kinds, delete-then-insert per event so batches are
  idempotent and never duplicate rows.
- Filesystem: FsEventStore walks one idx/kind/<k>/ dir per step (linear,
  no re-sort); cursor is the next searchable kind. Idempotent linkFts, so
  nothing is wiped. Pauses between kinds.
- Wrappers delegate; new FtsReindexProgress value type carries cursor +
  progress + done.
- cli: `amy store reindex-fts` now loops the batched path to completion
  and reports processed/batch counts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BZqPFds2TPPUKkMmBngwys
2026-06-18 22:16:48 +00:00
Claude 0c67f63bad feat(quartz): add FTS reindex to SQLite and filesystem event stores
The set of event kinds that implement SearchableEvent — and the text
each contributes via indexableContent() — is baked into the quartz
build, so it changes across app versions. Events stored under older code
keep their old (or missing) NIP-50 full-text-search rows, so search
silently misses them after an upgrade.

Add IEventStore.reindexFullTextSearch() so the app can wipe and rebuild
the FTS index from already-stored events when it has spare cycles.

Speed: only kinds that currently map to a SearchableEvent are scanned.
Kind alone selects the event class in EventFactory, so a single probe per
distinct kind is authoritative, letting us push a `kind IN (...)` filter
(SQLite) / skip whole idx/kind dirs (filesystem) so the non-searchable
bulk — reactions, zaps, follow lists — is never deserialised.

- SQLite: FullTextSearchModule.reindexAll drops+recreates the virtual
  table (O(1) wipe) then streams only searchable-kind rows in one write
  transaction, reusing a single INSERT statement.
- Filesystem: rebuilds only idx/fts/, driving the walk from
  idx/kind/<searchable kind>/ via the new FsIndexer.linkFts.
- Wrappers (EventStore, ObservableEventStore, InterningEventStore)
  delegate; the observable layer emits nothing since no event changes.
- cli: `amy store reindex-fts`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BZqPFds2TPPUKkMmBngwys
2026-06-18 21:49:06 +00:00
Vitor PamplonaandGitHub 294be62fec Merge pull request #3272 from vitorpamplona/claude/stoic-lovelace-qv7vy8
Add Roadstr road event support (kinds 1315/1316)
2026-06-18 17:05:08 -04:00
Vitor PamplonaandGitHub e74b5ac308 Merge pull request #3271 from vitorpamplona/claude/quartz-fts-indexing-review-29o8us
Implement NIP-50 full-text search for additional event kinds
2026-06-18 17:03:15 -04:00
Claude ffe3af1035 feat: index direct message content (NIP-17 ChatMessageEvent)
ChatMessageEvent (kind 14) is the decrypted NIP-17 rumor; its content is the
plaintext message. Index it so DMs are locally searchable, consistent with
messaging-client search. Opt-in confirmed by the maintainer; private zaps
(9733) remain excluded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFdWREvyvixRXnmNXNzmmN
2026-06-18 20:58:11 +00:00
Claude 4662cb75d0 refactor(roadstr): match NIP-88 polls architecture; register in LocalCache
Restructure the experimental roadstr events to mirror the nip88Polls
file layout, and wire the kinds into LocalCache (previously unhandled,
so they hit the "Event Not Supported" fallback and were dropped).

- Split into report/ and confirmation/ sub-packages, each with
  XEvent.kt + TagArrayBuilderExt.kt + TagArrayExt.kt + tags/, matching
  nip88Polls/{poll,response}. build() now uses eventTemplate + typed
  builder-ext functions; accessors delegate to TagArray ext functions.
- Bundle each enum into its tag class (RoadEventType in RoadEventTypeTag,
  RoadEventStatus in RoadEventStatusTag), like PollType in PollTypeTag.
- Add RoadReportTag (a GenericETag `e`-reference) mirroring PollTag, and
  have RoadEventConfirmationEvent implement EventHintProvider so the
  referenced report is linkable — parallel to PollResponseEvent.
- Shared lat/lon/geohash tag codecs live in roadstr/tags/ with generic
  builder/parse extensions.
- LocalCache: consume kinds 1315/1316 as regular events.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cn4Lk53CcXqYFYHMut4YfY
2026-06-18 20:57:41 +00:00
Claude a7bf23a00d feat: index zap comments (LnZap request/receipt, onchain zap, nutzap)
Zaps carry an optional human-readable message; index it cheaply:

- LnZapRequestEvent (9734): content (public zap comment). Private-zap messages
  live encrypted in the `anon` tag, not content, so they stay out.
- LnZapEvent (9735): the comment is in the embedded zap request, which init{}
  already parses into `zapRequest` unconditionally — so indexing
  `zapRequest?.content` adds no extra parse cost.
- OnchainZapEvent (8333): content (optional message).
- NutzapEvent (9321): content (nutzap message).

LnZapPrivateEvent (9733) is left out: its content is a decrypted *private* zap
message (same privacy class as DMs) — deferred to an explicit opt-in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFdWREvyvixRXnmNXNzmmN
2026-06-18 20:54:11 +00:00
Claude 61255198b3 feat(roadstr): add kind 1315/1316 road event support
Adds Quartz protocol support and Amethyst rendering for the Roadstr
decentralized traffic-reporting events (https://github.com/jooray/roadstr):

Quartz:
- RoadEventReportEvent (kind 1315) and RoadEventConfirmationEvent
  (kind 1316), registered in EventFactory.
- RoadEventType (13 categories with client-side effective TTLs) and
  RoadEventStatus enums; lat/lon/status tag codecs with 7-decimal
  coordinate formatting; NIP-40 expiration + NIP-31 alt + multi-precision
  geohash (4/5/6) tags.
- Generalize the GeoHash encoder/decoder into commonMain
  (nip01Core.tags.geohash.GeoHash); the Android module keeps only the
  Location <-> GeoHash glue.
- Unit tests for parsing, building, TTLs, coordinate formatting and the
  geohash prefix hierarchy.

Amethyst:
- RenderRoadEventReport / RenderRoadEventConfirmation cards wired into
  NoteCompose (feed) and ThreadFeedView/NoteMaster (thread), mirroring
  the Birdex renderer pattern; reverse-geocoded location line and
  localized category labels.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cn4Lk53CcXqYFYHMut4YfY
2026-06-18 20:22:08 +00:00
Claude 340916aa86 feat: make polls, code snippets, audio, git/torrent replies & interest sets searchable
Adds SearchableEvent to more text-bearing kinds:

- PollEvent (1068, NIP-88): question (content) + option labels
- CodeSnippetEvent (1337, NIP-C0): name, description + code
- GitReplyEvent (1622) and TorrentCommentEvent (2004): reply/comment content
- AudioHeaderEvent (1808): description (content)
- AudioTrackEvent (31337): subject
- InterestSetEvent (30015): title, description + public interest hashtags
  (private hashtags in NIP-44 content are not indexed)
- SoftwareReleaseEvent (30063): release notes (content) — note: kind 30063 is
  registered to NIP-51 ReleaseArtifactSetEvent in EventFactory, so this is for
  completeness and not exercised at runtime.

CommunityRulesEvent (34551) was intentionally left out: it is the NIP-9B
machine-readable rules companion; the human-readable community rules live on
CommunityDefinitionEvent.rules(), which is already indexed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFdWREvyvixRXnmNXNzmmN
2026-06-18 20:21:08 +00:00
Claude c5c5ef706f fix(quartz): de-flake NostrClientRepeatSubTest re-subscription test
The test asserted exactly two EOSEs (`assertEquals(2, eoseCount)`) and
looped `while (eoseCount < 2)`. But a re-REQ on the same subscription id
silently replaces the previous subscription (NIP-01), and
RelaySession.handleReq cancels the in-flight query coroutine without
emitting an EOSE for the superseded filter. So when both mid-stream
re-subscriptions get collapsed before reaching EOSE, only the final,
never-superseded filter emits one — the consumer loop then blocks on
receive() until the 30s timeout and the assertion fails.

Drain until the relay goes quiet (idle-gap timeout) instead of counting
on a fixed number of EOSEs, and assert only invariants that hold for
every interleaving: at least one EOSE arrives, every non-EOSE entry is a
valid 64-char id, the final advertised-relay-list filter actually
streamed its events, and the total stays under a sane upper bound.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018e1bfPf4HZDCvwcfiEHECk
2026-06-18 20:16:02 +00:00
Claude 79f6bf1b7c fix: drop label prefix from WikiNoteEvent FTS content
Missed in the earlier label-removal pass. Index bare title/summary/content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFdWREvyvixRXnmNXNzmmN
2026-06-18 20:07:23 +00:00
Claude d184e4369c feat: make remaining text-bearing events searchable
Adds SearchableEvent to the rest of the event kinds that carry
human-readable fields, indexing only natural-language values (no structural
labels), consistent with the existing implementations.

Public/discovery: relay groups (NIP-29), file/media headers (NIP-94), web
bookmarks (NIP-B0), podcasts (NIP-F4 episode + show), static sites (NIP-5a),
live clips and meeting rooms (NIP-53), contact cards (NIP-85), and the
NIP-51 curation sets (article/video/picture/app curation, media starter
pack, release artifacts).

Personal lists (NIP-51): bookmark/labeled-bookmark/follow/people lists and
relay sets — indexes the list's own title/description so users can find
their lists by name.

Experimental: software apps (NIP-82), fundraisers, interactive stories,
workout records, birdex, attestations, NIP-95 file storage header, profile
gallery.

content is indexed only where it is confirmed human-readable prose
(file caption, podcast notes, clip caption, app/fundraiser/story/workout
body). It is excluded where content is HTML (static sites), base64 (NIP-95),
NIP-44 encrypted (contact cards), an encrypted/JSON private-tag blob (lists),
or always empty — those index their parsed tag fields only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFdWREvyvixRXnmNXNzmmN
2026-06-18 19:59:45 +00:00
Claude d87de452e6 refactor: drop structural labels from FTS indexable content
The "title: ", "summary: ", "name: ", "Subject: ", "Option: " etc. prefixes
were tokenized into the single FTS content column as literal words, so every
event of a type matched bare terms like "title" or "summary" and the index
carried useless tokens — the same field-name pollution we avoid for JSON
kinds. The FTS table has one content column and search is plain-text MATCH,
so the labels enabled no fielded search; they were pure noise.

Index bare field values instead (listOfNotNull(...).joinToString("\n")),
which also drops the "null" token the older one-liners produced for absent
fields. Applied across all SearchableEvent implementations for consistency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFdWREvyvixRXnmNXNzmmN
2026-06-18 19:41:09 +00:00
Claude e400a48dbf feat: index parsed JSON fields of profile/channel/app-handler events
Kinds 0 (profile), 40/41 (channel create/metadata) and 31990 (app handler)
store their data as JSON in content. Implement SearchableEvent on them by
parsing the JSON (via the existing UserMetadata/ChannelData/AppMetadata
accessors) and indexing only the meaningful fields — names, bio/about, and
the addresses people search by: nip05 email, lightning addresses
(lud06/lud16), and website/picture/banner URLs. This avoids indexing the
JSON keys and structural punctuation that raw-content indexing would add.

Updates the FsSearchTest "non-searchable" case to use an unknown kind, since
MetadataEvent is now searchable, and adds SearchTest coverage for profile
and channel JSON fields (name, about, email, lightning, URL).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFdWREvyvixRXnmNXNzmmN
2026-06-18 19:33:10 +00:00
Claude a689d5cb19 revert: keep original event_fts table design, drop FTS migration
Reverts the event_fts rowid-alignment refactor and the background reindex
that it required. Aligning the FTS rowid with event_headers.row_id was a
schema change, which forced a v2->v3 migration to rebuild the index from
~all cached events — and on large caches that reindex was the expensive,
risky part (slow startup, all-or-nothing transaction, resumability and
malformed-row concerns). The cleanup it bought (not tokenizing the numeric
foreign key into the index) isn't worth that cost.

Restores the original design: event_fts keeps its dedicated
event_header_row_id column, queries join on it, DATABASE_VERSION stays 2,
and there is no FTS migration or reindex at all.

Kept: the newly searchable event kinds (they implement SearchableEvent and
work unchanged with the original table) and their test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFdWREvyvixRXnmNXNzmmN
2026-06-18 18:47:37 +00:00
Claude 1027324da8 perf: run post-migration FTS reindex in the background
The v2->v3 upgrade previously rebuilt the entire full-text index inside
the migration transaction. With a large cache (e.g. 100k events) that
blocked every DB operation behind a single long transaction at startup:
the app appeared frozen, risked an ANR if reached on the main thread, and
— because it was all-or-nothing with the version bumped only on success —
a crash, kill, or one malformed cached row could roll everything back and
retry from scratch on every launch (worst case: an unrecoverable boot loop)
while the WAL ballooned.

Decouple the reindex from the migration:

- The migration now only recreates the empty FTS table and writes a
  persistent `fts_reindex` marker holding a progress cursor, then bumps the
  version. It is cheap and atomic.
- A background coroutine (Dispatchers.IO, cancelled on close()) backfills the
  index from event_headers in small committed batches via useWriter, so live
  relay inserts/queries interleave between batches instead of waiting.
- Backfill is idempotent (INSERT OR IGNORE), resumable (cursor persists, so a
  kill resumes on next launch), and resilient (a row that fails to parse/index
  is skipped while the cursor still advances — no stuck retries).

Search is merely degraded (partial results) until the backfill finishes,
never blocked. Adds a test covering backfill + marker clearing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFdWREvyvixRXnmNXNzmmN
2026-06-18 18:42:09 +00:00
Claude 8f6074e85b feat: index natural-language fields of more event kinds for FTS
Several event kinds carry human-readable text (titles, summaries,
descriptions, names, free-text content) but were never added to the
full-text search index. Implement SearchableEvent on them so their
natural-language fields become searchable, while keeping non-prose data
(hex ids, URLs, relay hints, hashtags, geohashes, JSON config) out of the
index.

Kinds added:
- Classifieds (30402): title + summary + content
- Calendar (31924) + date/time slots (31922/31923): title + summary + content
- Community Definition (34550): name + description + rules
- Live Activities (30311): title + summary + content
- Meeting Space (30312): room + summary
- Status (30315): content
- Picture (20) and Video (NIP-71, all variants): title + content
- Goal (9041): summary + content
- Torrent (2003): title + content
- Git Repository (30617): name + description
- Git Pull Request (1618): subject + content
- Git Patch (1617): content
- Badge Definition (30009): name + description
- Emoji Pack (30030): title + description
- Feed Definition (31890): title only (content is JSON config)

JSON-content kinds (profile, channel, app handler) are intentionally left
out for now since they require parsing the content JSON to extract only
the natural-language fields. Existing v2->v3 FTS reindex repopulates the
index for already-cached events of these kinds on upgrade.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFdWREvyvixRXnmNXNzmmN
2026-06-18 16:34:27 +00:00
Claude 6a2d8baf43 refactor: align event_fts rowid with event_headers.row_id
The FTS table declared event_header_row_id as a regular full-text column,
which means the numeric foreign key was tokenized into the searchable
index — a bare MATCH could match an event by its internal row id, and the
column wasted index space.

Drop the dedicated column and instead align the FTS table's implicit
rowid with event_headers.row_id at insert time, joining on it (rowid
joins are also the fastest possible). This works across fts3/4/5.

Also make FullTextSearchModule.drop() remove its trigger explicitly so
the module is self-contained, and add a v2->v3 migration that rebuilds
the FTS index in place from event_headers, preserving the cached events.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFdWREvyvixRXnmNXNzmmN
2026-06-18 15:51:06 +00:00
Claude 1bef6ab2ed fix: index poll option text cleanly in ZapPollEvent FTS
ZapPollEvent.indexableContent() concatenated a List onto a String, so
String.plus(Any?) appended the list's toString() — leaking literal "[",
"]" and ", " separators into the full-text index
(e.g. "Best color?[\nOption: Red, \nOption: Blue]"). Build the string
explicitly so only the natural-language poll descriptors are indexed,
matching the buildString style used by the music events.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFdWREvyvixRXnmNXNzmmN
2026-06-18 03:48:07 +00:00
Claude 88a1755ae0 feat(cashu): add stop-receiving-nutzaps and delete-wallet actions
Adds two user-facing teardown options to the Cashu wallet settings:

- "Stop receiving nutzaps": replaces kind:10019 with an empty event (the
  durable signal, honored by every relay since it's a replaceable-event
  replacement) and then NIP-09 deletes it (best-effort, since deletions are
  optional on Nostr). The wallet and balance are untouched.
- "Delete wallet": withdraws the nutzap advertisement as above, then NIP-09
  deletes the kind:17375 wallet definition. Held kind:7375 proofs are not
  deleted (the ecash still exists at the mint), with a UI warning that any
  remaining balance / unredeemed nutzaps may become unrecoverable.

The on-disk backups of kind:17375 / kind:10019 are cleared when those events
are deleted, so a relaunch doesn't resurrect a deleted wallet from settings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXRAunSJS2dBx7B79qTMew
2026-06-17 22:31:57 +00:00
Vitor PamplonaandGitHub 93186a559c Merge pull request #3255 from vitorpamplona/claude/great-edison-nf92b4
fix(cashu): NUT-02 per-keyset input fees in melt/swap (fixes coinos send-LN)
2026-06-17 18:20:19 -04:00
Vitor PamplonaandGitHub 8282c5f609 Merge pull request #3254 from vitorpamplona/claude/beautiful-hawking-qfcq4w
Fix lone surrogates in truncated strings (emoji safety)
2026-06-17 18:15:37 -04:00