From 350ea4716fff8c0160b3fb0b2bf91f8a90e8cb84 Mon Sep 17 00:00:00 2001 From: Jim Hamill Date: Wed, 25 Jun 2025 10:29:11 +0000 Subject: [PATCH] Mnemonic Language Detection Utils --- packages/primitives/src/mnemonic.ts | 103 ++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 4 deletions(-) diff --git a/packages/primitives/src/mnemonic.ts b/packages/primitives/src/mnemonic.ts index 5b460c6..041f8a9 100644 --- a/packages/primitives/src/mnemonic.ts +++ b/packages/primitives/src/mnemonic.ts @@ -1,9 +1,19 @@ import { - bip39WordListEnglish, decodeBip39MnemonicNonStandard, deriveSeedFromBip39Mnemonic, encodeBip39MnemonicNonStandard, generateBip39MnemonicNonStandard, + // Word lists. + bip39WordListChineseSimplified, + bip39WordListChineseTraditional, + bip39WordListCzech, + bip39WordListEnglish, + bip39WordListFrench, + bip39WordListItalian, + bip39WordListJapanese, + bip39WordListKorean, + bip39WordListPortuguese, + bip39WordListSpanish, } from '@bitauth/libauth'; import { Bytes } from './bytes.js'; @@ -253,15 +263,79 @@ export class Mnemonic { * Checks if a word exists in the provided dictionary. * * @param word Word to check. - * @param dictionary Optional wordlist (defaults to English BIP-39 wordlist). + * @param wordList Optional wordlist (defaults to English BIP-39 wordlist). * * @returns boolean indicating if the word is valid. */ public static isValidWord( word: string, - dictionary: string[] = bip39WordListEnglish, + wordList: string[] = bip39WordListEnglish, ): boolean { - return dictionary.includes(word.toLowerCase()); + return wordList.includes(word.toLowerCase()); + } + + /** + * Detect which languages (word-lists) the given mnemonic matches. + * + * @param words The mnemonic words to check + * @param wordLists The BIP39 word lists to check against, keyed by language name + * @param allowPartialMatches If true, treats incomplete words as valid matches (useful for autocomplete) + * + * @returns Object mapping language names to their word lists for all matching languages + */ + public static detectLanguages( + words: Array, + wordLists: { [name: string]: Array }, + allowPartialMatches = false, + ): { [name: string]: Array } { + // Define our match function. + const isWordMatch = (word: string, wordList: Array): boolean => { + if (allowPartialMatches && word.length === 0) return true; + if (wordList.includes(word)) return true; + return ( + allowPartialMatches && + wordList.some((fullWord) => fullWord.startsWith(word)) + ); + }; + + // Iterate over each word-list and check whether it's a match. + return Object.fromEntries( + Object.entries(wordLists).filter(([, wordList]) => + words.every((word) => isWordMatch(word, wordList)), + ), + ); + } + + /** + * Get all valid BIP39 words from languages that match the given mnemonic words. + * + * Uses partial matching to determine eligible languages, then returns all words + * from those languages (useful for autocomplete and word validation). + * + * @param words The mnemonic words to check (can be partial/incomplete) + * @param wordLists The BIP39 word lists to check against, keyed by language name + * @returns Array of all unique words from matching languages, sorted alphabetically + * + * @example + * ```typescript + * // Get all valid words for languages that match partial input + * const eligibleWords = Mnemonic.getEligibleWords(['aban', 'abil'], wordLists); + * // Returns all words from languages where 'aban*' and 'abil*' are valid prefixes + * + * // Filter for autocomplete + * const completions = eligibleWords.filter(word => word.startsWith('acc')); + * ``` + */ + public static getEligibleWords( + words: Array, + wordLists: { [name: string]: Array }, + ): Array { + // Given the words provided, find eligible languages/word-lists. + const matchingLanguages = this.detectLanguages(words, wordLists, true); + + // Merge all eligible word completions into an Array. + // NOTE: We use a Set to de-deuplicate words that might appear in multiple word-lists. + return [...new Set(Object.values(matchingLanguages).flat())]; } /** @@ -347,3 +421,24 @@ export class Mnemonic { return this.mnemonic.passphrase; } } + +// NOTE: This is handled exceptionally due to tree-shaking considerations. +// Word lists incur a large cost on the bundle size and LibAuth considers non-English word-lists as non-standard: +// https://github.com/bitauth/libauth/blob/master/src/lib/key/bip39.ts#L178 +// And the BIP39 spec warns against their usage: +// https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#wordlists +// If we include this in the class, it cannot be tree-shaken by most tooling, so we define a stand-alone function instead. +export function getAllBIP39WordLists() { + return { + 'Chinese Simplified': bip39WordListChineseSimplified, + 'Chinese Traditional': bip39WordListChineseTraditional, + Czech: bip39WordListCzech, + English: bip39WordListEnglish, + French: bip39WordListFrench, + Italian: bip39WordListItalian, + Japanese: bip39WordListJapanese, + Korean: bip39WordListKorean, + Portuguese: bip39WordListPortuguese, + Spanish: bip39WordListSpanish, + }; +}