feat(docker): add dev.sh with auto-funding and ATM app setup

- Add dev.sh script for managing regtest development environment
- Implement cmd_fund to fund ATM app owner via Lightning.Pub API
- Add --fund flag to cmd_up for automatic funding on startup
- Update setup_atm_app to write VITE_APP_ID to machine .env
- Fix Electron IPC to pass appId and extensionApiUrl to renderer
- Restructure repo from nested lamassu-next/ to root

The dev.sh script now supports:
- ./dev.sh up --fund  # Start regtest and auto-fund ATM
- ./dev.sh fund       # Fund existing ATM app
- ./dev.sh status     # Show environment status
- ./dev.sh reset      # Clean restart

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-02-15 14:19:16 -05:00
commit c98f126ba7
180 changed files with 2695 additions and 9587 deletions

View file

@ -0,0 +1,132 @@
/**
* Electron Main Process
*
* Creates the ATM kiosk window and handles IPC with the renderer.
* In production, runs in kiosk mode (fullscreen, no decorations).
* HAL hardware drivers run here (Node.js environment).
*/
import { app, BrowserWindow, ipcMain } from 'electron'
import path from 'node:path'
import fs from 'node:fs'
import { fileURLToPath } from 'node:url'
// ESM equivalent of __dirname
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// Load .env file manually (Electron main process doesn't have Vite's env loading)
function loadEnvFile() {
const envPath = path.join(__dirname, '..', '.env')
try {
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, 'utf-8')
for (const line of envContent.split('\n')) {
const trimmed = line.trim()
if (trimmed && !trimmed.startsWith('#')) {
const [key, ...valueParts] = trimmed.split('=')
if (key && valueParts.length > 0) {
process.env[key] = valueParts.join('=')
}
}
}
console.log('[Electron] Loaded .env file from:', envPath)
}
} catch (e) {
console.warn('[Electron] Failed to load .env file:', e)
}
}
loadEnvFile()
// Determine if we're in development
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged
// Window dimensions (vertical ATM display)
const WINDOW_WIDTH = 1080
const WINDOW_HEIGHT = 1920
let mainWindow: BrowserWindow | null = null
function createWindow() {
mainWindow = new BrowserWindow({
width: WINDOW_WIDTH,
height: WINDOW_HEIGHT,
resizable: isDev, // Allow resize in dev for debugging
frame: isDev, // Show window frame in dev
kiosk: !isDev, // Kiosk mode in production
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
})
// Load the app
if (isDev) {
// Development: load from Vite dev server
const devServerUrl = process.env.VITE_DEV_SERVER_URL || 'http://localhost:1420'
mainWindow.loadURL(devServerUrl)
mainWindow.webContents.openDevTools()
} else {
// Production: load from built files
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'))
}
mainWindow.on('closed', () => {
mainWindow = null
})
}
// IPC Handlers
ipcMain.handle('get-version', () => {
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',
extensionApiUrl: process.env.VITE_EXTENSION_API_URL || 'http://localhost:1777',
atmPrivateKey: process.env.VITE_ATM_PRIVATE_KEY || '',
adminToken: process.env.VITE_ADMIN_TOKEN || '',
appId: process.env.VITE_APP_ID || '',
// 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.whenReady().then(() => {
createWindow()
app.on('activate', () => {
// macOS: re-create window when dock icon clicked
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
}
})
})
app.on('window-all-closed', () => {
// Quit on all platforms (ATM doesn't need macOS dock behavior)
app.quit()
})
// Security: prevent new window creation
app.on('web-contents-created', (_, contents) => {
contents.setWindowOpenHandler(() => {
return { action: 'deny' }
})
})

View file

@ -0,0 +1,50 @@
/**
* Electron Preload Script
*
* Exposes a secure API to the renderer process via contextBridge.
* This is the only way for the Vue app to communicate with the main process.
*/
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
extensionApiUrl: string
atmPrivateKey: string
adminToken: string
appId: string
machineModel: string
fiatCode: string
validatorDevice?: string
dispenserDevice?: string
cassettes?: string
}
// Expose protected methods to renderer
contextBridge.exposeInMainWorld('electronAPI', {
// Get app 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: process.platform,
})
// Type declaration for the exposed API
declare global {
interface Window {
electronAPI: {
getVersion: () => Promise<string>
getConfig: () => Promise<RuntimeConfig>
platform: NodeJS.Platform
}
}
}

View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"outDir": "../dist-electron",
"rootDir": "."
},
"include": ["main.ts"],
"exclude": ["preload.ts"]
}

View file

@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"outDir": "../dist-electron",
"rootDir": "."
},
"include": ["preload.ts"]
}