diff --git a/src.ts/wallet/base-wallet.ts b/src.ts/wallet/base-wallet.ts index 72e75a1eb..4ef6bd7b8 100644 --- a/src.ts/wallet/base-wallet.ts +++ b/src.ts/wallet/base-wallet.ts @@ -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(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 { 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 { - 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 { // Replace any Addressable or ENS name with an address @@ -76,11 +93,14 @@ export class BaseWallet extends AbstractSigner { } async signMessage(message: string | Uint8Array): Promise { - 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; } diff --git a/src.ts/wallet/hdwallet.ts b/src.ts/wallet/hdwallet.ts index 00b310229..902c38c83 100644 --- a/src.ts/wallet/hdwallet.ts +++ b/src.ts/wallet/hdwallet.ts @@ -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 { + 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); } diff --git a/src.ts/wallet/wallet.ts b/src.ts/wallet/wallet.ts index fc4602118..56bad579d 100644 --- a/src.ts/wallet/wallet.ts +++ b/src.ts/wallet/wallet.ts @@ -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 { 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 { + 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);