Commit Graph
14276 Commits
Author SHA1 Message Date
Claude b20bb4e1d0 fix(cashu): scratchpad Bdhke.blind + hashToCurve — covers restore path
Latest crash log showed no CashuTrace markers between the mint HTTP
response and the SIGSEGV — the tracing was only around swapToLocked
and unblindAll. The crash is in the NUT-09 restore loop, which calls
Bdhke.blind hundreds of times per batch (one per counter slot) and
each blind call still allocated ~5 short-lived Fe4 / MutablePoint
holders inside hashToCurve + the blind step itself.

Same JIT-bug shape as unblind had. Same fix: pre-allocate the holders
inside BdhkeScratchpad, pass it to blind / hashToCurveInto so the
methods write through caller-owned holders instead of allocating
fresh ones. Adds 6 fields to the scratchpad (kept distinct from the
unblind ones so future nesting can't silently overwrite live state).

Wired:
- CashuMintOperations.restore: one batchScratch per HTTP batch,
  reused across all per-counter Bdhke.blind calls in that batch.
- secretOutputsFor: one scratch per call, reused across the
  mapIndexed loop.
- Pre-existing scratchpads in unblindAll, melt-change loop, NUT-09
  restore unblind loop unchanged.

Added a `restore: batch counter=N size=M denoms=K` trace line so
the next failure (if any) points at restore unambiguously.

Tests: 17/17 in BdhkeTest still pass (spec vector + round-trips).
2026-05-28 00:41:57 +00:00
Claude 35c260f572 fix(cashu): allocation-free Bdhke.unblind via BdhkeScratchpad
The earlier "split into smaller methods" mitigation didn't hold —
ART's JIT inliner re-merged them at compile time and the same
SIGSEGV at 0x48 in "Jit thread pool" still hit on Android 15+. The
crash was visible in NUT-09 restore (hundreds of unblinds in a
loop), in zap-send, and in send-token.

Root cause is ART 15+'s escape-analysis scalarization pass choking
on the allocation density inside Bdhke.unblind — about 10 short-
lived Fe4 / MutablePoint holders per call. Splitting doesn't help
because once the JIT inlines, the holders are back in one body.

This commit eliminates the allocations entirely. Bdhke.unblind
gains an overload that takes a [BdhkeScratchpad] holding pre-
allocated holders; the internal helpers (parseAffinePointInto,
computeNegRkInto) write through them instead of constructing new
ones. The JIT sees objects coming from outside the method, marks
them as escaping, and the buggy scalarization pass never runs.

unblindAll, the melt-change loop, and the NUT-09 restore loop each
allocate ONE scratchpad and reuse it across every iteration —
1 allocation per loop instead of ~10 per output. Big perf win for
restore on top of dodging the crash.

The scratchpad-less unblind(blindSignature, r, mintPubKey) overload
still exists for tests and one-off callers; it just delegates to
the scratchpad variant with a fresh allocation.

If this still crashes, the trace logging now points at the
swap-level frames (swapToLocked / fetchKeyset / POST swap / swap
response / unblindAll begin / end). If it survives, the JIT had
nothing left in the hot path to choke on and the underlying ART
bug is no longer reachable from our code.
2026-05-28 00:24:27 +00:00
Claude 8d997a9973 fix(cashu): split Bdhke.unblind into smaller helpers — dodge ART JIT bug
CashuTrace showed the Android 15+ JIT compiler crash (SIGSEGV 0x48
in "Jit thread pool") firing while ART was compiling Bdhke.unblind:

  unblindOne[0/2] begin amount=2
    unblindOne: Bdhke.unblind begin
  >>> Fatal signal 11 (SIGSEGV) <<<  in Jit thread pool
    unblindOne: Bdhke.unblind end       ← app thread keeps running
  unblindOne[0/2] end
  unblindOne[1/2] begin amount=8
    unblindOne: Bdhke.unblind begin
    unblindOne: Bdhke.unblind end
  unblindOne[1/2] end
  […300ms of other work, then process death tombstone…]

The unblind itself runs fine to completion (twice). The crash is the
JIT compiler thread choking on Bdhke.unblind's bytecode — specifically
the escape-analysis pass on the 10 short-lived Fe4 / MutablePoint
allocations packed into one ~30-line method body. ART decides the
process is unstable a few hundred ms later and tears it down.

Split the body into three: an orchestrator + parseAffinePoint() +
computeNegRk(). Same semantics, three smaller bytecode bodies for
the JIT to compile, each well under the threshold that triggers the
bad optimizer path.

If this still crashes, the trace will tell us which of the three
methods the JIT was compiling and we move to the next mitigation
(reusable per-thread buffer pools to drop allocations further).
2026-05-27 23:45:22 +00:00
Claude 5d64d3829e fix(rich-text): render cashu preview in the no-preview path too
CashuPreview parses tokens locally via CachedCashuParser — no network,
no link unfurl, just CBOR decode of the cashuB string and a small
card. The canPreview = false gate is there to suppress network
previews (images, link unfurls, lightning invoice lookups); cashu
tokens have no such cost.

Suppressing it was the reason a cashuB pasted into a DM rendered as
a wall of base64 even though the same token in the Redeem button
worked fine. Flip the no-preview branch to render the same CashuPreview.

Also add CashuTrace logging around the entire zap path (sendNutzap,
swapToLocked, unblindAll, unblindOne, secretOutputsFor, Bdhke.unblind,
signer.sign + publish for nutzap/keep/delete/history events). The
JIT crash on Android 15+ keeps surfacing in different code paths
even after the DLEQ-on-our-outputs skip; the trace lets us see the
last frame before the SIGSEGV so we can target the right hot spot.
2026-05-27 23:25:20 +00:00
Claude ec44b001fc fix(cashu): NUT-13 counters move to a synchronous per-account store
"Mint error HTTP 400: outputs already signed" started hitting every
send-token / send-LN after the wallet had crashed once. Root cause:
AccountSettings.reserveCashuCounters wrote the counter advance to
the same MutableStateFlow that drives the global settings save —
debounced by 1000 ms before the disk write fires.

The race window is exactly the time between "we ask the mint to
sign" and "the mint replies": ~200 ms. Any crash in that window
(ART JIT crash on Android 15+, signer dialog dismiss, OOM) loses
the counter advance even though the mint has already persisted its
side. Next reservation pulls the same slot, derives the same
deterministic blinded message, mint rejects with 10002.

Move the counter to a dedicated CashuPreferences SharedPreferences
file per account, written with `commit = true` so every reserve()
is durable BEFORE returning. AccountSettings.reserveCashuCounters
now delegates; a one-time migration on first read seeds the new
store from the legacy cashuKeysetCounters map so users on the old
build don't reset to zero. Plain (non-encrypted) prefs because
counters aren't secret — they don't carry value and aren't the seed.

Per Vitor's suggestion: the file is sized for the broader "Cashu
state that needs its own store" idea; today it only holds counters,
but the structure is in place for the kind:17375 / kind:10019 backups
to move out of the debounced settings path too if we later want.
2026-05-27 23:03:32 +00:00
Claude 23ea046d06 fix(cashu): skip NUT-12 DLEQ on our own mint outputs — ART JIT crash
Android 15 ART crashes (SIGSEGV at 0x48 in "Jit thread pool") when
the post-swap unblind loop runs Bdhke.verifyDleq once per signed
denomination in tight sequence. The pure-Kotlin elliptic curve
math allocates many short-lived MutablePoint / Fe4 holder objects;
ART 15+'s JIT compiler hits a bug compiling that pattern under load
and takes the whole process down right after a swap (or mint, or
swap-to-locked) returns. User reported it on auto-redeem first; now
hits send-token too, exactly 3 ms after the swap response — the
unblind loop barely started before the JIT crashed.

unblindOne / unblindAll only ever processes outputs WE asked the
mint to sign. A malicious mint can just refuse the request or
silently substitute a key — both are caught at next-spend when the
mint rejects our proofs, so the pre-emptive DLEQ check buys "fail
fast" vs. "fail at next op", not actual security. CDK and nutshell
wallets skip this check for the same reason.

Drop the DLEQ block from unblindOne. Third-party proof verification
(incoming cashu tokens, nutzap redeems) goes through the separate
verifyTokenDleq / verifyDleqCarol path and is unchanged — that's
where the untrust boundary actually is.
2026-05-27 22:44:54 +00:00
Claude a1f991d14f ui(cashu): mint discovery moves inline, rows surface follower avatars
Reworks the add-cashu-wallet mint-discovery flow per UI feedback.

Was: two surfaces — the mint URL text field + a separate "Browse"
button that opened a bottom sheet (MintPickerSheet) listing
recommended mints. Each row showed two chips ("X from people you
follow" + "Y recommendations") which read as a count but didn't say
*who*.

Now: one surface. The Browse button + the bottom sheet are gone.
The autocomplete under the URL field always shows the directory:
"Popular mints" header when the field is empty (the old Browse
content, ranked by follows-then-total), "Matching mints" header
when typed. Both states render the same MintDirectoryRow.

MintDirectoryRow replaces the two chips with a single line —
"Recommended by [avatar gallery of up to 6 follows] +N more" —
where +N rolls in remaining followers and every non-follow
recommender. Falls back to "Recommended by N others" when no
follow has signed off, or "No recommendations yet" otherwise.
Rows are visually discrete via OutlinedCard + HorizontalDivider,
addressing the "need a more visible border between two items"
note.

Data plumbing:

- CashuMintDirectoryEntry gains followsRecommenderPubkeys (capped
  at MAX_FOLLOWS_RECOMMENDER_AVATARS=6 so the gallery doesn't
  blow up on broadly-recommended mints — the total is still in
  followsRecommendationCount).
- CashuMintDirectoryState.search(query, limit) — substring filter
  ranked by the same comparator as entries; empty query returns
  the top mints, which is exactly what the inline-popular state
  needs.

MintPickerSheet.kt deleted; the only caller was the now-removed
Browse button. cashu_browse_mints + cashu_mint_picker_* strings
removed alongside it. New strings use <plurals> for the count-
bearing forms per the project's plural-handling rule.

Future Step Bob's request mentioned: the autocomplete in
CashuWalletSettingsScreen could be migrated to the same row in a
follow-up — same shape, different host.
2026-05-27 22:11:51 +00:00
Claude 2a97511bd3 fix(cashu): NUT-09 restore — match echoed outputs by bTick only
Mint failure "Signature amount 8 != output amount 4" hit on the
resume-pending-invoice flow because some mints (observed on
minibits) match their internal restore table by the blind point
alone — when the wallet probes (B_=X, amount=4) and (B_=X, amount=8),
those mints return ONE entry with echo.amount=4 (the first matching
output we sent) and sig.amount=8 (the actually-signed denomination).
The strict amount-equality check in unblindOne then trips.

The cryptographic source of truth is the signature, not the echo:
the mint can only sign one denomination per blind point because the
keys are per-denomination. Switch the restore loop to look up
counter materials by bTick alone, then reconstruct the BlindOutput
with the signature's amount before unblinding.

Side benefits: the per-counter map shrinks from N_denoms entries
to 1 entry, and a HashSet<Long> dedups counters in case a tolerant
mint echoes the same blind point under multiple amounts.
2026-05-27 21:56:44 +00:00
Claude a1cbaea644 fix(cashu): cap NUT-09 restore request size to mint validation limit
The "Issuing proofs" dialog hung at 80% for 30 seconds and the mint
returned "List should have at most 1000 items after validation, not
6300" on the resume-pending-invoice flow. Two compounding causes:

1. CashuMintOperations.restore packs `batchSize * denominations.size`
   blinded outputs into each /v1/restore body. The default
   batchSize=100 against a typical keyset with the full power-of-2
   denomination set (~63 amounts) hits 6300 outputs per request —
   nutshell / CDK / minibits all enforce a 1000-item Pydantic cap
   and reject the body.

   Auto-clamp the effective batch size so total outputs stay under
   MAX_RESTORE_REQUEST_ITEMS (500, leaving headroom for the
   response doubling — the mint echoes outputs alongside signatures).
   Honours the caller's batchSize when it already fits.

2. recoverPreviouslyIssuedProofs (the "outputs already signed"
   fallback) was scanning every keyset denomination at every counter
   in the rewind window. But the prior /v1/mint/bolt11 reserved only
   `splitAmountIntoDenominations(amountSats).size` counters and the
   mint signed exactly one output per slot — so the relevant denoms
   are a handful, not 63.

   Pass `amounts = splitAmountIntoDenominations(amountSats)` plus a
   single-batch sweep (`batchSize = rewind`, `emptyBatchesToStop = 1`)
   so the recovery makes one HTTP call against ~3-6 denominations
   instead of 32 batches of 63.
2026-05-27 21:37:43 +00:00
Vitor PamplonaandClaude Opus 4.7 233a5b25b6 fix(nwc): trim subscription filter on response to avoid relay replays
Some NWC relays (notably relay-nwc.rizful.com) replay cached kind-23195
events every time a REQ filter mutates. Because we previously left each
reqId in the filter for a full 60s after sending its request, every new
NWC call added a fresh reqId to a filter that still listed several
stale ones, and the relay would re-deliver every matching cached
response. That arrived here as a flurry of NO_MATCH events, wasted work
on each new send, and made the transactions screen feel stuck.

Cleanup is now driven by the response itself:

- NwcSignerState cancels the 60s safety-net job on response and asks
  the assembler to drop the filter through unsubscribeSoon, which is
  debounced 1.5s so a burst of responses produces one relay-side
  filter update instead of one per response.
- subscribeAndFlush drains any pending unsubscribes synchronously
  before adding the new query, so the relay sees a direct {old}->{new}
  transition instead of the {old}->{old,new}->{new} that triggered
  Rizful's replay path.
- The 60s timeout remains as a safety net for wallets that never reply.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:18:25 -04:00
Claude 0b1fa9e775 fix(cashu): receive flow — gate concurrent mint, NUT-09 recover stuck quotes
Two bugs in the receive-LN path that together produced
"Mint error HTTP 400: outputs already signed" after paying the
invoice, with the old balance + pending banner stuck on screen.

1. Race in the 3s polling loop. checkAndCompleteMint did its
   paid-check before flipping _mintState to Completing, so two
   polls 3s apart both saw AwaitingPayment and both called
   completeMintFromLightning. Poll 1 spent the mint quote and
   poll 2 hit code 10002. Same hazard on a double-tapped
   "resume pending quote" banner.

   Fix: atomic compareAndSet to Completing before any await,
   in both checkAndCompleteMint and resumeMintQuote. A
   concurrent caller sees the flipped state and bails. If the
   paid check returns "not yet", we roll the state back to the
   previous AwaitingPayment so the poll resumes.

2. No recovery when the mint already issued. If the prior
   attempt's /v1/mint/bolt11 succeeded at the mint but we
   failed to publish the kind:7375 locally (signer dialog
   dismissed, app killed, etc.), the mint considers the
   quote consumed and rejects every retry with "outputs
   already signed" — the wallet sat stuck forever.

   Fix: catch that exact failure mode in
   completeMintFromLightning and fall back to NUT-09 — re-
   derive the deterministic blinded outputs from the wallet
   seed (scanning a 32-counter rewind window, then forward
   via the standard gap-limit heuristic), ask the mint
   /v1/restore which it has signed, checkstate the results
   to filter spent ones, unblind into proofs, and publish
   them as kind:7375 + kind:7376 + kind:5 of the quote
   exactly like a fresh mint. End state matches the
   original happy path.

   Threading: CashuWalletOps gains three optional callbacks
   (seedForRestore, peekCashuCounter, reserveCashuCounters)
   alongside the existing seedWarmer and DeterministicSecretFactory
   wiring; CashuWalletState supplies them from ensureSeed() and
   AccountSettings.{peekCashuCounter,reserveCashuCounters}.
2026-05-27 21:13:45 +00:00
Claude 02e611c986 docs(cli): plan — Cashu (NIP-60/61/87) in amy as an Amethyst test harness
The framing: Amy gains every cashu action the Amethyst UI exposes, each
one reusing the exact same quartz + commons code the Android wallet
runs in production. The deliverable is a shell harness under
cli/tests/cashu/ that walks two Amy accounts through the full wallet
lifecycle against a production mint, so regressions in the cashu code
path fail on the JVM in CI without an emulator.

Plan covers:
- Three extractions before any verb lands: cashu token parsers to
  quartz, CashuWalletOps to commons, CashuWalletReader projection
  helpers to commons.
- One storage addition: ~/.amy/<account>/cashu.json for NUT-13 keyset
  counters (deterministic secrets need durable counter state across
  invocations).
- Command surface under amy cashu …: wallet, mint, balance, receive,
  send, mint-rec, maintenance — mirrors every action the Android
  wallet exposes.
- Stable --json shape per verb.
- 9-PR sequencing: extractions first, then verbs grouped by user
  intent, then the 10-scenario interop harness.
- Acceptance criteria: harness passes against mint.minibits.cash and
  the on-relay events are byte-equivalent to what Amethyst would
  produce.
2026-05-27 21:04:32 +00:00
Claude d28458553a Merge remote-tracking branch 'origin/main' into claude/cashu-wallet-amethyst-sdOWe 2026-05-27 20:02:35 +00:00
Vitor PamplonaandGitHub f898e4be57 Merge pull request #3067 from vitorpamplona/claude/confident-allen-AOGU6
Add music tracks and playlists support with NIP-51 events
2026-05-27 15:59:41 -04:00
Claude bf02a235e0 Merge remote-tracking branch 'origin/main' into claude/confident-allen-AOGU6
# Conflicts:
#	amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt
#	amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt
2026-05-27 19:55:07 +00:00
Claude 6dc873a5d2 fix(cashu): resume mint quote — auto-complete on PAID, drop dead quotes
Two bugs in the pending-quote resume path:

1. resumeMintQuote was hard-coding amountSats=0 because kind:7374 doesn't
   carry the amount and the mint quote status DTO doesn't echo it. When
   the user paid the invoice in an external wallet and then tapped the
   pending banner, the 3s polling loop would fire checkAndCompleteMint
   with amountSats=0 — completeMintFromLightning couldn't mint zero
   proofs and the mint would either reject or, more commonly, the next
   /v1/mint/quote/bolt11/{quote} call would already say "quote not found"
   because issuance had been GC'd.

   Fix: decode the amount from the bolt11 invoice in status.request via
   LnInvoiceUtil.getAmountInSats. If the mint reports paid AND we have a
   valid amount, complete the mint inline; otherwise drop to
   AwaitingPayment with the recovered amount so the polling loop has
   something real to drive.

2. checkAndCompleteMint and resumeMintQuote treated "quote not found"
   like any other error — the local kind:7374 stayed alive and the
   pending banner kept pointing at dead state. NIP-09 delete the quote
   event when the mint says it's gone (HTTP 404, or 400 with detail
   containing "quote … not found / does not exist / unknown"), and
   reset to Idle.

isQuoteGoneError matches by substring across common mint phrasings —
the cashu spec doesn't pin the error message and minibits/nutshell/cdk
all word it slightly differently.
2026-05-27 19:50:45 +00:00
Vitor PamplonaandGitHub 08520f553f Merge pull request #3076 from mstrofnone/feat/desktop-namecoin-pinned-certs
feat(desktop): persist TOFU-pinned Namecoin ElectrumX certs
2026-05-27 15:45:19 -04:00
Claude fa484c00eb ui(wallet): lead the wallet row with the drag handle
The DragIndicator sat between the wallet name column and the balance
column with verticalAlignment=CenterVertically, so it visually centered
between the balance amount and the "sats" subtitle — landing in dead
space between two lines.

Move it to the start of the row, matching the relay-reorder convention
(BasicRelaySetupInfoClickableRow). Same icon, same modifier, same
RelayDragState wiring; just promoted from middle to leading slot.
2026-05-27 19:44:37 +00:00
m b5b70fe693 feat(desktop): persist TOFU-pinned Namecoin ElectrumX certs
Mirrors Android's NamecoinSharedPreferences pinned-cert API on Desktop so
user-accepted TLS pins survive process restart. Same JSON-list shape, same
distinct-append semantics, same wipe-on-reset behaviour.

What's new
- DesktopNamecoinPreferences gains addPinnedCert / loadPinnedCerts /
  clearPinnedCerts (sync rather than suspend, since java.util.prefs is
  synchronous). reset() now clears pinned certs too, matching Android.
- DesktopNamecoinNameService accepts a pinnedCertsProvider and pushes the
  loaded list into ElectrumXClient.setDynamicCerts at init, mirroring
  Android's AppModules.kt wiring. Exposes the underlying client so the
  Settings UI can call testServer() and re-apply pins live.
- Desktop NamecoinSettingsSection grows an optional Test Connection + TOFU
  pin sub-section: runs ElectrumXClient.testServer per active server,
  collects PEM + SHA-256 fingerprint from successful TLS handshakes, and
  prompts the user to pin each new cert via AlertDialog. UI hidden when
  no service is wired (so existing call sites stay valid).
- Main.kt wires both halves together and updates the freshly-pinned cert
  list into the live client without waiting for restart.

Persistence is plain java.util.prefs (same backing store as the rest of
DesktopNamecoinPreferences) — explicitly NOT EncryptedSharedPreferences.
Pinned cert PEMs are public material; no secrets stored.

Tests
- DesktopNamecoinPreferencesTest: +6 cases covering empty default,
  persistence + reload, dedup, blank input ignored, reset wipes, and
  independence from settings copies.

Verification
- ./gradlew :desktopApp:compileKotlin — BUILD SUCCESSFUL
- ./gradlew :desktopApp:test — BUILD SUCCESSFUL (16 tests, 0 failures)
- ./gradlew :amethyst:compilePlayDebugKotlin — BUILD SUCCESSFUL
- ./gradlew :amethyst:spotlessCheck :commons:spotlessCheck :desktopApp:spotlessCheck — BUILD SUCCESSFUL

Stack note
Stacked behind #3072 (merged 2026-05-27). Next: PR-C for the full
Namecoin Core RPC backend + composite fallback persistence + UI.
2026-05-28 05:41:22 +10:00
David KasparandGitHub 7c4220ba3d Merge pull request #3073 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-27 21:41:18 +02:00
Claude 8a55bba1f2 feat(music): parallel uploads + survive-screen-leave + visible progress
Two issues with the send flow on the new-music-track screen:

 1. Cover + audio uploaded sequentially. As cover finished, its picker
    reverted to the placeholder while audio was still going, making the
    user think the operation was done.
 2. The coroutine ran on the screen's viewModelScope, so leaving the
    screen cancelled it.

Rework the upload pipeline:

 * Snapshot the form into an immutable SendSnapshot before launching, so
   user edits to the form during the in-flight upload don't poison what
   gets published, and the coroutine sees stable inputs even after the
   per-screen VM is cleared.
 * Run cover + audio uploads in parallel via async/awaitAll. Either
   side failing throws an UploadException that the outer catch routes
   to the global toast manager.
 * Launch through accountViewModel.launchSigner so the work runs on the
   AccountViewModel.viewModelScope — survives screen leave. Errors and
   success both surface as global toasts.
 * Keep coverMedia/audioMedia/pickedAudioName populated through the
   ENTIRE upload+publish, not just per-phase. Only clear them on
   success, so each picker keeps showing the user the file it's still
   uploading.
 * Single isSending flag replaces the separate isUploading/isPublishing
   booleans; drives an inline progress banner + disables the Send
   button + dims the pickers to read-only while in flight.
 * One-shot completionEvents SharedFlow that the screen subscribes to.
   If the user is still on the screen when the operation finishes, we
   popBack; if they've already left, no one is listening and we don't
   pop someone else's stack frame.

Also tighten the playback Row height in MusicTrack from 100dp to 80dp.
With a 75dp play button the previous 100dp container left a ~12.5dp
empty band top + bottom that the user flagged as too tall.
2026-05-27 19:39:50 +00:00
Vitor PamplonaandGitHub 80991c6c8b Merge pull request #3074 from vitorpamplona/claude/gallant-darwin-VfniN
Fix route hint re-firing on activity recreation
2026-05-27 15:33:28 -04:00
Vitor PamplonaandGitHub e937b460e1 Merge pull request #3072 from mstrofnone/feat/commons-namecoin-settings-extension
refactor(namecoin): consolidate NamecoinSettings into commons
2026-05-27 15:31:19 -04:00
Claude 187cb407c9 fix(new-user): stop Import Follow List from reappearing after dismiss
The route hint set by newKey() (Route.ImportFollowsSelectUser) was stored
on AccountState.LoggedIn and never cleared. LoggedInPage re-assigned
accountViewModel.firstRoute = route on every recomposition, so any
Activity recreation (rotation, dark-mode toggle, returning from
background) made the screen pop up again even after the user dismissed
it via popBack.

Consume the route once via remember(state) in AccountScreen, and gate the
firstRoute assignment behind LaunchedEffect(Unit) so it only fires once
per composition lifetime.
2026-05-27 19:26:28 +00:00
Claude 8a3bdb0d78 ui(wallet): imePadding on screens whose body holds the text fields
AddCashuWalletScreen, AddNwcWalletScreen, and CashuWalletSettingsScreen
host their OutlinedTextFields directly in the Scaffold body — mint URL,
wallet name, NWC URI, mint-recommendation add row — so the keyboard
covers the field when the user taps to type.

Apply the codebase's canonical insets recipe (matching AllMediaServersScreen):

    .padding(padding)
    .consumeWindowInsets(padding)
    .imePadding()

The other new wallet screens are untouched on purpose: AddWalletScreen
has no text inputs; WalletScreen + CashuWalletScreen route every input
through AlertDialog (which handles its own IME); MintPickerSheet rides
ModalBottomSheet which moves with the keyboard.
2026-05-27 19:21:40 +00:00
Claude 3082095a4b feat(music): auto-fill metadata from picked audio + drop audio URL field
Five changes:
 1. Drop the audio-URL text field from the composer. Upload tile at the
    top is the single source of truth; the ViewModel still threads the
    existing url through MusicTrackEvent.edit so edit mode keeps it.
 2. Auto-fill title / artist / album / duration from MediaMetadataRetriever
    when an audio file is picked. Only fills empty fields — anything the
    user already typed wins. Duration is the exception: derived numbers
    overwrite, since they're not opinions.
 3. Rename the SavingTopBar 'Save' button to 'Send' for this screen
    (introduces SendingTopBar + R.string.send so other screens can reuse).
 4. Fix silent data corruption in AddToMusicPlaylistViewModel.init:
    the previous `if account already set, return` guard kept the FIRST
    track address the VM ever saw. Subsequent invocations with a
    different trackAddress (process recreation, navigation reusing the
    back-stack entry) routed toggle() at the stale track, adding the
    wrong track to playlists. Now refreshes trackAddress on every call
    and restarts the scan job when it changes.
 5. Inset modifier order on NewMusicTrackScreen tightened in the prior
    commit, kept here for continuity.
2026-05-27 19:18:41 +00:00
Claude 9c9cf8844d fix(music): save audio to MediaStore.Audio (not Video) and trim composer
Three fixes in one:

1. MediaSaverToDisk crash when saving a music URL. The fall-through
   branch routed every non-image, non-PDF MIME to
   MediaStore.Video.EXTERNAL_CONTENT_URI; an audio/mpeg blob inserted
   into the Video collection fails with IllegalArgumentException. Add a
   dedicated audio/* branch that writes to MediaStore.Audio with
   DIRECTORY_MUSIC.

2. Drop the redundant cover-URL text field from the new-music-track
   composer. The upload tile at the top is the source of truth for the
   cover — having a second field at the bottom asked the user to set
   the same thing twice. ViewModel still threads coverUrl through to
   MusicTrackEvent.edit so editing an existing track keeps its image.

3. Tighten the new-music-track Scaffold's inset modifier order:
   padding(pad) -> consumeWindowInsets(pad) -> imePadding() ->
   horizontal padding -> verticalScroll. Without consumeWindowInsets,
   imePadding double-counts the nav-bar inset on Android 11+ and the
   bottom field gets shoved too far up when the keyboard opens.
2026-05-27 19:08:54 +00:00
Crowdin Bot 2c9d9c3129 New Crowdin translations by GitHub Action 2026-05-27 19:08:46 +00:00
Vitor PamplonaandGitHub 58f1ee9606 Merge pull request #3069 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-27 15:07:14 -04:00
Vitor PamplonaandGitHub 63b91012fb Merge pull request #3071 from vitorpamplona/claude/trusting-mccarthy-SA1aI
Add Software Apps feed with follow list filtering
2026-05-27 15:07:05 -04:00
m 67edb32fa7 refactor(namecoin): consolidate NamecoinSettings into commons
Two NamecoinSettings classes had drifted:

- commons (used by Desktop): only enabled + customServers
- amethyst service.namecoin (used by Android): full schema with backend,
  namecoinCoreRpc, fallbackToCustomElectrumx, fallbackToDefaultElectrumx

This left Desktop unable to persist any of the Namecoin Core RPC or
fallback-policy state introduced in the Android settings UI. Promote
the rich Android version into commons as the single source of truth
and delete the Android duplicate.

- Move the rich schema (backend, namecoinCoreRpc, fallback toggles,
  hasUsableCoreRpc, toFallbackPolicy) into the commons NamecoinSettings.
- Delete amethyst/service/namecoin/NamecoinSettings.kt and its test.
- Repoint the two Android imports (NamecoinSharedPreferences,
  NamecoinSettingsSection) at the commons class. No behaviour change on
  Android.
- Fold the Android-only backend/RPC/fallback test cases into the commons
  NamecoinSettingsTest so the shared schema stays covered.

Desktop persistence (DesktopNamecoinPreferences) still only reads/writes
enabled + customServers; the extra commons fields fall back to defaults
on the existing Desktop store. Wiring those new fields into Desktop is
the next change.
2026-05-28 05:06:44 +10:00
Vitor PamplonaandGitHub 7692bee0a5 Merge pull request #3070 from mstrofnone/fix/namecoin-search-id-d-prefixes
fix(search): route d/ and id/ Namecoin namespaces through the resolution row
2026-05-27 15:06:10 -04:00
m 216176eeb7 fix(search): route d/ and id/ Namecoin namespaces through the resolution row
The inline Namecoin resolution row in the global search bar (and the
on-chain zap recipient field) is gated by looksLikeNamecoinIdentifier,
which previously only matched the '.bit' shapes. Direct namespace
references like 'id/mstrofnone' or 'd/mstrofnone' — both accepted by
NamecoinNameResolver.isNamecoinIdentifier — fell through the gate and
the resolver was never called, so no on-chain feedback appeared in the
search bar.

Bring the UI gate in line with the resolver:

  - accept 'd/<name>' (domain namespace, direct reference)
  - accept 'id/<name>' (identity namespace)
  - lower the length floor so single-character labels (valid, if
    expensive, on chain) still trigger; '.bit' inputs keep their
    5-char floor

Doc comment for the row's behaviour and the NamecoinResolutionRow
KDoc are updated to reflect the broader accepted set. New unit tests
cover both new prefixes (with case-insensitivity and leading '@'),
short single-label names, bare-prefix rejection, and explicit
non-routing for other Namecoin namespaces ('a/', 'u/').
2026-05-28 04:46:55 +10:00
Claude 81b87634a3 fix(cashu): don't hammer mint on un-redeemable inbound nutzaps
The auto-redeem loop on a fresh-start cache was hitting mint /v1/keys
once per inbound nutzap, every time triggerAutoRedeem fired (so once
per arriving event batch). For nutzaps locked to a stale wallet P2PK
key (sender saw an old kind:10019) this was pure waste — the P2PK
check happens after the DLEQ network call, so we paid for keysets
just to throw.

Two changes:

1. Move the P2PK lock check ahead of NUT-12 DLEQ inside redeemNutzap.
   No-network rejection of wrong-key nutzaps now lands in microseconds.

2. Track deterministic failures (IllegalArgumentException — wrong P2PK,
   no mint tag, no proofs, not P2PK-locked) in a session-local
   sessionUnredeemableNutzaps set, parallel to sessionRedeemedNutzaps.
   redeemPendingNutzapsSerialized skips both sets on every sweep, so
   one auto-redeem cycle per nutzap is enough to learn it's a lost
   cause; subsequent triggerAutoRedeem firings cost nothing.

Also flattens the try/catch from runCatching+onSuccess+onFailure to
make the bytecode straightforward for the JIT (the user's tombstone
showed a SIGSEGV in Jit thread pool right after three failed redeem
attempts; whether that's the cause or just a coincident ART bug, the
flatter form is friendlier for the verifier).
2026-05-27 18:44:49 +00:00
Claude ea227f1968 fix(music): playlist cover chip is wrap-content again, not full-cover
The previous cover used SubcomposeAsyncImage with a single content lambda
that stacked the loaded image and the chip as BoxScope siblings. Under
Coil's subcompose-layout pass the chip's wrap-content Box got measured
to the cover's full size, painting a translucent black square over the
whole artwork.

Drop SubcomposeAsyncImage. Use rememberAsyncImagePainter + a plain
Image + painter-state observation inside a normal Box. The image fills
the box (Modifier.matchParentSize) and the chip sits as a normal
BoxScope child with Modifier.align(BottomStart).padding(12.dp) —
predictable wrap-content sizing.
2026-05-27 18:33:22 +00:00
Claude 506ed94bfd fix(software-apps): add top-nav filter and route NIP-82 events through LocalCache
The Apps screen was missing the top-nav follow-list spinner that other
single-feed screens (Articles, Longs, Pictures) use, and nothing was
loading because the relay subscription only queried the user's own
outbox and `SoftwareApplicationEvent` (kind 32267) was never consumed
by `LocalCache.justConsumeInnerInner` — it fell through to the
"Event Not Supported" else branch and got dropped. `ReleaseArtifactSetEvent`
(kind 30063) and `SoftwareAssetEvent` (kind 3063) had the same gap.

- Register `SoftwareApplicationEvent`, `SoftwareAssetEvent`, and
  `ReleaseArtifactSetEvent` in the `LocalCache` consume dispatch.
- Add `defaultSoftwareAppsFollowList` setting (Account/Settings/Prefs)
  and `liveSoftwareAppsFollowLists{,PerRelay}` flows.
- Reshape `SoftwareAppsSubAssembler` to extend
  `PerUserAndFollowListEoseManager<_, TopFilter>` and assemble per-list
  relay filters via `makeSoftwareAppsFilter`, mirroring Pictures/Longs.
- Add a `SoftwareAppsTopBar` with the standard `FeedFilterSpinner` and
  a `WatchAccountForSoftwareAppsScreen` to invalidate the DAL when the
  selected list changes.
- DAL now applies `FilterByListParams` so the rendered feed honors the
  active top-nav list.
2026-05-27 18:25:39 +00:00
Crowdin Bot 2b6798e78a New Crowdin translations by GitHub Action 2026-05-27 18:25:22 +00:00
Claude 510514644d fix(cashu): heal stale proofs inline on every user-initiated spend
The prior startup scrub was racing with kind:7375 events arriving from
relays after start() returned — by the time those events landed in
_tokenEntries the sweep had already finished against an empty list, so
ghost proofs persisted and the user still hit "proofs already spent" on
self-zap / self-send.

Move the scrub inline: sendNutzap / sendAsToken / meltToLightning all
call scrubLocallyStaleProofs(mintUrl) right before selecting proofs,
narrowed to the mint about to be spent. The scrub now also drops stale
entries from internal indexes via removeEvents() instead of waiting for
the bundled newEventBundles round-trip — without that, the very next
read of _tokenEntries (microseconds later, in the same coroutine) would
still see the ghosts.

Drops the mixed-state skip-and-log branch in favour of treating any
entry with ≥1 SPENT proof as stale — keeping mixed entries around just
lets them resurface as HTTP 400 on the next swap. The unspent portion
of a mixed entry is recoverable via NUT-09 restore.

Wraps sendAsToken + meltToLightning on CashuWalletState (parallel to
the existing sendNutzap wrapper) so the viewmodel calls go through the
same heal path. Drops redundant validation from the viewmodel.
2026-05-27 18:23:39 +00:00
Vitor PamplonaandGitHub d5cca1e3e4 Merge pull request #3068 from mstrofnone/feat/namecoin-core-rpc-tofu
feat(namecoin): TOFU pin for Namecoin Core RPC TLS path
2026-05-27 14:23:25 -04:00
mstrofnone a1071b5189 fix(namecoin): move setConfig into the bootstrap launch
Addresses review feedback on PR #3068: keep the entire NamecoinCoreRpcClient
bootstrap (config push + pinned-cert load) inside the single applicationIOScope
launch instead of doing setConfig synchronously in the by-lazy block. Matches
the ElectrumXClient init shape above.
2026-05-28 04:18:15 +10:00
Claude 4177390591 feat(music): swap bookmark rows for playlist row in track dropdown menu
Music tracks (kind 36787) belong in playlists (kind 34139), not in the
generic bookmark list — but the dropdown menu was showing both, which
made the bookmark rows feel like the default place to save tracks.

Mirror the EmojiPack pattern instead: one curation flow per kind. For a
MusicTrackEvent the Timestamp & Bookmarks section now shows only
'Add to playlist' (which opens the toggle sheet that already handles
add/remove across every owned playlist). Everything else still gets the
standard Manage / Private bookmark / Public bookmark trio.
2026-05-27 18:18:11 +00:00
Claude 82d2290e43 feat(music): cover image and audio file pickers on new-track composer
The new-track composer was URL-only — users had to upload audio and
artwork elsewhere and paste links. Match the New Badge composer flow
instead: pick the files up front, Save uploads them through the user's
default Blossom/NIP-96 server and publishes the kind 36787 event with
the resulting URLs.

Cover image picker is the first item on the screen (square upload tile,
same style as Badge). Audio file picker sits below it. URL text fields
stay below the pickers so power users who source audio from Wavlake /
Stemstr / their own server can still paste links instead of uploading.

ViewModel changes:
 - coverMedia / audioMedia: MultiOrchestrator slots fed by the pickers
 - saveAndPublish() runs cover upload then audio upload then publish;
   either failure short-circuits and surfaces a toast
 - isValid() accepts either a picked audio file or a non-blank URL
 - All Compose state writes from IO hop through Main.immediate

Audio picker uses OpenDocument(audio/*) — we don't reuse the shared
FileSelect because it also accepts application/pdf.
2026-05-27 18:11:44 +00:00
Claude 71d50504fd feat(music): split tracks and playlists into separate filter pipelines
Up until now both screens shared one combined REQ that asked for both
kinds (36787 + 34139). That meant a single 'makeMusicTracksFilter' had
to cover both feeds, and the since cursor had to be the min of both
feeds' lastNoteCreatedAt to avoid over-fetching.

Split the pipeline so each screen owns its own kind:
 - filterMusicEventsByX helpers are parameterized by kinds: List<Int>
 - MUSIC_TRACK_KINDS = [36787], MUSIC_PLAYLIST_KINDS = [34139]
 - makeMusicTracksFilter(...) and makeMusicPlaylistsFilter(...) are
   thin wrappers picking the right kind list
 - MusicTracksSubAssembler / MusicPlaylistsSubAssembler each use their
   own helper and their own feed's cursor (no min-of-both)

Also:
 - Cover the rest of the top-bar selectors that were previously falling
   through to emptyList: Hashtag, Geohash (Location), AllCommunities,
   SingleCommunity. Music feeds now respect all of them.
 - Drop TimeUtils.oneWeekAgo() floor from filterMusicEventsGlobal —
   music kinds are sparse on most relays, the floor silently hid older
   content the user hadn't seen yet.

Tracks referenced by a playlist are still loaded on demand by each
PlaylistTrackRow's own observeNoteEvent, so the playlists REQ no longer
needs to bundle kind 36787.
2026-05-27 17:52:28 +00:00
Claude 4db226d5f2 fix(cashu): heal stale proofs + harden auto-redeem against double-spend
Two changes that together address "Mint error (HTTP 400): proofs already
spent" on self-send / self-zap:

1. Replace the auto keyset-migration on wallet load with a non-destructive
   NUT-07 sweep. The migration performed a swap-then-publish that wasn't
   atomic — a failure mid-sequence (cancelled signer prompt, transient
   network) left the mint with the source proofs marked spent while our
   local kind:7375 still held them. The next user-initiated send would
   then pick those ghost proofs and the mint would reject the swap.

   The new sweep does a read-only /v1/checkstate per mint and NIP-09
   deletes any kind:7375 whose proofs are all reported SPENT. Mixed-state
   entries (rare; partial sub-swap landed) are logged and skipped — they
   self-resolve when the user spends the entry. migrateStaleKeysets()
   stays around for explicit on-demand wiring.

2. Track redeemed nutzap ids in an in-memory set inside the redeem mutex.
   Without this, a second triggerAutoRedeem firing in the ~1s window
   between publishing kind:7376 and the bundled newEventBundles emission
   could re-pick the same nutzap and the mint would 400 with "proofs
   already spent" on the P2PK swap.
2026-05-27 17:45:29 +00:00
m cfb3f1b9fa feat(namecoin): TOFU pin for Namecoin Core RPC TLS path
When the user picks the Namecoin Core RPC backend and points at a
self-hosted node behind a self-signed cert (StartOS / Start9, umbrel,
LAN reverse proxy, …) the previous flow only worked if the cert's CA
was already in the device trust store. There was no in-app way to
inspect or pin the certificate, so users had to install the StartOS
root CA at the OS level — or settle for an unencrypted onion path.

This change brings the Namecoin Core RPC path up to parity with the
existing ElectrumX path:

  - NamecoinCoreRpcClient.probe() now opens a short-lived, no-auth TLS
    socket alongside the JSON-RPC call to capture the server's leaf
    certificate (PEM + SHA-256 fingerprint). Capture is best-effort
    and only runs for https:// URLs. Credentials are never sent over
    the inspection socket.

  - RpcProbeResult exposes serverCertPem, certFingerprint, and
    tlsHandshakeFailed so the Settings UI can react. New fields are
    nullable / default false so existing callers compile unchanged.

  - NamecoinCoreRpcClient maintains its own dynamic-cert keystore and
    a lazy pinned SSLSocketFactory (same shape as ElectrumXClient's,
    minus the hardcoded list — Core RPC has no public defaults). When
    cfg.usePinnedTrustStore is true and the URL is https, callRpc()
    routes through the pinned factory with a permissive hostname
    verifier (LAN/onion certs commonly carry IP-only SANs).

  - NamecoinSettingsSection's Namecoin Core RPC card now shows a
    'Trust Server Certificate?' AlertDialog after Test RPC when the
    probe captured a cert and the user hasn't pinned yet, reusing the
    existing namecoin_pin_cert_* strings. Accept persists the PEM
    AND flips usePinnedTrustStore=true on the config. The result
    card also displays the captured fingerprint and a '(pinned)'
    marker so the user can see the current trust state at a glance.

  - The pinned PEM list is stored in the existing
    KEY_PINNED_CERTS DataStore entry, so a single TOFU confirmation
    covers both backends. AppModules' namecoinCoreRpcClient init now
    bootstraps the pinned list on app start, matching ElectrumX.

  - Tests cover the new probe fields' defaults and the addPinnedCert
    / setDynamicCerts surface.

Local verification: builds clean (assembleFdroidDebug), :quartz:jvmTest
NamecoinCoreRpcClientTest all green, :amethyst:testFdroidDebugUnitTest
namecoin suites all green.
2026-05-28 03:32:28 +10:00
Claude e876a162a4 fix(music): review feedback round
- isPublic() now always returns !isPrivate() so a playlist tagged with
  both public=true and private=true is consistently reported as private,
  matching the 'isPrivate wins' contract documented on isPrivate().
- AddToMusicPlaylistViewModel + NewMusicPlaylistFab: hop to
  Dispatchers.Main.immediate for every Compose State write. The wrapping
  coroutines (rescan loop, launchSigner) run on Dispatchers.IO; Snapshot
  tolerates off-main writes but the codebase convention is main-only.
- MusicTracksSubAssembler + MusicPlaylistsSubAssembler: the single REQ
  asks both kinds 36787+34139, so the since cursor must be the min of
  both feeds' lastNoteCreatedAt to avoid over-fetching the lagging kind.
  Both assemblers now also listen to the other feed's cursor flow.
- Extract formatTrackDuration into MusicFormatting.kt; MusicTrack and
  MusicPlaylist share it.
- syntheticWaveformFor: replace inline FQN
  com.vitorpamplona.amethyst.service.playback.composable.WaveformData
  with an import.
- NewMusicPlaylistFab: drop the second .trim() — the dialog's confirm
  button already trims before invoking onCreate.
2026-05-27 17:27:44 +00:00
Claude 5a4cb5cc16 fix(music): apply avatar+chip pattern to loading and error covers too
The previous fix only ported the no-cover branch. Loading and error
fallbacks were still rendering banner-only with the chip floating alone,
hiding the author's avatar entirely whenever a cover URL was set but
hadn't loaded.

Drop the MyAsyncImage wrapper here and drive SubcomposeAsyncImage
directly so every painter state picks its own overlay:
 - Success: real cover + floating chip alone (no avatar to collide with)
 - Loading: blurred banner + avatar+chip side-by-side
 - Error / no image: banner + avatar+chip side-by-side
2026-05-27 17:21:00 +00:00
Claude 93a27abf23 fix(music): playlist cover chip no longer collides with author avatar
When a playlist has no `image` tag, the cover falls back to the
author's profile banner with their avatar baked into the bottom-left by
DefaultImageHeader. The track-count chip was also pinned to BottomStart,
so it landed on top of the avatar.

Render the banner alone (DefaultImageBanner) and place the avatar and
the chip side-by-side in a single bottom row instead. Extract the chip
into a small TrackCountChip composable so both branches share the same
visual.
2026-05-27 16:28:10 +00:00
Claude 1190abb30d refactor(user): eager-init pinned addressable notes
The three pinned per-user replaceable notes (NIP-65 / DM relays /
nutzap info) were lazy fields. Lazy delegation here adds a synchronized
read on every access for no gain — User is constructed via
LocalCache.getOrCreate, and the pinned notes are read on essentially
every interaction with the user. Resolving them at construction also
lets us drop the stored UserContext reference.
2026-05-27 16:19:37 +00:00
Claude 052592b91b fix(cashu): NUT-12 hash input — uncompressed bytes, hex-encoded, UTF-8
Real minibits / nutshell / CDK-backed mints were rejecting every mint
with "DLEQ verification failed for amount X — mint signature does not
match its published keyset key". The hash input format was wrong on
two axes:

1. Used 33-byte COMPRESSED points; spec uses 65-byte UNCOMPRESSED
   (`04 || X || Y`). CDK's `hash_e` in crates/cashu/src/dhke.rs is
   the authoritative reference — it calls `.to_uncompressed_bytes()`
   then `hex::encode`.

2. Hashed RAW BYTES; spec hashes the UTF-8 of the hex-encoded form.
   So the SHA-256 input is 520 ASCII chars (4 points × 130 hex chars),
   not 132 raw bytes.

The earlier round-trip test for `signFull → verifyDleq` hid this
because BOTH halves agreed on the wrong format. The fix re-roots
against the verbatim NUT-12 spec vector from
cashubtc/nuts/tests/12-tests.md, which catches any future drift
without needing a live mint.

Carol path (NUT-12 §3) also fixed in the same commit. The previous
`verifyDleqCarol` passed the UNBLINDED `C` to `verifyDleq` as if it
were `C'` — DLEQ math is over the blinded form, so it always
returned false. Per spec, Carol reconstructs both:

  B' = hashToCurve(secret) + r·G       (what Alice sent)
  C' = C + r·A                         (what the mint returned)

`Bdhke.addRTimesA(C, r, A)` is the inverse of [unblind]'s
`C' - r·A` step. Renamed the `blindSignature` parameter to
`unblindedC` to make the contract clear at call sites.

What still works after the fix:
- Round-trip self-consistency (signFull ↔ verifyDleq) — same algo on
  both sides, still verifies.
- All existing tampered-input rejection cases.
- The new dleqProofTestVector pins against the spec's published
  proof values; reproducible offline.
- The Carol verification call site in CashuMintOperations.verifyTokenDleq
  was updated to the new parameter name.

https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP
2026-05-27 15:43:38 +00:00