diff --git a/src/components/SeedBlender.tsx b/src/components/SeedBlender.tsx index a1cf4d8..282d4e3 100644 --- a/src/components/SeedBlender.tsx +++ b/src/components/SeedBlender.tsx @@ -3,38 +3,68 @@ * @summary Main component for the Seed Blending feature. */ import { useState, useEffect } from 'react'; -import { QrCode, X, Plus, CheckCircle2, AlertTriangle } from 'lucide-react'; +import { QrCode, X, Plus, CheckCircle2, AlertTriangle, RefreshCw, Sparkles, EyeOff, Lock } from 'lucide-react'; import QRScanner from './QRScanner'; -import { blendMnemonicsAsync, checkXorStrength, mnemonicToEntropy } from '../lib/seedblend'; +import { + blendMnemonicsAsync, + checkXorStrength, + mnemonicToEntropy, + DiceStats, + calculateDiceStats, + detectBadPatterns, + diceToBytes, + hkdfExtractExpand, + entropyToMnemonic, + mixWithDiceAsync, +} from '../lib/seedblend'; // A simple debounce function function debounce any>(func: F, waitFor: number) { let timeout: ReturnType | null = null; - - return (...args: Parameters): Promise> => - new Promise(resolve => { - if (timeout) { - clearTimeout(timeout); - } - - timeout = setTimeout(() => resolve(func(...args)), waitFor); - }); + return (...args: Parameters): void => { + if (timeout) clearTimeout(timeout); + timeout = setTimeout(() => func(...args), waitFor); + }; } export function SeedBlender() { + // Step 1 State const [mnemonics, setMnemonics] = useState(['']); const [validity, setValidity] = useState>([null]); const [showQRScanner, setShowQRScanner] = useState(false); const [scanTargetIndex, setScanTargetIndex] = useState(null); - // State for Step 2 - const [blendedResult, setBlendedResult] = useState<{ blendedMnemonic12: string; blendedMnemonic24?: string; } | null>(null); + // Step 2 State + const [blendedResult, setBlendedResult] = useState<{ blendedEntropy: Uint8Array; blendedMnemonic12: string; blendedMnemonic24?: string; } | null>(null); const [xorStrength, setXorStrength] = useState<{ isWeak: boolean; uniqueBytes: number; } | null>(null); const [blendError, setBlendError] = useState(''); const [blending, setBlending] = useState(false); + // Step 3 State + const [diceRolls, setDiceRolls] = useState(''); + const [diceStats, setDiceStats] = useState(null); + const [dicePatternWarning, setDicePatternWarning] = useState(null); + const [diceOnlyMnemonic, setDiceOnlyMnemonic] = useState(null); - // Effect to validate and blend mnemonics + // Step 4 State + const [finalMnemonic, setFinalMnemonic] = useState(null); + const [mixing, setMixing] = useState(false); + + // Main clear function + const handleLockAndClear = () => { + setMnemonics(['']); + setValidity([null]); + setBlendedResult(null); + setXorStrength(null); + setBlendError(''); + setDiceRolls(''); + setDiceStats(null); + setDicePatternWarning(null); + setDiceOnlyMnemonic(null); + setFinalMnemonic(null); + } + + // Effect to validate and blend mnemonics (Step 2) useEffect(() => { const processMnemonics = async () => { setBlending(true); @@ -84,12 +114,34 @@ export function SeedBlender() { setBlending(false); }; - // Debounce the processing to avoid running on every keystroke - const debouncedProcess = debounce(processMnemonics, 300); - debouncedProcess(); - + debounce(processMnemonics, 300)(); }, [mnemonics]); + // Effect to process dice rolls (Step 3) + useEffect(() => { + const processDice = async () => { + setDiceStats(calculateDiceStats(diceRolls)); + + const pattern = detectBadPatterns(diceRolls); + setDicePatternWarning(pattern.message || null); + + if (diceRolls.length >= 50) { + try { + const diceBytes = diceToBytes(diceRolls); + const outputByteLength = blendedResult?.blendedEntropy.length === 32 ? 32 : 16; + const diceOnlyEntropy = await hkdfExtractExpand(diceBytes, outputByteLength, new TextEncoder().encode('dice-only')); + const mnemonic = await entropyToMnemonic(diceOnlyEntropy); + setDiceOnlyMnemonic(mnemonic); + } catch (e) { + setDiceOnlyMnemonic(null); + } + } else { + setDiceOnlyMnemonic(null); + } + }; + debounce(processDice, 200)(); + }, [diceRolls, blendedResult]); + const handleAddMnemonic = () => { setMnemonics([...mnemonics, '']); @@ -127,6 +179,24 @@ export function SeedBlender() { setScanTargetIndex(null); }; + const handleFinalMix = async () => { + if (!blendedResult) return; + setMixing(true); + try { + const outputBits = blendedResult.blendedEntropy.length === 32 ? 256 : 128; + const result = await mixWithDiceAsync(blendedResult.blendedEntropy, diceRolls, outputBits); + setFinalMnemonic(result.finalMnemonic); + } catch(e) { + // handle error + } finally { + setMixing(false); + } + }; + + const handleClearFinal = () => { + setFinalMnemonic(null); + } + const getBorderColor = (isValid: boolean | null) => { if (isValid === true) return 'border-green-500 focus:ring-green-500'; if (isValid === false) return 'border-red-500 focus:ring-red-500'; @@ -139,6 +209,13 @@ export function SeedBlender() { {/* Header */}

Seed Blender

+
{/* Step 1: Input Mnemonics */}