Refactor wallet inheritance.

This commit is contained in:
Richard Moore 2022-11-27 21:49:24 -05:00
parent cf4a579bf5
commit 1966c2d6d4
3 changed files with 145 additions and 28 deletions

@ -6,33 +6,60 @@ import {
defineProperties, resolveProperties, assert, assertArgument
} from "../utils/index.js";
import {
encryptKeystoreJson, encryptKeystoreJsonSync,
} from "./json-keystore.js";
import type { SigningKey } from "../crypto/index.js";
import type { TypedDataDomain, TypedDataField } from "../hash/index.js";
import type { Provider, TransactionRequest } from "../providers/index.js";
import type { TransactionLike } from "../transaction/index.js";
import type { ProgressCallback } from "../crypto/index.js";
/**
* The **BaseWallet** is a stream-lined implementation of a
* [[Signer]] that operates with a private key.
*
* It is preferred to use the [[Wallet]] class, as it offers
* additional functionality and simplifies loading a variety
* of JSON formats, Mnemonic Phrases, etc.
*
* This class may be of use for those attempting to implement
* a minimal Signer.
*/
export class BaseWallet extends AbstractSigner {
/**
* The wallet address.
*/
readonly address!: string;
readonly #signingKey: SigningKey;
/**
* Creates a new BaseWallet for %%privateKey%%, optionally
* connected to %%provider%%.
*
* If %%provider%% is not specified, only offline methods can
* be used.
*/
constructor(privateKey: SigningKey, provider?: null | Provider) {
super(provider);
assertArgument(privateKey && typeof(privateKey.sign) === "function", "invalid private key", "privateKey", "[ REDACTED ]");
this.#signingKey = privateKey;
const address = computeAddress(this.signingKey.publicKey);
defineProperties<BaseWallet>(this, { address });
}
// Store these in getters to reduce visibility in console.log
// Store private values behind getters to reduce visibility
// in console.log
/**
* The [[SigningKey]] used for signing payloads.
*/
get signingKey(): SigningKey { return this.#signingKey; }
/**
* The private key for this wallet.
*/
get privateKey(): string { return this.signingKey.privateKey; }
async getAddress(): Promise<string> { return this.address; }
@ -41,16 +68,6 @@ export class BaseWallet extends AbstractSigner {
return new BaseWallet(this.#signingKey, provider);
}
async encrypt(password: Uint8Array | string, progressCallback?: ProgressCallback): Promise<string> {
const account = { address: this.address, privateKey: this.privateKey };
return await encryptKeystoreJson(account, password, { progressCallback });
}
encryptSync(password: Uint8Array | string): string {
const account = { address: this.address, privateKey: this.privateKey };
return encryptKeystoreJsonSync(account, password);
}
async signTransaction(tx: TransactionRequest): Promise<string> {
// Replace any Addressable or ENS name with an address
@ -76,11 +93,14 @@ export class BaseWallet extends AbstractSigner {
}
async signMessage(message: string | Uint8Array): Promise<string> {
return this.signingKey.sign(hashMessage(message)).serialized;
return this.signMessageSync(message);
}
// @TODO: Add a secialized signTx and signTyped sync that enforces
// all parameters are known?
/**
* Returns the signature for %%message%% signed with this wallet.
*/
signMessageSync(message: string | Uint8Array): string {
return this.signingKey.sign(hashMessage(message)).serialized;
}

@ -1,23 +1,34 @@
/**
* Explain HD Wallets..
*
* @_subsection: api/wallet:HD Wallets [hd-wallets]
*/
import { computeHmac, randomBytes, ripemd160, SigningKey, sha256 } from "../crypto/index.js";
import { VoidSigner } from "../providers/index.js";
import { computeAddress } from "../transaction/index.js";
import {
concat, dataSlice, decodeBase58, defineProperties, encodeBase58,
getBytes, hexlify, isBytesLike,
getNumber, toBigInt, toHex,
getNumber, toArray, toBigInt, toHex,
assertPrivate, assert, assertArgument
} from "../utils/index.js";
import { langEn } from "../wordlists/lang-en.js";
import { LangEn } from "../wordlists/lang-en.js";
import { Mnemonic } from "./mnemonic.js";
import { BaseWallet } from "./base-wallet.js";
import { Mnemonic } from "./mnemonic.js";
import {
encryptKeystoreJson, encryptKeystoreJsonSync,
} from "./json-keystore.js";
import type { BytesLike, Numeric } from "../utils/index.js";
import type { ProgressCallback } from "../crypto/index.js";
import type { Provider } from "../providers/index.js";
import type { BytesLike, Numeric } from "../utils/index.js";
import type { Wordlist } from "../wordlists/index.js";
import type { KeystoreAccount } from "./json-keystore.js";
export const defaultPath = "m/44'/60'/0'/0/0";
export const defaultPath: string = "m/44'/60'/0'/0/0";
// "Bitcoin seed"
@ -114,6 +125,9 @@ export class HDNodeWallet extends BaseWallet {
readonly index!: number;
readonly depth!: number;
/**
* @private
*/
constructor(guard: any, signingKey: SigningKey, parentFingerprint: string, chainCode: string, path: null | string, index: number, depth: number, mnemonic: null | Mnemonic, provider: null | Provider) {
super(signingKey, provider);
assertPrivate(guard, _guard, "HDNodeWallet");
@ -134,6 +148,45 @@ export class HDNodeWallet extends BaseWallet {
this.chainCode, this.path, this.index, this.depth, this.mnemonic, provider);
}
#account(): KeystoreAccount {
const account: KeystoreAccount = { address: this.address, privateKey: this.privateKey };
const m = this.mnemonic;
if (this.path && m && m.wordlist.locale === "en" && m.password === "") {
account.mnemonic = {
path: this.path,
locale: "en",
entropy: m.entropy
};
}
return account;
}
/**
* Resolves to a [JSON Keystore Wallet](json-wallets) encrypted with
* %%password%%.
*
* If %%progressCallback%% is specified, it will receive periodic
* updates as the encryption process progreses.
*/
async encrypt(password: Uint8Array | string, progressCallback?: ProgressCallback): Promise<string> {
return await encryptKeystoreJson(this.#account(), password, { progressCallback });
}
/**
* Returns a [JSON Keystore Wallet](json-wallets) encryped with
* %%password%%.
*
* It is preferred to use the [async version](encrypt) instead,
* which allows a [[ProgressCallback]] to keep the user informed.
*
* This method will block the event loop (freezing all UI) until
* it is complete, which may be a non-trivial duration.
*/
encryptSync(password: Uint8Array | string): string {
return encryptKeystoreJsonSync(this.#account(), password);
}
get extendedKey(): string {
// We only support the mainnet values for now, but if anyone needs
// testnet values, let me know. I believe current sentiment is that
@ -195,7 +248,7 @@ export class HDNodeWallet extends BaseWallet {
}
static fromExtendedKey(extendedKey: string): HDNodeWallet | HDNodeVoidWallet {
const bytes = getBytes(decodeBase58(extendedKey)); // @TODO: redact
const bytes = toArray(decodeBase58(extendedKey)); // @TODO: redact
assertArgument(bytes.length === 82 || encodeBase58Check(bytes.slice(0, 78)) === extendedKey,
"invalid extended key", "extendedKey", "[ REDACTED ]");
@ -228,7 +281,7 @@ export class HDNodeWallet extends BaseWallet {
static createRandom(password?: string, path?: string, wordlist?: Wordlist): HDNodeWallet {
if (password == null) { password = ""; }
if (path == null) { path = defaultPath; }
if (wordlist == null) { wordlist = langEn; }
if (wordlist == null) { wordlist = LangEn.wordlist(); }
const mnemonic = Mnemonic.fromEntropy(randomBytes(16), password, wordlist)
return HDNodeWallet.#fromSeed(mnemonic.computeSeed(), mnemonic).derivePath(path);
}
@ -241,7 +294,7 @@ export class HDNodeWallet extends BaseWallet {
static fromPhrase(phrase: string, password?: string, path?: string, wordlist?: Wordlist): HDNodeWallet {
if (password == null) { password = ""; }
if (path == null) { path = defaultPath; }
if (wordlist == null) { wordlist = langEn; }
if (wordlist == null) { wordlist = LangEn.wordlist(); }
const mnemonic = Mnemonic.fromPhrase(phrase, password, wordlist)
return HDNodeWallet.#fromSeed(mnemonic.computeSeed(), mnemonic).derivePath(path);
}
@ -263,6 +316,9 @@ export class HDNodeVoidWallet extends VoidSigner {
readonly index!: number;
readonly depth!: number;
/**
* @private
*/
constructor(guard: any, address: string, publicKey: string, parentFingerprint: string, chainCode: string, path: null | string, index: number, depth: number, provider: null | Provider) {
super(address, provider);
assertPrivate(guard, _guard, "HDNodeVoidWallet");
@ -330,7 +386,10 @@ export class HDNodeVoidWallet extends VoidSigner {
export class HDNodeWalletManager {
#root: HDNodeWallet;
constructor(phrase: string, password: string = "", path: string = "m/44'/60'/0'/0", locale: Wordlist = langEn) {
constructor(phrase: string, password?: null | string, path?: null | string, locale?: null | Wordlist) {
if (password == null) { password = ""; }
if (path == null) { path = "m/44'/60'/0'/0"; }
if (locale == null) { locale = LangEn.wordlist(); }
this.#root = HDNodeWallet.fromPhrase(phrase, password, path, locale);
}

@ -6,6 +6,7 @@ import { HDNodeWallet } from "./hdwallet.js";
import { decryptCrowdsaleJson, isCrowdsaleJson } from "./json-crowdsale.js";
import {
decryptKeystoreJson, decryptKeystoreJsonSync,
encryptKeystoreJson, encryptKeystoreJsonSync,
isKeystoreJson
} from "./json-keystore.js";
import { Mnemonic } from "./mnemonic.js";
@ -21,6 +22,16 @@ function stall(duration: number): Promise<void> {
return new Promise((resolve) => { setTimeout(() => { resolve(); }, duration); });
}
/**
* A **Wallet** manages a single private key which is used to sign
* transactions, messages and other common payloads.
*
* This class is generally the main entry point for developers
* that wish to use a private key directly, as it can create
* instances from a large variety of common sources, including
* raw private key, [[link-bip-39]] mnemonics and encrypte JSON
* wallets.
*/
export class Wallet extends BaseWallet {
constructor(key: string | SigningKey, provider?: null | Provider) {
@ -32,6 +43,33 @@ export class Wallet extends BaseWallet {
return new Wallet(this.signingKey, provider);
}
/**
* Resolves to a [JSON Keystore Wallet](json-wallets) encrypted with
* %%password%%.
*
* If %%progressCallback%% is specified, it will receive periodic
* updates as the encryption process progreses.
*/
async encrypt(password: Uint8Array | string, progressCallback?: ProgressCallback): Promise<string> {
const account = { address: this.address, privateKey: this.privateKey };
return await encryptKeystoreJson(account, password, { progressCallback });
}
/**
* Returns a [JSON Keystore Wallet](json-wallets) encryped with
* %%password%%.
*
* It is preferred to use the [async version](encrypt) instead,
* which allows a [[ProgressCallback]] to keep the user informed.
*
* This method will block the event loop (freezing all UI) until
* it is complete, which may be a non-trivial duration.
*/
encryptSync(password: Uint8Array | string): string {
const account = { address: this.address, privateKey: this.privateKey };
return encryptKeystoreJsonSync(account, password);
}
static #fromAccount(account: null | CrowdsaleAccount | KeystoreAccount): HDNodeWallet | Wallet {
assertArgument(account, "invalid JSON wallet", "json", "[ REDACTED ]");
@ -67,7 +105,7 @@ export class Wallet extends BaseWallet {
return Wallet.#fromAccount(account);
}
static fromEncryptedJsonSync(json: string, password: Uint8Array | string): Wallet {
static fromEncryptedJsonSync(json: string, password: Uint8Array | string): HDNodeWallet | Wallet {
let account: null | CrowdsaleAccount | KeystoreAccount = null;
if (isKeystoreJson(json)) {
account = decryptKeystoreJsonSync(json, password);