// HD key tree for the wallet. Root = 32 bytes from api.vault.derive treated as // a BIP32 master seed; account = m/44'/145'/0' (BCH, SLIP-44). Branch 0 is // receive, branch 1 is change. Private keys never leave this module except // through sign() / signRecoverable() for a specific entry. module.exports = function makeKeys({ HDKey, secp256k1, sha256, ripemd160, cashaddr }) { const hash160 = (b) => ripemd160(sha256(b)); const p2pkhScript = (h160) => Uint8Array.from([0x76, 0xa9, 0x14, ...h160, 0x88, 0xac]); const p2shScript = (h160) => Uint8Array.from([0xa9, 0x14, ...h160, 0x87]); const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join(""); // electrum scripthash: sha256(script), byte-reversed, hex. const scripthash = (script) => toHex(sha256(script).slice().reverse()); class WalletKeys { constructor(root32, accountPath, prefix) { this.prefix = prefix; this.accountPath = accountPath; this._account = HDKey.fromMasterSeed(root32).derive(accountPath); this._branch = [this._account.deriveChild(0), this._account.deriveChild(1)]; this._cache = new Map(); // "branch/index" -> entry } get xpub() { return this._account.publicExtendedKey; } // Revealed only on explicit user action in Settings (show recovery info). get xprv() { return this._account.privateExtendedKey; } entry(branch, index) { const k = branch + "/" + index; let e = this._cache.get(k); if (!e) { const node = this._branch[branch].deriveChild(index); const h160 = hash160(node.publicKey); const script = p2pkhScript(h160); e = { branch, index, path: this.accountPath + "/" + branch + "/" + index, publicKey: node.publicKey, h160, script, scriptHex: toHex(script), scripthash: scripthash(script), address: cashaddr.encode(this.prefix, 0, h160), _node: node, }; this._cache.set(k, e); } return e; } findByScriptHex(scriptHex) { for (const e of this._cache.values()) if (e.scriptHex === scriptHex) return e; return null; } // ECDSA over a 32-byte digest, DER-encoded, low-S (BCH consensus rule). sign(entry, digest32) { return secp256k1.sign(digest32, entry._node.privateKey, { prehash: false, lowS: true, format: "der" }); } // 65-byte BIP-137 signature: [27 + recid + 4 (compressed)] || r || s. signRecoverable(entry, digest32) { const sig = secp256k1.sign(digest32, entry._node.privateKey, { prehash: false, lowS: true, format: "recovered" }); const out = new Uint8Array(65); out[0] = 27 + sig[0] + 4; out.set(sig.subarray(1), 1); return out; } wipe() { for (const e of this._cache.values()) { try { e._node.wipePrivateData(); } catch {} } this._cache.clear(); for (const b of this._branch) { try { b.wipePrivateData(); } catch {} } try { this._account.wipePrivateData(); } catch {} } } return { WalletKeys, hash160, p2pkhScript, p2shScript, scripthash, toHex }; };