import { bip39WordListEnglish, decodeBip39MnemonicNonStandard, deriveSeedFromBip39Mnemonic, encodeBip39MnemonicNonStandard, generateBip39MnemonicNonStandard, } from '@bitauth/libauth'; import { Bytes } from './bytes.js'; export type MnemonicRaw = { words: string[]; passphrase?: string; }; /** * Class representing a BIP39 Mnemonic phrase. * Supports 12, 15, 18, 21, and 24 word phrases with optional passphrase. * * @example * * ```ts * // Import our primitives. * import { Mnemonic } from '@xocash/primitives'; * * // Generate a random mnemonic. * const mnemonic = Mnemonic.generateRandom(); * * // Output the mnemonic phrase. * console.log(mnemonic.toPhrase()); * * // Derive the seed from the mnemonic. * console.log(mnemonic.toSeed()); * ``` */ export class Mnemonic { protected static VALID_WORD_COUNTS = new Set([12, 15, 18, 21, 24]); /** * Instantiate Mnemonic from a phrase string. * * @param phrase The complete phrase (mnemonic words + optional passphrase). * @param wordList Optional wordlist (defaults to English BIP-39 wordlist). * * @throws {Error} If the mnemonic is invalid. */ protected constructor(private mnemonic: MnemonicRaw) {} /** * Instantiates a mnemonic from a string or array of words * * @param phraseOrWords The mnemonic phrase as a string or array of words * @param wordList Optional wordlist (defaults to English BIP-39 wordlist). * * @throws {Error} If mnemonic could not be generated. * * @returns Instance of Mnemonic. */ public static from( phraseOrWords: string | Array, wordList: Array = bip39WordListEnglish, ) { return Array.isArray(phraseOrWords) ? this.fromWords(phraseOrWords, wordList) : this.fromPhrase(phraseOrWords, wordList); } /** * Instantiates a mnemonic from entropy. * * @param entropy The entropy to use to derive the mnemonic words. * @param wordList Optional wordlist (defaults to English BIP-39 wordlist). * * @throws {Error} If mnemonic could not be generated. * * @returns Instance of Mnemonic. */ public static fromEntropy(entropy: Uint8Array, wordList: Array = bip39WordListEnglish) { const mnemonic = encodeBip39MnemonicNonStandard(entropy, wordList); if (typeof mnemonic === 'string') { throw new Error(mnemonic); } return this.fromPhrase(mnemonic.phrase, wordList); } /** * Instantiates a mnemonic from a string * * @param phrase The mnemonic phrase as a string * @param wordList Optional wordlist (defaults to English BIP-39 wordlist). * * @throws {Error} If mnemonic could not be generated. * * @returns Instance of Mnemonic. */ public static fromPhrase( phrase: string, wordList: Array = bip39WordListEnglish, ) { // Split into words and remove any whitespace. const allWords = phrase .trim() .split(/\s+/) .map((word) => word.trim()); return this.fromWords(allWords, wordList); } /** * Instantiates a mnemonic from an array of words. * * @param words Array of words (strings) that represent the mnemonic * @param wordList Optional wordlist (defaults to English BIP-39 wordlist). * * @throws {Error} If mnemonic could not be generated. * * @returns Instance of Mnemonic. */ public static fromWords( words: Array, wordList: Array = bip39WordListEnglish, ) { // Check if last word is a passphrase by looking at total count const hasPassphrase = words.length === 13 || words.length === 16 || words.length === 19 || words.length === 22 || words.length === 25; // Parse words and optional passphrase const mnemonicRaw: MnemonicRaw = { words: hasPassphrase ? words.slice(0, -1) : words, }; if (hasPassphrase) { mnemonicRaw.passphrase = words[words.length - 1]; } // Validate word count if (!Mnemonic.VALID_WORD_COUNTS.has(mnemonicRaw.words.length)) { throw new Error( `Invalid word count ${mnemonicRaw.words.length}. BIP-39 requires 12, 15, 18, 21, or 24 words` + `${hasPassphrase ? ' (excluding passphrase)' : ''}`, ); } // Normalize and validate words against dictionary const wordSet = new Set(wordList); // Convert all words to lower-case. mnemonicRaw.words = mnemonicRaw.words.map((word) => word.toLowerCase()); // Find any words that are not present in our dictionary (i.e. invalid words). const invalidWords = mnemonicRaw.words.reduce((invalidWords, word) => { if (!wordSet.has(word)) { invalidWords.push(word); } return invalidWords; }, [] as Array); // If there are invalid words that are not present in the provided dictionary, throw an error. if (invalidWords.length) { throw new Error( `The following words are not valid with the provided dictionary: ${invalidWords.join(' ')}`, ); } return new this(mnemonicRaw); } /** * Instantiates a mnemonic from its raw (serialized) mnemonic representation. * * @remarks this method is unsafe and should only use trusted data. * * @param mnemonicRaw The raw menmonic data. * @throws {Error} If mnemonic could not be generated. * * @returns Instance of Mnemonic. */ public static fromRaw(mnemonicRaw: MnemonicRaw) { return new this(mnemonicRaw); } /** * Generates a random mnemonic phrase. * * @param dictionary Optional wordlist (defaults to English BIP-39 wordlist). * @throws {Error} If mnemonic could not be generated. * * @returns Instance of Mnemonic. */ public static generateRandom( passphrase?: string, dictionary: string[] = bip39WordListEnglish, ): Mnemonic { // Attempt to generate mnemonic. const mnemonicResult = generateBip39MnemonicNonStandard(dictionary); if (typeof mnemonicResult === 'string') { throw new Error(mnemonicResult); } // Turn phrase parts (phrase) into an array so that we can append the passphrase later (if present). const words = mnemonicResult.phrase.split(/\s+/); // If there is a passphrase, append it. if (passphrase) { words.push(passphrase); } // Return the generated phrase (with passphrase appended - if present). return this.fromWords(words, dictionary); } /** * Validates a mnemonic phrase. * * @param phraseOrWords The mnemonic phrase or words to validate. * @param wordList Optional wordlist (defaults to English BIP-39 wordlist). * * @returns boolean indicating if the phrase is valid. */ public static isValid( phraseOrWords: string | Array, wordList: string[] = bip39WordListEnglish, ): boolean { try { this.from(phraseOrWords, wordList); return true; } catch { return false; } } /** * Checks if a word exists in the provided dictionary. * * @param word Word to check. * @param dictionary Optional wordlist (defaults to English BIP-39 wordlist). * * @returns boolean indicating if the word is valid. */ public static isValidWord( word: string, dictionary: string[] = bip39WordListEnglish, ): boolean { return dictionary.includes(word.toLowerCase()); } /** * Returns the resulting seed for this mnemonic. * * @returns The seed (for use with HDPrivateNodes). */ public toSeed(): Uint8Array { return deriveSeedFromBip39Mnemonic(this.toPhrase()); } /** * Returns the complete phrase as a string. * * @param includePassphrase Whether to include the passphrase in the output. * * @returns Complete phrase as a string. */ public toPhrase(includePassphrase: boolean = true): string { // Join the menmonic words, separating them using a space character. const mnemonicWords = this.mnemonic.words.join(' '); // If we should include the passphrase - and a passphrase is present... if (includePassphrase && this.mnemonic.passphrase) { return `${mnemonicWords} ${this.mnemonic.passphrase}`; } // Return the words. return mnemonicWords; } /** * Gets the number of words in the mnemonic (excluding passphrase). * * @returns Number of words. */ public toWords(includePassphrase: boolean = true): Array { const words = [...this.mnemonic.words]; if (this.mnemonic.passphrase && includePassphrase) { words.push(this.mnemonic.passphrase); } return words; } /** * Returns the entropy for this mnemonic. * * @param wordList Optional wordlist (defaults to English BIP-39 wordlist). * * @throws {Error} If entropy could not be decoded from the mnemonic using the provided wordlist. * * @returns The entropy used to derive the mnemonic words. */ public toEntropy(wordList: Array = bip39WordListEnglish): Bytes { const entropy = decodeBip39MnemonicNonStandard(this.toPhrase(), wordList); if (typeof entropy === 'string') { throw new Error(entropy); } return Bytes.fromBytes(entropy); } /** * Returns a serializable represntation of the class * * @returns A serializable reprentation of the class. */ public toRaw(): MnemonicRaw { return { ...this.mnemonic }; } /** * Gets the passphrase if one exists. * * @returns The passphrase or undefined if none exists. */ public getPassphrase(): string | undefined { return this.mnemonic.passphrase; } }