aboutsummaryrefslogtreecommitdiffstats
path: root/src/sp-worker.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-06-28 03:21:07 -0700
committerAdam Soltys <[email protected]>2026-06-28 03:21:07 -0700
commit935adfe4a28cedd17f97ea3a6be7ccbafe6c7cec (patch)
treeb4618b068e379e7feedd80b2a623433ed240018e /src/sp-worker.js
parente8edfd88b14ad07f5f97f5cb1d5d603d825a413d (diff)
downloadcoinosv3-935adfe4a28cedd17f97ea3a6be7ccbafe6c7cec.tar.gz
coinosv3-935adfe4a28cedd17f97ea3a6be7ccbafe6c7cec.zip
Run silent-payment scanning in a Web Worker
The EC crypto (candidate derivation + full scan) now runs in a worker, off the main thread, so even a large mainnet scan can't jank the UI at all. The worker is bundled separately and embedded as base64 (single-file app), spawned from a Blob URL; it holds the keys + per-tweak cache. If workers are unavailable or the bundle is missing, scanning falls back to the main thread (still cached + yielding). scanSilentPayments / _scanSpItems / _scanSpMempool now go through _spHitBlocks and _spScanItems, which dispatch to the worker (or the fallback). +27KB to index.html for the worker's crypto bundle. Verified via CDP: worker runs keys/candidates/scan correctly in-browser; a real reload + mainnet scan threw 0 exceptions and the SP address renders. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'src/sp-worker.js')
-rw-r--r--src/sp-worker.js69
1 files changed, 69 insertions, 0 deletions
diff --git a/src/sp-worker.js b/src/sp-worker.js
new file mode 100644
index 0000000..59a1769
--- /dev/null
+++ b/src/sp-worker.js
@@ -0,0 +1,69 @@
+// Web Worker: the EC-heavy silent-payment crypto, kept off the main thread so a
+// large scan never janks the UI. Memoizes per tweak (a tweak's outcome is fixed),
+// so repeated mempool pushes — which re-send the same tweaks — stay cheap.
+//
+// Protocol (request/response by id):
+// { id, op:'keys', scanPriv, spendPub } → { id, ok:true }
+// { id, op:'candidates', blocks:[{height,tweaks,filter}] } → { id, hits:[height] }
+// { id, op:'scan', items:[{txid,tweak,outputs}] } → { id, found:[utxo] }
+
+import { hex } from '@scure/base';
+import { silentPaymentScan, silentPaymentCandidate, bloomHas } from './silentpay.js';
+
+let scanPriv = null;
+let spendPub = null;
+const scanCache = new Map(); // tweakHex -> matches
+const candCache = new Map(); // tweakHex -> candidate x-only hex
+
+self.onmessage = (e) => {
+ const { id, op } = e.data;
+ try {
+ if (op === 'keys') {
+ scanPriv = e.data.scanPriv;
+ spendPub = e.data.spendPub;
+ scanCache.clear();
+ candCache.clear();
+ self.postMessage({ id, ok: true });
+ return;
+ }
+ if (op === 'candidates') {
+ // Return the heights of blocks whose Bloom filter could contain one of our
+ // outputs (derive a candidate per tweak, test the filter, stop on first hit).
+ const hits = [];
+ for (const b of e.data.blocks) {
+ for (const tw of b.tweaks) {
+ let c = candCache.get(tw);
+ if (!c) {
+ if (candCache.size > 100000) candCache.clear();
+ c = silentPaymentCandidate({ scanPriv, spendPub, tweak: hex.decode(tw) });
+ candCache.set(tw, c);
+ }
+ if (bloomHas(b.filter, c)) { hits.push(b.height); break; }
+ }
+ }
+ self.postMessage({ id, hits });
+ return;
+ }
+ if (op === 'scan') {
+ // Full scan: return resolved utxos {txid,vout,value,xonly,tweak} for ours.
+ const found = [];
+ for (const it of e.data.items) {
+ let m = scanCache.get(it.tweak);
+ if (!m) {
+ if (scanCache.size > 100000) scanCache.clear();
+ m = silentPaymentScan({ scanPriv, spendPub, tweak: hex.decode(it.tweak), outputs: it.outputs.map((o) => o.xonly) });
+ scanCache.set(it.tweak, m);
+ }
+ for (const mm of m) {
+ const o = it.outputs.find((x) => x.xonly === mm.output);
+ if (o) found.push({ txid: it.txid, vout: o.vout, value: o.value, xonly: mm.output, tweak: mm.tweak });
+ }
+ }
+ self.postMessage({ id, found });
+ return;
+ }
+ self.postMessage({ id, error: 'unknown op ' + op });
+ } catch (err) {
+ self.postMessage({ id, error: String((err && err.message) || err) });
+ }
+};