refactor(machine): canonical sat-amount vocabulary + fix 100× fee bug

Aligns lamassu-next with the canonical sat-amount vocabulary agreed
across lnbits/bitspire/satmachineadmin (satmachineadmin@d717a6e,
coordination log 2026-05-26T17:10Z):

- `feePercent` / `cashInFeePercent` / `cashOutFeePercent`
  → `feeFraction` / `cashInFeeFraction` / `cashOutFeeFraction`
  (canonical: unit fraction in [0, 1], NEVER a percentage)
- `cashInFeeRate` / `cashOutFeeRate` (config option names)
  → `cashInFeeFraction` / `cashOutFeeFraction`
- `fee_percent` (wire field on Payment.extra + state.db column)
  → `fee_fraction`

Bug fix bundled with the rename:
`lightning.ts:780` previously stamped `Payment.extra.fee_percent =
context.feePercent * 100` (0.05 → 5.0). state.db stored the unit
fraction (0.05) but Payment.extra carried the percent (5.0) — 100×
divergence that any consumer reading Payment.extra computed fees
wrong by exactly 100×. Now stamps `fee_fraction` directly as unit
fraction. Display layers (atm-tui, view components) multiply by 100
themselves.

Defensive invariants added:
- `computeFeeSats` (atm store) throws if `feeFraction` outside [0, 1]
  or if cash-in `feeSats > principalSats` (would mean negative payout)
- `recordTransaction` (state-store) throws on the same range
- state-machine + electron + Vue views propagate the rename

state.db migration v6 → v7: `ALTER TABLE transactions RENAME COLUMN
fee_percent TO fee_fraction`. Historical migrations preserved
verbatim (they wrote `fee_percent`, future installs see the same
sequence followed by the v7 rename).

12/12 typecheck + 18/18 state-machine tests green. Coordinated with
~/dev/bitspire/atm-tui (separate commit) reading `fee_fraction`
from the new column.

refs: log:2026-05-26T17:10Z, log:2026-05-26T18:50Z,
satmachineadmin@d717a6e

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-27 21:53:53 +02:00
commit 6a627e5b4a
12 changed files with 92 additions and 56 deletions

View file

@ -298,8 +298,8 @@ ipcMain.handle('get-config', () => {
maintenanceMode: process.env.VITE_MAINTENANCE_MODE === 'true',
// Fee rates — accepts percentage (5.55) or decimal (0.0555), auto-detected
cashInFeeRate: parseFee(process.env.VITE_CASH_IN_FEE || '0.0333'),
cashOutFeeRate: parseFee(process.env.VITE_CASH_OUT_FEE || '0.0777'),
cashInFeeFraction: parseFee(process.env.VITE_CASH_IN_FEE || '0.0333'),
cashOutFeeFraction: parseFee(process.env.VITE_CASH_OUT_FEE || '0.0777'),
// Operator branding (logo/title/theme) — null when no override
branding: loadBranding(),
@ -560,7 +560,7 @@ function startCommandPoller(): void {
fiatCents: totalFiatCents,
sats: 0,
feeSats: 0,
feePercent: 0,
feeFraction: 0,
exchangeRate: 0,
currency: fiatCode,
bills: parsed.bills,