feat(machine): persist exchange rate and currency in transactions

Add exchange_rate (sats per fiat unit) and currency columns to the
transactions table so transaction economics can be audited after the
fact. Includes schema migration v2 (fee columns) and v3 (rate/currency),
updated IPC types, and atm-transactions CLI output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-03-10 01:30:18 -04:00
commit 310d0edb99
5 changed files with 65 additions and 4 deletions

View file

@ -15,7 +15,7 @@ import fs from 'node:fs'
let db: Database.Database | null = null
const SCHEMA_VERSION = '1'
const SCHEMA_VERSION = '3'
function getDbPath(): string {
const prodDir = '/var/lib/lamassu-atm'
@ -68,6 +68,10 @@ export function initDatabase(dbPath?: string): void {
type TEXT NOT NULL CHECK (type IN ('cash_in', 'cash_out')),
fiat_cents INTEGER NOT NULL,
sats INTEGER NOT NULL,
fee_sats INTEGER NOT NULL DEFAULT 0,
fee_percent REAL NOT NULL DEFAULT 0,
exchange_rate REAL NOT NULL DEFAULT 0,
currency TEXT NOT NULL DEFAULT 'GTQ',
created_at INTEGER NOT NULL
);
@ -79,12 +83,31 @@ export function initDatabase(dbPath?: string): void {
);
`)
// Seed meta + cashbox if first run
// Seed meta + cashbox if first run, or run migrations
const existing = db.prepare('SELECT value FROM meta WHERE key = ?').get('schema_version') as
| { value: string }
| undefined
if (!existing) {
db.prepare('INSERT INTO meta (key, value) VALUES (?, ?)').run('schema_version', SCHEMA_VERSION)
} else if (existing.value === '1') {
// Migration v1 → v2: add fee columns to transactions
db.exec(`
ALTER TABLE transactions ADD COLUMN fee_sats INTEGER NOT NULL DEFAULT 0;
ALTER TABLE transactions ADD COLUMN fee_percent REAL NOT NULL DEFAULT 0;
`)
db.prepare('UPDATE meta SET value = ? WHERE key = ?').run('2', 'schema_version')
console.log('[StateStore] Migrated schema v1 → v2 (added fee columns)')
existing.value = '2'
}
if (existing && existing.value === '2') {
// Migration v2 → v3: add exchange rate and currency
db.exec(`
ALTER TABLE transactions ADD COLUMN exchange_rate REAL NOT NULL DEFAULT 0;
ALTER TABLE transactions ADD COLUMN currency TEXT NOT NULL DEFAULT 'GTQ';
`)
db.prepare('UPDATE meta SET value = ? WHERE key = ?').run('3', 'schema_version')
console.log('[StateStore] Migrated schema v2 → v3 (added exchange_rate, currency)')
}
const cashboxRow = db.prepare('SELECT id FROM cashbox WHERE id = 1').get()
@ -214,6 +237,10 @@ interface TransactionInput {
type: 'cash_in' | 'cash_out'
fiatCents: number
sats: number
feeSats: number
feePercent: number
exchangeRate: number
currency: string
bills: { denomination: number; count: number }[]
}
@ -224,7 +251,7 @@ export function recordTransaction(tx: TransactionInput): void {
if (!db) throw new Error('Database not initialized')
const insertTx = db.prepare(
'INSERT INTO transactions (txid, type, fiat_cents, sats, created_at) VALUES (?, ?, ?, ?, ?)'
'INSERT INTO transactions (txid, type, fiat_cents, sats, fee_sats, fee_percent, exchange_rate, currency, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
)
const insertBill = db.prepare(
'INSERT INTO transaction_bills (txid, denomination, count) VALUES (?, ?, ?)'
@ -237,7 +264,17 @@ export function recordTransaction(tx: TransactionInput): void {
)
const run = db.transaction((t: TransactionInput) => {
insertTx.run(t.txid, t.type, t.fiatCents, t.sats, Date.now())
insertTx.run(
t.txid,
t.type,
t.fiatCents,
t.sats,
t.feeSats,
t.feePercent,
t.exchangeRate,
t.currency,
Date.now()
)
for (const bill of t.bills) {
insertBill.run(t.txid, bill.denomination, bill.count)