Mnemonic Language Detection Utils

This commit is contained in:
Jim Hamill
2025-06-25 10:29:11 +00:00
parent 5487cc4ec8
commit 350ea4716f
+99 -4
View File
@@ -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<string>,
wordLists: { [name: string]: Array<string> },
allowPartialMatches = false,
): { [name: string]: Array<string> } {
// Define our match function.
const isWordMatch = (word: string, wordList: Array<string>): 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<string>,
wordLists: { [name: string]: Array<string> },
): Array<string> {
// 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<string>(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,
};
}