feat(single-address): require filename and add optional encrypted JSON export

- Add single-encrypt IPC/CLI flow for private key field encryption
- Keep filename empty after generation and validate it on save
- Reorder single-address tabs to p2pk, p2pkh, p2sh, p2wpkh, p2tr
This commit is contained in:
2026-03-09 14:57:15 +01:00
parent 506741c559
commit 73ce43637d
5 changed files with 69 additions and 8 deletions

View File

@@ -52,6 +52,7 @@ 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('save-wallet', (_, { filename, data }) => {
ensureWalletDir()

View File

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

View File

@@ -2,11 +2,11 @@ import { useState } from 'react'
import CopyButton from './CopyButton'
const TABS = [
{ id: 'p2pk', label: 'P2PK', desc: 'Pay-to-PubKey (no address)' },
{ id: 'p2pkh', label: 'P2PKH', desc: 'Legacy · starts with 1 / m' },
{ id: 'p2sh', label: 'P2SH', desc: 'Multisig · starts with 3 / 2' },
{ id: 'p2wpkh', label: 'P2WPKH', desc: 'Native SegWit · bc1q / tb1q' },
{ id: 'p2tr', label: 'P2TR', desc: 'Taproot · bc1p / tb1p' },
{ id: 'p2pk', label: 'P2PK', desc: 'Pay-to-PubKey (no address)' },
{ id: 'p2sh', label: 'P2SH', desc: 'Multisig · starts with 3 / 2' },
]
export default function SingleAddress({ initialTab = 'p2pkh' }) {
@@ -19,6 +19,7 @@ export default function SingleAddress({ initialTab = 'p2pkh' }) {
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [filename, setFilename] = useState('')
const [savePassword, setSavePassword] = useState('')
const [saving, setSaving] = useState(false)
const [saveMsg, setSaveMsg] = useState(null)
@@ -35,7 +36,8 @@ export default function SingleAddress({ initialTab = 'p2pkh' }) {
const fn = window.electronAPI[tab]
const data = await fn(args)
setResult(data)
setFilename(`${tab}_${network}_${Date.now()}`)
setFilename('')
setSavePassword('')
} catch (e) {
setError(e.message)
} finally {
@@ -45,12 +47,21 @@ export default function SingleAddress({ initialTab = 'p2pkh' }) {
const save = async () => {
if (!result) return
if (!filename.trim()) {
setSaveMsg('Error: Filename is required.')
return
}
setSaving(true)
setSaveMsg(null)
try {
const name = (filename.trim() || `${tab}_${network}`) + '.json'
const savedPath = await window.electronAPI.saveWallet(name, result)
setSaveMsg(`Saved: ${savedPath}`)
let data = result
if (savePassword.trim()) {
data = await window.electronAPI.singleEncrypt({ wallet: result, password: savePassword })
}
const name = filename.trim().endsWith('.json') ? filename.trim() : `${filename.trim()}.json`
const savedPath = await window.electronAPI.saveWallet(name, data)
setSaveMsg(savePassword.trim() ? `Saved (encrypted): ${savedPath}` : `Saved: ${savedPath}`)
} catch (e) {
setSaveMsg('Error: ' + e.message)
} finally {
@@ -67,7 +78,7 @@ 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); setSaveMsg(null) }}>
<button key={t.id} className={`tab${tab === t.id ? ' active' : ''}`} onClick={() => { setTab(t.id); setResult(null); setError(null); setSaveMsg(null); setFilename(''); setSavePassword('') }}>
{t.label}
</button>
))}
@@ -126,12 +137,23 @@ export default function SingleAddress({ initialTab = 'p2pkh' }) {
<label className="form-label">Filename</label>
<input
className="form-input"
placeholder={`${tab}_${network}`}
placeholder="Required"
value={filename}
onChange={e => setFilename(e.target.value)}
/>
<span className="form-hint">.json · saved to ~/.wallet-generator/wallet/</span>
</div>
<div className="form-group">
<label className="form-label">Encryption password (optional)</label>
<input
className="form-input"
type="password"
placeholder="Leave blank to save unencrypted"
value={savePassword}
onChange={e => setSavePassword(e.target.value)}
/>
<span className="form-hint">Encrypts private keys in JSON</span>
</div>
</div>
<button className="btn btn-secondary" onClick={save} disabled={saving}>
{saving ? <><span className="spinner" style={{borderTopColor:'var(--text)'}} /> Saving</> : 'Save JSON'}

View File

@@ -14,6 +14,7 @@ try:
from src.p2sh import generate_p2sh_multisig
from src.p2wpkh import generate_segwit_address
from src.p2tr import generate_p2tr_address
from src.single_wallet import encrypt_single_wallet
except ImportError:
from hd_wallet import generate_hd_wallet, encrypt_wallet, decrypt_wallet
from p2pk import generate_p2pk
@@ -21,6 +22,7 @@ except ImportError:
from p2sh import generate_p2sh_multisig
from p2wpkh import generate_segwit_address
from p2tr import generate_p2tr_address
from single_wallet import encrypt_single_wallet
COMMANDS = {
'hd_generate': lambda a: generate_hd_wallet(**a),
@@ -31,6 +33,7 @@ COMMANDS = {
'p2sh': lambda a: generate_p2sh_multisig(**a),
'p2wpkh': lambda a: generate_segwit_address(**a),
'p2tr': lambda a: generate_p2tr_address(**a),
'single_encrypt': lambda a: encrypt_single_wallet(a['wallet'], a['password']),
}
if __name__ == '__main__':

34
src/single_wallet.py Normal file
View File

@@ -0,0 +1,34 @@
import copy
from typing import Any, Dict
try:
from src.crypto import pw_encode
except ImportError:
from crypto import pw_encode
SENSITIVE_FIELDS = {'private_key_hex', 'private_key_wif'}
def _encrypt_sensitive(data: Any, password: str) -> Any:
if isinstance(data, dict):
out = {}
for key, value in data.items():
if key in SENSITIVE_FIELDS and isinstance(value, str):
out[key] = pw_encode(value, password)
else:
out[key] = _encrypt_sensitive(value, password)
return out
if isinstance(data, list):
return [_encrypt_sensitive(item, password) for item in data]
return data
def encrypt_single_wallet(wallet: Dict[str, Any], password: str) -> Dict[str, Any]:
if not password:
raise ValueError("Password is required for encryption.")
encrypted = _encrypt_sensitive(copy.deepcopy(wallet), password)
if isinstance(encrypted, dict):
encrypted['use_encryption'] = True
return encrypted