diff --git a/src/_tests/test-wallet-hd.ts b/src/_tests/test-wallet-hd.ts index f168cfa7..e5b87ea5 100644 --- a/src/_tests/test-wallet-hd.ts +++ b/src/_tests/test-wallet-hd.ts @@ -1,22 +1,14 @@ -//! TODO: Fix this test -/* import assert from 'assert'; import { loadTests } from './utils.js'; +import { HDNodeWallet, HDNodeVoidWallet } from '../wallet/index.js'; -import { - getBytes, wordlists, - QuaiHDWallet, Mnemonic, - QiHDWallet, -} from "../index.js"; +import { getBytes, wordlists, Mnemonic } from '../quais.js'; -import type { Wordlist } from "../wordlists/index.js"; - -import type { TestCaseMnemonic, TestCaseMnemonicNode } from "./types.js"; - -// import type { HDWalletStatic } from "../wallet/hdwallet.js"; +import type { Wordlist } from '../wordlists/index.js'; +import type { TestCaseMnemonic, TestCaseMnemonicNode } from './types.js'; const decoder = new TextDecoder(); function fromHex(hex: string): string { @@ -33,21 +25,21 @@ type Test = { test: TestCaseMnemonic; }; -describe("Test HDWallets", function() { - function checkWallet(wallet: QuaiHDWallet | QiHDWallet, test: TestCaseMnemonicNode): void { - assert.equal(wallet.chainCode, test.chainCode, "chainCode"); - assert.equal(wallet.depth, test.depth, "depth"); - assert.equal(wallet.index, test.index, "index"); - assert.equal(wallet.fingerprint, test.fingerprint, "fingerprint"); - assert.equal(wallet.accountFingerprint, test.parentFingerprint, "parentFingerprint"); - assert.equal(wallet.publicKey, test.publicKey, "publicKey"); - - if (wallet instanceof QuaiHDWallet) { - assert.equal(wallet.extendedKey, test.xpriv, "xpriv"); - assert.equal(wallet.privateKey, test.privateKey, "privateKey"); - assert.equal(wallet.neuter().extendedKey, test.xpub, "xpub"); - } else if (wallet instanceof QiHDWallet) { - assert.equal(wallet.extendedKey, test.xpub, "xpub"); +describe('Test HDWallets', function () { + function checkWallet(wallet: HDNodeWallet | HDNodeVoidWallet, test: TestCaseMnemonicNode): void { + assert.equal(wallet.chainCode, test.chainCode, 'chainCode'); + assert.equal(wallet.depth, test.depth, 'depth'); + assert.equal(wallet.index, test.index, 'index'); + assert.equal(wallet.fingerprint, test.fingerprint, 'fingerprint'); + assert.equal(wallet.parentFingerprint, test.parentFingerprint, 'parentFingerprint'); + assert.equal(wallet.publicKey, test.publicKey, 'publicKey'); + + if (wallet instanceof HDNodeWallet) { + assert.equal(wallet.extendedKey, test.xpriv, 'xpriv'); + assert.equal(wallet.privateKey, test.privateKey, 'privateKey'); + assert.equal(wallet.neuter().extendedKey, test.xpub, 'xpub'); + } else if (wallet instanceof HDNodeVoidWallet) { + assert.equal(wallet.extendedKey, test.xpub, 'xpub'); } } @@ -90,10 +82,9 @@ describe("Test HDWallets", function() { for (const { test, checkMnemonic, phrase, password, wordlist } of checks) { it(`computes the HD keys by mnemonic: ${test.name}`, function () { for (const subtest of test.nodes) { - const WalletClass = QuaiHDWallet as typeof QuaiHDWallet & HDWalletStatic; - const w = WalletClass.fromPhrase(phrase, password, subtest.path, wordlist); - assert.ok(w instanceof QuaiHDWallet, "instanceof QuaiHDWallet"); - assert.equal(w.path, subtest.path, "path") + const w = HDNodeWallet.fromPhrase(phrase, subtest.path, password, wordlist); + assert.ok(w instanceof HDNodeWallet, 'instanceof HDNodeWallet'); + assert.equal(w.path, subtest.path, 'path'); checkWallet(w, subtest); assert.ok(!!w.mnemonic, 'has mnemonic'); checkMnemonic(w.mnemonic as Mnemonic); @@ -102,12 +93,11 @@ describe("Test HDWallets", function() { } for (const { test } of checks) { - it(`computes the HD keys by entropy: ${ test.name }`, function() { - const WalletClass = QuaiHDWallet as typeof QuaiHDWallet & HDWalletStatic; - const seedRoot = WalletClass.fromSeed(test.seed); + it(`computes the HD keys by entropy: ${test.name}`, function () { + const seedRoot = HDNodeWallet.fromSeed(test.seed); for (const subtest of test.nodes) { const w = seedRoot.derivePath(subtest.path); - assert.ok(w instanceof QuaiHDWallet, 'instanceof QuaiHDWallet'); + assert.ok(w instanceof HDNodeWallet, 'instanceof HDNodeWallet'); assert.equal(w.path, subtest.path, 'path'); checkWallet(w, subtest); assert.equal(w.mnemonic, null); @@ -118,41 +108,49 @@ describe("Test HDWallets", function() { for (const { test } of checks) { it(`computes the HD keys by enxtended private key: ${test.name}`, function () { for (const subtest of test.nodes) { - const w = QuaiHDWallet.fromExtendedKey(subtest.xpriv); - assert.ok(w instanceof QuaiHDWallet, 'instanceof QuaiHDWallet'); + const w = HDNodeWallet.fromExtendedKey(subtest.xpriv); + assert.ok(w instanceof HDNodeWallet, 'instanceof HDNodeWallet'); checkWallet(w, subtest); assert.equal(w.mnemonic, null); } }); } - // ! TODO: Fix this test - // for (const { test, phrase, password, wordlist } of checks) { - // it(`computes the neutered HD keys by paths: ${ test.name }`, function() { - // const root = QuaiHDWallet.fromPhrase(phrase, "m", password, wordlist).neuter(); - // for (const subtest of test.nodes) { - // if (subtest.path.indexOf("'") >= 0) { - // assert.throws( - // () => { - // const w = root.derivePath(subtest.path); - // console.log(w); - // }, - // (error: any) => { - // return ( - // error.code === 'UNSUPPORTED_OPERATION' && - // error.message.match(/^cannot derive child of neutered node/) && - // error.operation === 'deriveChild' - // ); - // }, - // ); - // } else { - // const w = root.derivePath(subtest.path); - // assert.ok(w instanceof HDNodeVoidWallet, 'instanceof HDNodeVoidWallet'); - // assert.equal(w.path, subtest.path, 'path'); - // checkWallet(w, subtest); - // } - // } - // }); - // } + for (const { test, phrase, password, wordlist } of checks) { + it(`computes the neutered HD keys by paths: ${test.name}`, function () { + const root = HDNodeWallet.fromPhrase(phrase, 'm', password, wordlist).neuter(); + for (const subtest of test.nodes) { + if (subtest.path.indexOf("'") >= 0) { + assert.throws( + () => { + const w = root.derivePath(subtest.path); + console.log(w); + }, + (error: any) => { + return ( + error.code === 'UNSUPPORTED_OPERATION' && + error.message.match(/^cannot derive child of neutered node/) && + error.operation === 'deriveChild' + ); + }, + ); + } else { + const w = root.derivePath(subtest.path); + assert.ok(w instanceof HDNodeVoidWallet, 'instanceof HDNodeVoidWallet'); + assert.equal(w.path, subtest.path, 'path'); + checkWallet(w, subtest); + } + } + }); + } + + for (const { test } of checks) { + it(`computes the neutered HD keys by enxtended public key: ${test.name}`, function () { + for (const subtest of test.nodes) { + const w = HDNodeWallet.fromExtendedKey(subtest.xpub); + assert.ok(w instanceof HDNodeVoidWallet, 'instanceof HDNodeVoidWallet'); + checkWallet(w, subtest); + } + }); + } }); -*/ \ No newline at end of file diff --git a/src/wallet/hdnodewallet.ts b/src/wallet/hdnodewallet.ts index 6fe2dceb..9e918520 100644 --- a/src/wallet/hdnodewallet.ts +++ b/src/wallet/hdnodewallet.ts @@ -1,86 +1,106 @@ -import { computeHmac, randomBytes, ripemd160, SigningKey, sha256 } from "../crypto/index.js"; -import { VoidSigner } from "../signers/index.js"; -import { computeAddress } from "../address/index.js"; -import { decodeBase58, encodeBase58 } from "../encoding/index.js"; +import { computeHmac, randomBytes, ripemd160, SigningKey, sha256 } from '../crypto/index.js'; +import { VoidSigner } from '../signers/index.js'; +import { computeAddress } from '../address/index.js'; +import { decodeBase58, encodeBase58 } from '../encoding/index.js'; import { - concat, dataSlice, defineProperties, - getBytes, hexlify, isBytesLike, - getNumber, toBeArray, toBigInt, toBeHex, - assertPrivate, assert, assertArgument -} from "../utils/index.js"; -import { LangEn } from "../wordlists/lang-en.js"; - -import { BaseWallet } from "./base-wallet.js"; -import { Mnemonic } from "./mnemonic.js"; -import { - encryptKeystoreJson, encryptKeystoreJsonSync, -} from "./json-keystore.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"; + concat, + dataSlice, + defineProperties, + getBytes, + hexlify, + isBytesLike, + getNumber, + toBeArray, + toBigInt, + toBeHex, + assertPrivate, + assert, + assertArgument, +} from '../utils/index.js'; +import { LangEn } from '../wordlists/lang-en.js'; + +import { BaseWallet } from './base-wallet.js'; +import { Mnemonic } from './mnemonic.js'; +import { encryptKeystoreJson, encryptKeystoreJsonSync } from './json-keystore.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'; // "Bitcoin seed" -const MasterSecret = new Uint8Array([ 66, 105, 116, 99, 111, 105, 110, 32, 115, 101, 101, 100 ]); +const MasterSecret = new Uint8Array([66, 105, 116, 99, 111, 105, 110, 32, 115, 101, 101, 100]); const HardenedBit = 0x80000000; -const N = BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); +const N = BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'); -const Nibbles = "0123456789abcdef"; +const Nibbles = '0123456789abcdef'; function zpad(value: number, length: number): string { - let result = ""; + let result = ''; while (value) { result = Nibbles[value % 16] + result; value = Math.trunc(value / 16); } - while (result.length < length * 2) { result = "0" + result; } - return "0x" + result; + while (result.length < length * 2) { + result = '0' + result; + } + return '0x' + result; } function encodeBase58Check(_value: BytesLike): string { const value = getBytes(_value); const check = dataSlice(sha256(sha256(value)), 0, 4); - const bytes = concat([ value, check ]); + const bytes = concat([value, check]); return encodeBase58(bytes); } -const _guard = { }; +const _guard = {}; -function ser_I(index: number, chainCode: string, publicKey: string, privateKey: null | string): { IL: Uint8Array, IR: Uint8Array } { +function ser_I( + index: number, + chainCode: string, + publicKey: string, + privateKey: null | string, +): { IL: Uint8Array; IR: Uint8Array } { const data = new Uint8Array(37); if (index & HardenedBit) { - assert(privateKey != null, "cannot derive child of neutered node", "UNSUPPORTED_OPERATION", { - operation: "deriveChild" + assert(privateKey != null, 'cannot derive child of neutered node', 'UNSUPPORTED_OPERATION', { + operation: 'deriveChild', }); // Data = 0x00 || ser_256(k_par) data.set(getBytes(privateKey), 1); - } else { // Data = ser_p(point(k_par)) data.set(getBytes(publicKey)); } // Data += ser_32(i) - for (let i = 24; i >= 0; i -= 8) { data[33 + (i >> 3)] = ((index >> (24 - i)) & 0xff); } - const I = getBytes(computeHmac("sha512", chainCode, data)); + for (let i = 24; i >= 0; i -= 8) { + data[33 + (i >> 3)] = (index >> (24 - i)) & 0xff; + } + const I = getBytes(computeHmac('sha512', chainCode, data)); return { IL: I.slice(0, 32), IR: I.slice(32) }; } -type HDNodeLike = { depth: number, deriveChild: (i: number) => T }; +type HDNodeLike = { depth: number; deriveChild: (i: number) => T }; function derivePath>(node: T, path: string): T { - const components = path.split("/"); + const components = path.split('/'); - assertArgument(components.length > 0, "invalid path", "path", path); + assertArgument(components.length > 0, 'invalid path', 'path', path); - if (components[0] === "m") { - assertArgument(node.depth === 0, `cannot derive root path (i.e. path starting with "m/") for a node at non-zero depth ${ node.depth }`, "path", path); + if (components[0] === 'm') { + assertArgument( + node.depth === 0, + `cannot derive root path (i.e. path starting with "m/") for a node at non-zero depth ${node.depth}`, + 'path', + path, + ); components.shift(); } @@ -90,16 +110,14 @@ function derivePath>(node: T, path: string): T { if (component.match(/^[0-9]+'$/)) { const index = parseInt(component.substring(0, component.length - 1)); - assertArgument(index < HardenedBit, "invalid path index", `path[${ i }]`, component); + assertArgument(index < HardenedBit, 'invalid path index', `path[${i}]`, component); result = result.deriveChild(HardenedBit + index); - } else if (component.match(/^[0-9]+$/)) { const index = parseInt(component); - assertArgument(index < HardenedBit, "invalid path index", `path[${ i }]`, component); + assertArgument(index < HardenedBit, 'invalid path index', `path[${i}]`, component); result = result.deriveChild(index); - } else { - assertArgument(false, "invalid path component", `path[${ i }]`, component); + assertArgument(false, 'invalid path component', `path[${i}]`, component); } } @@ -107,125 +125,151 @@ function derivePath>(node: T, path: string): T { } /** - * An **HDNodeWallet** is a [[Signer]] backed by the private key derived - * from an HD Node using the [[link-bip-32]] standard. + * An **HDNodeWallet** is a [[Signer]] backed by the private key derived from an HD Node using the [[link-bip-32]] + * standard. * - * An HD Node forms a hierarchical structure with each HD Node having a - * private key and the ability to derive child HD Nodes, defined by - * a path indicating the index of each child. + * An HD Node forms a hierarchical structure with each HD Node having a private key and the ability to derive child HD + * Nodes, defined by a path indicating the index of each child. */ export class HDNodeWallet extends BaseWallet { /** - * The compressed public key. - * @type {string} + * The compressed public key. + * + * @type {string} */ readonly publicKey!: string; /** - * The fingerprint. + * The fingerprint. + * + * A fingerprint allows a quick way to detect parent and child nodes, but developers should be prepared to deal with + * collisions as it is only 4 bytes. * - * A fingerprint allows a quick way to detect parent and child nodes, - * but developers should be prepared to deal with collisions as it - * is only 4 bytes. - * @type {string} + * @type {string} */ readonly fingerprint!: string; /** - * The parent fingerprint. - * @type {string} + * The parent fingerprint. + * + * @type {string} */ readonly parentFingerprint!: string; /** - * The mnemonic used to create this HD Node, if available. + * The mnemonic used to create this HD Node, if available. * - * Sources such as extended keys do not encode the mnemonic, in - * which case this will be `null`. - * @type {null | Mnemonic} + * Sources such as extended keys do not encode the mnemonic, in which case this will be `null`. + * + * @type {null | Mnemonic} */ readonly mnemonic!: null | Mnemonic; /** - * The chaincode, which is effectively a public key used - * to derive children. - * @type {string} + * The chaincode, which is effectively a public key used to derive children. + * + * @type {string} */ readonly chainCode!: string; /** - * The derivation path of this wallet. + * The derivation path of this wallet. + * + * Since extended keys do not provide full path details, this may be `null`, if instantiated from a source that does + * not encode it. * - * Since extended keys do not provide full path details, this - * may be `null`, if instantiated from a source that does not - * encode it. - * @type {null | string} + * @type {null | string} */ readonly path!: null | string; /** - * The child index of this wallet. Values over `2 ** 31` indicate - * the node is hardened. - * @type {number} + * The child index of this wallet. Values over `2 ** 31` indicate the node is hardened. + * + * @type {number} */ readonly index!: number; /** - * The depth of this wallet, which is the number of components - * in its path. - * @type {number} + * The depth of this wallet, which is the number of components in its path. + * + * @type {number} */ readonly depth!: number; /** - * @private - * @param {any} guard - * @param {SigningKey} signingKey - * @param {string} parentFingerprint - * @param {string} chainCode - * @param {null | string} path - * @param {number} index - * @param {number} depth - * @param {null | Mnemonic} mnemonic - * @param {null | Provider} provider - */ - constructor(guard: any, signingKey: SigningKey, parentFingerprint: string, chainCode: string, path: null | string, index: number, depth: number, mnemonic: null | Mnemonic, provider: null | Provider) { + * @private + * @param {any} guard + * @param {SigningKey} signingKey + * @param {string} parentFingerprint + * @param {string} chainCode + * @param {null | string} path + * @param {number} index + * @param {number} depth + * @param {null | Mnemonic} mnemonic + * @param {null | Provider} provider + */ + 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"); + assertPrivate(guard, _guard, 'HDNodeWallet'); defineProperties(this, { publicKey: signingKey.compressedPublicKey }); const fingerprint = dataSlice(ripemd160(sha256(this.publicKey)), 0, 4); defineProperties(this, { - parentFingerprint, fingerprint, - chainCode, path, index, depth + parentFingerprint, + fingerprint, + chainCode, + path, + index, + depth, }); defineProperties(this, { mnemonic }); } /** - * Connects the wallet to a provider. - * @param {null | Provider} provider - * @returns {HDNodeWallet} + * Connects the wallet to a provider. + * + * @param {null | Provider} provider + * + * @returns {HDNodeWallet} */ connect(provider: null | Provider): HDNodeWallet { - return new HDNodeWallet(_guard, this.signingKey, this.parentFingerprint, - this.chainCode, this.path, this.index, this.depth, this.mnemonic, provider); + return new HDNodeWallet( + _guard, + this.signingKey, + this.parentFingerprint, + this.chainCode, + this.path, + this.index, + this.depth, + this.mnemonic, + provider, + ); } /** - * @private - * @returns {KeystoreAccount} + * @private + * @returns {KeystoreAccount} */ #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 === "") { + if (this.path && m && m.wordlist.locale === 'en' && m.password === '') { account.mnemonic = { path: this.path, - locale: "en", - entropy: m.entropy + locale: 'en', + entropy: m.entropy, }; } @@ -233,41 +277,42 @@ export class HDNodeWallet extends BaseWallet { } /** - * Resolves to a [JSON Keystore Wallet](json-wallets) encrypted with - * `password`. + * Resolves to a [JSON Keystore Wallet](json-wallets) encrypted with `password`. + * + * If `progressCallback` is specified, it will receive periodic updates as the encryption process progresses. * - * If `progressCallback` is specified, it will receive periodic - * updates as the encryption process progresses. - * @param {Uint8Array | string} password - * @param {ProgressCallback} [progressCallback] - * @returns {Promise} + * @param {Uint8Array | string} password + * @param {ProgressCallback} [progressCallback] + * + * @returns {Promise} */ async encrypt(password: Uint8Array | string, progressCallback?: ProgressCallback): Promise { return await encryptKeystoreJson(this.#account(), password, { progressCallback }); } /** - * Returns a [JSON Keystore Wallet](json-wallets) encrypted with - * `password`. + * Returns a [JSON Keystore Wallet](json-wallets) encrypted 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. * - * It is preferred to use the [async version](encrypt) instead, - * which allows a `ProgressCallback` to keep the user informed. + * @param {Uint8Array | string} password * - * This method will block the event loop (freezing all UI) until - * it is complete, which may be a non-trivial duration. - * @param {Uint8Array | string} password - * @returns {string} + * @returns {string} */ encryptSync(password: Uint8Array | string): string { return encryptKeystoreJsonSync(this.#account(), password); } /** - * The extended key. + * The extended key. * - * This key will begin with the prefix `xpriv` and can be used to - * reconstruct this HD Node to derive its children. - * @returns {string} + * This key will begin with the prefix `xpriv` and can be used to reconstruct this HD Node to derive its children. + * + * @returns {string} */ get extendedKey(): string { // We only support the mainnet values for now, but if anyone needs @@ -280,106 +325,134 @@ export class HDNodeWallet extends BaseWallet { operation: 'extendedKey', }); - const myKey = encodeBase58Check( + return encodeBase58Check( concat([ '0x0488ADE4', - '0x' + zpad(this.depth, 2), - this.parentFingerprint ?? '', - '0x' + zpad(this.index, 4), + zpad(this.depth, 1), + this.parentFingerprint, + zpad(this.index, 4), this.chainCode, concat(['0x00', this.privateKey]), ]), ); - return myKey; } /** - * Returns true if this wallet has a path, providing a Type Guard - * that the path is non-null. - * @returns {boolean} + * Returns true if this wallet has a path, providing a Type Guard that the path is non-null. + * + * @returns {boolean} */ - hasPath(): this is { path: string } { return (this.path != null); } + hasPath(): this is { path: string } { + return this.path != null; + } /** - * Returns a neutered HD Node, which removes the private details - * of an HD Node. + * Returns a neutered HD Node, which removes the private details of an HD Node. * - * A neutered node has no private key, but can be used to derive - * child addresses and other public data about the HD Node. - * @returns {HDNodeVoidWallet} + * A neutered node has no private key, but can be used to derive child addresses and other public data about the HD + * Node. + * + * @returns {HDNodeVoidWallet} */ neuter(): HDNodeVoidWallet { - return new HDNodeVoidWallet(_guard, this.address, this.publicKey, - this.parentFingerprint, this.chainCode, this.path, this.index, - this.depth, this.provider); + return new HDNodeVoidWallet( + _guard, + this.address, + this.publicKey, + this.parentFingerprint, + this.chainCode, + this.path, + this.index, + this.depth, + this.provider, + ); } /** - * Return the child for `index`. - * @param {Numeric} _index - * @returns {HDNodeWallet} + * Return the child for `index`. + * + * @param {Numeric} _index + * + * @returns {HDNodeWallet} */ deriveChild(_index: Numeric): HDNodeWallet { - const index = getNumber(_index, "index"); - assertArgument(index <= 0xffffffff, "invalid index", "index", index); + const index = getNumber(_index, 'index'); + assertArgument(index <= 0xffffffff, 'invalid index', 'index', index); // Base path let path = this.path; if (path) { - path += "/" + (index & ~HardenedBit); - if (index & HardenedBit) { path += "'"; } + path += '/' + (index & ~HardenedBit); + if (index & HardenedBit) { + path += "'"; + } } const { IR, IL } = ser_I(index, this.chainCode, this.publicKey, this.privateKey); const ki = new SigningKey(toBeHex((toBigInt(IL) + BigInt(this.privateKey)) % N, 32)); - return new HDNodeWallet(_guard, ki, this.fingerprint, hexlify(IR), - path, index, this.depth + 1, this.mnemonic, this.provider); - + return new HDNodeWallet( + _guard, + ki, + this.fingerprint, + hexlify(IR), + path, + index, + this.depth + 1, + this.mnemonic, + this.provider, + ); } /** - * Return the HDNode for `path` from this node. - * @param {string} path - * @returns {HDNodeWallet} + * Return the HDNode for `path` from this node. + * + * @param {string} path + * + * @returns {HDNodeWallet} */ derivePath(path: string): HDNodeWallet { return derivePath(this, path); } /** - * @private - * @param {BytesLike} _seed - * @param {null | Mnemonic} mnemonic - * @returns {HDNodeWallet} + * @private + * @param {BytesLike} _seed + * @param {null | Mnemonic} mnemonic + * + * @returns {HDNodeWallet} */ static #fromSeed(_seed: BytesLike, mnemonic: null | Mnemonic): HDNodeWallet { - assertArgument(isBytesLike(_seed), "invalid seed", "seed", "[REDACTED]"); + assertArgument(isBytesLike(_seed), 'invalid seed', 'seed', '[REDACTED]'); - const seed = getBytes(_seed, "seed"); - assertArgument(seed.length >= 16 && seed.length <= 64 , "invalid seed", "seed", "[REDACTED]"); + const seed = getBytes(_seed, 'seed'); + assertArgument(seed.length >= 16 && seed.length <= 64, 'invalid seed', 'seed', '[REDACTED]'); - const I = getBytes(computeHmac("sha512", MasterSecret, seed)); + const I = getBytes(computeHmac('sha512', MasterSecret, seed)); const signingKey = new SigningKey(hexlify(I.slice(0, 32))); - return new HDNodeWallet(_guard, signingKey, "0x00000000", hexlify(I.slice(32)), - "m", 0, 0, mnemonic, null); + return new HDNodeWallet(_guard, signingKey, '0x00000000', hexlify(I.slice(32)), 'm', 0, 0, mnemonic, null); } /** - * Creates a new HD Node from `extendedKey`. + * Creates a new HD Node from `extendedKey`. + * + * If the `extendedKey` will either have a prefix or `xpub` or `xpriv`, returning a neutered HD Node + * ([[HDNodeVoidWallet]]) or full HD Node ([[HDNodeWallet]]) respectively. + * + * @param {string} extendedKey * - * If the `extendedKey` will either have a prefix or `xpub` or - * `xpriv`, returning a neutered HD Node ([[HDNodeVoidWallet]]) - * or full HD Node ([[HDNodeWallet]]) respectively. - * @param {string} extendedKey - * @returns {HDNodeWallet | HDNodeVoidWallet} + * @returns {HDNodeWallet | HDNodeVoidWallet} */ static fromExtendedKey(extendedKey: string): HDNodeWallet | HDNodeVoidWallet { const bytes = toBeArray(decodeBase58(extendedKey)); // @TODO: redact - assertArgument(bytes.length === 82 || encodeBase58Check(bytes.slice(0, 78)) === extendedKey, - "invalid extended key", "extendedKey", "[ REDACTED ]"); + assertArgument( + bytes.length === 82 || encodeBase58Check(bytes.slice(0, 78)) === extendedKey, + 'invalid extended key', + 'extendedKey', + '[ REDACTED ]', + ); const depth = bytes[4]; const parentFingerprint = hexlify(bytes.slice(5, 9)); @@ -389,66 +462,103 @@ export class HDNodeWallet extends BaseWallet { switch (hexlify(bytes.slice(0, 4))) { // Public Key - case "0x0488b21e": case "0x043587cf": { + case '0x0488b21e': + case '0x043587cf': { const publicKey = hexlify(key); - return new HDNodeVoidWallet(_guard, computeAddress(publicKey), publicKey, - parentFingerprint, chainCode, null, index, depth, null); + return new HDNodeVoidWallet( + _guard, + computeAddress(publicKey), + publicKey, + parentFingerprint, + chainCode, + null, + index, + depth, + null, + ); } // Private Key - case "0x0488ade4": case "0x04358394 ": - if (key[0] !== 0) { break; } - return new HDNodeWallet(_guard, new SigningKey(key.slice(1)), - parentFingerprint, chainCode, null, index, depth, null, null); + case '0x0488ade4': + case '0x04358394 ': + if (key[0] !== 0) { + break; + } + return new HDNodeWallet( + _guard, + new SigningKey(key.slice(1)), + parentFingerprint, + chainCode, + null, + index, + depth, + null, + null, + ); } - - assertArgument(false, "invalid extended key prefix", "extendedKey", "[ REDACTED ]"); + assertArgument(false, 'invalid extended key prefix', 'extendedKey', '[ REDACTED ]'); } /** - * Creates a new random HDNode. - * @param {string} path - * @param {string} [password] - * @param {Wordlist} [wordlist] - * @returns {HDNodeWallet} + * Creates a new random HDNode. + * + * @param {string} path + * @param {string} [password] + * @param {Wordlist} [wordlist] + * + * @returns {HDNodeWallet} */ static createRandom(path: string, password?: string, wordlist?: Wordlist): HDNodeWallet { - if (password == null) { password = ""; } - if (wordlist == null) { wordlist = LangEn.wordlist(); } - const mnemonic = Mnemonic.fromEntropy(randomBytes(16), password, wordlist) + if (password == null) { + password = ''; + } + if (wordlist == null) { + wordlist = LangEn.wordlist(); + } + const mnemonic = Mnemonic.fromEntropy(randomBytes(16), password, wordlist); return HDNodeWallet.#fromSeed(mnemonic.computeSeed(), mnemonic).derivePath(path); } /** - * Create an HD Node from `mnemonic`. - * @param {Mnemonic} mnemonic - * @param {string} path - * @returns {HDNodeWallet} + * Create an HD Node from `mnemonic`. + * + * @param {Mnemonic} mnemonic + * @param {string} path + * + * @returns {HDNodeWallet} */ static fromMnemonic(mnemonic: Mnemonic, path: string): HDNodeWallet { return HDNodeWallet.#fromSeed(mnemonic.computeSeed(), mnemonic).derivePath(path); } /** - * Creates an HD Node from a mnemonic `phrase`. - * @param {string} phrase - * @param {string} path - * @param {string} [password] - * @param {Wordlist} [wordlist] - * @returns {HDNodeWallet} + * Creates an HD Node from a mnemonic `phrase`. + * + * @param {string} phrase + * @param {string} path + * @param {string} [password] + * @param {Wordlist} [wordlist] + * + * @returns {HDNodeWallet} */ - static fromPhrase(phrase: string, path: string, password?: string, wordlist?: Wordlist): HDNodeWallet { - if (password == null) { password = ""; } - if (wordlist == null) { wordlist = LangEn.wordlist(); } - const mnemonic = Mnemonic.fromPhrase(phrase, password, wordlist) + static fromPhrase(phrase: string, path: string, password?: string, wordlist?: Wordlist): HDNodeWallet { + if (password == null) { + password = ''; + } + if (wordlist == null) { + wordlist = LangEn.wordlist(); + } + const mnemonic = Mnemonic.fromPhrase(phrase, password, wordlist); return HDNodeWallet.#fromSeed(mnemonic.computeSeed(), mnemonic).derivePath(path); } /** - * Creates an HD Node from a `seed`. - * @param {BytesLike} seed - * @returns {HDNodeWallet} + * Creates an HD Node from a `seed`. + * + * @param {BytesLike} seed + * + * @returns {HDNodeWallet} */ static fromSeed(seed: BytesLike): HDNodeWallet { return HDNodeWallet.#fromSeed(seed, null); @@ -456,107 +566,136 @@ export class HDNodeWallet extends BaseWallet { } /** - * A **HDNodeVoidWallet** cannot sign, but provides access to - * the children nodes of a [[link-bip-32]] HD wallet addresses. + * A **HDNodeVoidWallet** cannot sign, but provides access to the children nodes of a [[link-bip-32]] HD wallet + * addresses. * - * They can be created by using an extended `xpub` key to - * [[HDNodeWallet_fromExtendedKey]] or by - * [neutering](HDNodeWallet-neuter) a [[HDNodeWallet]]. + * They can be created by using an extended `xpub` key to [[HDNodeWallet_fromExtendedKey]] or by + * [neutering](HDNodeWallet-neuter) a [[HDNodeWallet]]. */ export class HDNodeVoidWallet extends VoidSigner { /** - * The compressed public key. - * @type {string} + * The compressed public key. + * + * @type {string} */ readonly publicKey!: string; /** - * The fingerprint. + * The fingerprint. * - * A fingerprint allows a quick way to detect parent and child nodes, - * but developers should be prepared to deal with collisions as it - * is only 4 bytes. - * @type {string} + * A fingerprint allows a quick way to detect parent and child nodes, but developers should be prepared to deal with + * collisions as it is only 4 bytes. + * + * @type {string} */ readonly fingerprint!: string; /** - * The parent node fingerprint. - * @type {string} + * The parent node fingerprint. + * + * @type {string} */ readonly parentFingerprint!: string; /** - * The chaincode, which is effectively a public key used - * to derive children. - * @type {string} + * The chaincode, which is effectively a public key used to derive children. + * + * @type {string} */ readonly chainCode!: string; /** - * The derivation path of this wallet. + * The derivation path of this wallet. * - * Since extended keys do not provide full path details, this - * may be `null`, if instantiated from a source that does not - * encode it. - * @type {null | string} + * Since extended keys do not provide full path details, this may be `null`, if instantiated from a source that does + * not encode it. + * + * @type {null | string} */ readonly path!: null | string; /** - * The child index of this wallet. Values over `2 ** 31` indicate - * the node is hardened. - * @type {number} + * The child index of this wallet. Values over `2 ** 31` indicate the node is hardened. + * + * @type {number} */ readonly index!: number; /** - * The depth of this wallet, which is the number of components - * in its path. - * @type {number} + * The depth of this wallet, which is the number of components in its path. + * + * @type {number} */ readonly depth!: number; /** - * @private - * @param {any} guard - * @param {string} address - * @param {string} publicKey - * @param {string} parentFingerprint - * @param {string} chainCode - * @param {null | string} path - * @param {number} index - * @param {number} depth - * @param {null | Provider} provider - */ - constructor(guard: any, address: string, publicKey: string, parentFingerprint: string, chainCode: string, path: null | string, index: number, depth: number, provider: null | Provider) { + * @private + * @param {any} guard + * @param {string} address + * @param {string} publicKey + * @param {string} parentFingerprint + * @param {string} chainCode + * @param {null | string} path + * @param {number} index + * @param {number} depth + * @param {null | Provider} provider + */ + 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"); + assertPrivate(guard, _guard, 'HDNodeVoidWallet'); defineProperties(this, { publicKey }); const fingerprint = dataSlice(ripemd160(sha256(publicKey)), 0, 4); defineProperties(this, { - publicKey, fingerprint, parentFingerprint, chainCode, path, index, depth + publicKey, + fingerprint, + parentFingerprint, + chainCode, + path, + index, + depth, }); } /** - * Connects the wallet to a provider. - * @param {null | Provider} provider - * @returns {HDNodeVoidWallet} + * Connects the wallet to a provider. + * + * @param {null | Provider} provider + * + * @returns {HDNodeVoidWallet} */ connect(provider: null | Provider): HDNodeVoidWallet { - return new HDNodeVoidWallet(_guard, this.address, this.publicKey, - this.parentFingerprint, this.chainCode, this.path, this.index, this.depth, provider); + return new HDNodeVoidWallet( + _guard, + this.address, + this.publicKey, + this.parentFingerprint, + this.chainCode, + this.path, + this.index, + this.depth, + provider, + ); } /** - * The extended key. + * The extended key. + * + * This key will begin with the prefix `xpub` and can be used to reconstruct this neutered key to derive its + * children addresses. * - * This key will begin with the prefix `xpub` and can be used to - * reconstruct this neutered key to derive its children addresses. - * @returns {string} + * @returns {string} */ get extendedKey(): string { // We only support the mainnet values for now, but if anyone needs @@ -565,39 +704,47 @@ export class HDNodeVoidWallet extends VoidSigner { // - Mainnet: public=0x0488B21E, private=0x0488ADE4 // - Testnet: public=0x043587CF, private=0x04358394 - assert(this.depth < 256, "Depth too deep", "UNSUPPORTED_OPERATION", { operation: "extendedKey" }); + assert(this.depth < 256, 'Depth too deep', 'UNSUPPORTED_OPERATION', { operation: 'extendedKey' }); - return encodeBase58Check(concat([ - "0x0488B21E", - zpad(this.depth, 1), - this.parentFingerprint, - zpad(this.index, 4), - this.chainCode, - this.publicKey, - ])); + return encodeBase58Check( + concat([ + '0x0488B21E', + zpad(this.depth, 1), + this.parentFingerprint, + zpad(this.index, 4), + this.chainCode, + this.publicKey, + ]), + ); } /** - * Returns true if this wallet has a path, providing a Type Guard - * that the path is non-null. - * @returns {boolean} + * Returns true if this wallet has a path, providing a Type Guard that the path is non-null. + * + * @returns {boolean} */ - hasPath(): this is { path: string } { return (this.path != null); } + hasPath(): this is { path: string } { + return this.path != null; + } /** - * Return the child for `index`. - * @param {Numeric} _index - * @returns {HDNodeVoidWallet} + * Return the child for `index`. + * + * @param {Numeric} _index + * + * @returns {HDNodeVoidWallet} */ deriveChild(_index: Numeric): HDNodeVoidWallet { - const index = getNumber(_index, "index"); - assertArgument(index <= 0xffffffff, "invalid index", "index", index); + const index = getNumber(_index, 'index'); + assertArgument(index <= 0xffffffff, 'invalid index', 'index', index); // Base path let path = this.path; if (path) { - path += "/" + (index & ~HardenedBit); - if (index & HardenedBit) { path += "'"; } + path += '/' + (index & ~HardenedBit); + if (index & HardenedBit) { + path += "'"; + } } const { IR, IL } = ser_I(index, this.chainCode, this.publicKey, null); @@ -605,15 +752,25 @@ export class HDNodeVoidWallet extends VoidSigner { const address = computeAddress(Ki); - return new HDNodeVoidWallet(_guard, address, Ki, this.fingerprint, hexlify(IR), - path, index, this.depth + 1, this.provider); - + return new HDNodeVoidWallet( + _guard, + address, + Ki, + this.fingerprint, + hexlify(IR), + path, + index, + this.depth + 1, + this.provider, + ); } /** - * Return the signer for `path` from this node. - * @param {string} path - * @returns {HDNodeVoidWallet} + * Return the signer for `path` from this node. + * + * @param {string} path + * + * @returns {HDNodeVoidWallet} */ derivePath(path: string): HDNodeVoidWallet { return derivePath(this, path); @@ -621,35 +778,35 @@ export class HDNodeVoidWallet extends VoidSigner { } /** - * Returns the [[link-bip-32]] path for the account at `index`. + * Returns the [[link-bip-32]] path for the account at `index`. * - * This is the pattern used by wallets like Ledger. + * This is the pattern used by wallets like Ledger. * - * There is also an [alternate pattern](getIndexedAccountPath) used by - * some software. - * @param {Numeric} _index - * @returns {string} + * There is also an [alternate pattern](getIndexedAccountPath) used by some software. + * + * @param {Numeric} _index + * + * @returns {string} */ export function getAccountPath(_index: Numeric): string { - const index = getNumber(_index, "index"); - assertArgument(index >= 0 && index < HardenedBit, "invalid account index", "index", index); - return `m/44'/60'/${ index }'/0/0`; + const index = getNumber(_index, 'index'); + assertArgument(index >= 0 && index < HardenedBit, 'invalid account index', 'index', index); + return `m/44'/60'/${index}'/0/0`; } /** - * Returns the path using an alternative pattern for deriving accounts, - * at `index`. + * Returns the path using an alternative pattern for deriving accounts, at `index`. + * + * This derivation path uses the `index` component rather than the `account` component to derive sequential accounts. * - * This derivation path uses the `index` component rather than the - * `account` component to derive sequential accounts. + * This is the pattern used by wallets like MetaMask. * - * This is the pattern used by wallets like MetaMask. - * @param {Numeric} _index - * @returns {string} + * @param {Numeric} _index + * + * @returns {string} */ export function getIndexedAccountPath(_index: Numeric): string { - const index = getNumber(_index, "index"); - assertArgument(index >= 0 && index < HardenedBit, "invalid account index", "index", index); - return `m/44'/60'/0'/0/${ index }`; + const index = getNumber(_index, 'index'); + assertArgument(index >= 0 && index < HardenedBit, 'invalid account index', 'index', index); + return `m/44'/60'/0'/0/${index}`; } - diff --git a/src/wallet/index.ts b/src/wallet/index.ts index f0044266..9389d342 100644 --- a/src/wallet/index.ts +++ b/src/wallet/index.ts @@ -31,4 +31,4 @@ export type { KeystoreAccount, EncryptOptions } from './json-keystore.js'; export { QiHDWallet } from './qi-hdwallet.js'; -export { HDNodeVoidWallet } from "./hdnodewallet.js"; +export { HDNodeVoidWallet, HDNodeWallet } from './hdnodewallet.js';