theseus/bundled-addons/bchwallet/lib/keys.js

67 lines
3 KiB
JavaScript
Raw Normal View History

// 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 };
};