Resolve "WIP: UTXO Selection and Storage"

This commit is contained in:
Jim Hamill
2025-01-11 08:39:24 +00:00
parent f934216d68
commit 33fd9bac4d
156 changed files with 13304 additions and 4595 deletions
+246
View File
@@ -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 };
}
}