feat: add runtime config via Electron IPC

- Add .env.example with all configuration options documented
- Add get-config IPC handler in Electron main process
- Expose getConfig() in preload for renderer access
- Update lightning.ts to load config at runtime in Electron
- Fall back to Vite build-time env vars for browser dev mode
- Add TypeScript declarations for electronAPI

This allows the same build to be deployed to different machines
with different configurations (relay URL, Lightning.Pub pubkey, etc.)
set via environment variables at runtime.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-01-30 11:00:12 -05:00
commit 56b1d594d7
5 changed files with 167 additions and 5 deletions

View file

@ -0,0 +1,49 @@
# Lamassu ATM Configuration
# Copy this file to .env and fill in your values
# =============================================================================
# Hardware Configuration
# =============================================================================
# Machine model preset (sintra, gaia, or custom)
VITE_LAMASSU_MACHINE_MODEL=sintra
# Fiat currency code (ISO 4217)
VITE_LAMASSU_FIAT_CODE=USD
# Custom device paths (optional - uses preset defaults if not set)
# VITE_LAMASSU_VALIDATOR_DEVICE=/dev/ttyJ5
# VITE_LAMASSU_DISPENSER_DEVICE=/dev/ttyJ7
# Cassette configuration (optional - JSON array)
# VITE_LAMASSU_CASSETTES='[{"denomination":20,"count":100}]'
# =============================================================================
# Lightning.Pub Connection (Required)
# =============================================================================
# Nostr relay WebSocket URL
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=
# Lightning.Pub HTTP API URL (optional)
VITE_LIGHTNING_PUB_API_URL=http://localhost:1776
# =============================================================================
# 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
VITE_ATM_PRIVATE_KEY=
# =============================================================================
# Development Only
# =============================================================================
# Lightning.Pub admin token (dev/testing only)
VITE_ADMIN_TOKEN=lamassu-dev-admin-token

View file

@ -53,6 +53,28 @@ ipcMain.handle('get-version', () => {
return app.getVersion() return app.getVersion()
}) })
/**
* Get runtime configuration from environment variables
* This allows configuration to be set at runtime (not baked in at build time)
*/
ipcMain.handle('get-config', () => {
return {
// Lightning.Pub connection
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',
atmPrivateKey: process.env.VITE_ATM_PRIVATE_KEY || '',
adminToken: process.env.VITE_ADMIN_TOKEN || '',
// Hardware configuration
machineModel: process.env.VITE_LAMASSU_MACHINE_MODEL || 'sintra',
fiatCode: process.env.VITE_LAMASSU_FIAT_CODE || 'USD',
validatorDevice: process.env.VITE_LAMASSU_VALIDATOR_DEVICE,
dispenserDevice: process.env.VITE_LAMASSU_DISPENSER_DEVICE,
cassettes: process.env.VITE_LAMASSU_CASSETTES,
}
})
// App lifecycle // App lifecycle
app.whenReady().then(() => { app.whenReady().then(() => {
createWindow() createWindow()

View file

@ -7,11 +7,31 @@
import { contextBridge, ipcRenderer } from 'electron' import { contextBridge, ipcRenderer } from 'electron'
/**
* Runtime configuration interface
* These values are read from environment variables at runtime (not build time)
*/
export interface RuntimeConfig {
relayUrl: string
lightningPubPubkey: string
lightningPubApiUrl: string
atmPrivateKey: string
adminToken: string
machineModel: string
fiatCode: string
validatorDevice?: string
dispenserDevice?: string
cassettes?: string
}
// Expose protected methods to renderer // Expose protected methods to renderer
contextBridge.exposeInMainWorld('electronAPI', { contextBridge.exposeInMainWorld('electronAPI', {
// Get app version // Get app version
getVersion: (): Promise<string> => ipcRenderer.invoke('get-version'), getVersion: (): Promise<string> => ipcRenderer.invoke('get-version'),
// Get runtime configuration (read from environment at runtime)
getConfig: (): Promise<RuntimeConfig> => ipcRenderer.invoke('get-config'),
// Platform info // Platform info
platform: process.platform, platform: process.platform,
}) })
@ -21,6 +41,7 @@ declare global {
interface Window { interface Window {
electronAPI: { electronAPI: {
getVersion: () => Promise<string> getVersion: () => Promise<string>
getConfig: () => Promise<RuntimeConfig>
platform: NodeJS.Platform platform: NodeJS.Platform
} }
} }

View file

@ -30,22 +30,42 @@ import {
import type { OfferRequest } from '@lamassu/clink' import type { OfferRequest } from '@lamassu/clink'
import type { ATMServices, ATMContext } from '@lamassu/state-machine' import type { ATMServices, ATMContext } from '@lamassu/state-machine'
// Import Electron types
import type {} from '@/types/electron'
// Check if we're in a browser environment // Check if we're in a browser environment
const isBrowser = typeof window !== 'undefined' const isBrowser = typeof window !== 'undefined'
// Check if we're running in Electron (electronAPI is exposed via preload)
const isElectron = typeof window !== 'undefined' && window.electronAPI !== undefined
/** /**
* Lightning configuration from environment variables * Lightning configuration
* *
* Environment variables (prefix with VITE_ for Vite): * In Electron: Read from environment at runtime via IPC
* In Browser (dev): Read from Vite's import.meta.env (build-time)
*
* Environment variables:
* - VITE_RELAY_URL: Nostr relay WebSocket URL * - VITE_RELAY_URL: Nostr relay WebSocket URL
* - VITE_LIGHTNING_PUB_PUBKEY: Lightning.Pub's Nostr pubkey (hex or npub) * - VITE_LIGHTNING_PUB_PUBKEY: Lightning.Pub's Nostr pubkey (hex or npub)
* - VITE_LIGHTNING_PUB_API_URL: Lightning.Pub HTTP API URL * - VITE_LIGHTNING_PUB_API_URL: Lightning.Pub HTTP API URL
* - VITE_ATM_PRIVATE_KEY: ATM's Nostr private key (hex or nsec) * - VITE_ATM_PRIVATE_KEY: ATM's Nostr private key (hex or nsec)
* - VITE_ADMIN_TOKEN: Lightning.Pub admin token (dev only) * - VITE_ADMIN_TOKEN: Lightning.Pub admin token (dev only)
*/ */
function loadLightningConfig() { interface LightningConfig {
relayUrl: string
lightningPubPubkey: string
lightningPubApiUrl: string
adminToken: string
atmPrivateKey: string
}
/**
* Load configuration - async to support Electron IPC
*/
async function loadLightningConfig(): Promise<LightningConfig> {
// Development defaults (local Docker infrastructure) // Development defaults (local Docker infrastructure)
const defaults = { const defaults: LightningConfig = {
relayUrl: 'ws://localhost:7777', relayUrl: 'ws://localhost:7777',
lightningPubPubkey: '', lightningPubPubkey: '',
lightningPubApiUrl: 'http://localhost:1776', lightningPubApiUrl: 'http://localhost:1776',
@ -53,6 +73,23 @@ function loadLightningConfig() {
atmPrivateKey: '', atmPrivateKey: '',
} }
// In Electron, get runtime config from main process
if (isElectron && window.electronAPI) {
try {
const runtimeConfig = await window.electronAPI.getConfig()
return {
relayUrl: runtimeConfig.relayUrl || defaults.relayUrl,
lightningPubPubkey: runtimeConfig.lightningPubPubkey || defaults.lightningPubPubkey,
lightningPubApiUrl: runtimeConfig.lightningPubApiUrl || defaults.lightningPubApiUrl,
adminToken: runtimeConfig.adminToken || defaults.adminToken,
atmPrivateKey: runtimeConfig.atmPrivateKey || defaults.atmPrivateKey,
}
} catch (e) {
console.warn('[Lightning] Failed to get runtime config from Electron:', e)
}
}
// 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: import.meta.env.VITE_LIGHTNING_PUB_PUBKEY || defaults.lightningPubPubkey,
@ -62,7 +99,8 @@ function loadLightningConfig() {
} }
} }
const CONFIG = loadLightningConfig() // Config is loaded async now - will be set in initializeLightningServices
let CONFIG: LightningConfig
interface LightningServices { interface LightningServices {
nostrClient: NostrClient nostrClient: NostrClient
@ -87,6 +125,10 @@ type PaymentReceivedCallback = (preimage: string) => void
*/ */
export async function initializeLightningServices(): Promise<LightningServices> { export async function initializeLightningServices(): Promise<LightningServices> {
console.log('[Lightning] Initializing services...') console.log('[Lightning] Initializing services...')
// Load configuration (async for Electron runtime config)
CONFIG = await loadLightningConfig()
console.log('[Lightning] Relay URL:', CONFIG.relayUrl) console.log('[Lightning] Relay URL:', CONFIG.relayUrl)
console.log('[Lightning] Lightning.Pub pubkey:', CONFIG.lightningPubPubkey || '(not configured)') console.log('[Lightning] Lightning.Pub pubkey:', CONFIG.lightningPubPubkey || '(not configured)')

View file

@ -0,0 +1,28 @@
/**
* Type declarations for Electron API exposed via preload
*/
export interface RuntimeConfig {
relayUrl: string
lightningPubPubkey: string
lightningPubApiUrl: string
atmPrivateKey: string
adminToken: string
machineModel: string
fiatCode: string
validatorDevice?: string
dispenserDevice?: string
cassettes?: string
}
declare global {
interface Window {
electronAPI?: {
getVersion: () => Promise<string>
getConfig: () => Promise<RuntimeConfig>
platform: NodeJS.Platform
}
}
}
export {}