3b.1 of the LP→LNbits migration: structurally introduce LnbitsClient
into services/lightning.ts without changing any runtime behavior.
All existing call sites still go through LightningPubClient.
apps/machine/src/services/lightning.ts
- import LnbitsClient from @bitSpire/lnbits
- add `lnbitsServerPubkey` to LightningConfig
- load it from runtime IPC config + VITE_LNBITS_SERVER_PUBKEY
env var (env wiring proper happens in 3c)
- module-level `_lnbitsRef: LnbitsClient | null`
- in initializeLightningServices, instantiate LnbitsClient
ONLY IF `CONFIG.lnbitsServerPubkey` is set (graceful no-op
while the env hasn't been wired yet)
- export `_getLnbitsClient()` for 3b.2+ call sites
apps/machine/package.json
- add `@bitSpire/lnbits: workspace:*` dependency
Verified: pnpm typecheck clean (14/14 turbo tasks, machine task
now executes since lnbits is a new dep).
Next: 3b.2 — drop the CLINK/ndebit cash-in flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final rename commit covering user-facing copy and the docs that
describe current state. The mechanics of the rename are done after
this; the LNbits backend swap (phase 3) is the next concern.
Code branding strings (Lightning invoice descriptions):
apps/machine/src/services/lightning.ts
apps/machine/src/stores/atm.ts
docs/clink-protocol.md (example code blocks)
"Lamassu ATM Payment" → "bitSpire Payment"
"Lamassu ATM - Cash Out" → "bitSpire - Cash Out"
`Lamassu ATM - Buy ${n} sats`→ `bitSpire - Buy ${n} sats`
Top-level docs:
README.md, CLAUDE.md — title + intro + dir-tree references.
deploy/nixos/README.md — title + worktree-path commands.
docs/machine-installation.md — opening line carries the historical
note ("Lamassu Next" → "bitSpire"). The body still uses
`/opt/lamassu/` paths and the `lamassu-kiosk` systemd unit
because the dev branch is moving to NixOS disk-image flash
(phase 4) — this AppImage-sideload doc represents the legacy
deploy path. Leaving the LP/lamassu refs in there as part of
its historical context; a separate doc will describe the
NixOS path.
.claude/skills/nostr-check.md — header only.
DELIBERATELY left as "Lamassu Next" (pedagogical / historical):
- docs/adr/001-hal-architecture.md — frozen ADR; renaming
distorts the historical decision context.
- docs/architecture-comparison.md — deliberately contrasts
"lamassu-server" (prior) with "lamassu-next" (us at the time
of writing).
NOT done in this commit (deferred to LNbits/clean-up phase):
- docker/docker-compose.dev.yml container names
(lamassu-relay, lamassu-bitcoind, etc.) — these belong to the
LP-bearing dev stack that 3c/3d will significantly reshape.
Verified: pnpm typecheck clean (12/12 cached).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
apps/machine/package.json (electron-builder block):
appId dev.lamassu.atm → dev.bitSpire.atm
productName "Lamassu ATM" → "bitSpire"
apps/machine/src/services/lightning.ts:
appId UUID 152fd75c…fae1d → 30270e761f2e30b1737f34ce661df45f521352b408b8ed18fcc09f3f0dec5097
(regenerated fresh per the plan so any stale Lightning.Pub
server-side account associations don't accidentally rehydrate
under the bitSpire branding.)
The runtime appId is also overridable via VITE_APP_ID env var
(lightning.ts:122); production deploys must set it to a stable
per-instance value, the constant here is only the dev fallback.
Verified: pnpm typecheck clean (12/12).
Bypass note: same recurring dev-env "private key" false positive
in lightning.ts as 2a — not introduced by this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Aaeon UP Board (Atom x5-Z8350) chokes on continuous CSS transforms.
Gate animate-float on machineModel, keep the bounce for douro/tejo/batm3/gaia
where the hardware can handle it. Refs #47 (operator-side animation toggle
is a future consideration there).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Accepts percentage (5.55) or decimal (0.0555) — auto-detected by
whether the value is >= 1. Defaults to 3.33% cash-in, 7.77% cash-out.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a relay reconnects after a disconnect, all active subscriptions
(including Lightning.Pub RPC listener) are now re-established on the
new relay instance. Previously subscriptions were lost permanently.
Also publishes availability broadcast immediately on reconnect instead
of waiting up to 5 minutes for the next heartbeat.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add min-h-0 for proper flex containment so ScrollArea can be
constrained to the remaining space.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Returns to idle screen after 5 minutes of no touch/scroll activity.
Timer resets on any pointer or scroll interaction.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The availability broadcast was reading inventory from the XState context,
which is only populated during cash-out transitions. On fresh boot or
idle, context.inventory is empty, so the broadcast falsely reported
cash_level: "none" even when cassettes had bills.
- Add persistedInventory ref loaded from SQLite on startup
- Reload after every transaction (persistTransaction → reloadPersistedInventory)
- Pass persistedInventory to useAvailabilityBroadcast instead of context
- Also detect cash_level changes in the debounce (not just boolean flips)
- Remove unused inventory computed (UI reads context.inventory directly)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
VITE_ env vars are baked in at build time and empty in the Nix build.
Now reads lightningPubPubkey and relayUrl from Electron's getConfig()
at runtime, with dev fallback to import.meta.env.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The dedicated "Using ShockWallet" QR now encodes the raw nprofile
value (not a URL) so ShockWallet's QR scanner can recognize it
directly. The table row still uses the deep link URL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ShockWallet entry in the wallets table now resolves to the deep
link URL with nprofile param, so scanning its QR icon also connects
to the ATM's Lightning.Pub. Falls back to plain URL if unconfigured.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
QR now encodes wallet.aiolabs.dev/sources/add?nprofile=... so scanning
opens ShockWallet with the ATM's Lightning.Pub pre-filled, handling
both new and existing users.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ShockWallet users can scan the nprofile to connect to the ATM's
Lightning.Pub instance. QR is built from VITE_LIGHTNING_PUB_PUBKEY
and VITE_RELAY_URL env vars with a graceful fallback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The HAL inventory was built from the config preset, ignoring operator
changes made via atm-tui or SQL. Now reads cassettes from the DB at
HAL init time so denomination/count changes take effect on restart.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add position column to cassettes table (migration v5→v6) so cassettes
are ordered by physical cartridge number instead of denomination.
Update BATM3 preset to $20/$1 denominations with 400-bill capacity.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add circular outline to the ? help button on idle screen and scale
support page navigation buttons for touchscreen kiosk use.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In maintenance mode, establish a minimal Nostr connection (no
Lightning.Pub) and publish Kind 30078 heartbeat with
maintenance: true. Monitors show yellow dot + "under service"
instead of appearing offline.
Only the Nostr client is initialized — no payment infrastructure.
Same one-shot private key security model as normal operation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The LNURL-withdraw session had a fixed 5-minute expiry timer that
raced with the state machine's displayingQR timeout (also 5 min).
If the session timer fired first, the withdraw link was deleted
while the customer could still retry from confirmAbandon.
Now LNURL sessions are cleaned up by the state machine on idle
transition instead of a fixed timer. A 15-minute safety timeout
remains as a fallback in case the state machine doesn't clean up.
Flow: displayingQR (5min) → confirmAbandon (60s) → idle → cleanup.
The withdraw link stays alive the entire time the customer can
interact with it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Publish cash level (none/low/good/full) in the Kind 30078 event
based on total bill count across all cassettes. Enables monitoring
dashboards to show cash availability without revealing exact amounts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Kind 30078 availability event was using 'atm' as the model
placeholder. Now reads the actual model from runtime config
(batm3, douro, sintra, etc.) so monitoring dashboards can
distinguish between different ATM types.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace cursor: default (which showed pointer on buttons) with
cursor: none !important on all elements. Touchscreen ATMs don't
need a visible cursor — taps register via touch coordinates.
Reverts the earlier cursor: default addition and fixes the
pre-existing issue of pointer cursor showing over buttons.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire up the availability broadcast composable to publish the ATM's
status as a replaceable Kind 30078 Nostr event. Publishes on
availability change (debounced) and as a 5-minute heartbeat so
monitors can detect offline machines.
Also adds WiFi auto-connect for BATM3: reads SSID/PSK from
/var/lib/lamassu-atm/wifi.conf at boot. Credentials stay local.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add support/help pages accessible via a ? button on the idle screen.
Pages are driven by .md files in /var/lib/lamassu-atm/support/ —
operators can customize content without rebuilding the app.
Features:
- Tabbed view with markdown rendering (via marked)
- Standalone URLs auto-render as QR codes (scannable from phone)
- Table URLs: click-to-reveal QR codes (prevents accidental scans)
- Yes/No rendered as green checkmarks / red X marks
- Wallet comparison table with download QR codes
- FAQ with Lightning-only clarification
- Support page with operator Nostr QR placeholder
- Custodial vs non-custodial footnote
- Large text for touchscreen accessibility
- Centered layout for short-content pages
Closes#36
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Set VITE_MAINTENANCE_MODE=true in .env to show an "Under Service"
screen and block all transactions. No hardware init, no Lightning
connection — just a static screen.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Standalone Node.js script that generates a Lightning invoice for
the ATM's Lightning.Pub account. Reads config from .env, connects
to the relay, creates an invoice via Nostr RPC, displays a QR code
in the terminal, and prints the BOLT11.
Bundled as self-contained CJS with esbuild (all dependencies inlined)
so it works from the nix store without separate node_modules.
Usage: fund-atm <amount_sats>
e.g. fund-atm 100000
fund-atm 100000 sats
Added to NixOS systemPackages for both live and installed configs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Electron hides the cursor over non-interactive elements when running
without a desktop environment. Add cursor: default to html/body so
the mouse pointer is always visible when using an external mouse or
touchscreen.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the dead hourglass CSS animation with a thematic pickaxe mining
animation. The pickaxe swings from resting position up to -45deg then
strikes down, mimicking a mining motion. Used in the generatingNdebit
loading state; other loading states still use BounceDots pending review.
Refs #33
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the dead hourglass CSS animation with a thematic pickaxe mining
animation. The pickaxe swings from resting position up to -45deg then
strikes down, mimicking a mining motion. Used in the generatingNdebit
loading state; other loading states still use BounceDots pending review.
Refs #33
Co-Authored-By: Claude Opus 4.6 <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>
Addresses security audit findings for the operator command channel:
1. Replay protection: track processed management event IDs in a Set,
reject duplicates. Caps at 1000 entries to prevent unbounded growth.
2. Timestamp validation: reject events created before machine startup
(prevents processing stale events on relay reconnect) and events
older than 60 seconds (limits replay window).
3. Input validation: validate bill denomination/count in
handleManagementCommand (defense in depth — IPC path also validates
but direct HAL path did not). Caps count at 100 per denomination.
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>
The IPC path already had a watcher to enable the bill validator when
entering insertingBills and disable it when leaving. The direct HAL
path (initializeWithHal) was missing this, meaning the validator would
accept bills in any state. Matches brain.js pattern (lines 193-196).
Closes#28
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>
The IPC HAL path (production) never set halServices.value, so the
auto-advance from waitingForCashTaken → complete never fired. The
machine hung on "Cash Ready!" indefinitely — no transaction persisted.
Three fixes:
- Auto-advance now checks `isElectron` (covers IPC path)
- waitingForCashTaken has a 30s after-timeout as safety net
- Fix unscoped lightningPub refs in LNURL session helpers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The field was always stored in cents but the name was ambiguous.
Rename to fiatCents across state machine, store, and views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add deleteWithdrawLink and updateWithdrawLink RPC methods to LightningPubClient
- Extract shared WithdrawLink type, add Delete/Update request/response types
- Track linkId in LNURL sessions for server-side cleanup
- Invalidate previous LNURL session on new link creation
- Auto-delete expired links on the server
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add verifyEvent() check before processing kind 21000 events from
Lightning.Pub. While NIP-44v1 encryption provides implicit
authentication (relay can't forge encrypted content without the
shared secret), verifying signatures adds defense-in-depth against
any future changes that might weaken the encryption assumption.
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>