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
|
|
@ -20,7 +20,7 @@ import {
|
|||
*/
|
||||
export function createATMMachine(
|
||||
services: Partial<ATMServices> = {},
|
||||
options?: { currency?: string; cashInFeeRate?: number; cashOutFeeRate?: number }
|
||||
options?: { currency?: string; cashInFeeFraction?: number; cashOutFeeFraction?: number }
|
||||
) {
|
||||
return setup({
|
||||
types: {
|
||||
|
|
@ -159,8 +159,8 @@ export function createATMMachine(
|
|||
...initialContext,
|
||||
currency: context.currency,
|
||||
inventory: context.inventory,
|
||||
cashInFeePercent: context.cashInFeePercent,
|
||||
cashOutFeePercent: context.cashOutFeePercent,
|
||||
cashInFeeFraction: context.cashInFeeFraction,
|
||||
cashOutFeeFraction: context.cashOutFeeFraction,
|
||||
cashInSessionId: null,
|
||||
dispenseResult: null,
|
||||
})),
|
||||
|
|
@ -170,10 +170,10 @@ export function createATMMachine(
|
|||
cashInSessionId: () => generateSessionId(),
|
||||
}),
|
||||
setCashInFee: assign({
|
||||
feePercent: ({ context }) => context.cashInFeePercent,
|
||||
feeFraction: ({ context }) => context.cashInFeeFraction,
|
||||
}),
|
||||
setCashOutFee: assign({
|
||||
feePercent: ({ context }) => context.cashOutFeePercent,
|
||||
feeFraction: ({ context }) => context.cashOutFeeFraction,
|
||||
}),
|
||||
addBill: assign({
|
||||
billsInserted: ({ context, event }) => {
|
||||
|
|
@ -190,7 +190,7 @@ export function createATMMachine(
|
|||
if (context.exchangeRate === 0) return 0
|
||||
const fiatDollars = context.fiatCents / 100
|
||||
const principalSats = Math.floor(fiatDollars * context.exchangeRate)
|
||||
const fee = Math.floor(principalSats * context.feePercent)
|
||||
const fee = Math.floor(principalSats * context.feeFraction)
|
||||
return principalSats - fee
|
||||
},
|
||||
}),
|
||||
|
|
@ -326,7 +326,7 @@ export function createATMMachine(
|
|||
const fiatDollars = context.fiatCents / 100
|
||||
const principalSats = Math.floor(fiatDollars * context.exchangeRate)
|
||||
// For cash-out, user pays the sats, so fee is added
|
||||
const fee = Math.floor(principalSats * context.feePercent)
|
||||
const fee = Math.floor(principalSats * context.feeFraction)
|
||||
return principalSats + fee
|
||||
},
|
||||
}),
|
||||
|
|
@ -373,7 +373,7 @@ export function createATMMachine(
|
|||
if (context.availableBalance <= 0 || context.exchangeRate === 0) return false
|
||||
const newFiatCents = context.fiatCents + event.denomination * 100
|
||||
const principalSats = Math.floor((newFiatCents / 100) * context.exchangeRate)
|
||||
const fee = Math.floor(principalSats * context.feePercent)
|
||||
const fee = Math.floor(principalSats * context.feeFraction)
|
||||
return principalSats - fee <= context.availableBalance
|
||||
},
|
||||
// Cash-out guards
|
||||
|
|
@ -404,9 +404,9 @@ export function createATMMachine(
|
|||
context: {
|
||||
...initialContext,
|
||||
...(options?.currency ? { currency: options.currency } : {}),
|
||||
...(options?.cashInFeeRate !== undefined ? { cashInFeePercent: options.cashInFeeRate } : {}),
|
||||
...(options?.cashOutFeeRate !== undefined
|
||||
? { cashOutFeePercent: options.cashOutFeeRate }
|
||||
...(options?.cashInFeeFraction !== undefined ? { cashInFeeFraction: options.cashInFeeFraction } : {}),
|
||||
...(options?.cashOutFeeFraction !== undefined
|
||||
? { cashOutFeeFraction: options.cashOutFeeFraction }
|
||||
: {}),
|
||||
},
|
||||
states: {
|
||||
|
|
|
|||
|
|
@ -59,12 +59,12 @@ export interface ATMContext {
|
|||
currency: string
|
||||
/** Exchange rate (sats per fiat unit) */
|
||||
exchangeRate: number
|
||||
/** Fee percentage (0.02 = 2%) — used at runtime for the active flow */
|
||||
feePercent: number
|
||||
/** Cash-in commission (0.02 = 2%) */
|
||||
cashInFeePercent: number
|
||||
/** Cash-out commission (0.02 = 2%) */
|
||||
cashOutFeePercent: number
|
||||
/** Fee as unit fraction in [0, 1] — 0.02 means 2%. Active flow's rate. */
|
||||
feeFraction: number
|
||||
/** Cash-in commission as unit fraction (0.02 = 2%) */
|
||||
cashInFeeFraction: number
|
||||
/** Cash-out commission as unit fraction (0.02 = 2%) */
|
||||
cashOutFeeFraction: number
|
||||
/** ATM's available balance in sats (for limiting cash-in transactions) */
|
||||
availableBalance: number
|
||||
|
||||
|
|
@ -163,9 +163,9 @@ export const initialContext: ATMContext = {
|
|||
satsAmount: 0,
|
||||
currency: 'USD',
|
||||
exchangeRate: 0,
|
||||
feePercent: 0.0333,
|
||||
cashInFeePercent: 0.0333,
|
||||
cashOutFeePercent: 0.0777,
|
||||
feeFraction: 0.0333,
|
||||
cashInFeeFraction: 0.0333,
|
||||
cashOutFeeFraction: 0.0777,
|
||||
availableBalance: 0,
|
||||
invoice: null,
|
||||
clinkOffer: null,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue