import { useEffect, useId, useRef, useState, type FormEvent } from 'react'; import { exportWallet, restoreWallet, protectWallet, unlockWallet, lockWallet, walletStatus, subscribeWallet, type WalletStatus, type Wallet } from './wallet.ts'; import { NETWORK } from '../shared/network.ts'; type Props = { wallet: Wallet | undefined; onRestore: (wallet: Wallet) => void; onNotice: (message: string) => void; }; export function WalletPanel({ wallet, onRestore, onNotice }: Props) { const id = useId(); const [password, setPassword] = useState(''); const [confirmation, setConfirmation] = useState(''); const [importPassword, setImportPassword] = useState(''); const [file, setFile] = useState(); const [replace, setReplace] = useState(false); const [busy, setBusy] = useState(false); const [status,setStatus] = useState(); const [localPassword,setLocalPassword] = useState(''); const [localConfirmation,setLocalConfirmation] = useState(''); const [error,setError] = useState(''); const inProgress = useRef(false); const fileInput = useRef(null); useEffect(()=>{ let active=true; const load=()=>{void walletStatus().then(s=>{if(active)setStatus(s);}).catch(()=>{if(active)setError('Wallet storage unavailable.');});}; load();const unsubscribe=subscribeWallet(()=>{ setPassword('');setConfirmation('');setImportPassword('');setLocalPassword('');setLocalConfirmation('');load(); }); const clear=()=>{if(document.visibilityState!=='visible'){setPassword('');setConfirmation('');setImportPassword('');setLocalPassword('');setLocalConfirmation('');}}; document.addEventListener('visibilitychange',clear); return()=>{active=false;unsubscribe();document.removeEventListener('visibilitychange',clear);}; },[]); async function open(event: FormEvent) { event.preventDefault(); if(inProgress.current||!status)return; if(status!=='locked'&&(localPassword.length<12||localPassword!==localConfirmation)) {setError('Choose at least 12 characters and confirm the local password.');return;} inProgress.current=true;setBusy(true);setError(''); try { const opened= status==='locked'?await unlockWallet(localPassword):await protectWallet(localPassword); onRestore(opened);onNotice('Wallet unlocked in this tab. Locking or leaving the tab clears its signing session.'); } catch(e) {setError(e instanceof Error?e.message:'Could not unlock wallet.');} finally {setLocalPassword('');setLocalConfirmation('');inProgress.current=false;setBusy(false);setStatus(await walletStatus());} } async function download(event: FormEvent) { event.preventDefault(); if (inProgress.current || !wallet) return; if (password.length < 12 || password.length > 1024 || password !== confirmation) { onNotice('Use a password of 12–1024 characters and enter it identically twice.'); return; } inProgress.current = true; setBusy(true); try { const backup = await exportWallet(wallet, password); const url = URL.createObjectURL(new Blob([JSON.stringify(backup, null, 2)], { type: 'application/json' })); const link = document.createElement('a'); link.href = url; link.download = `${NETWORK}-wallet-${wallet.address}.json`; document.body.append(link); try { link.click(); } finally { link.remove(); // Let the browser start consuming the download before revocation. setTimeout(() => URL.revokeObjectURL(url), 1000); } onNotice('Encrypted backup download requested. Keep the file and password safe; test recovery before relying on it.'); } catch { onNotice('Could not export the wallet. Check browser support and try again.'); } finally { setPassword(''); setConfirmation(''); inProgress.current = false; setBusy(false); } } async function recover(event: FormEvent) { event.preventDefault(); if (inProgress.current) return; if (!file || file.size === 0 || file.size > 32 * 1024) { onNotice('Choose a nonempty wallet backup JSON file no larger than 32 KB.'); return; } if (status !== 'empty' && !replace) { onNotice('Confirm replacement of the active wallet first. Back it up before continuing.'); return; } inProgress.current = true; setBusy(true); try { if(localPassword.length<12||localPassword!==localConfirmation)throw new Error('Choose and confirm a local encryption password of at least 12 characters.'); // Size checked before reading; the shared decoder validates all JSON fields. const backup: unknown = JSON.parse(await file.text()); const recovered = await restoreWallet(backup, importPassword, localPassword, replace); onRestore(recovered); onNotice('Wallet recovered into encrypted local storage. Other tabs are locked; keep the backup safe.'); setFile(undefined); setReplace(false); if (fileInput.current) fileInput.current.value = ''; } catch { onNotice('Recovery failed. Check the password, backup format and network, and browser storage. The backup must be intact.'); } finally { setImportPassword(''); setLocalPassword('');setLocalConfirmation(''); inProgress.current = false; setBusy(false); } } return

Wallet backup & recovery

Local P-256 wallet for {NETWORK}. Status: {status??'checking storage'}.

New and migrated keys are encrypted in IndexedDB. Unlocking keeps an exportable signing key only in this tab’s memory. Lock manually, after five idle minutes, after four hours, or automatically when the tab is hidden. Encryption does not protect an unlocked page from malicious same-origin scripts or extensions. Clearing site data without a backup loses access.

{status==='legacy'&&

Existing unencrypted wallet detected. Encrypt the same identity below; migration never generates a replacement key. The original record is replaced only after encrypted storage commits. Existing disk copies/backups may still contain the old plaintext key.

} {error&&

{error}

} {wallet?:
{status==='locked'?'Unlock encrypted local wallet':status==='legacy'?'Encrypt existing wallet':'Create encrypted wallet'} setLocalPassword(e.target.value)}/> {status!=='locked'&&<>setLocalConfirmation(e.target.value)}/>}
}

Backups are bound to this exact chain ID; a backup for a different genesis cannot be restored here. Encryption and recovery happen locally. No private key, backup file, or password is transmitted to a server by this panel. Public addresses, public keys and signatures may be shared during normal wallet use.

Forgotten backup passwords cannot be reset or recovered. Keep the encrypted file and a strong, unique password safe and separate. Anyone with both can control this wallet. Downloading does not guarantee the file was saved.

Download encrypted backup setPassword(event.target.value)} /> setConfirmation(event.target.value)} />
Recover from a local backup { setFile(event.target.files?.[0]); setReplace(false); }} /> setImportPassword(event.target.value)} /> setLocalPassword(e.target.value)}/> setLocalConfirmation(e.target.value)}/> {status!=='empty' && }
; } export default WalletPanel;