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:
parent
0af2a9bb29
commit
6a627e5b4a
12 changed files with 92 additions and 56 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||
fiatCents: number
|
||||
sats: number
|
||||
feeSats: number
|
||||
feePercent: number
|
||||
feeFraction: number
|
||||
exchangeRate: number
|
||||
currency: string
|
||||
bills: { denomination: number; count: number }[]
|
||||
|
|
@ -156,7 +156,7 @@ declare global {
|
|||
fiatCents: number
|
||||
sats: number
|
||||
feeSats: number
|
||||
feePercent: number
|
||||
feeFraction: number
|
||||
exchangeRate: number
|
||||
currency: string
|
||||
bills: { denomination: number; count: number }[]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue