123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586 |
- /**
- * 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, 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";
- /**
- * The default derivation path for Ethereum HD Nodes. (i.e. ``"m/44'/60'/0'/0/0"``)
- */
- export const defaultPath: string = "m/44'/60'/0'/0/0";
- // "Bitcoin seed"
- const MasterSecret = new Uint8Array([ 66, 105, 116, 99, 111, 105, 110, 32, 115, 101, 101, 100 ]);
- const HardenedBit = 0x80000000;
- const N = BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141");
- const Nibbles = "0123456789abcdef";
- function zpad(value: number, length: number): string {
- 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;
- }
- function encodeBase58Check(_value: BytesLike): string {
- const value = getBytes(_value);
- const check = dataSlice(sha256(sha256(value)), 0, 4);
- const bytes = concat([ value, check ]);
- return encodeBase58(bytes);
- }
- const _guard = { };
- 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"
- });
- // 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));
- return { IL: I.slice(0, 32), IR: I.slice(32) };
- }
- type HDNodeLike<T> = { depth: number, deriveChild: (i: number) => T };
- function derivePath<T extends HDNodeLike<T>>(node: T, path: string): T {
- const components = path.split("/");
- 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);
- components.shift();
- }
- let result: T = node;
- for (let i = 0; i < components.length; i++) {
- const component = components[i];
- if (component.match(/^[0-9]+'$/)) {
- const index = parseInt(component.substring(0, component.length - 1));
- 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);
- result = result.deriveChild(index);
- } else {
- assertArgument(false, "invalid path component", `path[${ i }]`, component);
- }
- }
- return result;
- }
- /**
- * An **HDNodeWallet** is a [[Signer]] backed by the private key derived
- * from an HD Node using the [[link-bip-32]] stantard.
- *
- * An HD Node forms a hierarchal 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.
- */
- readonly publicKey!: string;
- /**
- * The fingerprint.
- *
- * A fingerprint allows quick qay to detect parent and child nodes,
- * but developers should be prepared to deal with collisions as it
- * is only 4 bytes.
- */
- readonly fingerprint!: string;
- /**
- * The parent fingerprint.
- */
- readonly parentFingerprint!: string;
- /**
- * 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``.
- */
- readonly mnemonic!: null | Mnemonic;
- /**
- * The chaincode, which is effectively a public key used
- * to derive children.
- */
- readonly chainCode!: string;
- /**
- * 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.
- */
- readonly path!: null | string;
- /**
- * The child index of this wallet. Values over ``2 *\* 31`` indicate
- * the node is hardened.
- */
- readonly index!: number;
- /**
- * The depth of this wallet, which is the number of components
- * in its path.
- */
- 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");
- defineProperties<HDNodeWallet>(this, { publicKey: signingKey.compressedPublicKey });
- const fingerprint = dataSlice(ripemd160(sha256(this.publicKey)), 0, 4);
- defineProperties<HDNodeWallet>(this, {
- parentFingerprint, fingerprint,
- chainCode, path, index, depth
- });
- defineProperties<HDNodeWallet>(this, { mnemonic });
- }
- connect(provider: null | Provider): HDNodeWallet {
- return new HDNodeWallet(_guard, this.signingKey, this.parentFingerprint,
- 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);
- }
- /**
- * The extended key.
- *
- * This key will begin with the prefix ``xpriv`` and can be used to
- * reconstruct this HD Node to derive its children.
- */
- 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
- // we should always use mainnet, and use BIP-44 to derive the network
- // - Mainnet: public=0x0488B21E, private=0x0488ADE4
- // - Testnet: public=0x043587CF, private=0x04358394
- assert(this.depth < 256, "Depth too deep", "UNSUPPORTED_OPERATION", { operation: "extendedKey" });
- return encodeBase58Check(concat([
- "0x0488ADE4", zpad(this.depth, 1), this.parentFingerprint,
- zpad(this.index, 4), this.chainCode,
- concat([ "0x00", this.privateKey ])
- ]));
- }
- /**
- * Returns true if this wallet has a path, providing a Type Guard
- * that the path is non-null.
- */
- hasPath(): this is { path: string } { return (this.path != null); }
- /**
- * 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.
- */
- neuter(): HDNodeVoidWallet {
- 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%%.
- */
- deriveChild(_index: Numeric): HDNodeWallet {
- 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 += "'"; }
- }
- 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 the HDNode for %%path%% from this node.
- */
- derivePath(path: string): HDNodeWallet {
- return derivePath<HDNodeWallet>(this, path);
- }
- static #fromSeed(_seed: BytesLike, mnemonic: null | Mnemonic): HDNodeWallet {
- assertArgument(isBytesLike(_seed), "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 signingKey = new SigningKey(hexlify(I.slice(0, 32)));
- return new HDNodeWallet(_guard, signingKey, "0x00000000", hexlify(I.slice(32)),
- "m", 0, 0, mnemonic, null);
- }
- /**
- * 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.
- */
- 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 ]");
- const depth = bytes[4];
- const parentFingerprint = hexlify(bytes.slice(5, 9));
- const index = parseInt(hexlify(bytes.slice(9, 13)).substring(2), 16);
- const chainCode = hexlify(bytes.slice(13, 45));
- const key = bytes.slice(45, 78);
- switch (hexlify(bytes.slice(0, 4))) {
- // Public Key
- case "0x0488b21e": case "0x043587cf": {
- const publicKey = hexlify(key);
- 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);
- }
- assertArgument(false, "invalid extended key prefix", "extendedKey", "[ REDACTED ]");
- }
- /**
- * Creates a new random HDNode.
- */
- static createRandom(password?: string, path?: string, wordlist?: Wordlist): HDNodeWallet {
- if (password == null) { password = ""; }
- if (path == null) { path = defaultPath; }
- 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%%.
- */
- static fromMnemonic(mnemonic: Mnemonic, path?: string): HDNodeWallet {
- if (!path) { path = defaultPath; }
- return HDNodeWallet.#fromSeed(mnemonic.computeSeed(), mnemonic).derivePath(path);
- }
- /**
- * Creates an HD Node from a mnemonic %%phrase%%.
- */
- 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.wordlist(); }
- const mnemonic = Mnemonic.fromPhrase(phrase, password, wordlist)
- return HDNodeWallet.#fromSeed(mnemonic.computeSeed(), mnemonic).derivePath(path);
- }
- /**
- * Creates an HD Node from a %%seed%%.
- */
- static fromSeed(seed: BytesLike): HDNodeWallet {
- return HDNodeWallet.#fromSeed(seed, null);
- }
- }
- /**
- * A **HDNodeVoidWallet** cannot sign, but provides access to
- * the children nodes of a [[link-bip-32]] HD wallet addresses.
- *
- * The can be created by using an extended ``xpub`` key to
- * [[HDNodeWallet_fromExtendedKey]] or by
- * [nuetering](HDNodeWallet-neuter) a [[HDNodeWallet]].
- */
- export class HDNodeVoidWallet extends VoidSigner {
- /**
- * The compressed public key.
- */
- readonly publicKey!: string;
- /**
- * The fingerprint.
- *
- * A fingerprint allows quick qay to detect parent and child nodes,
- * but developers should be prepared to deal with collisions as it
- * is only 4 bytes.
- */
- readonly fingerprint!: string;
- /**
- * The parent node fingerprint.
- */
- readonly parentFingerprint!: string;
- /**
- * The chaincode, which is effectively a public key used
- * to derive children.
- */
- readonly chainCode!: string;
- /**
- * The derivation path of this wallet.
- *
- * Since extended keys do not provider full path details, this
- * may be ``null``, if instantiated from a source that does not
- * enocde it.
- */
- readonly path!: null | string;
- /**
- * The child index of this wallet. Values over ``2 *\* 31`` indicate
- * the node is hardened.
- */
- readonly index!: number;
- /**
- * The depth of this wallet, which is the number of components
- * in its path.
- */
- 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");
- defineProperties<HDNodeVoidWallet>(this, { publicKey });
- const fingerprint = dataSlice(ripemd160(sha256(publicKey)), 0, 4);
- defineProperties<HDNodeVoidWallet>(this, {
- publicKey, fingerprint, parentFingerprint, chainCode, path, index, depth
- });
- }
- connect(provider: null | Provider): HDNodeVoidWallet {
- return new HDNodeVoidWallet(_guard, this.address, this.publicKey,
- this.parentFingerprint, this.chainCode, this.path, this.index, this.depth, provider);
- }
- /**
- * 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.
- */
- 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
- // we should always use mainnet, and use BIP-44 to derive the network
- // - Mainnet: public=0x0488B21E, private=0x0488ADE4
- // - Testnet: public=0x043587CF, private=0x04358394
- 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,
- ]));
- }
- /**
- * Returns true if this wallet has a path, providing a Type Guard
- * that the path is non-null.
- */
- hasPath(): this is { path: string } { return (this.path != null); }
- /**
- * Return the child for %%index%%.
- */
- deriveChild(_index: Numeric): HDNodeVoidWallet {
- 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 += "'"; }
- }
- const { IR, IL } = ser_I(index, this.chainCode, this.publicKey, null);
- const Ki = SigningKey.addPoints(IL, this.publicKey, true);
- const address = computeAddress(Ki);
- 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.
- */
- derivePath(path: string): HDNodeVoidWallet {
- return derivePath<HDNodeVoidWallet>(this, path);
- }
- }
- /*
- export class HDNodeWalletManager {
- #root: HDNodeWallet;
- 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);
- }
- getSigner(index?: number): HDNodeWallet {
- return this.#root.deriveChild((index == null) ? 0: index);
- }
- }
- */
- /**
- * Returns the [[link-bip-32]] path for the account at %%index%%.
- *
- * This is the pattern used by wallets like Ledger.
- *
- * There is also an [alternate pattern](getIndexedAccountPath) used by
- * some software.
- */
- 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`;
- }
- /**
- * 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 is the pattern used by wallets like MetaMask.
- */
- 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}`;
- }
|