feat(machine): add SQLite persistence and remove npub linking

Add crash-safe persistence for cassette inventory, cashbox state, and
transaction history using better-sqlite3 in the Electron main process.
The state machine now loads inventory from the database at runtime
instead of using hardcoded values, and transactions are automatically
persisted on completion.

Remove the unnecessary npub linking code — Lightning.Pub auto-creates
and associates Nostr users when appId is included in RPC requests,
making the HTTP-based user creation and token linking redundant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-02-19 11:45:09 -05:00
commit f37555565e
15 changed files with 739 additions and 171 deletions

View file

@ -103,11 +103,22 @@ export function createATMMachine(services: Partial<ATMServices> = {}) {
),
getInventory: fromPromise(async () => {
if (!services.getInventory) {
// Return mock inventory if service not provided
return { 20: 50 } as Record<number, number>
return {} as Record<number, number>
}
return services.getInventory()
}),
getRateAndInventory: fromPromise(async ({ input }: { input: string }) => {
if (!services.getExchangeRate) {
throw new Error('getExchangeRate service not provided')
}
const [exchangeRate, inventory] = await Promise.all([
services.getExchangeRate(input),
services.getInventory
? services.getInventory()
: Promise.resolve({} as Record<number, number>),
])
return { exchangeRate, inventory }
}),
/**
* Callback actor that watches an invoice for payment
* Sends PAYMENT_RECEIVED when the invoice is paid
@ -140,8 +151,9 @@ export function createATMMachine(services: Partial<ATMServices> = {}) {
}),
},
actions: {
resetContext: assign(() => ({
resetContext: assign(({ context }) => ({
...initialContext,
inventory: context.inventory,
cashInSessionId: null,
})),
setStartTime: assign({
@ -494,12 +506,13 @@ export function createATMMachine(services: Partial<ATMServices> = {}) {
states: {
fetchingRate: {
invoke: {
src: 'getExchangeRate',
src: 'getRateAndInventory',
input: ({ context }) => context.currency,
onDone: {
target: 'selectingAmount',
actions: assign({
exchangeRate: ({ event }) => event.output,
exchangeRate: ({ event }) => event.output.exchangeRate,
inventory: ({ event }) => event.output.inventory,
}),
},
onError: {

View file

@ -144,8 +144,8 @@ export const initialContext: ATMContext = {
cashDispensed: false,
dispenseAmounts: [],
cashOutSelection: [],
// Mock inventory: $20 bills only for Phase 1
inventory: { 20: 50 },
// Populated at runtime from DB (empty = no cassettes loaded yet)
inventory: {},
userNpub: null,
error: null,
retryCount: 0,