aboutsummaryrefslogtreecommitdiffstats
path: root/src/sp-worker.js
blob: 59a1769d4c1741c2869f827b38480491a3aa1a6d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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) });
  }
};