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() })