Wallet core on mainnet:
- keys from api.vault.derive("bchwallet/mainnet/0") -> BIP32 m/44'/145'/0'
(@scure/bip32), never persisted; wiped on deactivate.
- lib/cashaddr.js (encode/decode + legacy Base58Check, spec vectors pass),
lib/keys.js (hash160, p2pkh, electrum scripthash, ECDSA DER + BIP-137
recoverable signing), lib/tx.js (serialization, SIGHASH_ALL|FORKID
digest, coin selection, fee estimate), lib/electrum.js (Fulcrum WSS
client with failover + subscriptions), lib/wallet.js (gap-limit scan,
balance, UTXOs, 25-tx history with per-tx deltas, cached public txs).
- qr.js: dependency-free QR encoder (byte mode, v1-10, EC M/L; verified
against jsQR).
- panel: balance header, Receive (QR, copy, next unused address, explorer),
History (deltas, confirmations, explorer links), Settings (derivation
path, electrum server list, xpub / approval-gated xprv reveal). Locked
and not-set-up vault states explained in-panel.
- host: api.import for ESM-only deps, api.openTab for explorer links; an
add-on whose activate() throws is no longer listed twice.
66 lines
3 KiB
JavaScript
66 lines
3 KiB
JavaScript
// 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 };
|
|
};
|