Two independent fixes, both found while exercising the routes headlessly.
**Mint fees (NUT-02).** A mint may charge a per-input fee and rejects any
swap whose outputs don't equal inputs minus that fee — `11005 Transaction
inputs should equal outputs less fee`, which is what sending hit against
testnut.cashu.space. We ignored the fee entirely, so the wallet could not
spend at ANY fee-charging mint; Minibits charges zero, which is why
production never saw it. `MintKeyset`/`KeysetInfo` now carry
`input_fee_ppk`, `swap_fee_for` computes the NUT-02 sum (rounded up), and
`MintClient::swap` reduces its outputs to cover it — applied there rather
than at each call site so send, receive and cross-mint swaps are all
covered at once. Inputs from a keyset the mint doesn't list contribute no
fee: the mint is the authority, and guessing high would burn the sender's
coins.
**Damaged-wallet erasure.** `load_wallet` used `unwrap_or_default()`, so a
truncated `ecash.json` read as an EMPTY wallet — and because the next
operation saves the wallet back, that empty state was then written over the
only copy of the proofs. A corrupt file became permanent loss. Now a file
that exists but doesn't parse fails with a message naming the file and
stating the coins are still in it, and the bytes are left untouched for
recovery; an empty file is still treated as a fresh wallet, since a create
that never got its first write is not damage. The accepted-mints list gets
the same treatment, where corruption would have silently reset the operator
to trusting only the default mint.
Writes are now atomic (temp + fsync + rename) for both files. The previous
plain write truncated the real file first, which is exactly how a wallet
ends up unparseable after a crash or power cut.
Tests cover: a damaged file errors and survives on disk, an empty file is
fresh, saving leaves no temp behind and round-trips, and — guarding the
on-disk contract against exactly this update — a verbatim pre-update wallet
file still loads with its balance, proofs and history intact.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>