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.
167 lines
6.1 KiB
TypeScript
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>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}
|