From 1d732479ed589fcf0118a98342f3735c5789938c Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 4 Jul 2026 23:40:26 -0400 Subject: [PATCH] zaps: end-to-end verify test + plan checklist status (#47) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/zap-tally-plan.md | 42 ++++++++++++++++++++------------ tests/zap-tally-verify.test.mjs | 43 +++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 16 deletions(-) diff --git a/docs/zap-tally-plan.md b/docs/zap-tally-plan.md index bafb8076..5764cf9d 100644 --- a/docs/zap-tally-plan.md +++ b/docs/zap-tally-plan.md @@ -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):** diff --git a/tests/zap-tally-verify.test.mjs b/tests/zap-tally-verify.test.mjs index 1c42fe63..3977fd5e 100644 --- a/tests/zap-tally-verify.test.mjs +++ b/tests/zap-tally-verify.test.mjs @@ -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,