feat: rename app to Wallet Generator and add wallet save to disk

- Rename app to 'Wallet Generator' (index.html, window title, sidebar, package.json)
- Add ~/.wallet-generator/wallet/ as save directory for all generated wallets
- Add save-wallet and get-wallet-dir IPC handlers in Electron main process
- Expose saveWallet/getWalletDir via preload contextBridge
- HDWallet: save JSON to disk with optional encryption, show saved path
- SingleAddress: add Save card with editable filename for all address types
This commit is contained in:
2026-03-09 14:31:40 +01:00
parent aeda67f72a
commit ca133132f6
7 changed files with 94 additions and 30 deletions

View File

@@ -2,12 +2,20 @@ 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_DIR = path.join(os.homedir(), '.wallet-generator', 'wallet')
function ensureWalletDir() {
fs.mkdirSync(WALLET_DIR, { recursive: true })
}
// ── Resolve Python executable ──────────────────────────────────────────────────
function findPython() {
const repoRoot = path.join(__dirname, '..', '..')
@@ -21,9 +29,8 @@ function callPython(command, args = {}) {
const python = findPython()
const repoRoot = path.join(__dirname, '..', '..')
const cliPath = path.join(repoRoot, 'src', 'cli.py')
const argsJson = JSON.stringify(args)
execFile(python, [cliPath, command, argsJson], { cwd: repoRoot }, (err, stdout, stderr) => {
execFile(python, [cliPath, command, JSON.stringify(args)], { cwd: repoRoot }, (err, stdout, stderr) => {
if (err) { reject(new Error(stderr || err.message)); return }
try {
const result = JSON.parse(stdout.trim())
@@ -46,6 +53,18 @@ ipcMain.handle('p2sh', (_, args) => callPython('p2sh', args))
ipcMain.handle('p2wpkh', (_, args) => callPython('p2wpkh', args))
ipcMain.handle('p2tr', (_, args) => callPython('p2tr', args))
ipcMain.handle('save-wallet', (_, { filename, data }) => {
ensureWalletDir()
const filePath = path.join(WALLET_DIR, filename)
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8')
return filePath
})
ipcMain.handle('get-wallet-dir', () => {
ensureWalletDir()
return WALLET_DIR
})
// ── Create window ──────────────────────────────────────────────────────────────
function createWindow() {
mainWindow = new BrowserWindow({
@@ -53,7 +72,7 @@ function createWindow() {
height: 750,
minWidth: 900,
minHeight: 600,
title: 'Bitcoin Address Generator',
title: 'Wallet Generator',
backgroundColor: '#0f1117',
webPreferences: {
nodeIntegration: false,

View File

@@ -9,4 +9,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
p2sh: (args) => ipcRenderer.invoke('p2sh', args),
p2wpkh: (args) => ipcRenderer.invoke('p2wpkh', args),
p2tr: (args) => ipcRenderer.invoke('p2tr', args),
saveWallet: (filename, data) => ipcRenderer.invoke('save-wallet', { filename, data }),
getWalletDir: () => ipcRenderer.invoke('get-wallet-dir'),
})

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icons/bitcoin.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bitcoin Address Generator</title>
<title>Wallet Generator</title>
</head>
<body>
<div id="root"></div>

View File

@@ -1,5 +1,5 @@
{
"name": "bitcoin-address-generator",
"name": "wallet-generator",
"private": true,
"version": "1.0.0",
"type": "module",

View File

@@ -51,14 +51,10 @@ export default function HDWallet() {
if (savePassword.trim()) {
data = await window.electronAPI.hdEncrypt({ wallet, password: savePassword })
}
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `wallet_${form.network}_${form.bip_type}.json`
a.click()
URL.revokeObjectURL(url)
setSaveMsg(savePassword ? 'Wallet saved (encrypted).' : 'Wallet saved (unencrypted).')
const filename = `wallet_${form.network}_${form.bip_type}_${Date.now()}.json`
const savedPath = await window.electronAPI.saveWallet(filename, data)
const encrypted = savePassword ? ' (encrypted)' : ''
setSaveMsg(`Saved${encrypted}: ${savedPath}`)
} catch (e) {
setSaveMsg('Error: ' + e.message)
} finally {

View File

@@ -17,7 +17,7 @@ export default function Sidebar({ active, onSelect }) {
<circle cx="14" cy="14" r="14" fill="#f7931a"/>
<path d="M18.5 11.8c.3-1.9-1.2-2.9-3.2-3.6l.7-2.6-1.6-.4-.6 2.5c-.4-.1-.9-.2-1.3-.3l.6-2.5-1.6-.4-.7 2.6-1-.3-2.2-.5-.4 1.7s1.2.3 1.1.3c.6.1.7.5.7.8l-1.7 6.9c-.1.2-.3.5-.8.4.0 0-1.1-.3-1.1-.3l-.8 1.8 2.1.5.9.3-.7 2.7 1.6.4.7-2.6c.4.1.9.2 1.3.3l-.7 2.6 1.6.4.7-2.7c2.9.5 5.1.3 6-2.3.7-2.1-.0-3.3-1.5-4.1 1.1-.3 1.9-1 2.1-2.5zm-3.8 5.3c-.5 2-3.9 1-5 .7l.9-3.5c1.1.3 4.7.8 4.1 2.8zm.5-5.3c-.5 1.8-3.3 1-4.3.7l.8-3.2c1 .3 4.1.7 3.5 2.5z" fill="white"/>
</svg>
<span>BTC Generator</span>
<span>Wallet Generator</span>
</div>
<nav className="sidebar-nav">

View File

@@ -18,18 +18,24 @@ export default function SingleAddress({ initialTab = 'p2pkh' }) {
const [result, setResult] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [filename, setFilename] = useState('')
const [saving, setSaving] = useState(false)
const [saveMsg, setSaveMsg] = useState(null)
const generate = async () => {
setLoading(true)
setError(null)
setResult(null)
setSaveMsg(null)
try {
let args = { network }
if (tab === 'p2sh') args = { ...args, m, n, compressed }
else if (tab !== 'p2tr') args = { ...args, compressed }
const fn = window.electronAPI[tab === 'p2pkh' ? 'p2pkh' : tab === 'p2wpkh' ? 'p2wpkh' : tab === 'p2tr' ? 'p2tr' : tab === 'p2pk' ? 'p2pk' : 'p2sh']
setResult(await fn(args))
const fn = window.electronAPI[tab]
const data = await fn(args)
setResult(data)
setFilename(`${tab}_${network}_${Date.now()}`)
} catch (e) {
setError(e.message)
} finally {
@@ -37,6 +43,21 @@ export default function SingleAddress({ initialTab = 'p2pkh' }) {
}
}
const save = async () => {
if (!result) return
setSaving(true)
setSaveMsg(null)
try {
const name = (filename.trim() || `${tab}_${network}`) + '.json'
const savedPath = await window.electronAPI.saveWallet(name, result)
setSaveMsg(`Saved: ${savedPath}`)
} catch (e) {
setSaveMsg('Error: ' + e.message)
} finally {
setSaving(false)
}
}
const currentTab = TABS.find(t => t.id === tab)
return (
@@ -46,13 +67,12 @@ export default function SingleAddress({ initialTab = 'p2pkh' }) {
<div className="tabs">
{TABS.map(t => (
<button key={t.id} className={`tab${tab === t.id ? ' active' : ''}`} onClick={() => { setTab(t.id); setResult(null); setError(null) }}>
<button key={t.id} className={`tab${tab === t.id ? ' active' : ''}`} onClick={() => { setTab(t.id); setResult(null); setError(null); setSaveMsg(null) }}>
{t.label}
</button>
))}
</div>
{/* ── Form ── */}
<div className="card">
<div className="card-title">{currentTab.label} <span style={{ color: 'var(--text-xs)', fontWeight: 400 }}> {currentTab.desc}</span></div>
@@ -95,20 +115,47 @@ export default function SingleAddress({ initialTab = 'p2pkh' }) {
{error && <div className="alert alert-error">{error}</div>}
{result && <ResultCard result={result} tab={tab} />}
{result && (
<>
<ResultCard result={result} tab={tab} />
<div className="card">
<div className="card-title">Save</div>
<div className="form-grid" style={{ marginBottom: 12 }}>
<div className="form-group">
<label className="form-label">Filename</label>
<input
className="form-input"
placeholder={`${tab}_${network}`}
value={filename}
onChange={e => setFilename(e.target.value)}
/>
<span className="form-hint">.json · saved to ~/.wallet-generator/wallet/</span>
</div>
</div>
<button className="btn btn-secondary" onClick={save} disabled={saving}>
{saving ? <><span className="spinner" style={{borderTopColor:'var(--text)'}} /> Saving</> : '💾 Save JSON'}
</button>
{saveMsg && (
<div className={`alert ${saveMsg.startsWith('Error') ? 'alert-error' : 'alert-success'}`} style={{ marginTop: 12, marginBottom: 0 }}>
{saveMsg}
</div>
)}
</div>
</>
)}
</div>
)
}
function ResultCard({ result, tab }) {
const fields = buildFields(result, tab)
return (
<div className="card">
<div className="card-title">Result</div>
{tab === 'p2sh' ? (
<P2SHResult result={result} />
) : (
fields.map(([label, value]) => (
buildFields(result).map(([label, value]) => (
<div className="kv-row" key={label}>
<span className="kv-label">{label}</span>
<span className="kv-value">{value}</span>
@@ -120,7 +167,7 @@ function ResultCard({ result, tab }) {
)
}
function buildFields(result, tab) {
function buildFields(result) {
const rows = [
['Network', result.network],
['Script type', result.script_type],