Resolve "WIP: UTXO Selection and Storage"
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
import {
|
||||
generateBip39MnemonicNonStandard,
|
||||
bip39WordListEnglish,
|
||||
} 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.
|
||||
*/
|
||||
export class Mnemonic {
|
||||
public 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.
|
||||
*/
|
||||
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 dictionary 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) {
|
||||
// If this is a list of words...
|
||||
if (Array.isArray(phraseOrWords)) {
|
||||
return this.fromWords(phraseOrWords, wordList);
|
||||
}
|
||||
|
||||
return this.fromString(phraseOrWords, wordList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a mnemonic from a string
|
||||
*
|
||||
* @param phrase The mnemonic phrase as a string
|
||||
* @param dictionary Optional wordlist (defaults to English BIP-39 wordlist).
|
||||
* @throws {Error} If mnemonic could not be generated.
|
||||
* @returns Instance of Mnemonic.
|
||||
*/
|
||||
public static fromString(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 dictionary 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());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the passphrase if one exists.
|
||||
*
|
||||
* @returns The passphrase or undefined if none exists.
|
||||
*/
|
||||
public getPassphrase(): string | undefined {
|
||||
return this.mnemonic.passphrase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the complete phrase as a string.
|
||||
*
|
||||
* @param includePassphrase Whether to include the passphrase in the output.
|
||||
* @returns Complete phrase as a string.
|
||||
*/
|
||||
public toString(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 };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user