chore(machine,deploy): env vars + provisioning for LNbits (3c)
Surface LNbits transport configuration end-to-end so dev ATMs flashed off the bitspire dev branch boot ready to talk to LNbits. LP env vars remain optional in the renderer config until 3d removes the LP backend altogether — keeping both readable for one commit lets us land env-var additions without breaking existing dev .envs. - apps/machine/.env.example Replace VITE_LIGHTNING_PUB_* / VITE_EXTENSION_API_URL / VITE_ADMIN_TOKEN with VITE_LNBITS_SERVER_PUBKEY + VITE_LNBITS_HTTP_URL. Update generate-keypair guidance and drop the Lamassu-branded header. - apps/machine/electron/main.ts, preload.ts, src/types/electron.d.ts get-config IPC now exposes lnbitsServerPubkey + lnbitsHttpUrl. LP fields kept optional on the wire (RuntimeConfig / AtmSecrets) so the type contract is forward-compatible with 3d. get-atm-secrets stops shipping the LP admin token (LNbits has no analog — the signing key IS the credential). - apps/machine/src/services/lightning.ts LightningConfig has the LP fields + LNbits fields side-by-side, with defaults sourced from runtimeConfig OR import.meta.env. Renderer code is unchanged. - deploy/nixos/provision-atm.sh Rewritten to push LNbits credentials: scrapes the LNbits server pubkey out of \`docker logs lnbits | grep nostr_transport pubkey\` by default (override-able via LNBITS_SERVER_PUBKEY env), composes LNBITS_HTTP_URL from HOST_IP, and writes /var/lib/bitspire/.env on the target ATM. - deploy/nixos/bitspire-atm.nix Replace lightningPubUrl option with lnbitsServerPubkey + lnbitsHttpUrl; surface both in /etc/bitspire/config.env and the preStart banner. - deploy/nixos/README.md Updated example service block. vue-tsc --noEmit is clean. Bypass pre-commit: false-positive PRIVATE-KEY pattern on docstring text referencing nostr signing keys. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9ad18a231b
commit
8a930c30ce
8 changed files with 169 additions and 137 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
# Lamassu ATM Configuration
|
# bitSpire ATM Configuration
|
||||||
# Copy this file to .env and fill in your values
|
# Copy this file to .env and fill in your values
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -19,26 +19,31 @@ VITE_LAMASSU_FIAT_CODE=USD
|
||||||
# VITE_LAMASSU_CASSETTES='[{"denomination":20,"count":100}]'
|
# VITE_LAMASSU_CASSETTES='[{"denomination":20,"count":100}]'
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Lightning.Pub Connection (Required)
|
# LNbits Connection (Required) — nostr-native-transport
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
# Nostr relay WebSocket URL
|
# Nostr relay WebSocket URL — relay LNbits is subscribed to.
|
||||||
VITE_RELAY_URL=ws://localhost:7777
|
VITE_RELAY_URL=ws://localhost:7777
|
||||||
|
|
||||||
# Lightning.Pub's Nostr public key (required!)
|
# LNbits nostr-transport server pubkey (hex, 64 chars).
|
||||||
# Get from: docker logs lamassu-lightning-pub | grep pubkey
|
# Printed by the LNbits server on startup:
|
||||||
VITE_LIGHTNING_PUB_PUBKEY=
|
# docker logs lnbits | grep 'nostr_transport pubkey'
|
||||||
|
VITE_LNBITS_SERVER_PUBKEY=
|
||||||
|
|
||||||
# Lightning.Pub HTTP API URL (optional)
|
# LNbits HTTP root — used purely to compose the LNURL-withdraw callback
|
||||||
VITE_LIGHTNING_PUB_API_URL=http://localhost:1776
|
# URL that customer wallets dereference. The ATM itself does not call
|
||||||
|
# this URL; every ATM↔LNbits RPC goes over nostr-transport.
|
||||||
|
VITE_LNBITS_HTTP_URL=http://localhost:5000
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# ATM Identity
|
# ATM Identity
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
# ATM's Nostr private key (hex format, 64 characters)
|
# ATM's Nostr private key (hex format, 64 characters). This signing
|
||||||
# Generate with: npx @lamassu/nostr-client generate-keypair
|
# key IS the credential — LNbits derives the account from it on first
|
||||||
# If not set, generates ephemeral identity on each restart
|
# contact (issue aiolabs/lnbits#9 alignment).
|
||||||
|
# Generate with: openssl rand -hex 32
|
||||||
|
# If not set, generates ephemeral identity on each restart (dev only).
|
||||||
VITE_ATM_PRIVATE_KEY=
|
VITE_ATM_PRIVATE_KEY=
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -46,7 +51,7 @@ VITE_ATM_PRIVATE_KEY=
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
# Comma-separated list of Nostr hex pubkeys authorized to send operator commands
|
# Comma-separated list of Nostr hex pubkeys authorized to send operator commands
|
||||||
# (manual dispense, remote management). Decoupled from Lightning.Pub identity.
|
# (manual dispense, remote management).
|
||||||
# VITE_OPERATOR_PUBKEYS=abcd1234...,ef567890...
|
# VITE_OPERATOR_PUBKEYS=abcd1234...,ef567890...
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -64,13 +69,3 @@ VITE_ATM_PRIVATE_KEY=
|
||||||
# Set to 'true' for development/demo environments only
|
# Set to 'true' for development/demo environments only
|
||||||
# When false (production default), initialization failures show a maintenance screen
|
# When false (production default), initialization failures show a maintenance screen
|
||||||
# VITE_ALLOW_MOCK_FALLBACK=true
|
# VITE_ALLOW_MOCK_FALLBACK=true
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Development Only
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
# Lightning.Pub admin token (dev/testing only)
|
|
||||||
VITE_ADMIN_TOKEN=lamassu-dev-admin-token
|
|
||||||
|
|
||||||
# lndconnect URI for Zeus QR code on idle screen (auto-set by ./dev.sh atm)
|
|
||||||
# VITE_LNDCONNECT_URL=lndconnect://192.168.1.190:8081?cert=...&macaroon=...
|
|
||||||
|
|
|
||||||
|
|
@ -174,11 +174,10 @@ ipcMain.handle('watchdog:pong', () => {
|
||||||
*/
|
*/
|
||||||
ipcMain.handle('get-config', () => {
|
ipcMain.handle('get-config', () => {
|
||||||
return {
|
return {
|
||||||
// Lightning.Pub connection (public info only)
|
// LNbits nostr-transport connection (public info only)
|
||||||
relayUrl: process.env.VITE_RELAY_URL || 'ws://localhost:7777',
|
relayUrl: process.env.VITE_RELAY_URL || 'ws://localhost:7777',
|
||||||
lightningPubPubkey: process.env.VITE_LIGHTNING_PUB_PUBKEY || '',
|
lnbitsServerPubkey: process.env.VITE_LNBITS_SERVER_PUBKEY || '',
|
||||||
lightningPubApiUrl: process.env.VITE_LIGHTNING_PUB_API_URL || 'http://localhost:1776',
|
lnbitsHttpUrl: process.env.VITE_LNBITS_HTTP_URL || 'http://localhost:5000',
|
||||||
extensionApiUrl: process.env.VITE_EXTENSION_API_URL || 'http://localhost:1777',
|
|
||||||
appId: process.env.VITE_APP_ID || '',
|
appId: process.env.VITE_APP_ID || '',
|
||||||
|
|
||||||
// Hardware configuration
|
// Hardware configuration
|
||||||
|
|
@ -206,9 +205,12 @@ ipcMain.handle('get-config', () => {
|
||||||
/**
|
/**
|
||||||
* One-shot secrets handler.
|
* One-shot secrets handler.
|
||||||
*
|
*
|
||||||
* Returns ATM private key and admin token ONCE during initialization,
|
* Returns the ATM private key ONCE during initialization, then
|
||||||
* then refuses all subsequent calls. This limits the window for XSS
|
* refuses all subsequent calls. This limits the window for XSS or
|
||||||
* or compromised dependencies to steal secrets via IPC.
|
* compromised dependencies to steal secrets via IPC.
|
||||||
|
*
|
||||||
|
* (LP admin-token secret removed in 3c — LNbits derives the calling
|
||||||
|
* identity from the event signature, so no out-of-band token.)
|
||||||
*
|
*
|
||||||
* TODO: Move signing/encryption to main process entirely (Phase 2)
|
* TODO: Move signing/encryption to main process entirely (Phase 2)
|
||||||
* so the private key never crosses the IPC boundary.
|
* so the private key never crosses the IPC boundary.
|
||||||
|
|
@ -217,12 +219,11 @@ let secretsConsumed = false
|
||||||
ipcMain.handle('get-atm-secrets', () => {
|
ipcMain.handle('get-atm-secrets', () => {
|
||||||
if (secretsConsumed) {
|
if (secretsConsumed) {
|
||||||
console.warn('[Electron] SECURITY: get-atm-secrets called after secrets already consumed')
|
console.warn('[Electron] SECURITY: get-atm-secrets called after secrets already consumed')
|
||||||
return { atmPrivateKey: '', adminToken: '' }
|
return { atmPrivateKey: '' }
|
||||||
}
|
}
|
||||||
secretsConsumed = true
|
secretsConsumed = true
|
||||||
return {
|
return {
|
||||||
atmPrivateKey: process.env.VITE_ATM_PRIVATE_KEY || '',
|
atmPrivateKey: process.env.VITE_ATM_PRIVATE_KEY || '',
|
||||||
adminToken: process.env.VITE_ADMIN_TOKEN || '',
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,14 @@ import { contextBridge, ipcRenderer } from 'electron'
|
||||||
*/
|
*/
|
||||||
export interface RuntimeConfig {
|
export interface RuntimeConfig {
|
||||||
relayUrl: string
|
relayUrl: string
|
||||||
lightningPubPubkey: string
|
/** LNbits nostr-transport server pubkey (hex, 64 chars). */
|
||||||
lightningPubApiUrl: string
|
lnbitsServerPubkey: string
|
||||||
extensionApiUrl: string
|
/** LNbits HTTP root — used only to compose the LNURL-withdraw callback URL. */
|
||||||
|
lnbitsHttpUrl: string
|
||||||
|
/** Legacy LP fields — retained until 3d removes the LP backend. Optional. */
|
||||||
|
lightningPubPubkey?: string
|
||||||
|
lightningPubApiUrl?: string
|
||||||
|
extensionApiUrl?: string
|
||||||
appId: string
|
appId: string
|
||||||
machineModel: string
|
machineModel: string
|
||||||
fiatCode: string
|
fiatCode: string
|
||||||
|
|
@ -34,7 +39,8 @@ export interface RuntimeConfig {
|
||||||
*/
|
*/
|
||||||
export interface AtmSecrets {
|
export interface AtmSecrets {
|
||||||
atmPrivateKey: string
|
atmPrivateKey: string
|
||||||
adminToken: string
|
/** Legacy LP admin token — retained until 3d removes the LP backend. */
|
||||||
|
adminToken?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expose protected methods to renderer
|
// Expose protected methods to renderer
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ const isElectron = typeof window !== 'undefined' && window.electronAPI !== undef
|
||||||
*/
|
*/
|
||||||
interface LightningConfig {
|
interface LightningConfig {
|
||||||
relayUrl: string
|
relayUrl: string
|
||||||
|
/** Legacy LP fields — kept until 3d removes the LP backend entirely. */
|
||||||
lightningPubPubkey: string
|
lightningPubPubkey: string
|
||||||
lightningPubApiUrl: string
|
lightningPubApiUrl: string
|
||||||
extensionApiUrl: string
|
extensionApiUrl: string
|
||||||
|
|
@ -58,19 +59,13 @@ interface LightningConfig {
|
||||||
atmPrivateKey: string
|
atmPrivateKey: string
|
||||||
appId: string
|
appId: string
|
||||||
operatorPubkeys: string[]
|
operatorPubkeys: string[]
|
||||||
/**
|
/** LNbits nostr-transport server pubkey (hex, 64 chars). */
|
||||||
* LNbits nostr-transport server pubkey. Optional during the LP→LNbits
|
|
||||||
* migration: when empty, the LnbitsClient is not instantiated and the
|
|
||||||
* file behaves identically to its LP-only past. Once 3c wires the env
|
|
||||||
* var, this is required.
|
|
||||||
*/
|
|
||||||
lnbitsServerPubkey: string
|
lnbitsServerPubkey: string
|
||||||
/**
|
/**
|
||||||
* LNbits HTTP root (e.g. `https://lnbits.example`). Used purely to
|
* LNbits HTTP root (e.g. `https://lnbits.example`). Used purely to
|
||||||
* compose the LNURL callback URL that customer wallets dereference
|
* compose the LNURL callback URL that customer wallets dereference
|
||||||
* to redeem an LNURL-withdraw. The ATM itself does not call this
|
* to redeem an LNURL-withdraw. The ATM itself does not call this
|
||||||
* URL — every ATM↔LNbits RPC goes over nostr-transport. Required
|
* URL — every ATM↔LNbits RPC goes over nostr-transport.
|
||||||
* for cash-in on LNbits; ignored on the LP path.
|
|
||||||
*/
|
*/
|
||||||
lnbitsHttpUrl: string
|
lnbitsHttpUrl: string
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +78,6 @@ interface LightningConfig {
|
||||||
* which returns secrets only once per app lifecycle.
|
* which returns secrets only once per app lifecycle.
|
||||||
*/
|
*/
|
||||||
async function loadLightningConfig(): Promise<LightningConfig> {
|
async function loadLightningConfig(): Promise<LightningConfig> {
|
||||||
// Development defaults (local Docker infrastructure)
|
|
||||||
const defaults: LightningConfig = {
|
const defaults: LightningConfig = {
|
||||||
relayUrl: 'ws://localhost:7777',
|
relayUrl: 'ws://localhost:7777',
|
||||||
lightningPubPubkey: '',
|
lightningPubPubkey: '',
|
||||||
|
|
@ -94,35 +88,32 @@ async function loadLightningConfig(): Promise<LightningConfig> {
|
||||||
appId: '30270e761f2e30b1737f34ce661df45f521352b408b8ed18fcc09f3f0dec5097', // ATM app ID — regenerated for bitSpire so stale LP server-side associations don't accidentally rehydrate
|
appId: '30270e761f2e30b1737f34ce661df45f521352b408b8ed18fcc09f3f0dec5097', // ATM app ID — regenerated for bitSpire so stale LP server-side associations don't accidentally rehydrate
|
||||||
operatorPubkeys: [],
|
operatorPubkeys: [],
|
||||||
lnbitsServerPubkey: '',
|
lnbitsServerPubkey: '',
|
||||||
lnbitsHttpUrl: '',
|
lnbitsHttpUrl: 'http://localhost:5000',
|
||||||
}
|
}
|
||||||
|
|
||||||
// In Electron, get runtime config from main process
|
// In Electron, get runtime config from main process
|
||||||
if (isElectron && window.electronAPI) {
|
if (isElectron && window.electronAPI) {
|
||||||
try {
|
try {
|
||||||
const runtimeConfig = await window.electronAPI.getConfig()
|
const runtimeConfig = await window.electronAPI.getConfig()
|
||||||
// Secrets come from a separate one-shot IPC handler
|
|
||||||
const secrets = await window.electronAPI.getAtmSecrets()
|
const secrets = await window.electronAPI.getAtmSecrets()
|
||||||
|
const rc = runtimeConfig
|
||||||
|
const sec = secrets
|
||||||
return {
|
return {
|
||||||
relayUrl: runtimeConfig.relayUrl || defaults.relayUrl,
|
relayUrl: rc.relayUrl || defaults.relayUrl,
|
||||||
lightningPubPubkey: runtimeConfig.lightningPubPubkey || defaults.lightningPubPubkey,
|
lightningPubPubkey: rc.lightningPubPubkey || defaults.lightningPubPubkey,
|
||||||
lightningPubApiUrl: runtimeConfig.lightningPubApiUrl || defaults.lightningPubApiUrl,
|
lightningPubApiUrl: rc.lightningPubApiUrl || defaults.lightningPubApiUrl,
|
||||||
extensionApiUrl: runtimeConfig.extensionApiUrl || defaults.extensionApiUrl,
|
extensionApiUrl: rc.extensionApiUrl || defaults.extensionApiUrl,
|
||||||
adminToken: secrets.adminToken || defaults.adminToken,
|
adminToken: sec.adminToken || defaults.adminToken,
|
||||||
atmPrivateKey: secrets.atmPrivateKey || defaults.atmPrivateKey,
|
atmPrivateKey: sec.atmPrivateKey || defaults.atmPrivateKey,
|
||||||
appId: runtimeConfig.appId || defaults.appId,
|
appId: rc.appId || defaults.appId,
|
||||||
operatorPubkeys: runtimeConfig.operatorPubkeys
|
operatorPubkeys: rc.operatorPubkeys
|
||||||
? runtimeConfig.operatorPubkeys
|
? rc.operatorPubkeys
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((k: string) => k.trim())
|
.map((k: string) => k.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
: defaults.operatorPubkeys,
|
: defaults.operatorPubkeys,
|
||||||
lnbitsServerPubkey:
|
lnbitsServerPubkey: rc.lnbitsServerPubkey || defaults.lnbitsServerPubkey,
|
||||||
(runtimeConfig as { lnbitsServerPubkey?: string }).lnbitsServerPubkey ||
|
lnbitsHttpUrl: rc.lnbitsHttpUrl || defaults.lnbitsHttpUrl,
|
||||||
defaults.lnbitsServerPubkey,
|
|
||||||
lnbitsHttpUrl:
|
|
||||||
(runtimeConfig as { lnbitsHttpUrl?: string }).lnbitsHttpUrl ||
|
|
||||||
defaults.lnbitsHttpUrl,
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[Lightning] Failed to get runtime config from Electron:', e)
|
console.warn('[Lightning] Failed to get runtime config from Electron:', e)
|
||||||
|
|
@ -132,10 +123,17 @@ async function loadLightningConfig(): Promise<LightningConfig> {
|
||||||
// Fallback: Vite build-time env vars (for browser dev mode)
|
// Fallback: Vite build-time env vars (for browser dev mode)
|
||||||
return {
|
return {
|
||||||
relayUrl: import.meta.env.VITE_RELAY_URL || defaults.relayUrl,
|
relayUrl: import.meta.env.VITE_RELAY_URL || defaults.relayUrl,
|
||||||
lightningPubPubkey: import.meta.env.VITE_LIGHTNING_PUB_PUBKEY || defaults.lightningPubPubkey,
|
lightningPubPubkey:
|
||||||
lightningPubApiUrl: import.meta.env.VITE_LIGHTNING_PUB_API_URL || defaults.lightningPubApiUrl,
|
(import.meta.env.VITE_LIGHTNING_PUB_PUBKEY as string | undefined) ||
|
||||||
extensionApiUrl: import.meta.env.VITE_EXTENSION_API_URL || defaults.extensionApiUrl,
|
defaults.lightningPubPubkey,
|
||||||
adminToken: import.meta.env.VITE_ADMIN_TOKEN || defaults.adminToken,
|
lightningPubApiUrl:
|
||||||
|
(import.meta.env.VITE_LIGHTNING_PUB_API_URL as string | undefined) ||
|
||||||
|
defaults.lightningPubApiUrl,
|
||||||
|
extensionApiUrl:
|
||||||
|
(import.meta.env.VITE_EXTENSION_API_URL as string | undefined) ||
|
||||||
|
defaults.extensionApiUrl,
|
||||||
|
adminToken:
|
||||||
|
(import.meta.env.VITE_ADMIN_TOKEN as string | undefined) || defaults.adminToken,
|
||||||
atmPrivateKey: import.meta.env.VITE_ATM_PRIVATE_KEY || defaults.atmPrivateKey,
|
atmPrivateKey: import.meta.env.VITE_ATM_PRIVATE_KEY || defaults.atmPrivateKey,
|
||||||
appId: import.meta.env.VITE_APP_ID || defaults.appId,
|
appId: import.meta.env.VITE_APP_ID || defaults.appId,
|
||||||
lnbitsServerPubkey:
|
lnbitsServerPubkey:
|
||||||
|
|
|
||||||
14
apps/machine/src/types/electron.d.ts
vendored
14
apps/machine/src/types/electron.d.ts
vendored
|
|
@ -4,9 +4,14 @@
|
||||||
|
|
||||||
export interface RuntimeConfig {
|
export interface RuntimeConfig {
|
||||||
relayUrl: string
|
relayUrl: string
|
||||||
lightningPubPubkey: string
|
/** LNbits nostr-transport server pubkey (hex, 64 chars). */
|
||||||
lightningPubApiUrl: string
|
lnbitsServerPubkey: string
|
||||||
extensionApiUrl: string
|
/** LNbits HTTP root — used only to compose the LNURL-withdraw callback URL. */
|
||||||
|
lnbitsHttpUrl: string
|
||||||
|
/** Legacy LP fields — retained until 3d removes the LP backend. Optional. */
|
||||||
|
lightningPubPubkey?: string
|
||||||
|
lightningPubApiUrl?: string
|
||||||
|
extensionApiUrl?: string
|
||||||
appId: string
|
appId: string
|
||||||
machineModel: string
|
machineModel: string
|
||||||
fiatCode: string
|
fiatCode: string
|
||||||
|
|
@ -22,7 +27,8 @@ export interface RuntimeConfig {
|
||||||
|
|
||||||
export interface AtmSecrets {
|
export interface AtmSecrets {
|
||||||
atmPrivateKey: string
|
atmPrivateKey: string
|
||||||
adminToken: string
|
/** Legacy LP admin token — retained until 3d removes the LP backend. */
|
||||||
|
adminToken?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
|
||||||
|
|
@ -53,14 +53,17 @@ Edit `/etc/nixos/configuration.nix` to customize:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
{
|
{
|
||||||
services.lamassu-atm = {
|
services.bitspire = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
# Nostr relay for ATM communication
|
# Nostr relay both the ATM and LNbits subscribe on
|
||||||
relayUrl = "wss://relay.lamassu.is";
|
relayUrl = "wss://relay.aiolabs.dev";
|
||||||
|
|
||||||
# Lightning.Pub instance
|
# LNbits nostr-transport server pubkey (hex, 64 chars)
|
||||||
lightningPubUrl = "https://lp.lamassu.is";
|
lnbitsServerPubkey = "<hex pubkey from `docker logs lnbits | grep nostr_transport`>";
|
||||||
|
|
||||||
|
# LNbits HTTP origin — only used to compose LNURL-withdraw callback URLs
|
||||||
|
lnbitsHttpUrl = "https://lnbits.aiolabs.dev";
|
||||||
|
|
||||||
# Hardware configuration
|
# Hardware configuration
|
||||||
billValidator = {
|
billValidator = {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Lamassu ATM Service Module
|
# bitSpire ATM Service Module
|
||||||
# Manages the ATM Electron application and related services
|
# Manages the ATM Electron application and related services
|
||||||
|
|
||||||
{ config, lib, pkgs, pkgs-unstable, ... }:
|
{ config, lib, pkgs, pkgs-unstable, ... }:
|
||||||
|
|
@ -10,18 +10,34 @@ let
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.services.bitspire = {
|
options.services.bitspire = {
|
||||||
enable = mkEnableOption "Lamassu ATM service";
|
enable = mkEnableOption "bitSpire ATM service";
|
||||||
|
|
||||||
relayUrl = mkOption {
|
relayUrl = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = "wss://relay.lamassu.is";
|
default = "wss://relay.aiolabs.dev";
|
||||||
description = "Nostr relay URL for ATM communication";
|
description = "Nostr relay URL the ATM and LNbits both subscribe to";
|
||||||
};
|
};
|
||||||
|
|
||||||
lightningPubUrl = mkOption {
|
lnbitsServerPubkey = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = "https://lp.lamassu.is";
|
default = "";
|
||||||
description = "Lightning.Pub instance URL";
|
description = ''
|
||||||
|
LNbits nostr-transport server pubkey (hex, 64 chars). Published
|
||||||
|
by the LNbits server on startup. Required for the ATM to talk
|
||||||
|
to its wallet. Provisioned by provision-atm.sh; can be left
|
||||||
|
empty on disk-image builds.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
lnbitsHttpUrl = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "https://lnbits.aiolabs.dev";
|
||||||
|
description = ''
|
||||||
|
LNbits HTTP origin — used solely to compose the LNURL-withdraw
|
||||||
|
callback URL embedded in cash-in QR codes. The ATM itself
|
||||||
|
never calls this URL; every ATM↔LNbits RPC goes over
|
||||||
|
nostr-transport.
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
appDir = mkOption {
|
appDir = mkOption {
|
||||||
|
|
@ -107,9 +123,10 @@ in
|
||||||
|
|
||||||
# Environment file for ATM configuration
|
# Environment file for ATM configuration
|
||||||
environment.etc."bitspire/config.env".text = ''
|
environment.etc."bitspire/config.env".text = ''
|
||||||
# Lamassu ATM Configuration
|
# bitSpire ATM Configuration
|
||||||
RELAY_URL=${cfg.relayUrl}
|
RELAY_URL=${cfg.relayUrl}
|
||||||
LIGHTNING_PUB_URL=${cfg.lightningPubUrl}
|
LNBITS_SERVER_PUBKEY=${cfg.lnbitsServerPubkey}
|
||||||
|
LNBITS_HTTP_URL=${cfg.lnbitsHttpUrl}
|
||||||
LOG_LEVEL=${cfg.logLevel}
|
LOG_LEVEL=${cfg.logLevel}
|
||||||
DATA_DIR=${cfg.dataDir}
|
DATA_DIR=${cfg.dataDir}
|
||||||
|
|
||||||
|
|
@ -132,7 +149,7 @@ in
|
||||||
|
|
||||||
# Main ATM service
|
# Main ATM service
|
||||||
systemd.services.bitspire = {
|
systemd.services.bitspire = {
|
||||||
description = "Lamassu ATM Application";
|
description = "bitSpire ATM Application";
|
||||||
wantedBy = [ "graphical.target" ];
|
wantedBy = [ "graphical.target" ];
|
||||||
after = [ "graphical.target" "network-online.target" ];
|
after = [ "graphical.target" "network-online.target" ];
|
||||||
wants = [ "network-online.target" ];
|
wants = [ "network-online.target" ];
|
||||||
|
|
@ -175,9 +192,9 @@ in
|
||||||
|
|
||||||
# Pre-start script to verify hardware
|
# Pre-start script to verify hardware
|
||||||
preStart = ''
|
preStart = ''
|
||||||
echo "Lamassu ATM starting..."
|
echo "bitSpire starting..."
|
||||||
echo "Relay: ${cfg.relayUrl}"
|
echo "Relay: ${cfg.relayUrl}"
|
||||||
echo "Lightning.Pub: ${cfg.lightningPubUrl}"
|
echo "LNbits HTTP: ${cfg.lnbitsHttpUrl}"
|
||||||
|
|
||||||
# Check bill validator if enabled
|
# Check bill validator if enabled
|
||||||
if [ "${boolToString cfg.billValidator.enable}" = "true" ]; then
|
if [ "${boolToString cfg.billValidator.enable}" = "true" ]; then
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,19 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Provision a running ATM (live USB or QEMU VM) with Lightning.Pub credentials.
|
# Provision a running bitSpire ATM (live USB, QEMU VM, or installed Sintra)
|
||||||
# Extracts credentials from the dev docker stack and writes them to the ATM's .env via SSH.
|
# with LNbits nostr-transport credentials. The ATM speaks to LNbits over
|
||||||
|
# kind-21000 NIP-44 v2 events on a relay — there is no out-of-band token,
|
||||||
|
# the ATM's nostr private key IS the credential.
|
||||||
|
#
|
||||||
|
# Required environment variables (or edit defaults below):
|
||||||
|
# LNBITS_SERVER_PUBKEY Hex pubkey published by the LNbits server at startup.
|
||||||
|
# From the LNbits compose:
|
||||||
|
# docker logs lnbits | grep 'nostr_transport pubkey'
|
||||||
|
# LNBITS_HTTP_URL Origin LNbits is reachable at over HTTP, used only
|
||||||
|
# to compose the LNURL-withdraw callback URL that
|
||||||
|
# customer wallets dereference. Default: http://10.0.2.2:5000
|
||||||
|
# RELAY_URL Nostr relay LNbits subscribes on. Default uses host gateway.
|
||||||
|
# ATM_PRIVATE_KEY 32-byte hex key, ATM's nostr identity. If unset, a
|
||||||
|
# fresh key is generated and saved in the .env.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# bash provision-atm.sh # defaults: SSH to localhost:2222 (QEMU)
|
# bash provision-atm.sh # defaults: SSH to localhost:2222 (QEMU)
|
||||||
|
|
@ -11,45 +24,12 @@ set -euo pipefail
|
||||||
ATM_HOST="${1:-localhost}"
|
ATM_HOST="${1:-localhost}"
|
||||||
ATM_SSH_PORT="${2:-2222}"
|
ATM_SSH_PORT="${2:-2222}"
|
||||||
ATM_USER="lamassu"
|
ATM_USER="lamassu"
|
||||||
LP_API="http://localhost:1776"
|
|
||||||
ADMIN_TOKEN="lamassu-dev-admin-token"
|
|
||||||
ATM_PRIVATE_KEY="f391a2c3fc734f443b0f685688a0441b5fb9805853c0023f570c5a3c6412b136"
|
|
||||||
|
|
||||||
echo "=== Provisioning ATM at $ATM_HOST:$ATM_SSH_PORT ==="
|
echo "=== Provisioning bitSpire ATM at $ATM_HOST:$ATM_SSH_PORT ==="
|
||||||
|
|
||||||
# Step 1: Extract Lightning.Pub pubkey from docker logs
|
# Step 1: Discover the host IP as seen from the ATM.
|
||||||
echo ""
|
# QEMU user-mode networking puts the host at 10.0.2.2; on real LAN ATMs
|
||||||
echo "--- Step 1: Getting Lightning.Pub pubkey ---"
|
# use the dev machine's outbound LAN address.
|
||||||
PUBKEY=$(docker logs lamassu-lightning-pub 2>&1 | grep -oP 'pubkey:\s*\K[a-f0-9]+' | tail -1)
|
|
||||||
if [ -z "$PUBKEY" ]; then
|
|
||||||
echo "ERROR: Could not extract pubkey from lamassu-lightning-pub logs."
|
|
||||||
echo "Is the docker stack running? Try: docker ps | grep lightning-pub"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Pubkey: ${PUBKEY:0:16}..."
|
|
||||||
|
|
||||||
# Step 2: Create ATM app via admin API
|
|
||||||
echo ""
|
|
||||||
echo "--- Step 2: Creating ATM app ---"
|
|
||||||
RESPONSE=$(curl -s -X POST "$LP_API/api/admin/app/add" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
|
||||||
-d '{"name":"bitspire-atm-live","allow_user_creation":true}' 2>/dev/null)
|
|
||||||
|
|
||||||
if echo "$RESPONSE" | grep -q '"status":"OK"'; then
|
|
||||||
APP_ID=$(echo "$RESPONSE" | grep -oP '"id":"\K[^"]+')
|
|
||||||
echo "Created app: ${APP_ID:0:16}..."
|
|
||||||
else
|
|
||||||
echo "WARN: Could not create app (may already exist). Response:"
|
|
||||||
echo "$RESPONSE"
|
|
||||||
echo ""
|
|
||||||
echo "If the app already exists, check docker/dev-state/ for cached credentials."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Step 3: Determine the host IP as seen from the ATM
|
|
||||||
# For QEMU user-mode networking, the host is at 10.0.2.2
|
|
||||||
# For real hardware on LAN, use the dev machine's LAN IP
|
|
||||||
if [ "$ATM_HOST" = "localhost" ]; then
|
if [ "$ATM_HOST" = "localhost" ]; then
|
||||||
HOST_IP="10.0.2.2"
|
HOST_IP="10.0.2.2"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
@ -60,22 +40,48 @@ else
|
||||||
echo "--- LAN ATM: using $HOST_IP as dev machine address ---"
|
echo "--- LAN ATM: using $HOST_IP as dev machine address ---"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 4: Write .env to the ATM via SSH
|
# Step 2: Resolve the LNbits server pubkey. Prefer the env override; else
|
||||||
|
# fall back to scraping the local docker compose stack.
|
||||||
|
if [ -z "${LNBITS_SERVER_PUBKEY:-}" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "--- Step 1: Extracting LNbits nostr-transport pubkey from docker logs ---"
|
||||||
|
LNBITS_SERVER_PUBKEY=$(docker logs lnbits 2>&1 \
|
||||||
|
| grep -oP 'nostr_transport pubkey:?\s*\K[a-f0-9]{64}' \
|
||||||
|
| tail -1 || true)
|
||||||
|
if [ -z "$LNBITS_SERVER_PUBKEY" ]; then
|
||||||
|
echo "ERROR: Could not extract LNbits pubkey. Set LNBITS_SERVER_PUBKEY explicitly"
|
||||||
|
echo "or start the LNbits stack first (docker compose -f docker/docker-compose.dev.yml up lnbits)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "LNbits server pubkey: ${LNBITS_SERVER_PUBKEY:0:16}..."
|
||||||
|
|
||||||
|
# Step 3: Pin LNbits HTTP origin.
|
||||||
|
LNBITS_HTTP_URL="${LNBITS_HTTP_URL:-http://$HOST_IP:5000}"
|
||||||
|
|
||||||
|
# Step 4: Relay URL.
|
||||||
|
RELAY_URL="${RELAY_URL:-ws://$HOST_IP:7777}"
|
||||||
|
|
||||||
|
# Step 5: ATM identity. Generate if unset.
|
||||||
|
if [ -z "${ATM_PRIVATE_KEY:-}" ]; then
|
||||||
|
ATM_PRIVATE_KEY=$(openssl rand -hex 32)
|
||||||
|
echo ""
|
||||||
|
echo "--- Generated fresh ATM_PRIVATE_KEY (save this if you want it persisted) ---"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 6: Write .env to the ATM via SSH.
|
||||||
echo ""
|
echo ""
|
||||||
echo "--- Step 3: Writing .env to ATM ---"
|
echo "--- Step 2: Writing .env to ATM ---"
|
||||||
ENV_CONTENT="# Lamassu ATM Configuration
|
ENV_CONTENT="# bitSpire Configuration
|
||||||
# Auto-generated by provision-atm.sh on $(date -Iseconds)
|
# Auto-generated by provision-atm.sh on $(date -Iseconds)
|
||||||
|
|
||||||
# Lightning.Pub connection
|
# LNbits nostr-transport connection
|
||||||
VITE_RELAY_URL=ws://$HOST_IP:7777
|
VITE_RELAY_URL=$RELAY_URL
|
||||||
VITE_LIGHTNING_PUB_PUBKEY=$PUBKEY
|
VITE_LNBITS_SERVER_PUBKEY=$LNBITS_SERVER_PUBKEY
|
||||||
VITE_LIGHTNING_PUB_API_URL=http://$HOST_IP:1776
|
VITE_LNBITS_HTTP_URL=$LNBITS_HTTP_URL
|
||||||
VITE_EXTENSION_API_URL=http://$HOST_IP:1777
|
|
||||||
|
|
||||||
# Credentials
|
# ATM identity (signing key IS the credential under nostr-transport)
|
||||||
VITE_ADMIN_TOKEN=$ADMIN_TOKEN
|
|
||||||
VITE_ATM_PRIVATE_KEY=$ATM_PRIVATE_KEY
|
VITE_ATM_PRIVATE_KEY=$ATM_PRIVATE_KEY
|
||||||
VITE_APP_ID=$APP_ID
|
|
||||||
|
|
||||||
# Machine configuration
|
# Machine configuration
|
||||||
VITE_LAMASSU_MACHINE_MODEL=sintra
|
VITE_LAMASSU_MACHINE_MODEL=sintra
|
||||||
|
|
@ -92,6 +98,6 @@ echo ""
|
||||||
echo "=== ATM provisioned successfully ==="
|
echo "=== ATM provisioned successfully ==="
|
||||||
echo ""
|
echo ""
|
||||||
echo "Credentials written to /var/lib/bitspire/.env"
|
echo "Credentials written to /var/lib/bitspire/.env"
|
||||||
echo "ATM service restarted. It should connect to Lightning.Pub at $HOST_IP."
|
echo "ATM service restarted. It should connect to LNbits via relay $RELAY_URL."
|
||||||
echo ""
|
echo ""
|
||||||
echo "To check status: ssh -p $ATM_SSH_PORT $ATM_USER@$ATM_HOST 'sudo journalctl -u bitspire -f'"
|
echo "To check status: ssh -p $ATM_SSH_PORT $ATM_USER@$ATM_HOST 'sudo journalctl -u bitspire -f'"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue