- 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
178 lines
6.4 KiB
JavaScript
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()
|
|
})
|
|
|