The table recreation migration failed on machines with existing
transaction_bills/cassette_bills rows due to FK constraints on
transactions(txid). Also clean up leftover transactions_new table
from any previous failed migration attempt.
Closes#38
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The command poller hardcoded 'GTQ' as the currency for manual dispense
transactions. Now reads VITE_LAMASSU_FIAT_CODE from env, defaulting
to 'USD'.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
If the manual dispense itself partially fails (e.g., cassette jam during
remediation), the original failed transaction must stay in error state
so the operator knows it still needs attention. Only mark as
'remediated' when result.dispensed === true (all requested bills out).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add operator_commands table and polling loop so the TUI (or other local
tools) can trigger manual dispenses by inserting a command row into
SQLite. The Electron main process polls every 2s, executes pending
commands via HAL, records the transaction, and updates the command
status with the result.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a Nostr-native operator command channel using Kind 21003 (CLINK
Manage) events. Operators listed in OPERATOR_PUBKEYS can send encrypted
commands to the machine.
Phase 1 implements manual dispense: operator sends a dispense command,
machine verifies sender, checks it's idle, performs a direct HAL
dispense (bypassing state machine), and records the transaction.
When ref_txid is provided, the referenced failed transaction is updated
to status 'remediated', closing the loop on dispense errors.
Changes:
- CLINK types: add 'machine' resource, MachineDispenseRequest type
- CLINK client: support operator pubkey list (string | string[])
- Runtime config: VITE_OPERATOR_PUBKEYS env var
- Schema v4→v5: manual_dispense type, remediated_by column
- Lightning services: wire onManagement callback
- ATM store: handleManagementCommand with idle check + remediation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The HAL init hung indefinitely when the validator device didn't exist
or failed to respond — blocking the entire init including the dispenser
and Lightning connection.
Now the validator is optional:
- Checks device exists (fs.existsSync) before attempting to open
- 15s timeout on validator.run() to prevent hanging
- On failure, logs warning and proceeds with dispenser-only mode
- All validator methods guarded with null checks
This enables the BATM3 to run cash-out only when the MEI validator
is not connected (e.g., during initial setup or testing).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Port MEI CashFlow SC / BNR Advance EBDS protocol from lamassu-machine
to TypeScript HAL. Adds 'batm3' machine model preset (EBDS validator +
F56 dispenser). Fixes hardcoded 'id003' validator type in device config
overrides so model presets correctly propagate their validator type.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Failed dispenses (sats debited, cash not dispensed) were invisible —
transactions only recorded on 'complete'. Now records on 'dispenseError'
with status ('dispense_error'|'partial'|'complete'), error message, and
per-cassette detail.
Also fixes a bug in both HAL services where dispense results were mapped
by amounts-array index instead of cassette position, causing swapped
denomination counts when cassette order differs from request order.
Schema v3→v4: adds status/error columns to transactions, new
cassette_bills table for per-cassette provisioned/dispensed/rejected.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After 6 days of uptime the Electron renderer silently crashed while the
main process kept running (blank screen, no recovery). Three-layer
detection: render-process-gone (instant), unresponsive (Chromium), and
IPC heartbeat (30s ping, 2 missed = reload). Reloads renderer via
loadFile/loadURL preserving HAL hardware state in main process.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After a dispense error, the F56/Puloon drivers call close() which sets
initialized=false. The next dispense would fail on a closed serial port.
Now checks dispenser.initialized before each dispense and re-inits if
needed, matching the lazy re-init pattern from brain.js (line 4072).
Closes#29
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
Only allow mock fallback when running in development mode (isDev).
In production (packaged Electron app), the VITE_ALLOW_MOCK_FALLBACK
env var is ignored entirely. This prevents an attacker with file
access from enabling mock services (fake payments, fake hardware)
by editing .env on the ATM.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Guard hal:stack-bill and hal:reject-bill IPC handlers against being
called when no bill is in escrow (pendingBillDenomination === null).
Previously, rapid-fire calls could double-accept or misattribute
bill denominations. Now the handlers silently ignore calls when
no bill is pending, preventing the race.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add CSP in two layers:
1. Meta tag in index.html (works for all builds)
2. HTTP header via Electron session API (defense-in-depth)
Policy: script-src 'self' blocks XSS from loading external scripts
or executing inline scripts. style-src allows 'unsafe-inline' for
Vue's style injection. connect-src allows ws/wss/http/https for
configurable relay and API endpoints.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add input validation to hal:dispense IPC handler:
- Reject non-array or empty amounts
- Validate denomination and count are numbers
- Reject non-positive or non-integer counts
- Verify denomination exists in loaded cassettes
- Verify requested count does not exceed available inventory
Prevents a compromised renderer from sending crafted dispense
requests (negative counts, unknown denominations, over-capacity).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move atmPrivateKey and adminToken out of the general get-config IPC
handler into a dedicated one-shot get-atm-secrets handler that returns
secrets only once per app lifecycle. Subsequent calls return empty
strings. This prevents XSS or DevTools from repeatedly querying
getConfig() to steal the ATM's Nostr private key.
TODO: Move signing/encryption to main process entirely (Phase 2)
so the private key never crosses the IPC boundary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The hal:dispense IPC handler in main.ts did not return the result of
dispenseCash(), causing the state machine guard to crash on undefined
output. This left the UI stuck on "Dispensing cash..." after successful
dispense.
- hal-service.ts: return DispenseResult instead of void/throwing
- main.ts: add missing return in IPC handler
- machine.ts: defensive guard (?. instead of .) as safety net
Bug found with the aid of Seoyoung at Trece Cielos.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cassette inventory was never initialized in SQLite, so
recordTransaction's UPDATE decrements were no-ops against an empty
table. Now the Electron main process seeds cassettes from
VITE_LAMASSU_CASSETTES or the model preset on first boot.
Also adds an atm-transactions CLI script (with --summary, --inventory,
--type, --today, --last, --since filters) and sqlite to the NixOS
system packages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When VITE_ALLOW_MOCK_FALLBACK is unset (production default), the ATM
now shows a maintenance screen instead of silently falling back to mock
services when hardware or Lightning initialization fails. Also disables
ndebit/CLINK in production since the static ndebit pointer is replayable
— cash-in uses LNURL-withdraw only (single-use by design).
- Add allowMockFallback config field (Electron IPC + types)
- Add strict config validation (no localhost, require private key)
- Gate all catch-block fallbacks behind allowMockFallback
- Disable debit approval service and ndebit generation in production
- Add maintenance screen in App.vue when initError is set
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix cassette denominations: Douro uses Q100/Q200, not Q20
- Add hal:get-inventory IPC so renderer can read HAL cassette inventory
- Add balance fetch/display to HAL+IPC init path and idle screen
- Enable/disable bill validator via watch on nested state transitions
- Pass fiatCode to state machine context (was hardcoded to USD)
- Preserve currency across state machine resetContext
- Add CANCEL handler to dispenseError state (was stuck)
- Fix remaining hardcoded $ symbols in CashInView
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
HAL hardware drivers (serialport) run in the main process since they
need Node.js. The renderer communicates via IPC for all hardware ops.
- hal-service.ts: bridge between HAL drivers and Electron IPC
- main.ts: HAL IPC handlers (init, dispense, validator stack/reject)
- preload.ts: expose HAL API to renderer via contextBridge
- atm.ts: IPC-based production init with validator event wiring
- hal.ts: add 'hold' mode for escrow (async stack/reject decision)
- electron.d.ts: HAL type declarations for window.electronAPI
Bills go to escrow first; the renderer checks balance before accepting.
Falls back to Lightning-only mock mode if HAL init fails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
- Switch Electron window to landscape (1920x1080) with fullscreen fallback
- Rearrange IdleView for horizontal layout (logo+badges left, action cards right)
- Fix hostname command in provision/build scripts (use ip route instead of hostname -I)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add NixOS configuration to build a bootable live USB ISO that runs the
ATM Electron app in kiosk mode on physical hardware (UpBoard). The ISO
boots from squashfs, auto-starts X11/openbox, and launches Electron in
production mode.
Key changes:
- deploy/nixos/live.nix: Live USB module (squashfs+tmpfs, no disk install)
- deploy/nixos/flake.nix: Nix flake with ISO build output
- deploy/nixos/provision-atm.sh: Auto-provision LP credentials via API
- deploy/nixos/build-iso.sh: End-to-end build workflow script
- apps/machine: Fix Electron production mode (ELECTRON_FORCE_PROD),
Vue Router hash mode for file:// protocol, relative asset paths
Build: cd deploy/nixos && bash build-iso.sh
Test: qemu-system-x86_64 -enable-kvm -m 2G -cdrom result/iso/*.iso
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Expose serialport APIs through Electron preload for bill validator
and dispenser communication. Update HAL service with device path
configuration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add dev.sh script for managing regtest development environment
- Implement cmd_fund to fund ATM app owner via Lightning.Pub API
- Add --fund flag to cmd_up for automatic funding on startup
- Update setup_atm_app to write VITE_APP_ID to machine .env
- Fix Electron IPC to pass appId and extensionApiUrl to renderer
- Restructure repo from nested lamassu-next/ to root
The dev.sh script now supports:
- ./dev.sh up --fund # Start regtest and auto-fund ATM
- ./dev.sh fund # Fund existing ATM app
- ./dev.sh status # Show environment status
- ./dev.sh reset # Clean restart
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>