ecash-lib
Version:
Library for eCash transaction building
242 lines • 9.13 kB
JavaScript
;
// Copyright (c) 2025 The Bitcoin developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
Object.defineProperty(exports, "__esModule", { value: true });
exports.HdNode = void 0;
const ecc_js_1 = require("./ecc.js");
const hmac_js_1 = require("./hmac.js");
const hash_js_1 = require("./hash.js");
const bytes_js_1 = require("./io/bytes.js");
const str_js_1 = require("./io/str.js");
const writerbytes_js_1 = require("./io/writerbytes.js");
const b58_ts_1 = require("b58-ts");
const legacyaddr_js_1 = require("./address/legacyaddr.js");
// BIP32 extended public key version bytes
// These match the values defined in Electrum ABC's networks.py
const XPUB_VERSION_MAINNET = 0x0488b21e;
const XPUB_VERSION_TESTNET = 0x043587cf;
const HIGHEST_BIT = 0x80000000;
class HdNode {
constructor(params) {
this._ecc = new ecc_js_1.Ecc();
this._seckey = params.seckey;
this._pubkey = params.pubkey;
this._chainCode = params.chainCode;
this._depth = params.depth;
this._index = params.index;
this._parentFingerprint = params.parentFingerprint;
}
seckey() {
return this._seckey;
}
pubkey() {
return this._pubkey;
}
pkh() {
return (0, hash_js_1.shaRmd160)(this._pubkey);
}
fingerprint() {
return this.pkh().slice(0, 4);
}
index() {
return this._index;
}
depth() {
return this._depth;
}
parentFingerprint() {
return this._parentFingerprint;
}
chainCode() {
return this._chainCode;
}
/**
* Encode this HdNode as an xpub (extended public key) string
*
* An xpub is a base58check-encoded string containing:
* - 4 bytes: version (0x0488B21E for mainnet xpub, 0x043587CF for testnet xpub)
* - 1 byte: depth
* - 4 bytes: parent fingerprint
* - 4 bytes: child index
* - 32 bytes: chain code (needed to derive child keys)
* - 33 bytes: public key (compressed)
*
* @param version - Version bytes (defaults to XPUB_VERSION_MAINNET)
* @returns Base58check-encoded xpub string
*/
xpub(version = XPUB_VERSION_MAINNET) {
// Validate public key is compressed
if (this._pubkey.length !== 33 ||
(this._pubkey[0] !== 0x02 && this._pubkey[0] !== 0x03)) {
throw new Error('Public key must be compressed (33 bytes, starts with 0x02 or 0x03)');
}
// Write xpub data (78 bytes total)
const writer = new writerbytes_js_1.WriterBytes(78);
writer.putU32(version, 'BE');
writer.putU8(this._depth);
writer.putU32(this._parentFingerprint, 'BE');
writer.putU32(this._index, 'BE');
writer.putBytes(this._chainCode);
writer.putBytes(this._pubkey);
// Encode with base58check
const checksum = (0, hash_js_1.sha256d)(writer.data);
const dataWithChecksum = new Uint8Array(78 + 4);
dataWithChecksum.set(writer.data, 0);
dataWithChecksum.set(checksum.subarray(0, 4), 78);
return (0, b58_ts_1.encodeBase58)(dataWithChecksum);
}
derive(index) {
const isHardened = index >= HIGHEST_BIT;
const data = new writerbytes_js_1.WriterBytes(1 + 32 + 4);
if (isHardened) {
if (this._seckey === undefined) {
throw new Error('Missing private key for hardened child key');
}
data.putU8(0);
data.putBytes(this._seckey);
}
else {
data.putBytes(this._pubkey);
}
data.putU32(index, 'BE');
const hashed = (0, hmac_js_1.hmacSha512)(this._chainCode, data.data);
const hashedLeft = hashed.slice(0, 32);
const hashedRight = hashed.slice(32);
// In case the secret key doesn't lie on the curve, we proceed with the
// next index. This is astronomically unlikely but part of the specification.
if (!this._ecc.isValidSeckey(hashedLeft)) {
return this.derive(index + 1);
}
let seckey;
let pubkey;
if (this._seckey !== undefined) {
try {
seckey = this._ecc.seckeyAdd(this._seckey, hashedLeft);
}
catch (ex) {
console.log('Skipping index', index, ':', ex);
return this.derive(index + 1);
}
pubkey = this._ecc.derivePubkey(seckey);
}
else {
try {
pubkey = this._ecc.pubkeyAdd(this._pubkey, hashedLeft);
}
catch (ex) {
console.log('Skipping index', index, ':', ex);
return this.derive(index + 1);
}
seckey = undefined;
}
return new HdNode({
seckey: seckey,
pubkey: pubkey,
chainCode: hashedRight,
depth: this._depth + 1,
index,
parentFingerprint: new bytes_js_1.Bytes(this.fingerprint()).readU32('BE'),
});
}
deriveHardened(index) {
if (index < 0 || index >= HIGHEST_BIT) {
throw new TypeError(`index must be between 0 and ${HIGHEST_BIT}, got ${index}`);
}
return this.derive(index + HIGHEST_BIT);
}
derivePath(path) {
let splitPath = path.split('/');
if (splitPath[0] === 'm') {
if (this._parentFingerprint) {
throw new TypeError('Expected master, got child');
}
splitPath = splitPath.slice(1);
}
// eslint-disable-next-line @typescript-eslint/no-this-alias
let hd = this;
for (const step of splitPath) {
if (step.slice(-1) === `'`) {
hd = hd.deriveHardened(parseInt(step.slice(0, -1), 10));
}
else {
hd = hd.derive(parseInt(step, 10));
}
}
return hd;
}
static fromPrivateKey(seckey, chainCode) {
return new HdNode({
seckey: seckey,
pubkey: new ecc_js_1.Ecc().derivePubkey(seckey),
chainCode,
depth: 0,
index: 0,
parentFingerprint: 0,
});
}
static fromSeed(seed) {
if (seed.length < 16 || seed.length > 64) {
throw new TypeError('Seed must be between 16 and 64 bytes long');
}
const hashed = (0, hmac_js_1.hmacSha512)((0, str_js_1.strToBytes)('Bitcoin seed'), seed);
const hashedLeft = hashed.slice(0, 32);
const hashedRight = hashed.slice(32);
return HdNode.fromPrivateKey(hashedLeft, hashedRight);
}
/**
* Create an HdNode from an xpub (extended public key) string
*
* An xpub is a base58check-encoded string containing:
* - 4 bytes: version (0x0488B21E for mainnet xpub, 0x043587CF for testnet xpub)
* - 1 byte: depth
* - 4 bytes: parent fingerprint
* - 4 bytes: child index
* - 32 bytes: chain code
* - 33 bytes: public key (compressed)
*
* The resulting HdNode will not have a private key (watch-only).
*
* @param xpub - The extended public key string
* @returns HdNode created from the xpub (without private key)
*/
static fromXpub(xpub) {
const payload = (0, legacyaddr_js_1.decodeBase58Check)(xpub);
if (payload.length !== 78) {
throw new Error(`Invalid xpub: expected 78 bytes, got ${payload.length}`);
}
const bytes = new bytes_js_1.Bytes(payload);
// Read version (4 bytes, big-endian)
const version = bytes.readU32('BE');
// Validate version (mainnet or testnet xpub)
if (version !== XPUB_VERSION_MAINNET &&
version !== XPUB_VERSION_TESTNET) {
throw new Error(`Invalid xpub version: expected 0x${XPUB_VERSION_MAINNET.toString(16).toUpperCase()} (mainnet) or 0x${XPUB_VERSION_TESTNET.toString(16).toUpperCase()} (testnet), got 0x${version.toString(16)}`);
}
// Read depth (1 byte)
const depth = bytes.readU8();
// Read parent fingerprint (4 bytes, big-endian)
const parentFingerprint = bytes.readU32('BE');
// Read child index (4 bytes, big-endian)
const index = bytes.readU32('BE');
// Read chain code (32 bytes)
const chainCode = bytes.readBytes(32);
// Read public key (33 bytes, compressed)
const pubkey = bytes.readBytes(33);
// Validate public key format (should start with 0x02 or 0x03 for compressed)
if (pubkey[0] !== 0x02 && pubkey[0] !== 0x03) {
throw new Error(`Invalid xpub: public key must be compressed (start with 0x02 or 0x03), got 0x${pubkey[0].toString(16)}`);
}
// Create HdNode without private key (watch-only)
return new HdNode({
seckey: undefined,
pubkey,
chainCode,
depth,
index,
parentFingerprint,
});
}
}
exports.HdNode = HdNode;
//# sourceMappingURL=hdwallet.js.map