Files
amethyst/commons
Claude 1397bc6f81 refactor: move NUT-13 counters to DataStore, hoisting the reservation
Step 6, built as option 2: the counter reservation moves up to the
suspend layer so secret derivation stays pure, rather than making
SecretFactory.nextSecrets suspend and dragging RandomSecretFactory — a
function that does nothing but generate randomness — along with it.

SecretFactory splits in two:

  suspend fun reserve(keysetId, count): SecretReservation
  fun derive(reservation, count): List<DerivedSecret>

reserve() is the durability boundary and the only part that touches
storage. derive() is pure: same reservation, same secrets, no
suspension. nextSecrets() remains as the convenience that does both.
CashuMintOperations.secretOutputsFor now calls them separately, so the
write that stands between a crash and a reused counter is visible at the
layer that can await it, instead of hidden inside derivation.

RandomSecretFactory reserves nothing. DeterministicSecretFactory reserves
nothing either when it has no seed yet, since it will fall back to random
and burning counters for secrets never derived from them is waste. If the
seed disappears between reserving and deriving, that batch falls back to
random and the reserved range goes unused — harmless, because counters
only ever move forward and an unused one is never replayed.

Storage: CashuKeysetCounterStore becomes suspend, and the Android
implementation moves to DataStore as DataStoreCashuCounterStore in
commons, so desktop gets one too rather than having none. Read and write
sit inside a single `edit`, which is what the previous @Synchronized was
for: two concurrent mints cannot observe the same starting index.
DataStore's edit suspends until its write lands and swaps the file
atomically — the same guarantee SharedPreferences.edit(commit = true)
gave, paid at a suspension rather than a blocked thread.

Both older layers still feed in and neither can move a counter backwards:
the per-account SharedPreferences file is copied in full on first read,
inside the same atomic write that records the copy happened, so a crash
cannot leave the marker set with the counters missing; and the older
AccountSettings.cashuKeysetCounters map is still applied per keyset
through seedIfMissing.

21 tests where there were none. This path decides whether ecash is
spendable and had no coverage at all while it was on SharedPreferences.
The ones that matter most: 25 concurrent reservations carve strictly
disjoint ranges, a reservation survives a reopen, seedIfMissing never
moves a counter backwards, the legacy migration carries every counter and
does not rewind reservations made after it ran, and a host with no store
wired fails loudly instead of answering 0.

Not verified here: no mint was contacted. The durability and concurrency
properties are covered by tests, but an end-to-end mint/melt against a
real mint is still worth doing before release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AXvKXakvup4inNFfAhhr4L
2026-09-23 19:50:58 +00:00
..