feat(wallet): add wallet viewer, secure file reads, and enforce save validation

- Introduce Wallet Viewer with HD/Single list, read, and decrypt support
- Add single_decrypt backend command and IPC bridge methods
- Require filename on save and add password confirm + visibility toggle"
This commit is contained in:
2026-03-09 16:16:27 +01:00
parent aad81e1cf3
commit 55284f95b7
10 changed files with 540 additions and 15 deletions

View File

@@ -25,6 +25,29 @@ function ensureWalletDir(kind) {
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 Python executable ──────────────────────────────────────────────────
function findPython() {
const repoRoot = path.join(__dirname, '..', '..')
@@ -62,14 +85,48 @@ 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 walletDir = ensureWalletDir(kind)
const filePath = path.join(walletDir, filename)
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

View File

@@ -10,6 +10,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
p2wpkh: (args) => ipcRenderer.invoke('p2wpkh', args),
p2tr: (args) => ipcRenderer.invoke('p2tr', args),
singleEncrypt:(args) => ipcRenderer.invoke('single-encrypt', args),
singleDecrypt:(args) => ipcRenderer.invoke('single-decrypt', args),
listWallets: () => ipcRenderer.invoke('list-wallets'),
readWallet: (args) => ipcRenderer.invoke('read-wallet', args),
saveWallet: (filename, data, kind) => ipcRenderer.invoke('save-wallet', { filename, data, kind }),
getWalletDir: () => ipcRenderer.invoke('get-wallet-dir'),
})

View File

@@ -2,6 +2,7 @@ import { useState } from 'react'
import Sidebar from './components/Sidebar'
import HDWallet from './components/HDWallet'
import SingleAddress from './components/SingleAddress'
import WalletViewer from './components/WalletViewer'
export default function App() {
const [page, setPage] = useState('hd')
@@ -9,6 +10,7 @@ export default function App() {
const renderPage = () => {
if (page === 'hd') return <HDWallet />
if (page === 'single') return <SingleAddress />
if (page === 'viewer') return <WalletViewer />
return null
}

View File

@@ -14,7 +14,10 @@ export default function HDWallet() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [seedVisible, setSeedVisible] = useState(false)
const [saveFilename, setSaveFilename] = useState('')
const [savePassword, setSavePassword] = useState('')
const [savePasswordConfirm, setSavePasswordConfirm] = useState('')
const [showSavePassword, setShowSavePassword] = useState(false)
const [saving, setSaving] = useState(false)
const [saveMsg, setSaveMsg] = useState(null)
@@ -25,6 +28,10 @@ export default function HDWallet() {
setError(null)
setWallet(null)
setSeedVisible(false)
setSaveFilename('')
setSavePassword('')
setSavePasswordConfirm('')
setShowSavePassword(false)
try {
const data = await window.electronAPI.hdGenerate({
network: form.network,
@@ -44,6 +51,21 @@ export default function HDWallet() {
const saveWallet = async () => {
if (!wallet) return
if (!saveFilename.trim()) {
setSaveMsg('Error: Filename is required.')
return
}
if (savePassword.trim() || savePasswordConfirm.trim()) {
if (!savePassword.trim() || !savePasswordConfirm.trim()) {
setSaveMsg('Error: Enter encryption password in both fields.')
return
}
if (savePassword !== savePasswordConfirm) {
setSaveMsg('Error: Encryption passwords do not match.')
return
}
}
setSaving(true)
setSaveMsg(null)
try {
@@ -51,7 +73,9 @@ export default function HDWallet() {
if (savePassword.trim()) {
data = await window.electronAPI.hdEncrypt({ wallet, password: savePassword })
}
const filename = `wallet_${form.network}_${form.bip_type}_${Date.now()}.json`
const filename = saveFilename.trim().endsWith('.json')
? saveFilename.trim()
: `${saveFilename.trim()}.json`
const savedPath = await window.electronAPI.saveWallet(filename, data, 'hd')
const encrypted = savePassword ? ' (encrypted)' : ''
setSaveMsg(`Saved${encrypted}: ${savedPath}`)
@@ -191,21 +215,50 @@ export default function HDWallet() {
<div className="card">
<div className="card-title">Save Wallet</div>
<div className="form-grid" style={{ marginBottom: 12 }}>
<div className="form-group">
<label className="form-label">Filename</label>
<input
className="form-input"
placeholder="Required"
value={saveFilename}
onChange={e => setSaveFilename(e.target.value)}
/>
<span className="form-hint">.json · saved to ~/.wallet-generator/hdwallet/</span>
</div>
<div className="form-group">
<label className="form-label">Encryption password (optional)</label>
<input
className="form-input"
type="password"
type={showSavePassword ? 'text' : 'password'}
placeholder="Leave blank to save unencrypted"
value={savePassword}
onChange={e => setSavePassword(e.target.value)}
/>
</div>
<div className="form-group">
<label className="form-label">Confirm encryption password</label>
<input
className="form-input"
type={showSavePassword ? 'text' : 'password'}
placeholder="Repeat encryption password"
value={savePasswordConfirm}
onChange={e => setSavePasswordConfirm(e.target.value)}
/>
<span className="form-hint">AES-256-CBC · Electrum-compatible</span>
</div>
</div>
<div className="btn-row">
<div className="checkbox-group" style={{ marginBottom: 12 }}>
<input
type="checkbox"
id="show-hd-save-password"
checked={showSavePassword}
onChange={e => setShowSavePassword(e.target.checked)}
/>
<label htmlFor="show-hd-save-password">Show encryption password</label>
</div>
<div className="btn-row save-actions">
<button className="btn btn-secondary" onClick={saveWallet} disabled={saving}>
{saving ? <><span className="spinner" style={{borderTopColor:'var(--text)'}} /> Saving</> : 'Download JSON'}
{saving ? <><span className="spinner" style={{borderTopColor:'var(--text)'}} /> Saving</> : 'Save JSON'}
</button>
</div>
{saveMsg && (

View File

@@ -1,6 +1,7 @@
const PAGES = [
{ id: 'hd', label: 'HD Wallet' },
{ id: 'single', label: 'Single Addresses' },
{ id: 'viewer', label: 'Wallet Viewer' },
]
export default function Sidebar({ active, onSelect }) {

View File

@@ -20,6 +20,8 @@ export default function SingleAddress({ initialTab = 'p2pkh' }) {
const [error, setError] = useState(null)
const [filename, setFilename] = useState('')
const [savePassword, setSavePassword] = useState('')
const [savePasswordConfirm, setSavePasswordConfirm] = useState('')
const [showSavePassword, setShowSavePassword] = useState(false)
const [saving, setSaving] = useState(false)
const [saveMsg, setSaveMsg] = useState(null)
@@ -38,6 +40,8 @@ export default function SingleAddress({ initialTab = 'p2pkh' }) {
setResult(data)
setFilename('')
setSavePassword('')
setSavePasswordConfirm('')
setShowSavePassword(false)
} catch (e) {
setError(e.message)
} finally {
@@ -51,6 +55,16 @@ export default function SingleAddress({ initialTab = 'p2pkh' }) {
setSaveMsg('Error: Filename is required.')
return
}
if (savePassword.trim() || savePasswordConfirm.trim()) {
if (!savePassword.trim() || !savePasswordConfirm.trim()) {
setSaveMsg('Error: Enter encryption password in both fields.')
return
}
if (savePassword !== savePasswordConfirm) {
setSaveMsg('Error: Encryption passwords do not match.')
return
}
}
setSaving(true)
setSaveMsg(null)
@@ -78,7 +92,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); setFilename(''); setSavePassword('') }}>
<button key={t.id} className={`tab${tab === t.id ? ' active' : ''}`} onClick={() => { setTab(t.id); setResult(null); setError(null); setSaveMsg(null); setFilename(''); setSavePassword(''); setSavePasswordConfirm(''); setShowSavePassword(false) }}>
{t.label}
</button>
))}
@@ -147,17 +161,38 @@ export default function SingleAddress({ initialTab = 'p2pkh' }) {
<label className="form-label">Encryption password (optional)</label>
<input
className="form-input"
type="password"
type={showSavePassword ? 'text' : 'password'}
placeholder="Leave blank to save unencrypted"
value={savePassword}
onChange={e => setSavePassword(e.target.value)}
/>
</div>
<div className="form-group">
<label className="form-label">Confirm encryption password</label>
<input
className="form-input"
type={showSavePassword ? 'text' : 'password'}
placeholder="Repeat encryption password"
value={savePasswordConfirm}
onChange={e => setSavePasswordConfirm(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'}
</button>
<div className="checkbox-group" style={{ marginBottom: 12 }}>
<input
type="checkbox"
id="show-single-save-password"
checked={showSavePassword}
onChange={e => setShowSavePassword(e.target.checked)}
/>
<label htmlFor="show-single-save-password">Show encryption password</label>
</div>
<div className="btn-row save-actions">
<button className="btn btn-secondary" onClick={save} disabled={saving}>
{saving ? <><span className="spinner" style={{borderTopColor:'var(--text)'}} /> Saving</> : 'Save JSON'}
</button>
</div>
{saveMsg && (
<div className={`alert ${saveMsg.startsWith('Error') ? 'alert-error' : 'alert-success'}`} style={{ marginTop: 12, marginBottom: 0 }}>
{saveMsg}

View File

@@ -0,0 +1,342 @@
import { useEffect, useMemo, useState } from 'react'
import CopyButton from './CopyButton'
function formatBytes(bytes) {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
function formatDate(ms) {
return new Date(ms).toLocaleString()
}
function isHDWallet(wallet) {
return Boolean(wallet?.keystore && wallet?.addresses)
}
function buildSingleRows(result) {
const rows = [
['Network', result.network],
['Script type', result.script_type],
]
if (result.address) rows.push(['Address', result.address])
if (result.public_key_hex) rows.push(['Public key (hex)', result.public_key_hex])
if (result.internal_pubkey_x_hex) rows.push(['Internal pubkey (x)', result.internal_pubkey_x_hex])
if (result.private_key_wif) rows.push(['Private key (WIF)', result.private_key_wif])
if (result.private_key_hex) rows.push(['Private key (hex)', result.private_key_hex])
return rows
}
export default function WalletViewer() {
const [files, setFiles] = useState({ hd: [], single: [] })
const [loadingFiles, setLoadingFiles] = useState(false)
const [loadingWallet, setLoadingWallet] = useState(false)
const [loadingDecrypt, setLoadingDecrypt] = useState(false)
const [selected, setSelected] = useState(null)
const [wallet, setWallet] = useState(null)
const [decryptedWallet, setDecryptedWallet] = useState(null)
const [password, setPassword] = useState('')
const [error, setError] = useState(null)
const visibleWallet = decryptedWallet || wallet
const walletKind = useMemo(() => (isHDWallet(visibleWallet) ? 'hd' : 'single'), [visibleWallet])
const refreshWallets = async () => {
setLoadingFiles(true)
setError(null)
try {
const data = await window.electronAPI.listWallets()
setFiles({ hd: data.hd || [], single: data.single || [] })
} catch (e) {
setError(e.message)
} finally {
setLoadingFiles(false)
}
}
useEffect(() => {
refreshWallets()
}, [])
const openWallet = async (kind, filename) => {
setLoadingWallet(true)
setLoadingDecrypt(false)
setError(null)
setPassword('')
setDecryptedWallet(null)
setWallet(null)
setSelected({ kind, filename })
try {
const data = await window.electronAPI.readWallet({ kind, filename })
setWallet(data)
if (!data?.use_encryption) {
setDecryptedWallet(data)
}
} catch (e) {
setError(e.message)
} finally {
setLoadingWallet(false)
}
}
const decryptWallet = async () => {
if (!wallet?.use_encryption || !selected) return
setLoadingDecrypt(true)
setError(null)
try {
const fn = selected.kind === 'hd' ? window.electronAPI.hdDecrypt : window.electronAPI.singleDecrypt
const data = await fn({ wallet, password })
setDecryptedWallet(data)
} catch (e) {
setError(e.message)
} finally {
setLoadingDecrypt(false)
}
}
return (
<div>
<div className="page-title">Wallet Viewer</div>
<div className="page-subtitle">Read and decrypt HD and Single wallet JSON files</div>
<div className="card">
<div className="card-title" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Wallet Files</span>
<button className="btn btn-secondary" onClick={refreshWallets} disabled={loadingFiles}>
{loadingFiles ? 'Refreshing…' : 'Refresh'}
</button>
</div>
<div className="form-grid">
<FileColumn
title="HD Wallets"
files={files.hd}
kind="hd"
selected={selected}
onOpen={openWallet}
/>
<FileColumn
title="Single Wallets"
files={files.single}
kind="single"
selected={selected}
onOpen={openWallet}
/>
</div>
</div>
{error && <div className="alert alert-error">{error}</div>}
{selected && (
<div className="card">
<div className="card-title">Selected Wallet</div>
<div className="kv-row">
<span className="kv-label">File</span>
<span className="kv-value">{selected.filename}</span>
</div>
<div className="kv-row">
<span className="kv-label">Source</span>
<span className="kv-value">{selected.kind === 'hd' ? 'HD folder' : 'Single folder'}</span>
</div>
<div className="kv-row">
<span className="kv-label">Encrypted</span>
<span className="kv-value">{wallet?.use_encryption ? 'yes' : 'no'}</span>
</div>
{loadingWallet && <div className="alert alert-info" style={{ marginTop: 12, marginBottom: 0 }}>Loading wallet</div>}
</div>
)}
{wallet?.use_encryption && !decryptedWallet && (
<div className="card">
<div className="card-title">Decrypt Wallet</div>
<div className="form-group" style={{ marginBottom: 14 }}>
<label className="form-label">Password</label>
<input
className="form-input"
type="password"
placeholder="Wallet encryption password"
value={password}
onChange={e => setPassword(e.target.value)}
onKeyDown={e => e.key === 'Enter' && decryptWallet()}
/>
</div>
<button className="btn btn-primary" onClick={decryptWallet} disabled={loadingDecrypt || !password.trim()}>
{loadingDecrypt ? 'Decrypting…' : 'Decrypt'}
</button>
</div>
)}
{visibleWallet && !loadingWallet && (
walletKind === 'hd'
? <HDWalletView wallet={visibleWallet} />
: <SingleWalletView wallet={visibleWallet} />
)}
</div>
)
}
function FileColumn({ title, files, kind, selected, onOpen }) {
return (
<div className="form-group">
<label className="form-label" style={{ marginBottom: 4 }}>{title}</label>
{files.length === 0 && (
<span className="form-hint">No wallet files found.</span>
)}
{files.map(file => {
const isActive = selected?.kind === kind && selected?.filename === file.name
return (
<button
key={file.name}
className="btn btn-secondary"
onClick={() => onOpen(kind, file.name)}
style={{
width: '100%',
justifyContent: 'space-between',
borderColor: isActive ? 'var(--accent)' : undefined,
color: isActive ? 'var(--accent)' : undefined,
marginBottom: 6,
}}
>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{file.name}</span>
<span style={{ fontSize: 11, color: 'var(--text-xs)' }}>
{formatBytes(file.size)} · {formatDate(file.mtimeMs)}
</span>
</button>
)
})}
</div>
)
}
function HDWalletView({ wallet }) {
const [seedVisible, setSeedVisible] = useState(false)
const ks = wallet?.keystore
if (!ks) return null
return (
<>
<div className="card">
<div className="card-title" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Seed Phrase</span>
<div className="btn-row">
<CopyButton text={ks.seed} />
<button className="btn btn-ghost btn-icon" onClick={() => setSeedVisible(v => !v)}>
{seedVisible ? 'Hide' : 'Show'}
</button>
</div>
</div>
<div className="seed-words">
{String(ks.seed || '').split(' ').map((word, idx) => (
<span key={idx} className={`seed-word${seedVisible ? '' : ' hidden'}`}>
<span>{idx + 1}.</span>{word}
</span>
))}
</div>
</div>
<div className="card">
<div className="card-title">Keystore</div>
{[
['Derivation', ks.derivation],
['Root fingerprint', ks.root_fingerprint],
['xpub', ks.xpub],
['xprv', ks.xprv],
].map(([label, value]) => (
<div className="kv-row" key={label}>
<span className="kv-label">{label}</span>
<span className="kv-value">{value}</span>
<span className="kv-actions"><CopyButton text={String(value || '')} /></span>
</div>
))}
</div>
<div className="card">
<div className="card-title">Receiving Addresses</div>
<table className="addr-table">
<thead>
<tr>
<th>#</th>
<th>Path</th>
<th>Address</th>
<th>WIF</th>
<th></th>
</tr>
</thead>
<tbody>
{(wallet.addresses?.receiving || []).map(addr => (
<tr key={addr.index}>
<td className="addr-index">{addr.index}</td>
<td className="addr-path">{addr.path}</td>
<td className="addr-mono">{addr.address}</td>
<td className="addr-mono" style={{ maxWidth: 160, overflow: 'hidden', textOverflow: 'ellipsis' }}>
{addr.private_key_wif}
</td>
<td>
<div className="btn-row">
<CopyButton text={String(addr.address || '')} />
<CopyButton text={String(addr.private_key_wif || '')} />
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)
}
function SingleWalletView({ wallet }) {
const isP2SH = Array.isArray(wallet?.participants)
return (
<div className="card">
<div className="card-title">Wallet Data</div>
{isP2SH ? (
<>
{[
['Network', wallet.network],
['Script type', wallet.script_type],
['Configuration', `${wallet.m}-of-${wallet.n}`],
['Address', wallet.address],
['Redeem script', wallet.redeem_script_hex],
].map(([label, value]) => (
<div className="kv-row" key={label}>
<span className="kv-label">{label}</span>
<span className="kv-value">{value}</span>
<span className="kv-actions"><CopyButton text={String(value || '')} /></span>
</div>
))}
<hr className="divider" />
<div className="card-title" style={{ marginTop: 4 }}>Participants</div>
{wallet.participants.map((p, idx) => (
<div className="participant-card" key={idx}>
<div className="participant-title">Key {idx + 1}</div>
{[
['Public key', p.public_key_hex],
['Private key (WIF)', p.private_key_wif],
['Private key (hex)', p.private_key_hex],
].map(([label, value]) => (
<div className="kv-row" key={label} style={{ paddingTop: 4, paddingBottom: 4 }}>
<span className="kv-label">{label}</span>
<span className="kv-value">{value}</span>
<span className="kv-actions"><CopyButton text={String(value || '')} /></span>
</div>
))}
</div>
))}
</>
) : (
buildSingleRows(wallet).map(([label, value]) => (
<div className="kv-row" key={label}>
<span className="kv-label">{label}</span>
<span className="kv-value">{value}</span>
<span className="kv-actions"><CopyButton text={String(value || '')} /></span>
</div>
))
)}
</div>
)
}

View File

@@ -255,6 +255,10 @@ html, body, #root {
flex-wrap: wrap;
}
.save-actions {
justify-content: flex-end;
}
/* ── Key/value rows ───────────────────────────────────────────────────────── */
.kv-row {
display: flex;

View File

@@ -14,7 +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
from src.single_wallet import encrypt_single_wallet, decrypt_single_wallet
except ImportError:
from hd_wallet import generate_hd_wallet, encrypt_wallet, decrypt_wallet
from p2pk import generate_p2pk
@@ -22,7 +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
from single_wallet import encrypt_single_wallet, decrypt_single_wallet
COMMANDS = {
'hd_generate': lambda a: generate_hd_wallet(**a),
@@ -34,6 +34,7 @@ COMMANDS = {
'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']),
'single_decrypt': lambda a: decrypt_single_wallet(a['wallet'], a['password']),
}
if __name__ == '__main__':

View File

@@ -2,9 +2,9 @@ import copy
from typing import Any, Dict
try:
from src.crypto import pw_encode
from src.crypto import pw_encode, pw_decode
except ImportError:
from crypto import pw_encode
from crypto import pw_encode, pw_decode
SENSITIVE_FIELDS = {'private_key_hex', 'private_key_wif'}
@@ -24,6 +24,20 @@ def _encrypt_sensitive(data: Any, password: str) -> Any:
return data
def _decrypt_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_decode(value, password)
else:
out[key] = _decrypt_sensitive(value, password)
return out
if isinstance(data, list):
return [_decrypt_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.")
@@ -32,3 +46,16 @@ def encrypt_single_wallet(wallet: Dict[str, Any], password: str) -> Dict[str, An
if isinstance(encrypted, dict):
encrypted['use_encryption'] = True
return encrypted
def decrypt_single_wallet(wallet: Dict[str, Any], password: str) -> Dict[str, Any]:
if not wallet.get('use_encryption'):
return copy.deepcopy(wallet)
if not password:
raise ValueError("Password is required for decryption.")
decrypted = _decrypt_sensitive(copy.deepcopy(wallet), password)
if isinstance(decrypted, dict):
decrypted['use_encryption'] = False
return decrypted