feat: Lightning zaps v1 (NIP-57) — static zap links + server-side receipt counts - #14
Merged
Conversation
…eipt counts (#12) - post pages get a JS-free zap footer (author lud16 required): njump naddr hand-off for attributed zaps, lightning: plain-tip fallback, and '⚡ N sats · M zaps' totals; discover/search feed rows show counts too - cron zap pass per claimed user: fetch kind 9735 (#p) from the user's relays + defaults, bind receipts to the author's LNURL-pay nostrPubkey (resolved from lud16, cached 24h in D1, byte-capped SSRF-guarded fetch), validate the embedded 9734 (p/a/amount vs bolt11 HRP), schnorr-verify, dedup by receipt id — watermarked and verification-capped like the post refresh, with an independent try so zap trouble never sinks post sync - migration 0007: zaps + zap_totals rollup (recomputed idempotently per address in the ingest batch) + lnurl_cache; render path is a PK lookup - BlogProfile carries lud16; safeLud16 shape-gates every href
… amount Adversarial review (partial — spend-limit-degraded; findings re-verified manually) confirmed a critical watermark defect plus three related holes: - only VERIFIED receipts advance the zap watermark now (stored rows and stored-dedup hits). The empty-candidates branch advanced it over arbitrary unverified junk — one spam 9735 per run with a near-future created_at would permanently outrun every real receipt; the parse-fail advance had the same hole (the pubkey field is attacker-claimable). Bounded junk is refetched each run instead, costing no crypto - a verified receipt deferred because its post is not mirrored yet holds the watermark below itself, so a newer receipt stored in the same run can no longer advance past it (it counts once the post lands) - the watermark persists only over a CLOSED relay window; cap-out no longer persists over a full page whose limit may have truncated older receipts - the 9734 amount tag is optional per NIP-57: the bolt11 invoice amount is authoritative, equality enforced only when both exist — wallets omitting the tag now count
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #12 (v1 scope).
What
JS-free zap affordance on every post (blog pages keep
BLOG_CSP's zero-script contract):⚡ Zap this post→ hand-off tonjump.me/<naddr>(our own NIP-19naddrEncode, first-party relay hinted) for an attributed zap whose 9735 references the post and feeds our countslightning:<lud16>plain-tip fallback (walletless; produces no receipt — not counted)⚡ N sats · M zapstotals on the post page and on discover/search feed rows (LEFT JOIN on thezap_totalsrollup — a PK lookup, no scan)lud16(from Configurable Nostr profile (kind 0): edit & publish metadata from the dashboard #11 / PR feat: configurable Nostr profile (kind 0) from the dashboard #13) passingsafeLud16— hostile relay data never reaches an href or fetch URLServer-side receipt ingestion (
src/services/zaps.ts, run per claimed user from the cron loop with independent error isolation):kind 9735(#p= author) from the user's relays + defaults — never the first-party relay (its kind allowlist can't hold receipts)nostrPubkey(resolved fromlud16, cached 24h in D1, byte-capped + timeout + shape-validated fetch — SSRF-guarded); embedded 9734 must target the author (p) and the post (a); bolt11 HRP amount is authoritative, 9734amountequality enforced when presentzaps;zap_totalsrebuilt idempotently per address inside the same atomic batch (migration 0007)cron/refresh.ts: only verified stored receipts advance it; deferred receipts (post not mirrored yet) hold it below themselves; persists only over a closed relay window; verification capped per run (ZAP_VERIFY_CAP)Review
The adversarial multi-agent review ran partially (spend limit killed 19/23 agents mid-run); every finding — including ones the harness mislabeled as refuted because their verify votes errored — was re-verified manually. Four confirmed defects are fixed in the second commit, the critical one being watermark suppression: one spam 9735 per cron run with a near-future
created_atcould permanently outrun all real receipts. The watermark now only ever advances over verified stored receipts.Known v1 bounds (documented in code)
e-tag-only receipts are not counted (our posts are addressable; clients zap them viaa)Tests
894 passing (21 new): lud16/bolt11/receipt validation matrix, cron pass end-to-end over the mock relay (LNURL cache, dedup, junk-suppression guard, deferred-receipt hold + later landing), post-page/feed rendering incl. hostile-lud16 and zero-script assertions.