Files
easy-wallet/frontend/electron/main.cjs
Davide Grilli 0a81f0db23 feat(linux): bundle Python CLI and fix Electron packaged build
- contrib/linux/Dockerfile: add libpython3.11 + PyInstaller to build
  standalone cli binary; fix venv isolation via .dockerignore
- frontend/electron/main.cjs: use bundled cli binary in prod via
  process.resourcesPath, fallback to venv/python3 in dev
- frontend/package.json: add extraResources for cli binary,
  set output dir to release/
- frontend/vite.config.js: set base './' for file:// protocol in prod
- .dockerignore: exclude venv/, node_modules/, dist/, .git/
- .gitignore: ignore release/ output directories
2026-03-10 09:00:27 +01:00

178 lines
6.4 KiB
JavaScript

const { app, BrowserWindow, ipcMain, shell } = require('electron')
const path = require('path')
const { execFile } = require('child_process')
const fs = require('fs')
const os = require('os')
const isDev = process.env.NODE_ENV === 'development'
const VITE_PORT = 5173
let mainWindow = null
// ── Wallet storage directory ───────────────────────────────────────────────────
const WALLET_ROOT_DIR = path.join(os.homedir(), '.wallet-generator')
const WALLET_DIRS = {
hd: path.join(WALLET_ROOT_DIR, 'hdwallet'),
single: path.join(WALLET_ROOT_DIR, 'single'),
}
function ensureWalletDir(kind) {
const walletDir = WALLET_DIRS[kind]
if (!walletDir) {
throw new Error(`Unsupported wallet kind: '${kind}'. Use 'hd' or 'single'.`)
}
fs.mkdirSync(walletDir, { recursive: true })
return walletDir
}
function normalizeFilename(filename) {
if (typeof filename !== 'string') {
throw new Error('Filename must be a string.')
}
const trimmed = filename.trim()
const safeName = path.basename(trimmed)
if (!safeName || safeName !== trimmed) {
throw new Error('Invalid filename.')
}
return safeName
}
function resolveWalletFile(kind, filename) {
const walletDir = ensureWalletDir(kind)
const safeName = normalizeFilename(filename)
const walletDirAbs = path.resolve(walletDir)
const filePath = path.resolve(walletDir, safeName)
if (!filePath.startsWith(walletDirAbs + path.sep)) {
throw new Error('Invalid file path.')
}
return filePath
}
// ── Resolve CLI command (dev: python + cli.py, prod: bundled binary) ──────────
function getCliCommand() {
if (!isDev) {
const bundled = path.join(process.resourcesPath, 'cli')
if (fs.existsSync(bundled)) return { exec: bundled, baseArgs: [] }
}
const repoRoot = path.join(__dirname, '..', '..')
const python = fs.existsSync(path.join(repoRoot, 'venv', 'bin', 'python'))
? path.join(repoRoot, 'venv', 'bin', 'python')
: 'python3'
return { exec: python, baseArgs: [path.join(repoRoot, 'src', 'cli.py')] }
}
// ── Call Python CLI ────────────────────────────────────────────────────────────
function callPython(command, args = {}) {
return new Promise((resolve, reject) => {
const { exec, baseArgs } = getCliCommand()
execFile(exec, [...baseArgs, command, JSON.stringify(args)], (err, stdout, stderr) => {
if (err) { reject(new Error(stderr || err.message)); return }
try {
const result = JSON.parse(stdout.trim())
if (result.ok) resolve(result.data)
else reject(new Error(result.error))
} catch {
reject(new Error('Invalid Python output: ' + stdout))
}
})
})
}
// ── IPC handlers ───────────────────────────────────────────────────────────────
ipcMain.handle('hd-generate', (_, args) => callPython('hd_generate', args))
ipcMain.handle('hd-encrypt', (_, args) => callPython('hd_encrypt', args))
ipcMain.handle('hd-decrypt', (_, args) => callPython('hd_decrypt', args))
ipcMain.handle('p2pk', (_, args) => callPython('p2pk', args))
ipcMain.handle('p2pkh', (_, args) => callPython('p2pkh', args))
ipcMain.handle('p2sh', (_, args) => callPython('p2sh', args))
ipcMain.handle('p2wpkh', (_, args) => callPython('p2wpkh', args))
ipcMain.handle('p2tr', (_, args) => callPython('p2tr', args))
ipcMain.handle('single-encrypt', (_, args) => callPython('single_encrypt', args))
ipcMain.handle('single-decrypt', (_, args) => callPython('single_decrypt', args))
ipcMain.handle('save-wallet', (_, { filename, data, kind }) => {
const filePath = resolveWalletFile(kind, filename)
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8')
return filePath
})
ipcMain.handle('list-wallets', () => {
const out = { hd: [], single: [] }
for (const kind of Object.keys(WALLET_DIRS)) {
const walletDir = ensureWalletDir(kind)
const files = fs.readdirSync(walletDir, { withFileTypes: true })
.filter(entry => entry.isFile() && entry.name.toLowerCase().endsWith('.json'))
.map(entry => {
const filePath = path.join(walletDir, entry.name)
const stat = fs.statSync(filePath)
return {
name: entry.name,
size: stat.size,
mtimeMs: stat.mtimeMs,
}
})
.sort((a, b) => b.mtimeMs - a.mtimeMs)
out[kind] = files
}
return out
})
ipcMain.handle('read-wallet', (_, { kind, filename }) => {
const filePath = resolveWalletFile(kind, filename)
if (!fs.existsSync(filePath)) {
throw new Error('Wallet file not found.')
}
try {
const content = fs.readFileSync(filePath, 'utf-8')
return JSON.parse(content)
} catch (err) {
throw new Error(`Invalid wallet JSON: ${err.message}`)
}
})
ipcMain.handle('get-wallet-dir', () => {
fs.mkdirSync(WALLET_ROOT_DIR, { recursive: true })
return WALLET_ROOT_DIR
})
// ── Create window ──────────────────────────────────────────────────────────────
function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 750,
minWidth: 320,
minHeight: 480,
title: 'wallet-gen',
backgroundColor: '#0f1117',
icon: path.join(__dirname, '..', 'public', 'icons', 'icon.png'),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.cjs'),
},
})
mainWindow.setMenuBarVisibility(false)
const url = isDev
? `http://localhost:${VITE_PORT}`
: `file://${path.join(__dirname, '..', 'dist', 'index.html')}`
mainWindow.loadURL(url)
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url)
return { action: 'deny' }
})
mainWindow.on('closed', () => { mainWindow = null })
}
// ── App lifecycle ──────────────────────────────────────────────────────────────
app.whenReady().then(createWindow)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})