diff --git a/deploy/nixos/README.md b/deploy/nixos/README.md index c8ba580..8800c7b 100644 --- a/deploy/nixos/README.md +++ b/deploy/nixos/README.md @@ -1,195 +1,265 @@ # bitSpire NixOS Deployment -NixOS configuration for deploying Lamassu Next ATM software on UP Board hardware. +NixOS module + tooling for deploying bitSpire to ATM hardware (Sintra, tejo, douro, batm3). The deploy pipeline produces a dd-able raw disk image; you flash it onto the ATM's internal storage and provision the runtime `.env` with LNbits credentials via SSH. -## Quick Start +> The production cutover lives on `main`; this README documents `dev` reality. Production ATMs (`batm3`, `douro`) still run from `main` with the Lightning.Pub backend until cutover. -### 1. Build Installation ISO +## File layout -```bash -cd deploy/nixos -nix build .#iso +``` +deploy/nixos/ +├── bitspire-atm.nix # NixOS module: services.bitspire option tree + systemd unit + udev rules +├── configuration.nix # Base system (NixOS 24.05, locale, kernel, packages, lamassu user) +├── live.nix # Live USB variant (squashfs + tmpfs root) — used by mkLiveConfig +├── hardware/ +│ ├── douro.nix # Dell OptiPlex 9030 AIO (batm3 sibling; SATA SSD, eGalax touch) +│ ├── batm3.nix # GeneralBytes BATM3 (Dell board, WireGuard wired in) +│ └── upboard.nix # Aaeon UP Board (Sintra + tejo; eMMC root via sdhci-acpi + mmc_block) +├── udev/ +│ └── 99-lamassu-hardware.rules # additional udev rules (loaded via configuration.nix) +├── provision-atm.sh # Push LNbits credentials to a deployed ATM via SSH +├── atm-transactions.sh # Operator query tool — reads /var/lib/bitspire/state.db +├── flash-douro-usb.sh # Helper for flashing a douro live USB +└── build-iso.sh # Convenience wrapper for `nix build .#iso-` ``` -The ISO will be in `result/iso/`. +## Build pipeline -### 2. Install on UP Board +Each ATM model has two flake outputs: -1. Write ISO to USB drive: +| Output (flake.nix) | Type | Purpose | +|---|---|---| +| `nixosConfigurations.` | live | squashfs live USB, tmpfs root, no persistence — for first-boot testing | +| `nixosConfigurations.-installed` | installed | full GPT + systemd-boot install, ext4 root, supports `nixos-rebuild switch` | +| `packages.x86_64-linux.iso-` | ISO | ISO image of the live variant | +| `packages.x86_64-linux.disk-image-` | raw image | dd-able full disk image of the installed variant | - ```bash - sudo dd if=result/iso/*.iso of=/dev/sdX bs=4M status=progress - ``` - -2. Boot UP Board from USB - -3. Run the installer: - - ```bash - sudo nixos-install --flake .#lamassu-atm - ``` - -4. Reboot and remove USB - -### 3. Post-Install Configuration - -SSH into the machine and configure: +Models: `douro`, `tejo`, `sintra`, `batm3`. ```bash -# Set up the ATM application -sudo mkdir -p /opt/lamassu-atm -sudo chown lamassu:lamassu /opt/lamassu-atm +# Build a Sintra disk image +nix build .#disk-image-sintra +# → result/nixos.img (~8.5 GB sparse, 6 GB actual, GPT-partitioned) -# Copy the built Electron app -scp -r apps/machine/dist/* lamassu@:/opt/lamassu-atm/ - -# Configure the ATM -sudo nano /etc/lamassu-atm/config.env +# Build a live ISO for tejo +nix build .#iso-tejo ``` -## Configuration Options +## Deploying to Sintra (full walkthrough) -Edit `/etc/nixos/configuration.nix` to customize: +The Sintra ships from the factory with whatever its previous OS was — Android, vendor Linux, or wiped. You need an Alpine live USB to act as your installer. + +### 1. Prep a flashing USB stick + +On your dev box: + +```bash +nix build .#disk-image-sintra +sudo dd if=result/nixos.img of=/dev/sdX bs=4M status=progress conv=fsync && sync +``` + +`/dev/sdX` is the USB stick you'll carry to the Sintra. **Verify the target is the stick, not your workstation's main disk** — `lsblk -f /dev/sdX` should show `Flash Drive` or similar. + +### 2. Boot Sintra into Alpine live + +You need a separate USB stick with a minimal Alpine ISO. Boot it on the Sintra, get to a shell, bring up networking: + +```sh +ip link set eth0 up +udhcpc -i eth0 +ver=$(cat /etc/alpine-release | cut -d. -f1-2) +cat > /etc/apk/repositories < 2>&1 | \ + grep -oP 'Public key \(share this\):\s*\K[a-f0-9]{64}' | tail -1) + +LNBITS_HTTP_URL=http://:5001 \ +RELAY_URL=ws://:5001/nostrrelay/test \ +LNBITS_SERVER_PUBKEY="$LNBITS_SERVER_PUBKEY" \ +ATM_PRIVATE_KEY=$(openssl rand -hex 32) \ +bash deploy/nixos/provision-atm.sh 22 +``` + +The script SSHes to `lamassu@:22`, writes `/var/lib/bitspire/.env`, and restarts `bitspire.service`. After a few seconds the kiosk should connect to LNbits over nostr-transport and show the live UI. + +> **Save the generated `ATM_PRIVATE_KEY`.** LNbits identifies this ATM by its public key; if you regenerate the key on a re-provision, LNbits will auto-create a fresh wallet and the old wallet's balance becomes inaccessible. + +## Auto-upgrade behaviour + +The `dev`-branch `flake.nix` pins the auto-upgrade source to `?ref=dev` so any ATM flashed from `dev` stays on `dev`: + +```nix +system.autoUpgrade = { + enable = true; + flake = "git+ssh://forgejo@git.atitlan.io/aiolabs/lamassu-next.git?ref=dev#${machineModel}-installed"; + dates = "04:00"; + allowReboot = false; +}; +``` + +Daily at 04:00 local time the ATM runs `nixos-rebuild switch` against the latest commit on `dev`. If the build fails (e.g., binary cache miss + local kernel compile failure), the existing system keeps running; nothing destructive happens. `allowReboot = false` means a kernel update lands but doesn't take effect until the next operator-initiated reboot. + +Production ATMs on `main` continue to read `main`'s flake (no `?ref=` pin → resolves to the repo default branch), so they keep pulling `main` and stay on the LP backend. + +## Quick reference — runtime layout on a deployed ATM + +| Path | Owner | Purpose | +|------|-------|---------| +| `/var/lib/bitspire/` | lamassu:lamassu, 0750 | Service data directory | +| `/var/lib/bitspire/.env` | lamassu:lamassu, 0600 | Runtime config — `VITE_RELAY_URL`, `VITE_LNBITS_SERVER_PUBKEY`, `VITE_LNBITS_HTTP_URL`, `VITE_ATM_PRIVATE_KEY`, … | +| `/var/lib/bitspire/state.db` | lamassu:lamassu | SQLite — cassette inventory, cashbox state, transaction history | +| `/var/lib/bitspire/logs/` | lamassu:lamassu, 0750 | Service logs (if app writes them) | +| `/opt/bitspire/` | lamassu:lamassu | Optional override drop for app assets (mostly unused — app comes from `/nix/store`) | +| `/etc/bitspire/config.env` | root:root | Static config emitted by the NixOS module (RELAY_URL, LNBITS_HTTP_URL — informational; the renderer reads `/var/lib/bitspire/.env` instead) | + +## Common operations + +### Service status + +```bash +sudo systemctl status bitspire +sudo journalctl -u bitspire -f +sudo journalctl -u bitspire --since "5 minutes ago" | grep -E '\[' # filter to renderer logs +``` + +### Re-provision (rotate credentials, fix wrong relay URL, etc.) + +Re-run `provision-atm.sh` with new env vars. The script overwrites `/var/lib/bitspire/.env` and restarts the service. + +### Push code without re-flashing + +From the dev box: + +```bash +nixos-rebuild switch --flake .#sintra-installed \ + --target-host lamassu@ --use-remote-sudo +``` + +Locally builds the new closure (binary-cache where possible), copies it to the ATM over SSH, activates the new generation. A kernel-or-initrd change still requires a reboot to take effect — `sudo systemctl reboot` over SSH afterwards. + +### Inspect transactions + +```bash +sudo bash /etc/nixos/atm-transactions.sh +# → reads /var/lib/bitspire/state.db and prints recent transactions +``` + +### Check hardware-side health + +```bash +# udev symlinks expected by upboard.nix +ls -la /dev/ttyJ4 /dev/ttyJ5 /dev/ttyJ7 + +# Real UART layout +sudo dmesg | grep -E 'ttyS[0-9]' + +# All USB serial bridges +ls -la /dev/serial/by-id/ +``` + +## NixOS module reference + +`services.bitspire` options (defined in `bitspire-atm.nix`): ```nix { services.bitspire = { - enable = true; + enable = true; + relayUrl = "wss://relay.aiolabs.dev"; # ATM ↔ LNbits relay + lnbitsServerPubkey = "<64-hex>"; # LNbits transport pubkey + lnbitsHttpUrl = "https://lnbits.aiolabs.dev"; # LNURL callback URL prefix + appDir = "/opt/bitspire"; # rarely overridden — defaults via flake + dataDir = "/var/lib/bitspire"; # rarely overridden + logLevel = "info"; # error | warn | info | debug - # Nostr relay both the ATM and LNbits subscribe on - relayUrl = "wss://relay.aiolabs.dev"; - - # LNbits nostr-transport server pubkey (hex, 64 chars) - lnbitsServerPubkey = ""; - - # LNbits HTTP origin — only used to compose LNURL-withdraw callback URLs - lnbitsHttpUrl = "https://lnbits.aiolabs.dev"; - - # Hardware configuration billValidator = { - enable = true; - device = "/dev/ttyUSB0"; - type = "id003"; # or "mei", "ccnet" + enable = true; + device = "/dev/ttyJ5"; # symlink emitted by upboard.nix udev rules + type = "id003"; # id003 | mei | ccnet }; billDispenser = { - enable = false; # Enable for two-way machines - device = "/dev/ttyUSB1"; - type = "puloon"; + enable = true; + device = "/dev/ttyJ7"; # symlink for the SoC MMIO UART on Sintra (was ttyS4) + type = "f56"; # puloon | genmega | f56 }; camera = { - enable = true; - device = "/dev/video0"; + enable = true; + device = "/dev/video0"; }; }; } ``` -## Hardware Support +Most of these are set automatically by the `mkInstalledConfig` helper in the root `flake.nix`. You typically only override them in a one-off NixOS config or for hardware different from what `flake.nix` knows about. -### Bill Validators +## Hardware support -- **ID-003** (JCM) - Most common in Lamassu machines -- **MEI** (Mars Electronics) -- **CCNET** (CashCode) +| Component | Drivers in `packages/hal/` | Tested on | +|---|---|---| +| Bill validator | id003, ccnet, cashflow_sc, bnr_advance, genmega, hcm2, gsr50 | iVIZION (id003) on Sintra | +| Bill dispenser | puloon, f56, genmega, hcm2, gsr50 | Fujitsu F56 on Sintra | +| Camera | any V4L2 USB device | Z-Star 0ac8:0345 on Sintra | +| Touchscreen | any libinput-compatible device | ILI 222a:0001 on Sintra, eGalax on douro | -### Bill Dispensers +### Sintra-specific gotchas -- **Puloon** - LCDM series -- **Genmega** +- **eMMC controller is ACPI-enumerated.** `upboard.nix` force-loads `sdhci-acpi` and `mmc_block` in `initrd.kernelModules` so root-by-label resolves in stage 1. +- **Dispenser is on `ttyS4`, the SoC MMIO UART.** This is the only on-carrier RS-232 besides the legacy 8250 at `ttyS0`. The kernel console must **not** be routed through `ttyS4` (use `console=tty0` only); routing it through `ttyS4` blocks userspace from opening the port for HAL. +- **`ttyS1`, `ttyS2`, `ttyS3` are placeholder kernel nodes.** Opening them returns `EIO`. Only `ttyS0` (legacy 16550A at I/O 0x3f8) and `ttyS4` (MMIO 16550A at 0xa171b000) are real UARTs on this hardware. -### Cameras +## Security notes -- Any V4L2-compatible USB camera - -## File Structure - -``` -deploy/nixos/ -├── flake.nix # Nix flake entry point -├── configuration.nix # Base system configuration -├── lamassu-atm.nix # ATM service module -├── hardware/ -│ └── upboard.nix # UP Board hardware config -├── udev/ -│ └── 99-lamassu-hardware.rules # Hardware device rules -└── README.md # This file -``` - -## Troubleshooting - -### Check ATM service status - -```bash -sudo systemctl status lamassu-atm -sudo journalctl -u lamassu-atm -f -``` - -### Check hardware detection - -```bash -# List serial devices -ls -la /dev/ttyUSB* /dev/ttyACM* - -# Check for bill validator symlink -ls -la /dev/bill-validator - -# Test camera -v4l2-ctl --list-devices -``` - -### Manual service control - -```bash -sudo systemctl restart lamassu-atm -sudo systemctl stop lamassu-atm -``` - -### Debug mode - -```bash -# Run manually with verbose output -sudo -u lamassu DISPLAY=:0 LOG_LEVEL=debug electron /opt/lamassu-atm -``` - -## Updating - -### Update system - -```bash -sudo nixos-rebuild switch --flake /etc/nixos#lamassu-atm -``` - -### Update ATM application - -```bash -# Build new version -cd bitSpire/apps/machine -pnpm run build - -# Copy to ATM -scp -r dist/* lamassu@:/opt/lamassu-atm/ - -# Restart service -ssh lamassu@ "sudo systemctl restart lamassu-atm" -``` - -## Development vs Production - -For development/testing, you can use the regtest docker environment: - -```bash -cd bitSpire -./docker/dev.sh up --fund -./docker/dev.sh atm -``` - -For production, deploy this NixOS configuration and point to your production Nostr relay and Lightning.Pub instance. - -## Security Notes - -- The default `lamassu` user has `wheel` access for initial setup -- Remove wheel access after configuration: `sudo gpasswd -d lamassu wheel` -- SSH is enabled by default - configure key-based auth and disable password auth -- Firewall blocks all incoming connections by default +- **`lamassu` user has `wheel`/passwordless-sudo** to allow remote `nixos-rebuild switch` via `--use-remote-sudo`. This is acceptable for a kiosk on a network you control. Remove `security.sudo.wheelNeedsPassword = false` if you want to require a password. +- **SSH password auth is enabled by default** to allow initial provisioning. Once you've baked your dev box's pubkey into `/home/lamassu/.ssh/authorized_keys`, you can disable password auth: `services.openssh.settings.PasswordAuthentication = false`. +- **`/var/lib/bitspire/.env` contains the ATM's nostr private key.** It's mode 0600, owned by `lamassu:lamassu`. Don't `scp` it off the device; if you need to rotate the key, generate fresh and re-provision. +- **No firewall is configured by default.** The ATM is meant to be on an operator-controlled network. If you expose it to a wider network, add a `networking.firewall` rule set restricting inbound to SSH from the operator's IPs only. diff --git a/docs/clink-protocol.md b/docs/clink-protocol.md index abed809..f7b59c8 100644 --- a/docs/clink-protocol.md +++ b/docs/clink-protocol.md @@ -1,5 +1,28 @@ # CLINK Protocol +> **Status on the `dev` branch: dormant.** The `@bitSpire/clink` package +> is still in the workspace, but `apps/machine/src/services/lightning.ts` +> no longer wires its offer/debit/management handlers — those were +> Lightning.Pub-paired and were removed when the backend switched to +> LNbits over the nostr-native-transport. The cash-out flow on dev uses +> BOLT11 + `subscribe_payments({payment_hash})`; the cash-in flow uses +> LNURL-withdraw + `subscribe_payments({tag, link_id})`. Both subsume +> CLINK's role for the ATM use case. +> +> This doc remains as the **protocol reference** — the wire format, +> event kinds, and encoding conventions are unchanged from the upstream +> [CLINK spec](https://github.com/shocknet/clink). Read it if you want +> to understand what kinds 21001-21003 mean, or if you're considering +> re-introducing CLINK flows on dev (e.g., a nostr-native cash-in path +> that doesn't go through LNURL). +> +> The `kind-21003` management surface (operator commands like "manual +> dispense") is the one piece of CLINK still actively wired on dev — +> `clink.onManagement` listens for those events and they have no LP +> dependency, so they survived the migration. + +--- + CLINK (Custodial Lightning Keys) is a Nostr-based protocol for Lightning payments. It enables wallets and services to communicate payment requests and authorizations through Nostr relays. ## Overview diff --git a/docs/ndebit-cash-in-flow.md b/docs/ndebit-cash-in-flow.md index d5c990d..e6b9688 100644 --- a/docs/ndebit-cash-in-flow.md +++ b/docs/ndebit-cash-in-flow.md @@ -1,5 +1,24 @@ # NDebit Cash-In Flow Implementation Guide +> **Historical reference — NOT the cash-in flow on the `dev` branch.** +> +> Cash-in on `dev` is **LNURL-withdraw + LNbits `subscribe_payments({tag:"withdraw", link_id})` push**, implemented in +> `apps/machine/src/services/lightning.ts → generateLnurlWithdraw()`. +> The ATM no longer renders ndebit URIs; `CashInView.vue` ignores +> `generateNdebit`'s output and shows the LNURL QR instead. The CLINK +> debit-approval listener and all kind-21002 handling were removed in +> commit `3c14eea` (the 3b.4 cleanup of LP-paired infrastructure). +> +> This doc is retained because it explains *why* the previous flow +> existed and what the ndebit/CLINK protocol surface looks like — useful +> if the project ever wants to reintroduce nostr-native cash-in that +> bypasses LNURL. The protocol itself (kinds 21001-21003) is unchanged; +> only our wiring of it has been removed. The `@bitSpire/clink` package +> still ships the encode/decode helpers if a future implementation needs +> them. + +--- + This document describes how to implement the ndebit scanning flow for ATM cash-in, where a user scans an ndebit QR code from an ATM to withdraw sats to their wallet. ## Overview