docs: refresh README + CLAUDE.md for LNbits-backend bitSpire on dev

Both top-level docs were stale after the lamassu-next → bitSpire +
Lightning.Pub → LNbits migration shipped on this branch. They still
listed @lamassu/* package scopes, treated Lightning.Pub as the backend,
described cash-in as ndebit/CLINK, and pointed at removed packages.

README.md: rewritten end-to-end. Calls out the branch model (main vs
dev) so contributors don't accidentally affect production ATMs.
Documents the actual cash-out (BOLT11 + subscribe_payments by hash)
and cash-in (LNURL-withdraw + subscribe_payments by tag/link_id) wire
flows. Quick-start uses the dev compose's bundled LNbits with
nostr-transport + the LNbits-internal nostrrelay extension (the relay
endpoint is ws://<host>:5001/nostrrelay/test — no separate strfry
container, this was a pitfall during Sintra provisioning). Disk-image
deployment summary points at deploy/nixos/README for full details.

CLAUDE.md: rewritten to describe dev-branch reality. Lists actual
package names (@bitSpire/*), notes packages/lightning was deleted in
3d, calls out the LightningBackend adapter pattern in services/
lightning.ts, and gives an env-var reference table + a wire-envelope
crib for kind-21000. Sintra-specific hardware notes (UART layout,
initrd modules for the ACPI eMMC controller) bake in everything we
hard-learned during the first flash.

Both docs now include an Acknowledgements / Provenance section that:
  - credits Lamassu Industries AG's open-source lamassu-machine /
    lamassu-server (the v8.1.5 release line) as the prior art the
    HAL drivers and state machine derive from — bitSpire wouldn't
    exist without that foundation
  - explicitly states Lamassu transitioned to a proprietary,
    source-available "Appendix A SLA" on 2024-01-26 with v8.1.6+
    gated behind a paid OSA subscription, and that bitSpire
    incorporates no code from v8.1.6 or later
  - declares bitSpire independent of Lamassu Industries AG
  - in CLAUDE.md specifically: a hard rule that future contributors
    (or future Claude runs) must not pull / port / copy code from
    lamassu-machine at v8.1.6+; only the 8.1.5 tree is in-scope

License clarification: AGPL-3.0 (matches LNbits, which we link
against) — dropped the earlier "matches LNbits + lamassu-machine
pedigree" phrasing since lamassu-machine is no longer under a free
license.

References:
  https://blog.lamassu.is/updates-to-our-lamassu-software-license/
  https://github.com/lamassu/lamassu-machine

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-14 07:46:14 +02:00
commit bac130dbf1
2 changed files with 236 additions and 437 deletions

443
CLAUDE.md
View file

@ -1,352 +1,213 @@
# CLAUDE.md # CLAUDE.md
This file provides guidance to Claude Code when working with the bitSpire codebase. Guidance for Claude Code when working in this repo. Read this before touching code.
## Project Overview ## Project Overview
**bitSpire** is a Nostr-native Lightning ATM system. Key principles: **bitSpire** is a Nostr-native Lightning ATM. Production ATMs (`batm3`, `douro`) currently run from `main` against Lightning.Pub; the `dev` branch — which is what this file describes — has been migrated to **LNbits over the nostr-native-transport**.
- **KYC-Free**: No identity collection, no compliance theater Core principles:
- **Lightning-Native**: Security encapsulated in Lightning protocol
- **Nostr as Infrastructure**: Relay for communication, keypairs for identity - **KYC-free** — no identity collection, no compliance theater
- **Open Source First**: Every component auditable and forkable - **No HTTP to the Lightning backend** — every ATM↔LNbits RPC goes over kind-21000 NIP-44 v2 events on a relay
- **The ATM's nostr private key IS the credential** — LNbits auto-creates the wallet on first contact (issue aiolabs/lnbits#9 alignment, `prvkey=NULL`)
- **Open source first** — every component auditable and forkable
## Provenance + legal status
The HAL drivers (validators / dispensers / printers) and the cash-flow state machine derive from Lamassu Industries AG's open-source `lamassu-machine` and `lamassu-server` repositories, **only up to v8.1.5** — the last release published under a fully-open license. Lamassu transitioned to a proprietary, source-available license (their custom "Appendix A SLA") on 2024-01-26 and gated v8.1.6+ behind a paid OSA subscription.
**Hard rule when working in this repo:** do not pull, port, or copy code from lamassu-machine / lamassu-server at v8.1.6 or later. If a HAL bug fix or feature exists upstream past 8.1.5, either (a) reimplement from protocol docs / hardware specs without looking at v8.1.6+ source, or (b) raise the question with the maintainer first. The 8.1.5 tree is fair game; everything after is licensed code we have no rights to.
bitSpire is an independent project under AGPL-3.0 and is not affiliated with Lamassu Industries AG.
## Branch model
- `main` — production. Lightning.Pub backend. The two production ATMs auto-pull from here daily at 04:00 (`flake.nix:152-160`). **DO NOT** push to `main` casually — a wrong commit gets baked into prod ATMs the next morning.
- `dev` — staging. LNbits backend. The Sintra dev unit auto-pulls from here (`?ref=dev` pin on this branch's `flake.nix`). Push freely; tag `pre-bitspire-cutover` is the rollback target if the migration ever needs to be reverted on prod.
## Architecture ## Architecture
``` ```
bitSpire/ bitSpire/
├── apps/ ├── apps/
│ ├── machine/ # Electron + Vue 3 ATM kiosk application ✅ │ └── machine/ # Electron + Vue 3 ATM kiosk
│ ├── dashboard/ # Vue 3 operator dashboard (planned)
│ └── relay/ # strfry relay configuration (planned)
├── packages/ ├── packages/
│ ├── hal/ # TypeScript Hardware Abstraction Layer ✅ │ ├── nostr-client/ # NIP-01 client, NIP-44 v2 (encryptContentV2/decryptContentV2)
│ ├── nostr-client/ # Nostr client library ✅ │ ├── lnbits/ # LnbitsClient — kind-21000 RPC over nostr-transport
│ ├── clink/ # CLINK protocol implementation ✅ │ ├── clink/ # CLINK protocol (kinds 21001-21003) — IN TREE BUT UNUSED ON DEV
│ ├── state-machine/ # XState v5 ATM state machine ✅ │ ├── state-machine/ # XState v5 ATM state machine
│ ├── lightning/ # Lightning.Pub RPC client ✅ │ ├── hal/ # Hardware abstraction (JCM iVIZION, Fujitsu F56, etc.)
│ ├── cashu/ # Cashu ecash (placeholder) │ ├── cashu/ # placeholder
│ └── ui-shared/ # Shared Vue components (placeholder) │ └── ui-shared/ # placeholder
└── docker/ # Development infrastructure ✅ └── deploy/nixos/ # NixOS module + provisioning for Sintra/tejo/douro/batm3
``` ```
## Implementation Status `packages/lightning/` (the Lightning.Pub RPC client) was deleted on `dev` in commit `a51d7af`. If a piece of code on `dev` needs LP semantics, look at the equivalent in `packages/lnbits/` or in `apps/machine/src/services/lightning.ts` (which has a `LightningBackend` interface implemented by an LNbits-backed adapter).
### Completed Packages ## Package status
| Package | Description | Tests | | Package | Scope | Notes |
| ------------------------ | ------------------------------------------------------ | ----- | |---|---|---|
| `@lamassu/nostr-client` | Nostr relay client with NIP-42 auth, NIP-44 encryption | 13 | | `@bitSpire/nostr-client` | active | NIP-01 + NIP-44 v2 helpers. Used by every other package. |
| `@lamassu/clink` | CLINK protocol (kinds 21001-21003), noffer encoding | 7 | | `@bitSpire/lnbits` | active | Wraps the nostr-native-transport API (10 core RPCs + lnurlw/lnurlp + subscribe_payments). Mirror peer of the old `@bitSpire/lightning`. |
| `@lamassu/lightning` | Lightning.Pub RPC client (kind 21000) | 10 | | `@bitSpire/state-machine` | active | XState v5. Tests pass. `generateNdebit` / `generateClinkOffer` / `generateNoffer` services are stubs returning placeholder strings (state machine contract preserved; outputs unused by views). |
| `@lamassu/state-machine` | XState v5 ATM state machine (idle, cashIn, cashOut) | 14 | | `@bitSpire/hal` | active | TypeScript HAL. JCM iVIZION + Fujitsu F56 drivers. |
| `@lamassu/hal` | Hardware drivers (ID003 validator, F56 dispenser) | - | | `@bitSpire/clink` | dormant | Code kept for future re-wire if direct nostr-to-nostr cash-in returns. **Don't import on dev — `services/lightning.ts` no longer wires it.** |
| `@bitSpire/cashu` | placeholder | empty |
| `@bitSpire/ui-shared` | placeholder | empty |
### Placeholder Packages ## Backend wiring
| Package | Description | `apps/machine/src/services/lightning.ts` is the central file. After the 3d cutover it:
| -------------------- | --------------------------------- |
| `@lamassu/cashu` | Cashu ecash for offline operation |
| `@lamassu/ui-shared` | Shared Vue 3 components |
### Completed Applications 1. Loads config from Electron IPC (`get-config` + one-shot `get-atm-secrets`) or from `import.meta.env` (browser dev)
2. Creates an `LnbitsClient`, calls `list_wallets`, picks the first wallet — fails-fast if either step doesn't return data
3. Exposes a tiny `LightningBackend` adapter on `services.lightningPub` that satisfies `apps/machine/src/stores/atm.ts`'s expectations:
- `getBalance()` → wraps `lnbits.getBalance(walletId)`
- `watchBalance(cb)` → wraps `lnbits.watchWallet` and refetches balance per push
- `createInvoice({amountSats, description})` → wraps `lnbits.createInvoice(walletId, {amount, memo, unit:'sat'})`
- `payInvoice(bolt11, amountSats)` → wraps `lnbits.payInvoice(walletId, {bolt11, max_sat})`
4. Implements the four flow-critical `ATMServices` methods on top of LNbits:
- `generateInvoice(msat)` → cash-out BOLT11
- `watchInvoice(bolt11, cb)` → `subscribe_payments({payment_hash, max_seconds:600})` push
- `generateLnurlWithdraw(ctx)` → `lnurlw_create_link({uses:1, ...})`, compose callback URL from `VITE_LNBITS_HTTP_URL`, bech32-encode with HRP `lnurl`, subscribe for `tag:"withdraw", link_id` settlement push
- `getAvailableBalance()` → wraps `lnbits.getBalance(walletId).balanceSats`
- **apps/machine** - Electron ATM kiosk with Vue 3 UI (HAL integrated, ready for hardware testing) `CashInView.vue` calls `atmStore.generateLnurlWithdraw()` directly when entering `displayingQR`; the state machine still invokes `generateNdebit` as an actor but its output is discarded (kept only to avoid an invasive state-machine rewrite).
### Planned Components ## Environment variables
- **apps/dashboard** - Operator dashboard for fleet management Renderer reads (Electron IPC or Vite `import.meta.env`):
### Critical Documentation | Var | Required | Notes |
|---|---|---|
| `VITE_RELAY_URL` | yes | `ws://...` of the relay both ATM and LNbits subscribe to. Dev: `ws://localhost:5001/nostrrelay/test` (LNbits's bundled `nostrrelay` extension — no separate strfry container) |
| `VITE_LNBITS_SERVER_PUBKEY` | yes | 64-char hex pubkey LNbits prints on startup (`docker logs lnbits \| grep 'Public key (share this)'`) |
| `VITE_LNBITS_HTTP_URL` | yes | `http(s)://...` origin used to compose LNURL-withdraw callback URLs. The ATM itself never calls this URL — it's only embedded in the bech32 string customer wallets dereference |
| `VITE_ATM_PRIVATE_KEY` | yes (prod) | 64-char hex. The ATM's nostr identity. Generates ephemeral on first boot if unset (dev only) |
| `VITE_OPERATOR_PUBKEYS` | optional | Comma-separated hex pubkeys allowed to send kind-21003 management commands |
- **`packages/lightning/TROUBLESHOOTING.md`** - Lightning.Pub integration gotchas. Read this BEFORE debugging payment issues. Contains solutions to 9 non-obvious issues that took 5+ hours to diagnose. The LP-era vars (`VITE_LIGHTNING_PUB_PUBKEY`, `VITE_LIGHTNING_PUB_API_URL`, `VITE_EXTENSION_API_URL`, `VITE_ADMIN_TOKEN`) are gone from the dev branch's `.env.example` and `LightningConfig` interface.
## Commands ## Commands
```bash ```bash
# Enter development environment # Inside devenv shell
devenv shell pnpm install # install all workspace deps
pnpm dev # start the machine app (Vite + Electron with hot reload)
pnpm typecheck # vue-tsc --noEmit
pnpm test # vitest across all packages
# Start development # In apps/machine specifically
pnpm dev cd apps/machine && pnpm build # full prod build: vue-tsc + vite build + electron tsc + fund-atm esbuild bundle
# Build all packages
pnpm build
# Run tests
pnpm test
# Infrastructure management
infra-up # Start all Docker services
infra-down # Stop all Docker services
infra-status # Show service status
infra-logs # Follow service logs
# Bitcoin/Lightning (regtest)
btccli # Bitcoin CLI
lncli # LND CLI (Lightning.Pub's node)
lncli-alice # LND CLI (Alice's node for testing payments)
mine-blocks # Mine regtest blocks (default: 1)
setup-channel # Setup channel between Alice and LND
alice-pay # Pay invoice from Alice's node
relay-test # Test Nostr relay connection
# Testing (E2E)
test-setup # Validate test environment (services, channels, payments)
test-payment # Quick e2e payment test (ATM → customer)
fund-atm # Fund ATM account (default: 100k sats)
alice-invoice # Create invoice on Alice's node
node-info # Show node pubkeys and channel info
``` ```
## Development Infrastructure Disk image build (for Sintra/douro/etc.):
The `docker/` directory contains a complete development environment:
| Service | Container | Port(s) | Description |
| ------------- | --------------------- | ----------- | ------------------------------------- |
| strfry | lamassu-relay | 7777 | Private Nostr relay |
| bitcoind | lamassu-bitcoind | 18443 | Bitcoin Core (regtest) |
| LND | lamassu-lnd | 10009, 8080 | Lightning node (Lightning.Pub's node) |
| LND Alice | lamassu-lnd-alice | 10010, 8081 | Second LND for payment testing |
| Lightning.Pub | lamassu-lightning-pub | 1776 | Nostr-native account system |
| PostgreSQL | lamassu-postgres | 5432 | Database for server-side state |
### Quick Start
```bash ```bash
devenv shell # Enter dev environment nix build .#disk-image-sintra # → result/nixos.img
infra-up # Start all services (30-60s first run)
mine-blocks 101 # Fund the regtest wallet
setup-channel # Open channel between Alice and LND
``` ```
### Testing Payments ## Nostr event kinds
The development setup includes two LND nodes to enable proper payment testing: | Kind | Role | Status on dev |
|---|---|---|
| 21000 | LNbits nostr-native-transport (request + ack + push) | **active** — all backend traffic |
| 21001 | CLINK Offer (invoice request/response) | dormant |
| 21002 | CLINK Debit (payment authorization) | dormant |
| 21003 | CLINK Manage (operator commands) | partially wired — `clink.onManagement` still listens for operator dispense, decoupled from LP |
| 30078 | Service Beacon (replaceable, ATM availability) | active — `startAvailabilityBroadcast()` heartbeats every 5 min |
| 30079 | Transaction Record (replaceable) | planned |
1. **LND** (`lamassu-lnd`) - Used by Lightning.Pub to create invoices ## Wire envelope (kind 21000 RPC)
2. **Alice** (`lamassu-lnd-alice`) - Used to pay invoices (simulates external payers)
```bash Plaintext JSON, encrypted as NIP-44 v2 inside the event `content`:
# Get Lightning.Pub admin token
curl -X POST "http://localhost:1776/api/admin/app/auth" \
-H "Authorization: Bearer lamassu-dev-admin-token" \
-d '{"name": "wallet"}'
# Create user and invoice ```jsonc
curl -X POST "http://localhost:1776/api/app/user/add" -H "Authorization: Bearer $APP_TOKEN" \ // Request (client → server)
-d '{"identifier": "test-user", "balance": 0}' {
"rpc_name": "create_invoice",
"request_id": "create_invoice-7-ab12cd",
"wallet_id": "<uuid>",
"body": { "amount": 1000, "memo": "...", "unit": "sat" }
}
curl -X POST "http://localhost:1776/api/app/user/add/invoice" -H "Authorization: Bearer $APP_TOKEN" \ // Reply (server → client, one-shot)
-d '{"receiver_identifier": "test-user", "payer_identifier": "external", "http_callback_url": "", "invoice_req": {"amountSats": 1000, "memo": "Test"}}' {
"status": "OK",
"request_id": "create_invoice-7-ab12cd",
"data": { /* Payment object */ }
}
# Pay from Alice // Subscription push (server → client, repeated)
alice-pay <invoice> {
"status": "OK",
"request_id": "sub-9-ef34gh",
"subscription_id": "<server-issued>",
"data": { "payment": { /* Payment */ } }
}
// Subscription close (server → client, terminal)
{
"status": "OK",
"request_id": "sub-9-ef34gh",
"subscription_id": "<server-issued>",
"data": { "closed": true, "reason": "ttl" | "unsubscribed" }
}
``` ```
### Comprehensive Regtest Integration See `packages/lnbits/src/types.ts` for the TypeScript surface and `~/dev/lnbits/nostr-transport/docs/devs/nostr-transport.md` for the authoritative wire spec.
For advanced testing with multiple Lightning implementations, use the regtest environment at `~/dev/local/docker/regtest`. This provides: ## Hardware drivers
| Service | Description | TypeScript drivers in `packages/hal/`. Coverage by device class:
| ------------------ | -------------------------------------------------------------- |
| 4 LND nodes | lnd-1 (hub), lnd-2 (Boltz), lnd-3 (LNbits), lnd-4 (standalone) |
| 3 CLN nodes | Core Lightning with REST/gRPC |
| 1 Eclair node | ACINQ Eclair implementation |
| LNbits | Lightning wallet platform (port 5001) |
| Boltz | Submarine swaps (port 9001) |
| Electrs | Electrum server (port 3002) |
| Lightning Terminal | Web UI for lnd-1 (port 8443) |
| Elements/Liquid | Sidechain (port 18884) |
```bash | Category | Drivers |
# Start regtest environment |---|---|
cd ~/dev/local/docker/regtest && ./start-regtest
source docker-scripts.sh
# Start lamassu services connected to regtest
cd bitSpire/docker && ./start-with-regtest.sh
# CLI helpers
bitcoin-cli-sim -generate 1 # Mine blocks
lncli-sim 4 getinfo # lnd-4 (Lightning.Pub's node)
lightning-cli-sim 1 getinfo # CLN node 1
```
The integration uses `lnd-4` as Lightning.Pub's backend, giving you access to test payments from multiple node types (LND, CLN, Eclair) and services (LNbits, Boltz).
### MCP Tools Available
Claude has access to these MCP servers for development:
| MCP Server | Purpose |
| ------------ | ----------------------------------------- |
| docker-mcp | Container management (logs, status, etc.) |
| nostr-mcp | Nostr operations (post notes, profiles) |
| postgres-mcp | Database queries and schema inspection |
| mcp-nixos | NixOS/Nix package queries |
| forgejo-mcp | Git operations on Forgejo |
Use these to interact with infrastructure directly during development.
## Key Technologies
| Component | Technology | Notes |
| ------------- | -------------- | --------------------------------------- |
| Runtime | Node.js 22 LTS | Strict TypeScript, ESM |
| ATM Shell | Electron | Node.js main process, Vue 3 renderer |
| State Machine | XState v5 | Actor model, service injection |
| Hardware | TypeScript | ID003, F56 drivers from lamassu-machine |
| Messaging | Nostr | NIP-01, NIP-42, NIP-44 |
| Payments | CLINK + RPC | Kind 21000 (RPC), 21001-21003 (CLINK) |
| Backend | Lightning.Pub | Nostr-native account system |
## Custom Skills
The following skills are available for development assistance:
### `/security` - Security Review
Audit code for Bitcoin/Lightning/ATM-specific vulnerabilities.
```
/security packages/lightning/src/
/security --staged
```
### `/nostr-check` - Nostr Conformity
Validate NIP compliance and Nostr protocol implementation.
```
/nostr-check packages/nostr-client/src/events.ts --nips NIP-01,NIP-44
```
### `/lightning-check` - Lightning.Pub Conformity
Validate CLINK protocol and Lightning.Pub integration.
```
/lightning-check packages/clink/src/ --clink
```
### `/test` - Testing Agent
Run tests, generate test cases, validate transaction flows.
```
/test coverage packages/state-machine/
/test flow cash-out
/test generate packages/lightning/src/client.ts
```
### `/docs` - Documentation Agent
Keep documentation synchronized with code.
```
/docs sync packages/clink/
/docs api packages/nostr-client/src/
```
### `/hal-check` - HAL Validation
Validate Rust HAL drivers against lamassu-machine implementations.
```
/hal-check port id003
/hal-check safety packages/hal/src/dispensers/
```
## Code Style
### TypeScript
- ESM only (`import`/`export`)
- Strict mode with `strictNullChecks` and `noUncheckedIndexedAccess`
- Zod for runtime validation
- No `any` types
### Rust (HAL)
- Stable toolchain
- `#![deny(unsafe_code)]` unless justified
- Error handling with `thiserror`
- Async with `tokio`
### Formatting
- Prettier for TypeScript (2 spaces, no semicolons, single quotes)
- rustfmt for Rust
- Pre-commit hooks enforce formatting
## Hardware Drivers
Drivers are ported from `lamassu-machine/lib/`:
| Category | Drivers |
| ---------- | ------------------------------------------------------------ |
| Validators | id003, ccnet, cashflow_sc, bnr_advance, genmega, hcm2, gsr50 | | Validators | id003, ccnet, cashflow_sc, bnr_advance, genmega, hcm2, gsr50 |
| Dispensers | puloon, f56, genmega, hcm2, gsr50 | | Dispensers | puloon, f56, genmega, hcm2, gsr50 |
| Recyclers | MEI SCR (planned — hardware exists in BATM3, no driver yet) | | Recyclers | MEI SCR (planned — hardware in BATM3, no driver yet) |
| Printers | nippon, zebra, genmega | | Printers | nippon, zebra, genmega |
### BATM3 Hardware Topology ### Sintra hardware specifics (Aaeon UP Board)
The GeneralBytes BATM3 has two separate cash-handling units: - Validator (JCM iVIZION, ID003): FTDI USB-serial bridge → `/dev/ttyUSB1` → udev symlink `/dev/ttyJ5`
- Dispenser (Fujitsu F56): SoC MMIO UART (the only on-carrier RS-232) → `/dev/ttyS4` → udev symlink `/dev/ttyJ7`. **ttyS4 must NOT be the kernel console** — `upboard.nix` keeps `console=tty0` only
- The kernel-enumerated `ttyS1`/`ttyS2`/`ttyS3` are placeholder nodes that error on any I/O — the legacy 16550A is `ttyS0`, the SoC MMIO is `ttyS4`. Confirm with `dmesg \| grep ttyS`
| Unit | Hardware | Role | Cassettes | ### Initrd modules for eMMC boot
| ----------------- | ---------- | ---------------------------------- | ------------------------------------ |
| MEI Cash Recycler | MEI SCR | Cash-in (accept bills) + recycling | 2 recycler (60 each) + cashbox (600) |
| F56 Dispenser | Puloon F56 | Cash-out (dispense only) | 2 dispenser cassettes |
**Current status**: MEI used as validator-only (`cashflowSc` driver), F56 for dispensing. UP Board enumerates its eMMC controller via ACPI, not PCI. `upboard.nix` force-loads `sdhci-acpi` and `mmc_block` in `initrd.kernelModules` so root-by-label resolves in stage 1. If you ever rebuild the disk image for a different SoC, double-check this list.
The MEI's recycler cassettes are untapped — accepted bills go to cashbox, not recycled.
When porting: ## Code style
1. Read JS driver thoroughly - TypeScript: ESM only, strict mode + `strictNullChecks` + `noUncheckedIndexedAccess`. No `any`. Zod for runtime validation at boundaries.
2. Document protocol from JS code - Vue 3: Composition API, `<script setup lang="ts">`. Pinia for state.
3. Implement TypeScript version - Formatting: Prettier (2 spaces, no semicolons, single quotes). Pre-commit hooks enforce.
4. Test against same hardware - Tests: Vitest. Cash-out is the critical-path flow — keep `packages/state-machine` coverage tight.
5. Use `/hal-check port <driver>` to validate
## Nostr Event Kinds ## Security priorities
| Kind | Description | 1. **Private keys** — Never log nsec. The ATM's `VITE_ATM_PRIVATE_KEY` lives in `/var/lib/bitspire/.env` with mode 0600, owned by `lamassu:lamassu`.
| ----- | ----------------------------------------------- | 2. **Payments** — Validate the bolt11 amount on cash-out before exposing the QR. Decode `payment_hash` from the bolt11 (cheap, avoids a roundtrip) and use it as the `subscribe_payments` filter.
| 21000 | Lightning.Pub RPC (generic request/response) | 3. **Replay** — LNURL-withdraw links use `uses:1` and are deleted on session abort.
| 21001 | CLINK Offer (invoice request/response) | 4. **Encryption** — All RPC content is NIP-44 v2. NIP-04 is forbidden.
| 21002 | CLINK Debit (payment authorization) |
| 21003 | CLINK Manage (offer management) |
| 30078 | Service Beacon (replaceable, service discovery) |
| 30079 | Transaction Record (replaceable) |
## Security Priorities ## Useful invariants when debugging
1. **Private keys** - Never log nsec, protect with 0600 permissions - The renderer logs prefix every line with a tag: `[Lightning]`, `[ATM]`, `[ATM Service]`, `[LNURL Session]`, `[CLINK]`, `[StateStore]`. `journalctl -u bitspire | grep '\['` is your friend.
2. **Payments** - Validate invoices, verify preimages, prevent double-pay - `bitspire.service` runs as the `lamassu` user; `/var/lib/bitspire` is its `dataDir` (ReadWritePaths). DB lives at `/var/lib/bitspire/state.db` (we previously had `/var/lib/lamassu-atm` — that path is gone on dev, see commit `9c455d6`).
3. **Hardware** - Validate dispense amounts, handle errors gracefully - The `lightning.lightningPub` field on `LightningServices` is a `LightningBackend` *adapter*, not a `LightningPubClient`. Don't try to call LP-only methods on it.
4. **Encryption** - Use NIP-44 for all sensitive data
## Testing Requirements ## Related documentation
- Unit tests for all packages - `docs/machine-installation.md` — Sintra deployment walkthrough (build, flash, provision)
- Integration tests for cross-package interactions - `deploy/nixos/README.md` — NixOS module options, runtime config layout
- E2E tests for full transaction flows - `docs/architecture-comparison.md` — Nostr-native vs traditional lamassu-server
- **Cash-out flow is critical path** (95%+ of activity) - `.claude/skills/*.md` — Custom skills (`/security`, `/nostr-check`, `/test`, etc.)
## Related Documentation External:
- [LNbits nostr-transport docs](~/dev/lnbits/nostr-transport/docs/devs/nostr-transport.md) — wire spec
- `docs/architecture-comparison.md` - Nostr-native vs traditional lamassu-server comparison - [NIP-44 encryption](https://github.com/nostr-protocol/nips/blob/master/44.md)
- `docs/ndebit-cash-in-flow.md` - Technical walkthrough of cash-in implementation - [CLINK Protocol Spec](https://github.com/shocknet/clink) (historical reference)
- `packages/lightning/TROUBLESHOOTING.md` - Lightning.Pub integration gotchas (must read!)
- `.claude/skills/*.md` - Custom skill documentation
## External Resources
- [CLINK Protocol Spec](https://github.com/shocknet/clink)
- [Lightning.Pub](https://github.com/shocknet/Lightning.Pub)
- [NIP-44 Encryption](https://github.com/nostr-protocol/nips/blob/master/44.md)
- [LND Hold Invoices](https://docs.lightning.engineering/lightning-network-tools/lnd/hold-invoices)

216
README.md
View file

@ -1,75 +1,59 @@
# bitSpire # bitSpire
A Nostr-native Lightning ATM system. KYC-free, open source, auditable. A Nostr-native Lightning ATM. KYC-free, open source, auditable. Talks to its Lightning backend over the nostr-native-transport (kind-21000 NIP-44 v2) on a relay — never HTTP — so the kiosk has no admin tokens to leak and no API surface to attack.
> **Considering adopting this approach?** See [Architecture Comparison](docs/architecture-comparison.md) for a detailed comparison with traditional lamassu-server/machine. > Originally `lamassu-next`. Renamed during the LNbits-backend transition on the `dev` branch (commits leading up to 2026-05-13). Production ATMs (`batm3`, `douro`) still run from `main` against Lightning.Pub until cutover; this README describes the `dev` branch state.
## What the ATM actually does
| Flow | Customer side | ATM side |
|------|---------------|----------|
| **Cash-out** (customer pays ATM, gets cash) | scans BOLT11 invoice, pays from any LN wallet | `lnbits.createInvoice()` over nostr → `subscribe_payments({payment_hash})` push fires on settlement → dispense |
| **Cash-in** (customer hands ATM cash, gets sats) | scans LNURL-withdraw QR, redeems with any LN wallet that supports LNURL-w | `lnbits.createWithdrawLink({uses:1})` over nostr → `subscribe_payments({tag:"withdraw", link_id})` push fires when LNbits settles → mark complete |
No HTTP to the Lightning backend. No admin tokens on the kiosk. The ATM's nostr private key *is* its credential — LNbits auto-creates the wallet on first contact via the signature (see [aiolabs/lnbits#9](https://git.atitlan.io/aiolabs/lnbits/issues/9)).
## Prerequisites ## Prerequisites
- [Nix](https://nixos.org/download.html) with flakes enabled - [Nix](https://nixos.org/download.html) with flakes enabled
- [devenv](https://devenv.sh/getting-started/) - [devenv](https://devenv.sh/getting-started/)
- [Docker](https://docs.docker.com/get-docker/) and Docker Compose - Docker + Docker Compose
- [Lightning.Pub](https://github.com/shocknet/Lightning.Pub) cloned to `~/dev/shocknet/Lightning.Pub` - A running LNbits instance with the `nostr-native-transport` branch built in. The local dev compose lives at `~/dev/local/docker/regtest` — that ships an LNbits with the transport pre-enabled and the relevant extensions (`withdraw`, `lnurlp`, `nostrrelay`) installed.
- Regtest environment at `~/dev/local/docker/regtest` (provides bitcoind + LND nodes)
The `nostrrelay` extension inside LNbits is what the ATM connects to — there is no separate strfry/khatru container in the dev compose. The relay URL is `ws://<host>:5001/nostrrelay/test`.
## Quick Start ## Quick Start
```bash ```bash
# 1. Clone the repository # 1. Clone
git clone ssh://forgejo@git.atitlan.io/aiolabs/lamassu-next.git git clone ssh://forgejo@git.atitlan.io/aiolabs/lamassu-next.git
cd bitSpire cd lamassu-next # repo name kept for now — rename to bitSpire is a follow-up
git checkout dev
# 2. Enter the development environment # 2. Enter the dev environment
devenv shell devenv shell
# 3. Install dependencies # 3. Install JS deps
pnpm install pnpm install
# 4. Start regtest infrastructure with auto-funding # 4. Start the regtest stack (bitcoind, LNDs, LNbits with nostr-transport, relay)
./docker/dev.sh up --fund cd ~/dev/local/docker/regtest && ./start-regtest
docker logs regtest-lnbits-1 | grep 'Public key (share this)'
# → copy that pubkey, you'll need it as VITE_LNBITS_SERVER_PUBKEY
# 5. Check status # 5. Configure the machine app for the dev LNbits
./docker/dev.sh status cat > apps/machine/.env <<EOF
VITE_RELAY_URL=ws://localhost:5001/nostrrelay/test
VITE_LNBITS_SERVER_PUBKEY=<paste pubkey from step 4>
VITE_LNBITS_HTTP_URL=http://localhost:5001
VITE_ATM_PRIVATE_KEY=$(openssl rand -hex 32)
EOF
# 6. Run the kiosk in browser dev mode
cd apps/machine && pnpm dev
``` ```
If status shows all services running and ATM funded, you're ready to develop! The kiosk should come up at `http://localhost:5173`, log `[Lightning] LNbits client initialized`, and report a wallet id once LNbits auto-creates one for the ATM's pubkey.
## Development Commands
### dev.sh (Regtest Environment)
```bash
./docker/dev.sh <command> [options]
```
| Command | Description |
| ---------------- | -------------------------------------- |
| `up` | Start regtest + Lightning.Pub + relay |
| `up --fund` | Start and auto-fund ATM with 100k sats |
| `up --machine` | Start and launch ATM app |
| `down` | Stop all services |
| `status` | Show service status and ATM balance |
| `atm` | Launch ATM application (Electron) |
| `zeus` | Show Zeus wallet connection QR code |
| `fund [sats]` | Fund ATM app owner (default: 100000) |
| `mine [blocks]` | Mine regtest blocks (default: 1) |
| `logs [service]` | Follow service logs |
| `reset` | Stop services and clear all state |
### pnpm Scripts
| Command | Description |
| ------------ | ----------------------------------- |
| `pnpm dev` | Start machine app (Vite + Electron) |
| `pnpm build` | Build all packages |
| `pnpm test` | Run unit tests |
### Machine App
```bash
cd apps/machine
pnpm dev # Start Electron app with hot reload
```
## Architecture ## Architecture
@ -78,110 +62,64 @@ bitSpire/
├── apps/ ├── apps/
│ └── machine/ # Electron + Vue 3 ATM kiosk │ └── machine/ # Electron + Vue 3 ATM kiosk
├── packages/ ├── packages/
│ ├── nostr-client/ # Nostr client (NIP-01, NIP-42, NIP-44) │ ├── nostr-client/ # NIP-01 relay client, NIP-44 v2 encryption
│ ├── clink/ # CLINK protocol (ndebit, noffer) │ ├── lnbits/ # LnbitsClient — talks to LNbits over kind-21000 transport
│ ├── lightning/ # Lightning.Pub RPC client │ ├── clink/ # CLINK protocol (kind-21001/2/3) — still in tree as a
│ ├── hal/ # Hardware Abstraction Layer (bill validators, dispensers) │ │ # reference; unused on dev since the LNbits backend
│ ├── state-machine/ # XState v5 ATM state machine │ │ # does cash-in via LNURL-withdraw and cash-out via
│ │ # BOLT11, both of which subsume CLINK's role
│ ├── hal/ # Hardware abstraction (JCM iVIZION validator, F56 dispenser)
│ ├── state-machine/ # XState v5 state machine driving cash-out + cash-in
│ ├── cashu/ # Cashu ecash (placeholder) │ ├── cashu/ # Cashu ecash (placeholder)
│ └── ui-shared/ # Shared Vue components (placeholder) │ └── ui-shared/ # Shared Vue components (placeholder)
└── docker/ # Development infrastructure (dev.sh, regtest) └── deploy/nixos/ # NixOS module + provisioning script for Sintra/tejo/douro/batm3
``` ```
## Infrastructure Services `packages/lightning/` (the Lightning.Pub RPC client) was removed on `dev` — see `git log packages/lightning` on `main` for the historical sources.
The development environment uses a shared regtest stack from `~/dev/local/docker/regtest/`: ## Deploying to real hardware
| Service | Port | Description | The Sintra/tejo/douro/batm3 disk-image pipeline lives in `flake.nix` + `deploy/nixos/`. See `deploy/nixos/README.md` for the full flow; the abbreviated path:
| ------------------ | ----- | --------------------------------- |
| strfry | 7777 | Nostr relay (ws://localhost:7777) |
| bitcoind | 18443 | Bitcoin regtest node (shared) |
| lnd-1 | 10001 | Hub node (funds lnd-3 and lnd-4) |
| lnd-3 | 10003 | Payment source for ATM funding |
| lnd-4 | 10004 | Lightning.Pub's backend node |
| Lightning.Pub | 1776 | Nostr-native accounts API |
| Withdraw Extension | 1777 | LNURL-withdraw for cash-in |
| PostgreSQL | 5432 | Lightning.Pub database |
**Automatic channel setup**: When `dev.sh up` runs, it automatically:
1. Ensures lnd-1 has funds (mines blocks if needed)
2. Opens a channel from lnd-1 → lnd-4 (so Lightning.Pub can receive payments)
3. Opens a channel from lnd-1 → lnd-3 (so lnd-3 can pay invoices for funding)
4. Mines blocks for channel confirmation and graph propagation
This makes the environment work from a clean Docker slate without manual intervention.
## Testing Cash-In Flow
After starting the environment with `./docker/dev.sh up --fund`:
```bash ```bash
# Start the ATM app # Build the disk image for a Sintra
cd apps/machine nix build .#disk-image-sintra
pnpm dev # → result/nixos.img
# Flash to a USB stick
sudo dd if=result/nixos.img of=/dev/sdX bs=4M status=progress conv=fsync && sync
# Boot Sintra from the USB, dd onto the eMMC from inside Alpine live (see
# deploy/nixos/README.md), then provision the .env from the dev box:
bash deploy/nixos/provision-atm.sh <sintra-lan-ip>
``` ```
1. Click "Cash In" on the ATM idle screen The auto-upgrade timer (`flake.nix:152-160`) pulls `dev` daily at 04:00, so the Sintra stays in sync with whatever's on the `dev` branch. Production ATMs run from `main` and are unaffected.
2. Insert simulated bills (dev mode)
3. Click "Done Inserting"
4. Choose payment method:
- **CLINK** - Scan with Shock Wallet (ndebit protocol)
- **LNURL** - Scan with any Lightning wallet (Phoenix, Zeus, etc.)
5. Claim the withdrawal in your wallet
## Troubleshooting
### Common Issues
**Services won't start:**
```bash
./docker/dev.sh reset
./docker/dev.sh up --fund
```
**ATM not funded / "not enough balance":**
```bash
./docker/dev.sh fund 100000
```
**Port already in use:**
```bash
./docker/dev.sh down
# Kill any orphan processes on ports 1776, 1777, 7777
./docker/dev.sh up
```
**LNURL-withdraw fails:**
- Ensure withdraw extension is running (`./docker/dev.sh status`)
- Check ATM app has valid `VITE_APP_ID` in `apps/machine/.env`
- Verify app owner is funded (not just app_user balance)
## Key Concepts
- **CLINK**: Protocol for Lightning payments over Nostr (ndebit, noffer)
- **ndebit**: Customer-initiated debit authorization (cash-in primary method)
- **LNURL-withdraw**: Standard Lightning withdrawal link (cash-in alternative)
- **Lightning.Pub**: Nostr-native account system wrapping LND
- **NIP-44**: Encryption standard for private Nostr messages
## Documentation ## Documentation
| Document | Description | | Document | Description |
| ---------------------------------------------------------- | ----------------------------------------------- | |----------|-------------|
| [Architecture Comparison](docs/architecture-comparison.md) | Nostr-native vs traditional lamassu-server | | [docs/machine-installation.md](docs/machine-installation.md) | Step-by-step Sintra install: build, flash, provision |
| [ndebit Cash-In Flow](docs/ndebit-cash-in-flow.md) | Technical walkthrough of cash-in implementation | | [deploy/nixos/README.md](deploy/nixos/README.md) | NixOS module options, runtime config layout, hardware variants |
| [Troubleshooting](packages/lightning/TROUBLESHOOTING.md) | Lightning.Pub integration issues and solutions | | [docs/architecture-comparison.md](docs/architecture-comparison.md) | Nostr-native ATM vs traditional lamassu-server |
| [CLAUDE.md](CLAUDE.md) | Development guidelines and code style | | [docs/device-configuration.md](docs/device-configuration.md) | Validator/dispenser hardware configuration |
| [docs/business-model.md](docs/business-model.md) | Deployment economics |
| [docs/adr/001-hal-architecture.md](docs/adr/001-hal-architecture.md) | HAL design decision record |
| [docs/clink-protocol.md](docs/clink-protocol.md) | CLINK protocol reference — historical, no longer wired on dev |
| [docs/ndebit-cash-in-flow.md](docs/ndebit-cash-in-flow.md) | Pre-LNbits cash-in flow — historical, replaced by LNURL-withdraw + subscribe_payments push |
| [CLAUDE.md](CLAUDE.md) | Development guidelines and code style (read this if using Claude Code) |
## Contributing ## Contributing
See `CLAUDE.md` for development guidelines and code style. See `CLAUDE.md` for development guidelines, package layout conventions, and code style.
## Acknowledgements
bitSpire's hardware drivers (JCM iVIZION / ID003, MEI EBDS, Fujitsu F56, etc.) and the cash-flow state machine derive from prior art first published as open source by Lamassu Industries AG in the [`lamassu-machine`](https://github.com/lamassu/lamassu-machine) and [`lamassu-server`](https://github.com/lamassu/lamassu-server) repositories, up to and including the **v8.1.5** release line — the last published under a fully-open license. bitSpire wouldn't exist without that foundation, and we're grateful for the years of operational hardening that went into it.
Lamassu Industries AG [transitioned to a proprietary, source-available license](https://blog.lamassu.is/updates-to-our-lamassu-software-license/) on 2024-01-26, with v8.1.6 and subsequent releases gated behind a paid Operator Support Agreement. **bitSpire incorporates no code from v8.1.6 or later**, is an independent project, and is not affiliated with or endorsed by Lamassu Industries AG.
## License ## License
MIT AGPL-3.0 (matches LNbits, which we link against).