291 lines
8.0 KiB
TypeScript
291 lines
8.0 KiB
TypeScript
import {
|
|
bip39WordListEnglish,
|
|
deriveSeedFromBip39Mnemonic,
|
|
generateBip39MnemonicNonStandard,
|
|
} from '@bitauth/libauth';
|
|
|
|
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<string>,
|
|
wordList: Array<string> = bip39WordListEnglish,
|
|
) {
|
|
return Array.isArray(phraseOrWords)
|
|
? this.fromWords(phraseOrWords, wordList)
|
|
: this.fromPhrase(phraseOrWords, 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<string> = 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<string>,
|
|
wordList: Array<string> = 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<string>);
|
|
|
|
// 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<string>,
|
|
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<string> {
|
|
const words = [...this.mnemonic.words];
|
|
|
|
if (this.mnemonic.passphrase && includePassphrase) {
|
|
words.push(this.mnemonic.passphrase);
|
|
}
|
|
|
|
return words;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|