// Full BIP39 validation, including checksum and wordlist membership. import wordlistTxt from '../bip39_wordlist.txt?raw'; // --- BIP39 Wordlist Loading --- export const BIP39_WORDLIST: readonly string[] = wordlistTxt.trim().split('\n'); export const WORD_INDEX = new Map( BIP39_WORDLIST.map((word, index) => [word, index]) ); if (BIP39_WORDLIST.length !== 2048) { throw new Error(`Invalid wordlist loaded: expected 2048 words, got ${BIP39_WORDLIST.length}`); } // --- Web Crypto API Helpers --- async function getCrypto(): Promise { if (globalThis.crypto?.subtle) { return globalThis.crypto.subtle; } try { const { webcrypto } = await import('crypto'); if (webcrypto?.subtle) { return webcrypto.subtle as SubtleCrypto; } } catch (e) { // Ignore import errors } throw new Error("SubtleCrypto not found in this environment"); } async function sha256(data: Uint8Array): Promise { const subtle = await getCrypto(); // Create a new Uint8Array to ensure the underlying buffer is not shared. const dataCopy = new Uint8Array(data); const hashBuffer = await subtle.digest('SHA-256', dataCopy); return new Uint8Array(hashBuffer); } // --- Public API --- export function normalizeBip39Mnemonic(words: string): string { return words.trim().toLowerCase().replace(/\s+/g, " "); } /** * Asynchronously validates a BIP39 mnemonic, including wordlist membership and checksum. * @param mnemonicStr The mnemonic string to validate. * @returns A promise that resolves to an object with a `valid` boolean and an optional `error` message. */ export async function validateBip39Mnemonic(mnemonicStr: string): Promise<{ valid: boolean; error?: string }> { const normalized = normalizeBip39Mnemonic(mnemonicStr); const words = normalized.length ? normalized.split(" ") : []; const validCounts = new Set([12, 15, 18, 21, 24]); if (!validCounts.has(words.length)) { return { valid: false, error: `Invalid word count: ${words.length}. Must be 12, 15, 18, 21, or 24.`, }; } // 1. Check if all words are in the wordlist for (const word of words) { if (!WORD_INDEX.has(word)) { return { valid: false, error: `Invalid word: "${word}" is not in the BIP39 wordlist.`, }; } } // 2. Reconstruct entropy and validate checksum try { let fullInt = 0n; for (const word of words) { fullInt = (fullInt << 11n) | BigInt(WORD_INDEX.get(word)!); } const totalBits = words.length * 11; const checksumBits = totalBits / 33; const entropyBits = totalBits - checksumBits; let entropyInt = fullInt >> BigInt(checksumBits); const entropyBytes = new Uint8Array(entropyBits / 8); for (let i = entropyBytes.length - 1; i >= 0; i--) { entropyBytes[i] = Number(entropyInt & 0xFFn); entropyInt >>= 8n; } const hashBytes = await sha256(entropyBytes); const computedChecksum = hashBytes[0] >> (8 - checksumBits); const originalChecksum = Number(fullInt & ((1n << BigInt(checksumBits)) - 1n)); if (originalChecksum !== computedChecksum) { return { valid: false, error: "Invalid mnemonic: Checksum mismatch.", }; } } catch (e) { return { valid: false, error: `An unexpected error occurred during validation: ${e instanceof Error ? e.message : 'Unknown error'}`, }; } return { valid: true }; }