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

@ -15,7 +15,7 @@ import fs from 'node:fs'
let db: Database.Database | null = null
const SCHEMA_VERSION = '6'
const SCHEMA_VERSION = '7'
function getDbPath(): string {
const prodDir = '/var/lib/bitspire'
@ -70,7 +70,7 @@ export function initDatabase(dbPath?: string): void {
fiat_cents INTEGER NOT NULL,
sats INTEGER NOT NULL,
fee_sats INTEGER NOT NULL DEFAULT 0,
fee_percent REAL NOT NULL DEFAULT 0,
fee_fraction REAL NOT NULL DEFAULT 0,
exchange_rate REAL NOT NULL DEFAULT 0,
currency TEXT NOT NULL DEFAULT 'GTQ',
status TEXT NOT NULL DEFAULT 'complete',
@ -215,6 +215,17 @@ export function initDatabase(dbPath?: string): void {
}
db.prepare('UPDATE meta SET value = ? WHERE key = ?').run('6', 'schema_version')
console.log('[StateStore] Migrated schema v5 → v6 (added cassette position)')
existing.value = '6'
}
if (existing && existing.value === '6') {
// Migration v6 → v7: rename fee_percent → fee_fraction. Same semantics
// (unit fraction in [0, 1]); the rename closes the 100× misinterpretation
// risk between satmachineadmin's consumer and bitspire's stamp.
// Coordinated with satmachineadmin commit d717a6e (v2-bitspire branch).
db.exec(`ALTER TABLE transactions RENAME COLUMN fee_percent TO fee_fraction`)
db.prepare('UPDATE meta SET value = ? WHERE key = ?').run('7', 'schema_version')
console.log('[StateStore] Migrated schema v6 → v7 (renamed fee_percent → fee_fraction)')
}
const cashboxRow = db.prepare('SELECT id FROM cashbox WHERE id = 1').get()
@ -352,7 +363,7 @@ interface TransactionInput {
fiatCents: number
sats: number
feeSats: number
feePercent: number
feeFraction: number
exchangeRate: number
currency: string
bills: { denomination: number; count: number }[]
@ -373,8 +384,15 @@ interface TransactionInput {
export function recordTransaction(tx: TransactionInput): void {
if (!db) throw new Error('Database not initialized')
if (tx.feeFraction < 0 || tx.feeFraction > 1) {
throw new Error(
`[StateStore] feeFraction out of range [0, 1]: ${tx.feeFraction}. ` +
`Unit fraction expected (0.05 = 5%), not a percentage.`
)
}
const insertTx = db.prepare(
'INSERT INTO transactions (txid, type, status, error, fiat_cents, sats, fee_sats, fee_percent, exchange_rate, currency, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
'INSERT INTO transactions (txid, type, status, error, fiat_cents, sats, fee_sats, fee_fraction, exchange_rate, currency, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
)
const insertBill = db.prepare(
'INSERT INTO transaction_bills (txid, denomination, count) VALUES (?, ?, ?)'
@ -398,7 +416,7 @@ export function recordTransaction(tx: TransactionInput): void {
t.fiatCents,
t.sats,
t.feeSats,
t.feePercent,
t.feeFraction,
t.exchangeRate,
t.currency,
Date.now()