zaps: foundation for the bitvid-native zap tally (#47) — schema + verify

First slice of docs/zap-tally-plan.md (decisions D1-D5 resolved in the doc):

- ZAP_TALLY_KIND = 30081 (addressable, d=payment_hash → one canonical event
  per real payment) + NOTE_TYPES.ZAP_TALLY schema + buildZapTallyEvent
  (nostrEventSchemas.js). Video + profile pointers.
- FEATURE_ZAP_TALLY flag (constants.js), default ON per D4.
- zapReceiptValidator.js: extractBolt11Fields (payment_hash + description_hash
  from the existing bech32 walker), verifyPaymentPreimage
  (sha256(preimage)==payment_hash), verifyBitvidZapTally — the security core:
  proves the invoice was paid AND bound to this zap request (description-hash
  binding blocks reusing a leaked preimage for another video). Amount is
  bolt11-derived, never the tag; pointers come from the embedded zap request.
- Schema-doc row for kind 30081.

Tests: tests/zap-tally-verify.test.mjs (8) — preimage check, bech32 field
round-trip, valid/bad-preimage/replay/zero-sats decisions, builder tag shape
(video + profile). Lint + build green.

No publishing or counting wired yet — next slices per the plan checklist.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Fable 5 committed 2026-07-04 23:32:12 -04:00
1 parent ce4904f1a4
commit a6b2996f4e
6 files changed
+542 -16

No files matched your search

+1
View File
@@ -196,6 +196,7 @@ Video posts should treat `videoRootId` as the stable series identifier that rema
| Legacy Direct Message (`NOTE_TYPES.LEGACY_DM`) | `4` | `['p', <recipient pubkey>]` | NIP-04 encrypted ciphertext |
| Zap request (`NOTE_TYPES.ZAP_REQUEST`) | `9734` | `['p', <recipient pubkey>]` plus optional `['e', <event id>]`, `['a', <coordinate>]`, `['amount', <msats>]`, `['lnurl', <bech32>]`, and `['relays', ...]` for receipt publishing | Optional plaintext zap note |
| Zap receipt (`NOTE_TYPES.ZAP_RECEIPT`) | `9735` | `['bolt11', <invoice>]`, `['description', <zap request JSON>]`, `['p', <recipient pubkey>]` plus optional `['e', <event id>]` and `['a', <coordinate>]` | Empty content; receipts are published by the recipient's LNURL server |
| Zap tally (`NOTE_TYPES.ZAP_TALLY`) | `30081` (addressable, `d = payment_hash`) | `['d', <payment_hash>]`, `['p', <recipient>]`, optional `['e', <event id>]`/`['a', <coordinate>]`, `['amount', <msats>]`, `['bolt11', <invoice>]`, `['preimage', <hex>]`, `['description', <zap request JSON>]`, `['client', 'bitvid']`, `['t', 'zap']` | bitvid-native, payer-signed, **preimage-verified** stand-in for a 9735 when the recipient's wallet doesn't publish one. Verified via `sha256(preimage)==payment_hash` + `description_hash==sha256(zapRequest)`. Counted alongside 9735s (deduped by `payment_hash`). See `docs/zap-tally-plan.md`. |
| HTTP Auth (`NOTE_TYPES.HTTP_AUTH`) | `27235` | `['u', <url>]`, `['method', <http method>]`, `['payload', <payload hash>]` | Optional plaintext content (NIP-98 recommends empty, but implementations may vary) |
| Report (`NOTE_TYPES.REPORT`) | `1984` | `['e', <event id>, <report type>]`, `['p', <pubkey>, <report type>]`, and optional `['t', <report type>]` | Plaintext report reason |
+21 -16
View File
@@ -426,23 +426,28 @@ tallies simply stop being counted. No data migration.
---
## 10. Open decisions (need a call before/at build time)
## 10. Decisions (RESOLVED 2026-07-04)
- **D1 — event kind.** Proposed `9736` (adjacent to the 9735 receipt, "bitvid zap
tally"). Must confirm it's unassigned in the NIP kind registry; if risky, pick a
clearly app-specific regular kind and reserve it in `docs/nostr-event-schemas.md`.
Everything references `ZAP_TALLY_KIND` so the number is a one-line change.
- **D2 — profile zaps in v1?** Video path is the ask; profile path is the same
primitives + a channel-page badge. Ship video-only first, profile as a
follow-up? (Recommended: yes, video-only v1.)
- **D3 — publish relays.** Publish the tally to the user's write relays (same as
video publishing) so bitvid clients querying those relays see it. Confirm the
store queries a superset (it queries `client.relays`). Likely fine; verify.
- **D4 — privacy default.** Default the feature flag off and add one line of UI
copy ("your zap will be publicly attributed on bitvid") before default-on?
- **D5 — chart Y-axis (§5.9).** Independent per-series scaling (each cumulative
line filling the height — simplest, trend-focused, **recommended**) vs. a dual
labeled Y-axis (left=views, right=sats — more precise, busier in a small modal).
- **D1 — event kind → ADDRESSABLE `ZAP_TALLY_KIND = 30081`, `d = payment_hash`.**
Chosen: a higher, app-specific kind in bitvid's own family (30078 video,
30079 view, **30081** zap tally). Addressable (30000–39999) with the
per-payment `payment_hash` as the `d` tag makes each real payment map to
exactly ONE canonical event — relay-level idempotency; a re-publish replaces
rather than duplicates. Still queried by its `#a`/`#e`/`#p` tags like a 9735.
Reserve the kind in `docs/nostr-event-schemas.md`.
- **D2 — scope → VIDEO + PROFILE zaps in v1.** Video path drives the badges/Most
Zapped; profile path reuses the same primitives with a `p`-only pointer and a
channel-page total (new small UI surface). Both ship in v1.
- **D3 — publish relays → user's WRITE relays** (same path as video publishing).
The store queries `client.relays` (a superset incl. write relays); verify at
wiring time.
- **D4 — publishing → ON BY DEFAULT.** `FEATURE_ZAP_TALLY` still exists for
rollback but defaults **true**; every successful zap publishes a tally. Keep a
short one-line disclosure in the zap UI ("zaps are publicly attributed on
bitvid") since it's honest and cheap, but no opt-in gate.
- **D5 — chart Y-axis → INDEPENDENT PER-SERIES scaling.** Each cumulative line
fills the height against its own max; the legend names the two units. No dual
labeled Y-axis.
---