fix(ecash): pay the mint's input fee, and stop a damaged wallet from being erased
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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
26638aa621
commit
ffec7d3114
@@ -308,6 +308,29 @@ pub struct KeysetInfo {
|
||||
pub id: String,
|
||||
pub unit: String,
|
||||
pub active: bool,
|
||||
/// NUT-02 input fee, in parts-per-thousand of a proof. A mint charges
|
||||
/// this per *input* on a swap/melt; zero at fee-free mints, which is why
|
||||
/// ignoring it went unnoticed against Minibits.
|
||||
#[serde(default)]
|
||||
pub input_fee_ppk: u64,
|
||||
}
|
||||
|
||||
/// NUT-02 swap fee for a set of inputs: the summed per-proof parts-per-
|
||||
/// thousand, rounded **up** to whole units. Inputs whose keyset the mint
|
||||
/// didn't list contribute nothing — the mint is the authority, and guessing
|
||||
/// high would silently burn the sender's coins.
|
||||
pub fn swap_fee_for(proofs: &[Proof], keysets: &[KeysetInfo]) -> u64 {
|
||||
let ppk: u64 = proofs
|
||||
.iter()
|
||||
.map(|p| {
|
||||
keysets
|
||||
.iter()
|
||||
.find(|k| k.id == p.id)
|
||||
.map(|k| k.input_fee_ppk)
|
||||
.unwrap_or(0)
|
||||
})
|
||||
.sum();
|
||||
ppk.div_ceil(1000)
|
||||
}
|
||||
|
||||
/// Mint keyset: maps denomination amounts to public keys.
|
||||
|
||||
Reference in New Issue
Block a user