diff --git a/CLAUDE.md b/CLAUDE.md index e4940b7..1f3dc24 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,352 +1,213 @@ # 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 -**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 -- **Lightning-Native**: Security encapsulated in Lightning protocol -- **Nostr as Infrastructure**: Relay for communication, keypairs for identity -- **Open Source First**: Every component auditable and forkable +Core principles: + +- **KYC-free** — no identity collection, no compliance theater +- **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 ``` bitSpire/ ├── apps/ -│ ├── machine/ # Electron + Vue 3 ATM kiosk application ✅ -│ ├── dashboard/ # Vue 3 operator dashboard (planned) -│ └── relay/ # strfry relay configuration (planned) +│ └── machine/ # Electron + Vue 3 ATM kiosk ├── packages/ -│ ├── hal/ # TypeScript Hardware Abstraction Layer ✅ -│ ├── nostr-client/ # Nostr client library ✅ -│ ├── clink/ # CLINK protocol implementation ✅ -│ ├── state-machine/ # XState v5 ATM state machine ✅ -│ ├── lightning/ # Lightning.Pub RPC client ✅ -│ ├── cashu/ # Cashu ecash (placeholder) -│ └── ui-shared/ # Shared Vue components (placeholder) -└── docker/ # Development infrastructure ✅ +│ ├── nostr-client/ # NIP-01 client, NIP-44 v2 (encryptContentV2/decryptContentV2) +│ ├── lnbits/ # LnbitsClient — kind-21000 RPC over nostr-transport +│ ├── clink/ # CLINK protocol (kinds 21001-21003) — IN TREE BUT UNUSED ON DEV +│ ├── state-machine/ # XState v5 ATM state machine +│ ├── hal/ # Hardware abstraction (JCM iVIZION, Fujitsu F56, etc.) +│ ├── cashu/ # placeholder +│ └── ui-shared/ # placeholder +└── 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 | -| ------------------------ | ------------------------------------------------------ | ----- | -| `@lamassu/nostr-client` | Nostr relay client with NIP-42 auth, NIP-44 encryption | 13 | -| `@lamassu/clink` | CLINK protocol (kinds 21001-21003), noffer encoding | 7 | -| `@lamassu/lightning` | Lightning.Pub RPC client (kind 21000) | 10 | -| `@lamassu/state-machine` | XState v5 ATM state machine (idle, cashIn, cashOut) | 14 | -| `@lamassu/hal` | Hardware drivers (ID003 validator, F56 dispenser) | - | +| Package | Scope | Notes | +|---|---|---| +| `@bitSpire/nostr-client` | active | NIP-01 + NIP-44 v2 helpers. Used by every other package. | +| `@bitSpire/lnbits` | active | Wraps the nostr-native-transport API (10 core RPCs + lnurlw/lnurlp + subscribe_payments). Mirror peer of the old `@bitSpire/lightning`. | +| `@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). | +| `@bitSpire/hal` | active | TypeScript HAL. JCM iVIZION + Fujitsu F56 drivers. | +| `@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 | -| -------------------- | --------------------------------- | -| `@lamassu/cashu` | Cashu ecash for offline operation | -| `@lamassu/ui-shared` | Shared Vue 3 components | +`apps/machine/src/services/lightning.ts` is the central file. After the 3d cutover it: -### 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 ```bash -# Enter development environment -devenv shell +# Inside 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 -pnpm dev - -# 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 +# In apps/machine specifically +cd apps/machine && pnpm build # full prod build: vue-tsc + vite build + electron tsc + fund-atm esbuild bundle ``` -## Development Infrastructure - -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 +Disk image build (for Sintra/douro/etc.): ```bash -devenv shell # Enter dev environment -infra-up # Start all services (30-60s first run) -mine-blocks 101 # Fund the regtest wallet -setup-channel # Open channel between Alice and LND +nix build .#disk-image-sintra # → result/nixos.img ``` -### 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 -2. **Alice** (`lamassu-lnd-alice`) - Used to pay invoices (simulates external payers) +## Wire envelope (kind 21000 RPC) -```bash -# 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"}' +Plaintext JSON, encrypted as NIP-44 v2 inside the event `content`: -# Create user and invoice -curl -X POST "http://localhost:1776/api/app/user/add" -H "Authorization: Bearer $APP_TOKEN" \ - -d '{"identifier": "test-user", "balance": 0}' +```jsonc +// Request (client → server) +{ + "rpc_name": "create_invoice", + "request_id": "create_invoice-7-ab12cd", + "wallet_id": "", + "body": { "amount": 1000, "memo": "...", "unit": "sat" } +} -curl -X POST "http://localhost:1776/api/app/user/add/invoice" -H "Authorization: Bearer $APP_TOKEN" \ - -d '{"receiver_identifier": "test-user", "payer_identifier": "external", "http_callback_url": "", "invoice_req": {"amountSats": 1000, "memo": "Test"}}' +// Reply (server → client, one-shot) +{ + "status": "OK", + "request_id": "create_invoice-7-ab12cd", + "data": { /* Payment object */ } +} -# Pay from Alice -alice-pay +// Subscription push (server → client, repeated) +{ + "status": "OK", + "request_id": "sub-9-ef34gh", + "subscription_id": "", + "data": { "payment": { /* Payment */ } } +} + +// Subscription close (server → client, terminal) +{ + "status": "OK", + "request_id": "sub-9-ef34gh", + "subscription_id": "", + "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 | -| ------------------ | -------------------------------------------------------------- | -| 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) | +TypeScript drivers in `packages/hal/`. Coverage by device class: -```bash -# 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 | -| ---------- | ------------------------------------------------------------ | +| Category | Drivers | +|---|---| | Validators | id003, ccnet, cashflow_sc, bnr_advance, genmega, hcm2, gsr50 | -| Dispensers | puloon, f56, genmega, hcm2, gsr50 | -| Recyclers | MEI SCR (planned — hardware exists in BATM3, no driver yet) | -| Printers | nippon, zebra, genmega | +| Dispensers | puloon, f56, genmega, hcm2, gsr50 | +| Recyclers | MEI SCR (planned — hardware in BATM3, no driver yet) | +| 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 | -| ----------------- | ---------- | ---------------------------------- | ------------------------------------ | -| 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 | +### Initrd modules for eMMC boot -**Current status**: MEI used as validator-only (`cashflowSc` driver), F56 for dispensing. -The MEI's recycler cassettes are untapped — accepted bills go to cashbox, not recycled. +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. -When porting: +## Code style -1. Read JS driver thoroughly -2. Document protocol from JS code -3. Implement TypeScript version -4. Test against same hardware -5. Use `/hal-check port ` to validate +- TypeScript: ESM only, strict mode + `strictNullChecks` + `noUncheckedIndexedAccess`. No `any`. Zod for runtime validation at boundaries. +- Vue 3: Composition API, `