zaps: end-to-end verify test + plan checklist status (#47)

Adds an integration test exercising the REAL verifier against a genuinely
valid, amount-bearing bolt11 (real bech32 decode + nip57 amount parse + real
preimage/description-hash math, no injected fields) — proves the tally pieces
compose. Marks the shipped slices in docs/zap-tally-plan.md §11 and lists the
remaining v1 work: profile-zap #p counting + channel-page UI, the retry-path
publish hook, and the popularity chart (§5.9).

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Fable 5 committed 2026-07-04 23:40:26 -04:00
1 parent 41e533e675
commit 1d732479ed
2 files changed
+69 -16

No files matched your search

+26 -16
View File
@@ -453,22 +453,32 @@ tallies simply stop being counted. No data migration.
## 11. Task checklist (ordered — pick up here across sessions)
- [ ] **D1** decide `ZAP_TALLY_KIND`; add constant + `docs/nostr-event-schemas.md` row.
- [ ] `constants.js`: `FEATURE_ZAP_TALLY` (default off) + runtime-flag plumbing.
- [ ] `nostrEventSchemas.js`: `NOTE_TYPES.ZAP_TALLY` schema + `buildZapTallyEvent`.
- [ ] `zapReceiptValidator.js`: `extractBolt11Fields` (payment_hash),
`verifyPaymentPreimage`, `verifyBitvidZapTally`. Unit tests first (vectors).
- [ ] `zapSplit.js`: expose `receipt.preimage`.
- [ ] `zapController.js`: extend `onZapSuccess` payload with per-share proof; wire
the retry path.
- [ ] `ModalManager.js` + a thin `app.publishZapTally`: build→sign→publish
(best-effort, flag-gated) → `store.ingestVerifiedZap`.
- [ ] `zapTotals.js`: query `ZAP_TALLY_KIND`, verify + dedup by payment_hash
(`paymentHashes` set), `ingestVerifiedZap`, flag-gate counting.
- [ ] Tests: verify, schema, cross-source dedup, wiring, flag-off; Playwright
seeded-event count/rank.
- [ ] `lint` + `build` + suites green; push to `unstable`.
- [ ] Rollout per §9; TODO entry updated.
- [x] **D1** `ZAP_TALLY_KIND = 30081` (addressable, d=payment_hash) + constant +
`docs/nostr-event-schemas.md` row. (commit a6b2996f)
- [x] `constants.js`: `FEATURE_ZAP_TALLY` (default ON per D4) + runtime plumbing.
- [x] `nostrEventSchemas.js`: `NOTE_TYPES.ZAP_TALLY` schema + `buildZapTallyEvent`.
- [x] `zapReceiptValidator.js`: `extractBolt11Fields`, `verifyPaymentPreimage`,
`verifyBitvidZapTally`. Tests incl. a real amount-bearing bolt11 e2e.
- [x] `zapSplit.js`: expose `receipt.preimage`. (commit 41e533e6)
- [x] `zapController.js`: `onZapSuccess` forwards per-share proof.
*(Retry path still TODO — see below.)*
- [x] `zapTallyPublisher.js` (buildTallyFromShare + publishZapTallies) +
`ModalManager.js` wiring: build→sign→publish (best-effort, flag-gated).
No `ingestVerifiedZap` needed — the store's existing "relay wins" prune
reconciles the optimistic ledger when the published tally is fetched back.
- [x] `zapTotals.js`: query `[9735, ZAP_TALLY_KIND]`, verify + dedup by
payment_hash (`paymentHashes` set), flag-gated counting. (commit 025ab348)
- [x] Tests: verify, schema, publisher, cross-source dedup, flag-off, e2e verify.
- [x] `lint` + `build` + suites green; pushed to `unstable`.
- [ ] **Remaining v1 work:**
- [ ] **Profile-zap counting:** the store queries `#a`/`#e` only — add
`#p` batches so a profile-only tally (no video pointer) is fetched,
and a channel-page total UI surface (D2 chose video+profile).
- [ ] **Retry path:** `zapController.executeRetry` doesn't fire
`onZapSuccess`; wire it so retried shares also publish tallies.
- [ ] **Popularity chart §5.9:** orange zaps line + legend + date axis.
- [ ] Live end-to-end on unstable (real zap → tally on relay → second
browser counts it); rollout per §9.
**Popularity chart (§5.9 — can start once the store's verify + dedup helpers
exist; the renderer/legend/axis parts have no dependency on publishing tallies):**
+43
View File
@@ -219,6 +219,49 @@ test("buildZapTallyEvent: addressable by payment_hash + carries the proof tags",
assert.deepEqual(tag("client"), ["client", "bitvid"]);
});
// End-to-end with the REAL verifier + a REAL amount-bearing bolt11: proves the
// pieces compose (no injected extractFields/getSats). The invoice encodes an
// amount (lnbc21u = 2100 sats), a payment_hash = sha256(preimage), and a
// description_hash = sha256(zap request); the real nip57 amount parser reads it.
test("verifyBitvidZapTally: accepts a fully valid tally end to end (real decode + amount)", async () => {
const { nip57 } = await import("nostr-tools");
const preimage = randomBytes(32);
const preimageHex = preimage.toString("hex");
const paymentHashBytes = createHash("sha256").update(preimage).digest();
const reqJson = zapRequestJson();
const descHashBytes = createHash("sha256").update(Buffer.from(reqJson, "utf8")).digest();
const pWords = bech32.toWords(new Uint8Array(paymentHashBytes));
const hWords = bech32.toWords(new Uint8Array(descHashBytes));
const words = [
0, 0, 0, 0, 0, 0, 0,
1, ...lenWords(pWords.length), ...pWords, // 'p' payment_hash
23, ...lenWords(hWords.length), ...hWords, // 'h' description_hash
...new Array(104).fill(0),
];
const bolt11 = bech32.encode("lnbc21u", words, 2000); // 21 micro-BTC = 2100 sats
const event = buildZapTallyEvent({
pubkey: PUB,
created_at: 1000,
paymentHash: paymentHashBytes.toString("hex"),
recipientPubkey: CREATOR,
eventId: EVENT_ID,
coordinate: COORD,
amountMsats: 2100000,
bolt11,
preimage: preimageHex,
zapRequestJson: reqJson,
});
const result = verifyBitvidZapTally(event, {
getSats: nip57.getSatoshisAmountFromBolt11, // real parser, no injection of fields
});
assert.equal(result.ok, true, "real verifier accepts a genuinely valid tally");
assert.equal(result.sats, 2100, "amount parsed from the real bolt11 hrp");
assert.equal(result.paymentHash, paymentHashBytes.toString("hex"));
});
test("buildZapTallyEvent: profile-only zap has p but no a/e", () => {
const event = buildZapTallyEvent({
pubkey: PUB,