diff --git a/apps/machine/.env.example b/apps/machine/.env.example index 1bc2c2e..e5aa292 100644 --- a/apps/machine/.env.example +++ b/apps/machine/.env.example @@ -1,4 +1,4 @@ -# Lamassu ATM Configuration +# bitSpire ATM Configuration # 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}]' # ============================================================================= -# 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 -# Lightning.Pub's Nostr public key (required!) -# Get from: docker logs lamassu-lightning-pub | grep pubkey -VITE_LIGHTNING_PUB_PUBKEY= +# LNbits nostr-transport server pubkey (hex, 64 chars). +# Printed by the LNbits server on startup: +# docker logs lnbits | grep 'nostr_transport pubkey' +VITE_LNBITS_SERVER_PUBKEY= -# Lightning.Pub HTTP API URL (optional) -VITE_LIGHTNING_PUB_API_URL=http://localhost:1776 +# LNbits HTTP root — used purely to compose the LNURL-withdraw callback +# 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's Nostr private key (hex format, 64 characters) -# Generate with: npx @lamassu/nostr-client generate-keypair -# If not set, generates ephemeral identity on each restart +# ATM's Nostr private key (hex format, 64 characters). This signing +# key IS the credential — LNbits derives the account from it on first +# 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= # ============================================================================= @@ -46,7 +51,7 @@ VITE_ATM_PRIVATE_KEY= # ============================================================================= # 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... # ============================================================================= @@ -64,13 +69,3 @@ VITE_ATM_PRIVATE_KEY= # Set to 'true' for development/demo environments only # When false (production default), initialization failures show a maintenance screen # 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=... diff --git a/apps/machine/electron/main.ts b/apps/machine/electron/main.ts index d6ad834..c4a4dd5 100644 --- a/apps/machine/electron/main.ts +++ b/apps/machine/electron/main.ts @@ -174,11 +174,10 @@ ipcMain.handle('watchdog:pong', () => { */ ipcMain.handle('get-config', () => { return { - // Lightning.Pub connection (public info only) + // LNbits nostr-transport connection (public info only) relayUrl: process.env.VITE_RELAY_URL || 'ws://localhost:7777', - lightningPubPubkey: process.env.VITE_LIGHTNING_PUB_PUBKEY || '', - lightningPubApiUrl: process.env.VITE_LIGHTNING_PUB_API_URL || 'http://localhost:1776', - extensionApiUrl: process.env.VITE_EXTENSION_API_URL || 'http://localhost:1777', + lnbitsServerPubkey: process.env.VITE_LNBITS_SERVER_PUBKEY || '', + lnbitsHttpUrl: process.env.VITE_LNBITS_HTTP_URL || 'http://localhost:5000', appId: process.env.VITE_APP_ID || '', // Hardware configuration @@ -206,9 +205,12 @@ ipcMain.handle('get-config', () => { /** * One-shot secrets handler. * - * Returns ATM private key and admin token ONCE during initialization, - * then refuses all subsequent calls. This limits the window for XSS - * or compromised dependencies to steal secrets via IPC. + * Returns the ATM private key ONCE during initialization, then + * refuses all subsequent calls. This limits the window for XSS or + * 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) * so the private key never crosses the IPC boundary. @@ -217,12 +219,11 @@ let secretsConsumed = false ipcMain.handle('get-atm-secrets', () => { if (secretsConsumed) { console.warn('[Electron] SECURITY: get-atm-secrets called after secrets already consumed') - return { atmPrivateKey: '', adminToken: '' } + return { atmPrivateKey: '' } } secretsConsumed = true return { atmPrivateKey: process.env.VITE_ATM_PRIVATE_KEY || '', - adminToken: process.env.VITE_ADMIN_TOKEN || '', } }) diff --git a/apps/machine/electron/preload.ts b/apps/machine/electron/preload.ts index ccc5760..b88fe35 100644 --- a/apps/machine/electron/preload.ts +++ b/apps/machine/electron/preload.ts @@ -15,9 +15,14 @@ import { contextBridge, ipcRenderer } from 'electron' */ export interface RuntimeConfig { relayUrl: string - lightningPubPubkey: string - lightningPubApiUrl: string - extensionApiUrl: string + /** LNbits nostr-transport server pubkey (hex, 64 chars). */ + lnbitsServerPubkey: 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 machineModel: string fiatCode: string @@ -34,7 +39,8 @@ export interface RuntimeConfig { */ export interface AtmSecrets { atmPrivateKey: string - adminToken: string + /** Legacy LP admin token — retained until 3d removes the LP backend. */ + adminToken?: string } // Expose protected methods to renderer diff --git a/apps/machine/src/services/lightning.ts b/apps/machine/src/services/lightning.ts index 79a01ce..e56aefe 100644 --- a/apps/machine/src/services/lightning.ts +++ b/apps/machine/src/services/lightning.ts @@ -51,6 +51,7 @@ const isElectron = typeof window !== 'undefined' && window.electronAPI !== undef */ interface LightningConfig { relayUrl: string + /** Legacy LP fields — kept until 3d removes the LP backend entirely. */ lightningPubPubkey: string lightningPubApiUrl: string extensionApiUrl: string @@ -58,19 +59,13 @@ interface LightningConfig { atmPrivateKey: string appId: string operatorPubkeys: string[] - /** - * 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. - */ + /** LNbits nostr-transport server pubkey (hex, 64 chars). */ lnbitsServerPubkey: string /** * LNbits HTTP root (e.g. `https://lnbits.example`). Used purely to * compose the LNURL callback URL that customer wallets dereference * to redeem an LNURL-withdraw. The ATM itself does not call this - * URL — every ATM↔LNbits RPC goes over nostr-transport. Required - * for cash-in on LNbits; ignored on the LP path. + * URL — every ATM↔LNbits RPC goes over nostr-transport. */ lnbitsHttpUrl: string } @@ -83,7 +78,6 @@ interface LightningConfig { * which returns secrets only once per app lifecycle. */ async function loadLightningConfig(): Promise { - // Development defaults (local Docker infrastructure) const defaults: LightningConfig = { relayUrl: 'ws://localhost:7777', lightningPubPubkey: '', @@ -94,35 +88,32 @@ async function loadLightningConfig(): Promise { appId: '30270e761f2e30b1737f34ce661df45f521352b408b8ed18fcc09f3f0dec5097', // ATM app ID — regenerated for bitSpire so stale LP server-side associations don't accidentally rehydrate operatorPubkeys: [], lnbitsServerPubkey: '', - lnbitsHttpUrl: '', + lnbitsHttpUrl: 'http://localhost:5000', } // In Electron, get runtime config from main process if (isElectron && window.electronAPI) { try { const runtimeConfig = await window.electronAPI.getConfig() - // Secrets come from a separate one-shot IPC handler const secrets = await window.electronAPI.getAtmSecrets() + const rc = runtimeConfig + const sec = secrets return { - relayUrl: runtimeConfig.relayUrl || defaults.relayUrl, - lightningPubPubkey: runtimeConfig.lightningPubPubkey || defaults.lightningPubPubkey, - lightningPubApiUrl: runtimeConfig.lightningPubApiUrl || defaults.lightningPubApiUrl, - extensionApiUrl: runtimeConfig.extensionApiUrl || defaults.extensionApiUrl, - adminToken: secrets.adminToken || defaults.adminToken, - atmPrivateKey: secrets.atmPrivateKey || defaults.atmPrivateKey, - appId: runtimeConfig.appId || defaults.appId, - operatorPubkeys: runtimeConfig.operatorPubkeys - ? runtimeConfig.operatorPubkeys + relayUrl: rc.relayUrl || defaults.relayUrl, + lightningPubPubkey: rc.lightningPubPubkey || defaults.lightningPubPubkey, + lightningPubApiUrl: rc.lightningPubApiUrl || defaults.lightningPubApiUrl, + extensionApiUrl: rc.extensionApiUrl || defaults.extensionApiUrl, + adminToken: sec.adminToken || defaults.adminToken, + atmPrivateKey: sec.atmPrivateKey || defaults.atmPrivateKey, + appId: rc.appId || defaults.appId, + operatorPubkeys: rc.operatorPubkeys + ? rc.operatorPubkeys .split(',') .map((k: string) => k.trim()) .filter(Boolean) : defaults.operatorPubkeys, - lnbitsServerPubkey: - (runtimeConfig as { lnbitsServerPubkey?: string }).lnbitsServerPubkey || - defaults.lnbitsServerPubkey, - lnbitsHttpUrl: - (runtimeConfig as { lnbitsHttpUrl?: string }).lnbitsHttpUrl || - defaults.lnbitsHttpUrl, + lnbitsServerPubkey: rc.lnbitsServerPubkey || defaults.lnbitsServerPubkey, + lnbitsHttpUrl: rc.lnbitsHttpUrl || defaults.lnbitsHttpUrl, } } catch (e) { console.warn('[Lightning] Failed to get runtime config from Electron:', e) @@ -132,10 +123,17 @@ async function loadLightningConfig(): Promise { // Fallback: Vite build-time env vars (for browser dev mode) return { relayUrl: import.meta.env.VITE_RELAY_URL || defaults.relayUrl, - lightningPubPubkey: import.meta.env.VITE_LIGHTNING_PUB_PUBKEY || defaults.lightningPubPubkey, - lightningPubApiUrl: import.meta.env.VITE_LIGHTNING_PUB_API_URL || defaults.lightningPubApiUrl, - extensionApiUrl: import.meta.env.VITE_EXTENSION_API_URL || defaults.extensionApiUrl, - adminToken: import.meta.env.VITE_ADMIN_TOKEN || defaults.adminToken, + lightningPubPubkey: + (import.meta.env.VITE_LIGHTNING_PUB_PUBKEY as string | undefined) || + defaults.lightningPubPubkey, + 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, appId: import.meta.env.VITE_APP_ID || defaults.appId, lnbitsServerPubkey: diff --git a/apps/machine/src/types/electron.d.ts b/apps/machine/src/types/electron.d.ts index ca1c854..5799774 100644 --- a/apps/machine/src/types/electron.d.ts +++ b/apps/machine/src/types/electron.d.ts @@ -4,9 +4,14 @@ export interface RuntimeConfig { relayUrl: string - lightningPubPubkey: string - lightningPubApiUrl: string - extensionApiUrl: string + /** LNbits nostr-transport server pubkey (hex, 64 chars). */ + lnbitsServerPubkey: 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 machineModel: string fiatCode: string @@ -22,7 +27,8 @@ export interface RuntimeConfig { export interface AtmSecrets { atmPrivateKey: string - adminToken: string + /** Legacy LP admin token — retained until 3d removes the LP backend. */ + adminToken?: string } declare global { diff --git a/deploy/nixos/README.md b/deploy/nixos/README.md index 4b0aa29..c8ba580 100644 --- a/deploy/nixos/README.md +++ b/deploy/nixos/README.md @@ -53,14 +53,17 @@ Edit `/etc/nixos/configuration.nix` to customize: ```nix { - services.lamassu-atm = { + services.bitspire = { enable = true; - # Nostr relay for ATM communication - relayUrl = "wss://relay.lamassu.is"; + # Nostr relay both the ATM and LNbits subscribe on + relayUrl = "wss://relay.aiolabs.dev"; - # Lightning.Pub instance - lightningPubUrl = "https://lp.lamassu.is"; + # 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 = { diff --git a/deploy/nixos/bitspire-atm.nix b/deploy/nixos/bitspire-atm.nix index 1b7ff93..892979f 100644 --- a/deploy/nixos/bitspire-atm.nix +++ b/deploy/nixos/bitspire-atm.nix @@ -1,4 +1,4 @@ -# Lamassu ATM Service Module +# bitSpire ATM Service Module # Manages the ATM Electron application and related services { config, lib, pkgs, pkgs-unstable, ... }: @@ -10,18 +10,34 @@ let in { options.services.bitspire = { - enable = mkEnableOption "Lamassu ATM service"; + enable = mkEnableOption "bitSpire ATM service"; relayUrl = mkOption { type = types.str; - default = "wss://relay.lamassu.is"; - description = "Nostr relay URL for ATM communication"; + default = "wss://relay.aiolabs.dev"; + description = "Nostr relay URL the ATM and LNbits both subscribe to"; }; - lightningPubUrl = mkOption { + lnbitsServerPubkey = mkOption { type = types.str; - default = "https://lp.lamassu.is"; - description = "Lightning.Pub instance URL"; + default = ""; + 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 { @@ -107,9 +123,10 @@ in # Environment file for ATM configuration environment.etc."bitspire/config.env".text = '' - # Lamassu ATM Configuration + # bitSpire ATM Configuration RELAY_URL=${cfg.relayUrl} - LIGHTNING_PUB_URL=${cfg.lightningPubUrl} + LNBITS_SERVER_PUBKEY=${cfg.lnbitsServerPubkey} + LNBITS_HTTP_URL=${cfg.lnbitsHttpUrl} LOG_LEVEL=${cfg.logLevel} DATA_DIR=${cfg.dataDir} @@ -132,7 +149,7 @@ in # Main ATM service systemd.services.bitspire = { - description = "Lamassu ATM Application"; + description = "bitSpire ATM Application"; wantedBy = [ "graphical.target" ]; after = [ "graphical.target" "network-online.target" ]; wants = [ "network-online.target" ]; @@ -175,9 +192,9 @@ in # Pre-start script to verify hardware preStart = '' - echo "Lamassu ATM starting..." + echo "bitSpire starting..." echo "Relay: ${cfg.relayUrl}" - echo "Lightning.Pub: ${cfg.lightningPubUrl}" + echo "LNbits HTTP: ${cfg.lnbitsHttpUrl}" # Check bill validator if enabled if [ "${boolToString cfg.billValidator.enable}" = "true" ]; then diff --git a/deploy/nixos/provision-atm.sh b/deploy/nixos/provision-atm.sh index d53b6f3..76da83c 100755 --- a/deploy/nixos/provision-atm.sh +++ b/deploy/nixos/provision-atm.sh @@ -1,6 +1,19 @@ #!/usr/bin/env bash -# Provision a running ATM (live USB or QEMU VM) with Lightning.Pub credentials. -# Extracts credentials from the dev docker stack and writes them to the ATM's .env via SSH. +# Provision a running bitSpire ATM (live USB, QEMU VM, or installed Sintra) +# 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: # bash provision-atm.sh # defaults: SSH to localhost:2222 (QEMU) @@ -11,45 +24,12 @@ set -euo pipefail ATM_HOST="${1:-localhost}" ATM_SSH_PORT="${2:-2222}" 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 -echo "" -echo "--- Step 1: Getting Lightning.Pub pubkey ---" -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 +# Step 1: Discover the host IP as seen from the ATM. +# QEMU user-mode networking puts the host at 10.0.2.2; on real LAN ATMs +# use the dev machine's outbound LAN address. if [ "$ATM_HOST" = "localhost" ]; then HOST_IP="10.0.2.2" echo "" @@ -60,22 +40,48 @@ else echo "--- LAN ATM: using $HOST_IP as dev machine address ---" 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 "--- Step 3: Writing .env to ATM ---" -ENV_CONTENT="# Lamassu ATM Configuration +echo "--- Step 2: Writing .env to ATM ---" +ENV_CONTENT="# bitSpire Configuration # Auto-generated by provision-atm.sh on $(date -Iseconds) -# Lightning.Pub connection -VITE_RELAY_URL=ws://$HOST_IP:7777 -VITE_LIGHTNING_PUB_PUBKEY=$PUBKEY -VITE_LIGHTNING_PUB_API_URL=http://$HOST_IP:1776 -VITE_EXTENSION_API_URL=http://$HOST_IP:1777 +# LNbits nostr-transport connection +VITE_RELAY_URL=$RELAY_URL +VITE_LNBITS_SERVER_PUBKEY=$LNBITS_SERVER_PUBKEY +VITE_LNBITS_HTTP_URL=$LNBITS_HTTP_URL -# Credentials -VITE_ADMIN_TOKEN=$ADMIN_TOKEN +# ATM identity (signing key IS the credential under nostr-transport) VITE_ATM_PRIVATE_KEY=$ATM_PRIVATE_KEY -VITE_APP_ID=$APP_ID # Machine configuration VITE_LAMASSU_MACHINE_MODEL=sintra @@ -92,6 +98,6 @@ echo "" echo "=== ATM provisioned successfully ===" echo "" 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 "To check status: ssh -p $ATM_SSH_PORT $ATM_USER@$ATM_HOST 'sudo journalctl -u bitspire -f'"