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:
@@ -2,12 +2,20 @@ const { app, BrowserWindow, ipcMain, shell } = require('electron')
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
const { execFile } = require('child_process')
|
const { execFile } = require('child_process')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
|
const os = require('os')
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV === 'development'
|
const isDev = process.env.NODE_ENV === 'development'
|
||||||
const VITE_PORT = 5173
|
const VITE_PORT = 5173
|
||||||
|
|
||||||
let mainWindow = null
|
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 ──────────────────────────────────────────────────
|
// ── Resolve Python executable ──────────────────────────────────────────────────
|
||||||
function findPython() {
|
function findPython() {
|
||||||
const repoRoot = path.join(__dirname, '..', '..')
|
const repoRoot = path.join(__dirname, '..', '..')
|
||||||
@@ -21,9 +29,8 @@ function callPython(command, args = {}) {
|
|||||||
const python = findPython()
|
const python = findPython()
|
||||||
const repoRoot = path.join(__dirname, '..', '..')
|
const repoRoot = path.join(__dirname, '..', '..')
|
||||||
const cliPath = path.join(repoRoot, 'src', 'cli.py')
|
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 }
|
if (err) { reject(new Error(stderr || err.message)); return }
|
||||||
try {
|
try {
|
||||||
const result = JSON.parse(stdout.trim())
|
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('p2wpkh', (_, args) => callPython('p2wpkh', args))
|
||||||
ipcMain.handle('p2tr', (_, args) => callPython('p2tr', 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 ──────────────────────────────────────────────────────────────
|
// ── Create window ──────────────────────────────────────────────────────────────
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
@@ -53,7 +72,7 @@ function createWindow() {
|
|||||||
height: 750,
|
height: 750,
|
||||||
minWidth: 900,
|
minWidth: 900,
|
||||||
minHeight: 600,
|
minHeight: 600,
|
||||||
title: 'Bitcoin Address Generator',
|
title: 'Wallet Generator',
|
||||||
backgroundColor: '#0f1117',
|
backgroundColor: '#0f1117',
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
const { contextBridge, ipcRenderer } = require('electron')
|
const { contextBridge, ipcRenderer } = require('electron')
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electronAPI', {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
hdGenerate: (args) => ipcRenderer.invoke('hd-generate', args),
|
hdGenerate: (args) => ipcRenderer.invoke('hd-generate', args),
|
||||||
hdEncrypt: (args) => ipcRenderer.invoke('hd-encrypt', args),
|
hdEncrypt: (args) => ipcRenderer.invoke('hd-encrypt', args),
|
||||||
hdDecrypt: (args) => ipcRenderer.invoke('hd-decrypt', args),
|
hdDecrypt: (args) => ipcRenderer.invoke('hd-decrypt', args),
|
||||||
p2pk: (args) => ipcRenderer.invoke('p2pk', args),
|
p2pk: (args) => ipcRenderer.invoke('p2pk', args),
|
||||||
p2pkh: (args) => ipcRenderer.invoke('p2pkh', args),
|
p2pkh: (args) => ipcRenderer.invoke('p2pkh', args),
|
||||||
p2sh: (args) => ipcRenderer.invoke('p2sh', args),
|
p2sh: (args) => ipcRenderer.invoke('p2sh', args),
|
||||||
p2wpkh: (args) => ipcRenderer.invoke('p2wpkh', args),
|
p2wpkh: (args) => ipcRenderer.invoke('p2wpkh', args),
|
||||||
p2tr: (args) => ipcRenderer.invoke('p2tr', args),
|
p2tr: (args) => ipcRenderer.invoke('p2tr', args),
|
||||||
|
saveWallet: (filename, data) => ipcRenderer.invoke('save-wallet', { filename, data }),
|
||||||
|
getWalletDir: () => ipcRenderer.invoke('get-wallet-dir'),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/icons/bitcoin.svg" />
|
<link rel="icon" type="image/svg+xml" href="/icons/bitcoin.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Bitcoin Address Generator</title>
|
<title>Wallet Generator</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "bitcoin-address-generator",
|
"name": "wallet-generator",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -51,14 +51,10 @@ export default function HDWallet() {
|
|||||||
if (savePassword.trim()) {
|
if (savePassword.trim()) {
|
||||||
data = await window.electronAPI.hdEncrypt({ wallet, password: savePassword })
|
data = await window.electronAPI.hdEncrypt({ wallet, password: savePassword })
|
||||||
}
|
}
|
||||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
const filename = `wallet_${form.network}_${form.bip_type}_${Date.now()}.json`
|
||||||
const url = URL.createObjectURL(blob)
|
const savedPath = await window.electronAPI.saveWallet(filename, data)
|
||||||
const a = document.createElement('a')
|
const encrypted = savePassword ? ' (encrypted)' : ''
|
||||||
a.href = url
|
setSaveMsg(`Saved${encrypted}: ${savedPath}`)
|
||||||
a.download = `wallet_${form.network}_${form.bip_type}.json`
|
|
||||||
a.click()
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
setSaveMsg(savePassword ? 'Wallet saved (encrypted).' : 'Wallet saved (unencrypted).')
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setSaveMsg('Error: ' + e.message)
|
setSaveMsg('Error: ' + e.message)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default function Sidebar({ active, onSelect }) {
|
|||||||
<circle cx="14" cy="14" r="14" fill="#f7931a"/>
|
<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"/>
|
<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>
|
</svg>
|
||||||
<span>BTC Generator</span>
|
<span>Wallet Generator</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="sidebar-nav">
|
<nav className="sidebar-nav">
|
||||||
|
|||||||
@@ -18,18 +18,24 @@ export default function SingleAddress({ initialTab = 'p2pkh' }) {
|
|||||||
const [result, setResult] = useState(null)
|
const [result, setResult] = useState(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
|
const [filename, setFilename] = useState('')
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [saveMsg, setSaveMsg] = useState(null)
|
||||||
|
|
||||||
const generate = async () => {
|
const generate = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
setResult(null)
|
setResult(null)
|
||||||
|
setSaveMsg(null)
|
||||||
try {
|
try {
|
||||||
let args = { network }
|
let args = { network }
|
||||||
if (tab === 'p2sh') args = { ...args, m, n, compressed }
|
if (tab === 'p2sh') args = { ...args, m, n, compressed }
|
||||||
else if (tab !== 'p2tr') args = { ...args, 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']
|
const fn = window.electronAPI[tab]
|
||||||
setResult(await fn(args))
|
const data = await fn(args)
|
||||||
|
setResult(data)
|
||||||
|
setFilename(`${tab}_${network}_${Date.now()}`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e.message)
|
setError(e.message)
|
||||||
} finally {
|
} 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)
|
const currentTab = TABS.find(t => t.id === tab)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -46,13 +67,12 @@ export default function SingleAddress({ initialTab = 'p2pkh' }) {
|
|||||||
|
|
||||||
<div className="tabs">
|
<div className="tabs">
|
||||||
{TABS.map(t => (
|
{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}
|
{t.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Form ── */}
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-title">{currentTab.label} <span style={{ color: 'var(--text-xs)', fontWeight: 400 }}>— {currentTab.desc}</span></div>
|
<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>}
|
{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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ResultCard({ result, tab }) {
|
function ResultCard({ result, tab }) {
|
||||||
const fields = buildFields(result, tab)
|
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-title">Result</div>
|
<div className="card-title">Result</div>
|
||||||
{tab === 'p2sh' ? (
|
{tab === 'p2sh' ? (
|
||||||
<P2SHResult result={result} />
|
<P2SHResult result={result} />
|
||||||
) : (
|
) : (
|
||||||
fields.map(([label, value]) => (
|
buildFields(result).map(([label, value]) => (
|
||||||
<div className="kv-row" key={label}>
|
<div className="kv-row" key={label}>
|
||||||
<span className="kv-label">{label}</span>
|
<span className="kv-label">{label}</span>
|
||||||
<span className="kv-value">{value}</span>
|
<span className="kv-value">{value}</span>
|
||||||
@@ -120,7 +167,7 @@ function ResultCard({ result, tab }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFields(result, tab) {
|
function buildFields(result) {
|
||||||
const rows = [
|
const rows = [
|
||||||
['Network', result.network],
|
['Network', result.network],
|
||||||
['Script type', result.script_type],
|
['Script type', result.script_type],
|
||||||
|
|||||||
Reference in New Issue
Block a user