Commit Graph
14321 Commits
Author SHA1 Message Date
Claude a5cd3e0dc1 refactor(richtext): zero-allocation markdown detector + test suite
Swap computeIsMarkdown for a single-pass character scan gated by a
preallocated trigger-char table (BooleanArray[128]). Drops the
chain of `String.contains` calls in favor of one O(n) walk with
O(1) per-char dispatch. Also broadens coverage to ordered lists,
unordered lists (-/*/+), tables (|), strikethrough (~~), code
spans (single backtick), setext underlines (=== / ---), and
markdown links with URL validation.

Cashu exemption kept up front — the new algorithm would still
false-positive on cashuB tokens because `_` is in the trigger
table and `__` returns true immediately.

Two refinements on top of the proposed algorithm to pass the
new test suite:
  - Setext heading now requires the underline line to be
    homogeneous (only `=` or only `-` plus whitespace). Without
    this, an ordinary sentence ending in `-` would be promoted
    to a heading underline at the next newline.
  - Markdown link detection now validates that the URL portion
    (between `(` and `)`) doesn't contain a newline, so
    `[link](url\nbroken)` no longer matches.

Test suite expanded to ~50 individual cases plus a parameterised
`testMarkdown()` that mirrors the user-provided case list with
pass/fail diagnostics to stdout. One case in the original list
(`"Standard divider line\n=========="`) was flipped from false
to true — the case name self-identified as "Valid Setext" and
CommonMark gives no length cap that would distinguish a long
underline from a heading.
2026-05-28 21:04:16 +00:00
Claude a19a025274 Revert "fix(richtext): don't truncate single-atom content mid-token"
This reverts commit 3dbf247108.
2026-05-28 20:44:14 +00:00
Claude f6b3eb0552 fix(richtext): don't markdown-detect cashu tokens
A cashuB token pasted into a chat rendered as raw base64 instead
of the redeem card, because the message was routed through the
markdown renderer rather than the rich-text renderer.

Root cause: computeIsMarkdown treats any content containing `__`
as markdown (bold). cashuB is base64url-encoded CBOR, and the
base64url alphabet uses `_`; a token whose CBOR ends with the
indefinite-length break marker (0xff) easily produces a trailing
run like `______`. The user's actual token did. The markdown
renderer has no CashuSegment support, so it just dumped the raw
text. The previous truncation patch was unrelated — the cutoff
guard already returned content.length for this 398-char token, so
truncation never fired.

Fix: when content contains `cashuA` or `cashuB` (case-insensitive),
short-circuit isMarkdown to false. The trade-off is that a chat
message mixing markdown formatting AND a cashu token will lose
the markdown rendering, but the cashu card showing up matters
more — and pure-markdown messages are unaffected.

Tests cover the exact user-reported token, the same token
embedded in a longer message, a synthetic cashuA, and a control
case proving plain `__bold__` is still classified as markdown.
2026-05-28 20:41:44 +00:00
Claude 3dbf247108 fix(richtext): don't truncate single-atom content mid-token
A cashuB token pasted into a DM didn't render the redeem card —
the user saw the raw base64 + a useless "Show more" button.

Root cause was in ExpandableTextCutOffCalculator. The user's
token was ~480 chars with no whitespace anywhere. The calculator
saw `min == content.length > TOO_FAR_SEARCH_THE_OTHER_WAY (450)`,
fell into the backward-search branch, found no space or newline
in the first SHORT_TEXT_LENGTH (350) chars either, and returned
350 — slicing the token mid-base64.

The truncated string still started with "cashuB", so the parser
matched a CashuSegment, but CashuPreview's base64 decode failed
on the corrupt body and it fell back to rendering the raw text.

Fix: when there's no whitespace boundary anywhere in the first
SHORT_TEXT_LENGTH chars during backward search, return
content.length — the entire content is one indivisible atom
(cashuA/cashuB, base64 data: URI, lnbc, single huge URL), so
cutting it can only corrupt the segment.

The pre-existing testImage was locking in the same bug for a
~11k-char data: URI (truncated to 350 → broken image segment);
updated it to assert image.length and added two new regression
tests around the user's exact cashuB token and a "preamble +
long token" case where the cut should land cleanly at the
boundary before the token. Also added a CashuTokenParserTest
covering the parser side (which was already correct) so any
future change that breaks cashuB detection is caught.
2026-05-28 20:27:53 +00:00
Claude 6940d32799 fix(cashu): dedup NUT-09 restore + heal prior Resync duplicates
Each Resync click was re-deriving the same UNSPENT proofs from the
NUT-13 seed and publishing them as a fresh kind:7375, inflating the
displayed balance (which sums proofs across every kind:7375) by the
unspent total on every click. The mint won't honor the duplicates,
but the local count drifts upward until the user tries to spend.

Two changes:

1. CashuWalletOps.restoreFromMint now takes existingSecrets: Set<String>
   and filters recovered proofs whose NUT-00 secret is already present
   locally. The wrapper in CashuWalletState collects the set from
   _tokenEntries before each Resync, so subsequent runs are no-ops.

2. CashuWalletState.cleanupDuplicateProofs scans held kind:7375 events
   and NIP-09 deletes any whose secret set is a (non-strict) subset of
   another's, keeping one canonical survivor per equivalence class
   (oldest createdAt, smallest id as tiebreaker). Called automatically
   before each Resync so the click also heals balance damage from
   prior Resync clicks that ran without the dedup. removeEvents fires
   inline so the de-duplicated balance is visible immediately.
2026-05-28 19:51:53 +00:00
Vitor PamplonaandClaude Opus 4.7 ebf8f195e4 refactor(cashu): delete wrong-theory dodge scaffolding
Removes ~700 lines of code added across ~10 prior commits trying to dodge
the ART JIT crash from the wrong angle. With the real root cause fixed
upstream (uLtInline inline-expansion), none of this is needed.

Deleted:
- BdhkeScratchpad.kt + 3 platform actuals (apple/jvmAndroid/linux). The
  thread-local Fe4/MutablePoint pool was added under the belief that
  per-call allocation density was triggering an ART escape-analysis
  bug. It wasn't. The original Bdhke functions allocated ~5-10 small
  objects per call — well under any TLAB pressure threshold.
- Bdhke.warmup() and the 2048-cycle blind+unblind loop it ran. The
  warmup was justified by "force the JIT compile to happen during init
  where a crash isn't user-facing" — except the warmup itself was what
  triggered the crash. ~4 seconds of wasted startup CPU.
- MintApiSerializerWarmup.kt (kotlinx.serialization decoder warmup).
  Same wrong theory, same wasted startup work.
- The `scope.launch(Dispatchers.Default) { Bdhke.warmup(); ... }` block
  in CashuWalletState.start() that called both warmups.

Reverted:
- Bdhke.kt to its pre-scratchpad shape. Drops `hashToCurveInto`,
  `parseAffinePointInto`, `computeNegRkInto`, `negateInto`,
  `toUncompressedOrNullScratch`, `compressedToUncompressedScratch`,
  `toCompressedScratch`, `@Volatile warmupDone`, `fun warmup()`, and
  the `JIT_WARMUP_ITERATIONS` constant. Restores the simple
  fresh-allocations-per-call form of `hashToCurve`, `blind`,
  `unblind`, `verifyDleq`, `addRTimesA`.

Stripped from CashuMintOperations.kt:
- Four `Log.i("CashuTrace") { ... }` diagnostic lines in the restore
  loop, added to chase the wrong hypothesis.
- The `import com.vitorpamplona.quartz.utils.Log` they were the only
  user of.
- Five "Bdhke uses a thread-local scratchpad internally" comments that
  referenced the now-deleted scratchpad.
- The "easier on the ART JIT" rationale in the per-counter dedup
  comment, replaced with the actual NUT-09 §2 echo-semantics
  explanation. The dedup itself is a real algorithmic win (~378 → ~6
  unblinds per batch), kept.

Cleaned in CashuPreferences.kt:
- "ART JIT crash on Android 15+" example in the durability rationale,
  replaced with generic "OOM, signer dialog dismiss, unexpected
  process death." The durability point stands regardless of crash
  source.

Net: -704 / +117 lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 15:15:18 -04:00
Vitor PamplonaandClaude Opus 4.7 6b9573906b revert(cashu): restore generated decoder for /v1/restore
The hand-rolled `parseRestoreResponse` was added under the (wrong) belief
that kotlinx.serialization's generated `RestoreResponseDto.serializer`
decode body was the ART JIT crash trigger. Real cause was `uLtInline`
inline-expansion downstream in `Bdhke.unblind` (see two commits back);
the decoder was never on the crash path.

The hand-roll is also actively worse than the generated path:
`runCatching { ... }.getOrNull() ?: empty` everywhere means a malformed
mint response that the generated decoder would have rejected with a
clean exception silently returns an empty `RestoreResponseDto`. The
restore driver reads that as "no matches in this batch → bump
empty-streak counter → terminate." Net effect: a misbehaving mint can
silently truncate your NUT-09 wallet restore. Fail-loud is the correct
posture for an untrusted endpoint.

Drops 6 kotlinx.serialization.json imports and the entire
`postWithManualResponseDecode` + `parseRestoreResponse` block (-110
lines). `restore` becomes a one-liner again, identical to every other
mint endpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 15:14:49 -04:00
Vitor PamplonaandClaude Opus 4.7 7d5573532f revert(cashu): re-enable NUT-12 DLEQ on own-mint outputs
Restores the per-signature `Bdhke.verifyDleq` check in
`CashuMintOperations.unblindOne` that was removed earlier on this branch
under the (wrong) belief that `verifyDleq`'s allocation density was
triggering the ART JIT crash. The real bug was `uLtInline`
inline-expansion in U256/ScalarN/FieldP (see preceding commit); with
that fixed, `verifyDleq` runs at 2048 iterations/process in the
regression suite (`r_verifyDleq_2048`) with no JIT trouble.

The skip-DLEQ commit argued "a malicious mint can just refuse the
request" so the check buys "fail fast vs fail-at-next-spend, not actual
security." That undersells NUT-12: a key-substituting mint produces
*unspendable* proofs, and pre-emptive DLEQ catches that at mint-receive
time, before the user considers the operation done. Without the check,
the failure surfaces at the next swap — by which point a sender has
already considered the payment complete and any sent token is dead.

Third-party proof verification (incoming cashu tokens, nutzap redeems)
continues to go through `verifyDleqCarol` / `verifyTokenDleq` —
unchanged, still the harder untrust boundary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 15:14:33 -04:00
Vitor PamplonaandClaude Opus 4.7 1f3630c4ae fix(quartz): defeat ART JIT InstructionSimplifier crash by uninlining uLtInline
Root-cause fix for a deterministic SIGSEGV in the ART JIT compile thread
on Android 16, fault address 0x48, inside
`art::HBasicBlock::RemoveInstruction +32` →
`art::InstructionSimplifierVisitor::Run` → `OptimizingCompiler::JitCompile`.

`uLtInline` was `internal inline`, so its body
`(a xor MIN_VALUE) < (b xor MIN_VALUE)` was duplicated at every call site.
With 199 sites across U256, ScalarN, FieldP, FieldMul*, and 12 sequential
`if (uLtInline(...)) 1L else 0L` patterns inside `ScalarN.reduceWideTo`
alone, ART's `InstructionSimplifier::VisitAnd` / `VisitBooleanNot` tried
to fold them as a group and null-deref'd while removing HIR nodes.

Threshold for the crash on this Android 16 emulator is ~48 invocations
of any function whose call tree reaches `ECPoint.mul` (NUT-09 restore,
swap, ECDH, NIP-44 conversation-key derivation — basically every Cashu
op).

Drop `inline`. The function-call boundary at each site hides the
xor+lt+Select(0L,1L) chain behind an HInvokeStatic returning bool, so
the simplifier no longer sees the group-foldable shape.

Cost: ~80 ns per call (vs zero inlined). ~12 calls per `reduceWideTo`,
~4 `reduceWideTo`s per `splitScalarInto`, ~1 `splitScalarInto` per
`ECPoint.mul` — order of microseconds per unblind. NUT-09 restore of a
typical wallet adds ~1 ms vs the network round-trip. Negligible.

Also adds BdhkeJitCrashTest.kt, an instrumented regression suite that
exercises every Cashu-reachable cryptographic primitive at 2048 calls
each, with byte-for-byte cross-validation against fr.acinq.secp256k1
JNI where applicable (catches both crashes and silent miscompiles):

  - blind/unblind workload (a, b, e, f)
  - hashToCurve, blind, sign, secp256k1 pubkeyCreate isolations (g..j)
  - acinq pubKeyTweakMul control (l)
  - ECDH x-only vs acinq (n)
  - Schnorr sign byte-match vs acinq (o)
  - Schnorr verify (p)
  - privKeyTweakAdd byte-match vs acinq (q)
  - NUT-12 mint-side DLEQ accept+reject (r)
  - NUT-12 Carol-side DLEQ roundtrip (covers addRTimesA) (s)
  - NIP-44 v2 cipher encrypt-decrypt roundtrip (t)

All 16 tests pass on the Android 16 emulator that previously
deterministically reproduced the crash.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 15:14:01 -04:00
Vitor PamplonaandClaude Opus 4.7 0dc1610975 test(androidTest): unblock dex + Account() ctor compile
Three pre-existing breakages on this branch that block the new
BdhkeJitCrashTest regression suite from dexing:

- NotificationFeedFilterModeOverrideTest, ThreadDualAxisChartAssemblerTest:
  add the cashuWalletFilterAssembler / cashuMintDirectoryFilterAssembler /
  okHttpClientForMoney parameters that Account() now requires.

- TorBootstrapInstrumentedTest: rename three backticked test methods to
  underscored identifiers. D8 rejects spaces in inner-class names prior
  to DEX version 040 (minSdk 35); Kotlin generates `Lambda$<methodName>`
  inner classes from backticked function names, so any space in the
  method name kills the dex step on the project's minSdk 26 target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 15:13:33 -04:00
Claude 82e1096e69 Merge remote-tracking branch 'origin/main' into claude/cashu-wallet-amethyst-sdOWe 2026-05-28 15:57:39 +00:00
Vitor PamplonaandGitHub e9a1215063 Merge pull request #3088 from vitorpamplona/claude/intelligent-fermat-8y2or
Auto-stick feeds to top on prepend with StickToTopOnPrepend
2026-05-28 11:55:51 -04:00
Vitor PamplonaandGitHub 938e9401b2 Merge pull request #3087 from vitorpamplona/claude/sweet-gates-pLvsz
Fix StrictMode violation in ML Kit translation initialization
2026-05-28 11:54:15 -04:00
Claude b1da3c2161 fix: move ML Kit translation off the UI dispatcher
LaunchedEffect runs on Dispatchers.Main by default. The first call to
LanguageTranslatorService triggers its class init, which loads a Properties
file from inside the play-services AAR via ZipFile/RandomAccessFile and
trips StrictMode's DiskReadViolation on the UI thread.

Wrap the translateAndCache call in withContext(Dispatchers.IO) so MLKit's
first-touch init happens off the UI dispatcher.
2026-05-28 15:52:21 +00:00
Claude b634a32ac6 fix(cashu): crank Bdhke warmup to 2048 iterations for ART tier-1
The last reproducer crashed AFTER the hand-rolled restore parser
moved past kotlinx.serialization: batches 1-3 unblinded fine, batch
4 SIGSEGV'd in the JIT thread between `deduped` and `batch unblinded`.
Now the JIT is choking on Bdhke.unblind itself at tier-1 (optimizing
compile threshold ~21 invocations on Android 15+).

Bump warmup from 32 → 2048 iterations of blind+unblind. At ~1 ms
per cycle on a mid-range Android 15 device that's ~4 s of background
warmup at app start (Dispatchers.Default coroutine, UI stays
responsive). Crossing tier-1 inside that window means the production
restore loop hits already-optimized code instead of triggering the
compiler mid-operation.

Also bump MintApiSerializerWarmup element count 32 → 128 for the
swap / mint / melt endpoints that still go through generated
deserializers (restore now uses the tree-API hand-roll).
2026-05-28 15:51:17 +00:00
Claude c8b7c4dddc fix(cashu): bypass kotlinx.serialization for /v1/restore response
The diagnostic logs confirmed the JIT crash hypothesis: three NUT-09
batches deserialize fine through the generated
RestoreResponseDto.serializer().deserialize(...), then the 4th batch
crosses ART's tier-1 (optimizing) compile threshold for the
generated decoder body, the optimizer crashes (SIGSEGV at offset
0x48 in Jit thread pool), and the process dies before the 4th
batch's "decoded" log can fire.

Trace shape on every reproducer:
  Bdhke.warmup begin / end             ← warmup OK
  MintApiSerializerWarmup begin / end  ← warmup OK
  restore POST batch 1, decoded, deduped, unblinded
  restore POST batch 2, decoded, deduped, unblinded
  restore POST batch 3, decoded, deduped, unblinded
  restore POST batch 4
  <SIGSEGV — never reaches "decoded">

Replace the generated decode path for /v1/restore only. The encode
side stays through the generated serializer (request payload is
small + cold). For the response: use Json.parseToJsonElement (the
JSON-tree API) and walk the tree by hand, extracting fields into the
existing DTOs. The tree API is one parser routine, completely
separate code from the per-class generated deserializers — much
smaller bytecode, no escape-analysis target shape, no JIT crash.

Defensive against missing fields (returns empty lists / null dleq)
so a misbehaving mint can't trip the hand-roll. Other endpoints
(swap, mint, melt, checkstate) keep their generated deserializers
since they don't go through the multi-batch tier-1 threshold.
2026-05-28 15:40:29 +00:00
Claude 9bef8d48c7 diag(cashu): targeted CashuTrace logs around restore HTTP boundary
JIT crash still recurs on Android 15+ even with ThreadLocal Bdhke
scratchpad + at-most-once warmups + serializer warmup. To isolate
whether the crash is:
  (a) the warmup never running,
  (b) kotlinx.serialization deserializing the restore response, or
  (c) downstream unblind work,
add four focused log points:

  - Bdhke.warmup begin / end
  - MintApiSerializerWarmup.warmup begin / end
  - restore: POST /v1/restore (req=N outputs)
  - restore: decoded sigs=N echoes=N      ← reached IFF deserialize OK
  - restore: deduped to N unique counter(s)
  - restore: batch unblinded

After the next crash, the last surviving log line says which phase
the JIT was compiling. If "decoded" never appears, (b) is confirmed
and we hand-roll the JSON parser for the restore endpoint.
2026-05-28 13:35:09 +00:00
David KasparandGitHub b9bc21d78e Merge pull request #3084 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-28 14:29:20 +02:00
Crowdin Bot 6e9a905e64 New Crowdin translations by GitHub Action 2026-05-28 12:28:31 +00:00
davotoulaandClaude Opus 4.7 d27d215d97 i18n: add cs/de/sv plurals for music track counts; teach skill to diff <plurals>
The find-missing-translations skill only diffed <string name=, so missing
<plurals> resources slipped through. Updated Steps 2, 2.5, 3 to diff
<plurals> independently and added 3 missing music playlist plurals
across cs/de/sv with correct CLDR category coverage (Czech: one/few/many/other).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:26:55 +02:00
David KasparandGitHub a70e0e6cf9 Merge pull request #3083 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-28 14:18:12 +02:00
Crowdin Bot a8d8d704c4 New Crowdin translations by GitHub Action 2026-05-28 12:16:37 +00:00
davotoulaandClaude Opus 4.7 b98c18351a i18n: add cs/de/sv translations for music tracks/playlists, video error fallback, wallet reorder
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:14:46 +02:00
David KasparandGitHub 03fef56d08 Merge pull request #3082 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-28 14:09:54 +02:00
Crowdin Bot e1b5c7e24b New Crowdin translations by GitHub Action 2026-05-28 12:08:43 +00:00
David KasparandGitHub e376f71d0b Merge pull request #3081 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-28 14:06:28 +02:00
Crowdin Bot a5aefc3233 New Crowdin translations by GitHub Action 2026-05-28 12:05:11 +00:00
davotoulaandClaude Opus 4.7 4596102a66 i18n: add Czech, German, Swedish translations for NIP-82 sections, music upload banner, send
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:01:26 +02:00
David KasparandGitHub 196175691e Merge pull request #3080 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-28 13:57:42 +02:00
Crowdin Bot 648f517a3f New Crowdin translations by GitHub Action 2026-05-28 11:38:10 +00:00
David KasparandGitHub 38fe2861b0 Merge pull request #3079 from davotoula/fix/namecoin-password-toggle-icon
Use distinct icons for password visibility toggle
2026-05-28 13:36:13 +02:00
davotoula 9d44c22401 fix(quartz): two more iOS compile errors in NIP-60 Cashu
Two issues surfaced once the @Volatile import fix (daa9bbff3) let the
iOS compiler proceed:

1. CashuDeterministic.bytesToLowercaseHex used `String(CharArray)`,
   which is `DeprecationLevel.ERROR` on Kotlin/Native. Swap for
   `CharArray.concatToString()` — identical semantics on all targets,
   and the only KMP-portable form.

2. MintExceptionTest lived in commonTest but referenced
   MintHttpException / MintProtocolException, which are defined in
   jvmAndroid/MintHttpClient.kt (HTTP-layer concerns, not portable).
   Move the test to jvmAndroidTest where its dependencies actually
   exist. No coverage change — these classes are JVM/Android-only.

Verified locally with `./gradlew :quartz:iosSimulatorArm64Test
:quartz:compileTestKotlinIosArm64`.
2026-05-28 13:06:47 +02:00
davotoulaandClaude Opus 4.7 daa9bbff3a fix(quartz): KMP-safe @Volatile import in Cashu warmup flags
`@Volatile` in commonMain resolves to `kotlin.jvm.Volatile` by default,
which doesn't exist on iOS targets. Two new NIP-60 Cashu files use
`@Volatile` without an explicit import, breaking
`:quartz:compileKotlinIosSimulatorArm64`:

  Bdhke.kt:577:6 Unresolved reference 'Volatile'.
  MintApiSerializerWarmup.kt:74:6 Unresolved reference 'Volatile'.

Add `import kotlin.concurrent.Volatile` to both files. Same pattern as
6f1292bfc (Note.kt) — semantics unchanged on JVM/Android, now also
resolves on iOS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 12:50:15 +02:00
davotoula ace6e979d9 fix(namecoin): use distinct icons for password visibility toggle
The trailing icon used MaterialSymbols.Lock in both branches of the
visibility conditional, making it a no-op. Switch to Visibility /
VisibilityOff to match the AccountBackupScreen convention.
2026-05-28 12:01:45 +02:00
Claude 63e9970340 refactor(cashu): Bdhke scratchpad via ThreadLocal — drop param API
Bdhke's allocation-free hot path used to expose `BdhkeScratchpad` as
an explicit parameter on every public function (`blind(secret, r, scratch)`,
`unblind(..., scratch)`, `verifyDleq(..., scratch)`, …). Every caller
in CashuMintOperations had to remember to allocate a scratchpad per
loop and thread it through.

Switch to a thread-local pool. New expect/actual:
  internal expect fun bdhkeScratchpad(): BdhkeScratchpad
    - jvmAndroid: ThreadLocal.withInitial { BdhkeScratchpad() }
    - apple / linux: fresh allocation per call (no Cashu in prod)

Each public Bdhke function pulls the scratchpad internally with one
`val scratch = bdhkeScratchpad()` at the top. Every thread that ever
touches Bdhke gets one scratchpad allocated lazily on first use and
reuses it across every subsequent call on that thread — same JIT-bug
mitigation, much cleaner API.

Removes:
- 0-arg and N+1-arg overloads on blind / unblind / verifyDleq /
  verifyDleqCarol / hashToCurveCompressed
- `scratch` parameter on private addRTimesA / unblindOne /
  unblindAll
- All `val scratch = BdhkeScratchpad()` boilerplate in
  CashuMintOperations.restore / meltToLightning / verifyTokenDleq /
  checkStates / secretOutputsFor

Also strips the diagnostic Log.i("CashuTrace") / Log.i("BdhkeTrace")
lines added during the JIT-bug investigation — the at-most-once
warmup + ThreadLocal pooling should resolve the crash, and the
traces were polluting logcat at info level.

Nested calls (verifyDleqCarol → blind + addRTimesA + verifyDleq)
all grab the same thread-local scratchpad; the holder field sets
are disjoint by design so nested use is safe.

BdhkeTest still 17/17 green.
2026-05-28 02:43:48 +00:00
Claude 6b3bcdfa53 fix(cashu): warmup must be at-most-once per process
The previous warmup change introduced a startup crash: each account's
CashuWalletState.start() spawned a coroutine on Dispatchers.Default
that ran Bdhke.warmup() (32 blind+unblind cycles) AND
MintApiSerializerWarmup.warmup() (decode 32-element synthetic
RestoreResponseDto). With two accounts, that's 128 BDHKE calls + 64
deserializations all in flight at once — far worse JIT pressure than
the original problem, since the warmup explicitly tries to make
methods hot.

The trace showed it clearly — multiple unblind/blind logs from
different threads interleaving mid-call ("unblind: parseAffinePointInto k"
appearing without a preceding "unblind: parseAffinePointInto cTick"
from the same logical call). ART's optimizer then crashed on the
flood.

Add a @Volatile flag to both warmups. First caller does the work;
every subsequent caller returns immediately. Plain volatile (not
atomic CAS or synchronized) because:
  - The race window is tiny (microseconds between read and write)
  - Two extra 32-cycle warmups in the worst case isn't a correctness
    or performance issue
  - Stays commonMain-portable without expect/actual or atomicfu
2026-05-28 02:13:16 +00:00
Claude ae05bb9abd refactor(feeds): hoist auto-stick into Saveable* wrappers
Pulls StickToTopOnPrepend out of every per-feed callsite and into
SaveableFeedContentState, SaveableGridFeedContentState, SaveableFeedState,
and SaveableGridFeedState — the same wrappers that already own
WatchScrollToTop. A new FeedContentState-keyed overload derives the head
key from feedContent → Loaded.feed → list.firstOrNull()?.idHex, so the
wrappers can wire auto-stick without any per-feed plumbing.

Removes the explicit StickToTopOnPrepend calls from FeedLoaded,
PictureFeedLoaded, ArticlesFeedLoaded, NestsFeedLoaded,
WebBookmarksFeedLoaded, GalleryFeedLoaded, DiscoverFeedLoaded,
DiscoverFeedColumnsLoaded, and ChatroomListFeedView — they all consume
listStates created by one of the four wrappers above.

Kept as explicit calls:
- UserFeedView (custom listState, no wrapper)
- CardFeedView (CardFeedContentState — different type)
- TabNotesNewThreads (custom listState, no wrapper)
- BrowseEmojiSetsScreen (doesn't use SaveableGridFeedContentState)

Also extracts the list/grid bodies to a private stickToTopOnPrepend
core that takes the state object as the LaunchedEffect key plus
lambdas for sampling / scroll, and switches the cached flag from
mutableStateOf to a plain BooleanArray holder (read only from effects,
never composition — no snapshot tracking needed). Adds the missing
"why isScrollInProgress gating is safe" line to the KDoc.
2026-05-28 02:09:40 +00:00
Claude 84dbf90076 fix(cashu): dedup restore unblind loop, pre-warm Bdhke + serializers
The trace from the last crash showed the restore unblind loop running
378 iterations per batch — minibits returns one signature per
(amount, bTick) we probed (63 denominations per counter × 6 unique
counters). Most iterations hit the recoveredCounters.add(...) continue
path. That's 60× more loop pressure than needed AND it's exactly the
kind of wide hot loop the ART JIT keeps trying to compile.

Changes:

1. Dedup BEFORE the unblind loop. Walk response.signatures once,
   keep one (counter -> first signature) entry per unique counter
   into a LinkedHashMap, then iterate that. Restore's per-batch
   inner loop drops from 378 to ~6 iterations and stops being a
   JIT compile target.

2. Pre-warm hot paths during CashuWalletState.start() on a background
   coroutine, so the synchronous JIT compile pause happens at app
   init (low pressure, no user waiting) instead of mid-restore where
   we observed a 13ms gap on the first Bdhke.blind of a new batch
   followed by a crash:

   - Bdhke.warmup() runs 32 blind+unblind cycles with synthetic
     keypair so the secp256k1 / hashToCurve / addPoints / toCompressed
     hot path tier-1 compiles once.

   - MintApiSerializerWarmup.warmup() decodes a synthetic 32-element
     RestoreResponseDto (with nested DleqProofDtos) plus a SwapResponseDto.
     kotlinx.serialization's generated decoder for BlindSignatureDto
     becomes JIT-compiled during init — that decoder is otherwise
     the heaviest allocation density we hit (~1.5k data-class
     instances + ~5k Strings per real restore response, ART optimizer
     bait on Android 15+).

If the JIT crash recurs, the next move is hand-rolling the JSON
parser for RestoreResponseDto / SwapResponseDto to bypass
kotlinx.serialization on the hot path entirely.
2026-05-28 02:00:59 +00:00
Claude 56e5aea79f fix(cashu): scratchpad toCompressed + deep Bdhke tracing
The previous round of scratchpadding still left two Fe4 allocations
per call inside toCompressed — the final point→bytes step at the
tail of every blind / unblind / addRTimesA. Across an NUT-09 restore
sweep that's ~250+ Fe4 allocations the ART JIT optimizer still gets
to chew on. After ART inlines toCompressed back into the outer
crypto bodies, those allocations end up in the same hot method
bodies the scratchpad was supposed to clear out.

Adds a [toCompressedScratch] variant that reuses two new holders on
[BdhkeScratchpad], and routes every hot-path caller through it
(unblind, blind, addRTimesA, hashToCurveCompressed). The plain
[toCompressed] stays for the cold paths (sign, signFull, others
only used by tests).

Also adds verbose BdhkeTrace / CashuTrace markers in:
  - Bdhke.blind: secKeyVerify → hashToCurveInto → mulG → addPoints → toCompressedScratch
  - Bdhke.unblind: parseAffinePoint cTick → parseAffinePoint k → computeNegRk → addPoints → toCompressedScratch
  - restore inner loop: per-counter (CashuDeterministic → Bdhke.blind → build dtos), HTTP, per-signature unblind begin/end

So when the next JIT crash happens (if it does), the last log line
points at the exact sub-step ART was compiling. Logs are at INFO
level; cheap enough to leave on while diagnosing, easy to strip
afterward.
2026-05-28 01:23:48 +00:00
Claude 509cbcb297 fix(cashu): scratchpad verifyDleq / Carol / addRTimesA / hashToCurveCompressed
Final pass on the Bdhke JIT-crash mitigation. Three more Bdhke
functions on the production hot path still allocated per-call holders
and could trigger the Android 15+ ART JIT escape-analysis crash:

  verifyDleq        ~17 short-lived Fe4 / MutablePoint per call
  addRTimesA         ~6 short-lived holders per call
  hashToCurveCompressed  ~3 per iter (plus ~3 inside hashToCurve)

verifyDleqCarol orchestrates blind + addRTimesA + verifyDleq, so the
Carol path (used by every inbound nutzap / cashu-token redeem) was
allocating ~40 short-lived holders per proof. Same shape that crashes
the JIT in swap output handling.

Adds scratchpadded overloads:
  verifyDleq(..., scratch)
  verifyDleqCarol(..., scratch)
  hashToCurveCompressed(x, scratch)
  addRTimesA's scratchpad variant is the only signature (it's private)

Plus three helper -Into variants for the negate / toUncompressedOrNull /
compressedToUncompressed steps verifyDleq inlines.

Wired:
  verifyTokenDleq → one scratch per call, reused across every per-proof
    Carol verification
  checkStates → one scratch per call, reused across the per-proof
    hashToCurveCompressed pre-image computation

Tests: 17/17 BdhkeTest still pass.

After this commit, every Bdhke function on the production hot path
runs allocation-free. Functions only used by tests / mint emulation
(sign, signFull, verify) are intentionally not refactored — they're
never invoked at runtime in the app. The remaining 2-allocation
helpers (toCompressed, toCompressedOrNull) are well below the JIT
crash threshold.
2026-05-28 01:02:30 +00:00
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 ac3f351458 feat(feeds): stick to top when items prepend and user is at top
Adds StickToTopOnPrepend, a Compose helper that auto-scrolls back to
index 0 whenever new items land at the head of a feed — but only if the
user was already at the very top right before the update. Wired into
every FeedLoaded variant: Home/Hashtag (FeedLoaded), Notifications
(CardFeedView), Pictures, Articles, Discover (list + grid),
WebBookmarks, ProfileGallery, BrowseEmojiSets, Chatroom list,
UserFeed, NestsFeed, and TabNotesNewThreads.

Why this was broken: every feed uses stable key = item.idHex, so when N
items prepend Compose preserves the user's visual anchor by shifting
firstVisibleItemIndex from 0 to N. The existing
WatchScrollToTop only fires on explicit tab-bar taps, and the
LaunchedEffect(items.firstOrNull()) { if (firstVisibleItemIndex <= 1) }
pattern (used in ChatFeedView and PublicChatsFeedLoaded) breaks the
moment more than one item arrives in the same batch.

How the helper avoids the race: it tracks "was at top" continuously via
snapshotFlow but only flips it true → false when isScrollInProgress is
true. Data-driven index shifts happen with isScrollInProgress == false,
so they never poison the cached value. When firstItemKey changes and
the cached value is still true, we snap back to 0 with an instant
(non-animated) scroll so the prepend appears as in-place growth instead
of a visible jump-then-scroll.

ChatFeedView is left alone — it already works because reverseLayout
masks the prepend shift.
2026-05-27 23:35:56 +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
Vitor PamplonaandGitHub 522a83c4de Merge pull request #3078 from vitorpamplona/claude/pensive-turing-6UC1W
Add dedicated NIP-82 software app detail screen
2026-05-27 18:38:03 -04:00
Claude a2b4e5fa70 feat(nip82): compact apps feed card + dedicated detail screen
Rework the NIP-82 Software Applications feed item so it scans cleanly at
list density and split the detail content into its own route.

Feed card (RenderSoftwareApplication): icon + name + summary, full
description (3 lines), platforms / license chips, and a latest-version
chip resolved from LocalCache. Drops the screenshots strip, website /
repo link rows, and #topic chips at feed scale. The card is tappable
and the standard ReactionsRow now hosts replies, boosts, likes, zaps,
and share underneath each card.

New SoftwareAppDetailScreen (Route.SoftwareAppDetail) reached via the
card tap, the routeFor() dispatch, and naddr deep links. Layout: 72dp
header, screenshots carousel, About, Platforms, Topics (#tag chips
clickable through to Route.Hashtag), Links, ReactionsRow, latest
release with bundled assets, a collapsible "show older releases"
section, and the NIP-22 comment thread inline (driven off the app's
address tag through ThreadFeedViewModel + ThreadFilterAssembler).
2026-05-27 22:23:59 +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