dgb-wallet/packages/web-ui/src/CreateWalletPage.tsx
Digibyte.X d50286a9a5 DigiByte.X Wallet — recovery-first web wallet monorepo
Packages: core (BIP39/32/44/49/84/86 derivation, WIF), keystore (PBKDF2 +
AES-GCM), psbt, p2p (BIP157/158 SPV daemon, Dandelion++ broadcast with
relay-echo verification), blockbook, rpc, electrum, net, bns, web-ui
(React/Vite), mobile (Expo). ops/ holds the nginx vhost, systemd unit and
push.sh deploy script for digibyte.x/web.
2026-09-20 20:16:52 +02:00

167 lines
6.1 KiB
TypeScript

import { useState } from 'react';
import { generateSeedPhrase, validateSeedPhrase } from '@dgb-wallet/core';
import { Keystore, LocalStorageAdapter } from '@dgb-wallet/keystore';
import { useActiveSeed } from './ActiveSeed.js';
import { useSessionAuth } from './session-auth.js';
// Create a fresh BIP39 wallet: generate → user writes it down + confirms
// → optionally encrypt-save under a label → activate. No paths cross
// with import. Kept its own page so users don't accidentally overwrite
// something.
const keystore = new Keystore(new LocalStorageAdapter());
type Step = 'show' | 'confirm' | 'save';
export function CreateWalletPage({ onBack }: { onBack: () => void }): JSX.Element {
const { setActive } = useActiveSeed();
const { masterPassword } = useSessionAuth();
const [phrase] = useState<string>(() => generateSeedPhrase(128)); // 12 words
const [step, setStep] = useState<Step>('show');
const [ack, setAck] = useState(false);
const [confirmPhrase, setConfirmPhrase] = useState('');
const [label, setLabel] = useState('primary');
const [busy, setBusy] = useState(false);
const [err, setErr] = useState<string | null>(null);
const words = phrase.split(' ');
const confirmOk = confirmPhrase.trim() === phrase && validateSeedPhrase(phrase);
async function onSave(): Promise<void> {
if (!confirmOk) return;
if (!masterPassword) { setErr('Session locked — unlock with the master password first.'); return; }
setBusy(true);
setErr(null);
try {
const already = await keystore.exists(label);
if (already) {
const ok = confirm(`Overwrite existing "${label}"? This can't be undone.`);
if (!ok) { setBusy(false); return; }
}
await keystore.save(label, phrase, masterPassword, true);
setActive(phrase, label);
onBack(); // returns to dashboard because the seed just became active
} catch (e) {
setErr((e as Error).message);
} finally {
setBusy(false);
}
}
function onActivateWithoutSaving(): void {
// Advance to the dashboard with the seed loaded in memory only.
// Same posture as pasting a throwaway seed via Import. Use the
// chosen label anyway so the header can display something better
// than a hex hash.
setActive(phrase, label);
onBack();
}
return (
<>
<div className="page-header">
<h2>Create a new wallet</h2>
<p className="page-sub">
A fresh BIP39 seed generated in this browser. It has never been on any
server. Write it down before doing anything else — if you lose it and
haven't set a passphrase, the funds are gone.
</p>
</div>
{step === 'show' && (
<>
<div className="warning-block">
⚠️ Anyone with these 12 words can spend your funds. Write them on
paper, store offline, never take a screenshot, never paste into
chat. Nothing on this screen is uploaded anywhere.
</div>
<div className="seed-grid">
{words.map((w, i) => (
<div key={i} className="seed-cell">
<span className="seed-idx">{i + 1}</span>
<span className="seed-word">{w}</span>
</div>
))}
</div>
<label className="ack-row">
<input type="checkbox" checked={ack} onChange={(e) => setAck(e.target.checked)} />
I have written these 12 words down somewhere I can find later.
</label>
<button className="primary" onClick={() => setStep('confirm')} disabled={!ack}>
Continue
</button>
<button className="text-btn" onClick={onBack} style={{ marginTop: 10, width: '100%' }}>
Cancel
</button>
</>
)}
{step === 'confirm' && (
<>
<div className="tip-block">
<strong>One more time.</strong> Type the whole phrase back to prove
you wrote it down. Copy-paste won't help you later if the paper is
gone.
</div>
<label htmlFor="confirm-phrase">Type the 12 words in order</label>
<textarea
id="confirm-phrase"
placeholder="word1 word2 word3 ... word12"
value={confirmPhrase}
onChange={(e) => setConfirmPhrase(e.target.value)}
spellCheck={false}
autoCapitalize="none"
autoCorrect="off"
/>
{confirmPhrase.length > 0 && !confirmOk && (
<div className="field-error">Doesn't match yet — check for typos or missing words.</div>
)}
{confirmOk && <div className="field-ok">Match ✓</div>}
<button className="primary" onClick={() => setStep('save')} disabled={!confirmOk}>
Continue
</button>
<button className="text-btn" onClick={() => setStep('show')} style={{ marginTop: 10, width: '100%' }}>
← Show seed again
</button>
</>
)}
{step === 'save' && (
<>
<div className="tip-block">
<strong>Save it in this browser (optional).</strong> Encrypted with
your master password and stored in localStorage. The paper backup
of your seed is the real safety net — losing both the encrypted
copy and the paper means the funds are gone.
</div>
<label htmlFor="save-label">Wallet label</label>
<input
id="save-label"
value={label}
onChange={(e) => setLabel(e.target.value)}
autoCapitalize="none"
autoCorrect="off"
/>
{err && <div className="field-error" style={{ marginTop: 8 }}>{err}</div>}
<button className="primary" onClick={onSave} disabled={busy || !label}>
{busy ? 'Encrypting…' : 'Save & open wallet'}
</button>
<button
className="text-btn"
onClick={onActivateWithoutSaving}
style={{ marginTop: 10, width: '100%' }}
>
Skip save — open in-memory only
</button>
</>
)}
</>
);
}