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:
@@ -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()
|
||||
|
||||
@@ -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'),
|
||||
})
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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
34
src/single_wallet.py
Normal 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
|
||||
Reference in New Issue
Block a user