randomx
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import { useId, useRef, useState, type FormEvent } from 'react';
|
||||
import { exportWallet, restoreWallet, type Wallet } from './wallet.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<File>();
|
||||
const [replace, setReplace] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const inProgress = useRef(false);
|
||||
const fileInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
async function download(event: FormEvent<HTMLFormElement>) {
|
||||
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 = `wikideck-testnet-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<HTMLFormElement>) {
|
||||
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 (wallet && !replace) {
|
||||
onNotice('Confirm replacement of the active wallet first. Back it up before continuing.');
|
||||
return;
|
||||
}
|
||||
inProgress.current = true;
|
||||
setBusy(true);
|
||||
try {
|
||||
// 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);
|
||||
onRestore(recovered);
|
||||
onNotice('Wallet recovered and saved locally. Other open tabs should reload before signing.');
|
||||
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('');
|
||||
inProgress.current = false;
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return <section aria-labelledby={`${id}-title`} aria-busy={busy}>
|
||||
<h2 id={`${id}-title`}>Wallet backup & recovery</h2>
|
||||
<p>This is a recoverable P-256 wallet for wikideck-randomx-testnet-2, separate from your old demo wallet.</p>
|
||||
<p>The exportable private key is stored locally in this browser’s IndexedDB, not encrypted by this password.
|
||||
Scripts running on this origin or someone with access to your browser profile may access it.
|
||||
Clearing site data without a backup loses access.</p>
|
||||
<p>Backup 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.</p>
|
||||
<p><strong>Forgotten backup passwords cannot be reset or recovered.</strong> 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.</p>
|
||||
<form onSubmit={download}>
|
||||
<fieldset disabled={busy || !wallet}>
|
||||
<legend>Download encrypted backup</legend>
|
||||
<label htmlFor={`${id}-password`}>Backup password (at least 12 characters)</label>
|
||||
<input id={`${id}-password`} type="password" autoComplete="new-password" minLength={12} maxLength={1024}
|
||||
required value={password} onChange={event => setPassword(event.target.value)} />
|
||||
<label htmlFor={`${id}-confirm`}>Confirm backup password</label>
|
||||
<input id={`${id}-confirm`} type="password" autoComplete="new-password" minLength={12} maxLength={1024}
|
||||
required value={confirmation} onChange={event => setConfirmation(event.target.value)} />
|
||||
<button type="submit">Download encrypted backup</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
<form onSubmit={recover}>
|
||||
<fieldset disabled={busy}>
|
||||
<legend>Recover from a local backup</legend>
|
||||
<label htmlFor={`${id}-file`}>Backup JSON file (maximum 32 KB)</label>
|
||||
<input ref={fileInput} id={`${id}-file`} type="file" accept=".json,application/json" required
|
||||
onChange={event => { setFile(event.target.files?.[0]); setReplace(false); }} />
|
||||
<label htmlFor={`${id}-import-password`}>Backup password</label>
|
||||
<input id={`${id}-import-password`} type="password" autoComplete="off" required maxLength={1024}
|
||||
value={importPassword} onChange={event => setImportPassword(event.target.value)} />
|
||||
{wallet && <label>
|
||||
<input type="checkbox" required checked={replace} onChange={event => setReplace(event.target.checked)} />
|
||||
I have backed up my active wallet and confirm replacing it. Recovery does not transfer its assets.
|
||||
</label>}
|
||||
<button type="submit" disabled={!file || (!!wallet && !replace)}>Recover and save wallet</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</section>;
|
||||
}
|
||||
|
||||
export default WalletPanel;
|
||||
Reference in New Issue
Block a user