Files
easy-wallet/frontend/src/components/SingleAddress.jsx
Davide Grilli 079b650d60 fix(android-storage): add writable directory fallback for wallet JSON files
- Resolve Android storage dir at runtime (Documents/Data) and reuse it for saves
- List/read wallets across candidate dirs with dedupe by latest mtime
- Validate wallet filenames to prevent invalid paths
- Update UI hints for Desktop vs Android storage locations
2026-03-10 15:35:46 +01:00

278 lines
10 KiB
JavaScript

import { useState } from 'react'
import CopyButton from './CopyButton'
import { api } from '../services/api.js'
const TABS = [
{ id: 'p2pk', label: 'P2PK', desc: 'Pay-to-PubKey' },
{ id: 'p2pkh', label: 'P2PKH', desc: 'Legacy' },
{ id: 'p2sh', label: 'P2SH', desc: 'Multisig' },
{ id: 'p2wpkh', label: 'P2WPKH', desc: 'Native SegWit' },
{ id: 'p2tr', label: 'P2TR', desc: 'Taproot' },
]
export default function SingleAddress({ initialTab = 'p2pkh' }) {
const [tab, setTab] = useState(initialTab)
const [network, setNetwork] = useState('mainnet')
const [compressed, setCompressed] = useState(true)
const [m, setM] = useState(2)
const [n, setN] = useState(3)
const [result, setResult] = useState(null)
const [loading, setLoading] = useState(false)
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)
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 = api[tab]
const data = await fn(args)
setResult(data)
setFilename('')
setSavePassword('')
setSavePasswordConfirm('')
setShowSavePassword(false)
} catch (e) {
setError(e.message)
} finally {
setLoading(false)
}
}
const save = async () => {
if (!result) return
if (!filename.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 {
let data = result
if (savePassword.trim()) {
data = await api.singleEncrypt({ wallet: result, password: savePassword })
}
const name = filename.trim().endsWith('.json') ? filename.trim() : `${filename.trim()}.json`
const savedPath = await api.saveWallet(name, data, 'single')
setSaveMsg(savePassword.trim() ? `Saved (encrypted): ${savedPath}` : `Saved: ${savedPath}`)
} catch (e) {
setSaveMsg('Error: ' + e.message)
} finally {
setSaving(false)
}
}
const currentTab = TABS.find(t => t.id === tab)
return (
<div>
<div className="page-title">Single Addresses</div>
<div className="page-subtitle">Generate individual Bitcoin addresses and key pairs</div>
<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(''); setSavePasswordConfirm(''); setShowSavePassword(false) }}>
{t.label}
</button>
))}
</div>
<div className="card">
<div className="card-title">{currentTab.label} <span style={{ color: 'var(--text-xs)', fontWeight: 400 }}> {currentTab.desc}</span></div>
<div className="form-grid" style={{ marginBottom: 14 }}>
<div className="form-group">
<label className="form-label">Network</label>
<select className="form-select" value={network} onChange={e => setNetwork(e.target.value)}>
<option value="mainnet">Mainnet</option>
<option value="testnet">Testnet</option>
<option value="regtest">Regtest</option>
</select>
</div>
{tab === 'p2sh' && (
<>
<div className="form-group">
<label className="form-label">Required signatures (m)</label>
<input className="form-input" type="number" min="1" max="16" value={m} onChange={e => setM(parseInt(e.target.value))} />
</div>
<div className="form-group">
<label className="form-label">Total keys (n)</label>
<input className="form-input" type="number" min="1" max="16" value={n} onChange={e => setN(parseInt(e.target.value))} />
<span className="form-hint">{m}-of-{n} multisig</span>
</div>
</>
)}
</div>
{tab !== 'p2tr' && (
<div className="checkbox-group" style={{ marginBottom: 14 }}>
<input type="checkbox" id="compressed" checked={compressed} onChange={e => setCompressed(e.target.checked)} />
<label htmlFor="compressed">Compressed public key (33 bytes)</label>
</div>
)}
<button className="btn btn-primary" onClick={generate} disabled={loading}>
{loading ? <><span className="spinner" /> Generating</> : 'Generate'}
</button>
</div>
{error && <div className="alert alert-error">{error}</div>}
{result && (
<>
<ResultCard result={result} tab={tab} />
<div className="card">
<div className="card-title">Save</div>
<div className="form-grid save-form-grid" style={{ marginBottom: 12 }}>
<div className="form-group">
<label className="form-label">Filename</label>
<input
className="form-input"
placeholder="Required"
value={filename}
onChange={e => setFilename(e.target.value)}
/>
<span className="form-hint">.json · Desktop: ~/.wallet-generator/single/ · Android: app storage</span>
</div>
<div className="form-group">
<label className="form-label">Encryption password (optional)</label>
<input
className="form-input"
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>
<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}
</div>
)}
</div>
</>
)}
</div>
)
}
function ResultCard({ result, tab }) {
return (
<div className="card">
<div className="card-title">Result</div>
{tab === 'p2sh' ? (
<P2SHResult result={result} />
) : (
buildFields(result).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={value} /></span>
</div>
))
)}
</div>
)
}
function buildFields(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
}
function P2SHResult({ result }) {
return (
<>
{[
['Network', result.network],
['Script type', result.script_type],
['Configuration', `${result.m}-of-${result.n}`],
['Address', result.address],
['Redeem script', result.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={value} /></span>
</div>
))}
<hr className="divider" />
<div className="card-title" style={{ marginTop: 4 }}>Participants</div>
{result.participants.map((p, i) => (
<div className="participant-card" key={i}>
<div className="participant-title">Key {i + 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={value} /></span>
</div>
))}
</div>
))}
</>
)
}