zaps: fix zap totals counting zero — coordinate used kind 0 instead of 30078

The zap write path (zapSplit.js derivePointerTag) built the `a` coordinate
tag straight from `videoEvent.kind`. bitvid's internal video objects don't
always carry the Nostr kind — it can be 0 or undefined — and the inline
builder only rejected non-finite kinds, so a kind-0 object produced a
`0:pubkey:d` coordinate (Number.isFinite(0) === true). This flowed into both
the kind-9734 zap request and the kind-30081 bitvid tally.

The read side (zapTotals / mostZappedFeed) fetches totals by querying
`30078:pubkey:d` via buildVideoAddressPointer, which defaults invalid/zero
kinds to 30078. So the writer emitted `0:...` while the reader queried
`30078:...` — they never matched and every zap counted as zero, even after
a cache clear + refresh. Confirmed against live relays: 4 published bitvid
tallies all carried `0:...` coordinates while the real video events are
kind 30078.

Fix: derivePointerTag now delegates to the same canonical
buildVideoAddressPointer the reader uses, keeping writer and reader
coordinates in lockstep. Adds a regression test driving splitAndZap with
kind 0 / undefined / non-numeric video objects, asserting the zap request's
`a` tag defaults to `30078:pubkey:d`.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-05 09:18:38 -04:00
1 parent cf82978060
commit 96228a451b
2 files changed
+59 -15

No files matched your search

+11 -15
View File
@@ -21,6 +21,7 @@ import {
resolvePlatformFeePercent,
} from "./platformFee.js";
import { HEX64_REGEX } from "../utils/hex.js";
import { buildVideoAddressPointer } from "../utils/videoPointer.js";
const ZAP_KIND = 9734;
@@ -271,21 +272,16 @@ function decodeAdminPubkey() {
}
function derivePointerTag(videoEvent) {
if (!videoEvent || typeof videoEvent !== "object") {
return null;
}
if (!Array.isArray(videoEvent.tags)) {
return null;
}
const dTag = videoEvent.tags.find((tag) => Array.isArray(tag) && tag[0] === "d" && tag[1]);
if (!dTag) {
return null;
}
if (!Number.isFinite(videoEvent.kind) || typeof videoEvent.pubkey !== "string") {
return null;
}
const value = `${videoEvent.kind}:${videoEvent.pubkey}:${dTag[1]}`;
return ["a", value];
// Delegate to the app's canonical pointer derivation — the SAME one the read
// side (zapTotals / mostZappedFeed) uses to fetch totals. bitvid's internal
// video objects don't always carry the Nostr `kind` (it can be 0/undefined),
// and the old inline builder trusted `videoEvent.kind` verbatim: a kind-0
// object produced a `0:pubkey:d` coordinate (Number.isFinite(0) === true),
// which the reader — querying `30078:pubkey:d` — could never match, so zaps
// silently counted as zero. buildVideoAddressPointer defaults invalid/zero
// kinds to 30078, keeping writer and reader coordinates in lockstep.
const value = buildVideoAddressPointer(videoEvent);
return value ? ["a", value] : null;
}
function resolveRelayUrls(wallet) {
+48
View File
@@ -334,6 +334,53 @@ async function testSplitMath() {
delete globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__;
}
// Regression: bitvid's internal video objects don't always carry the Nostr
// `kind` — it can be 0 or undefined. The zap request's `a` (coordinate) tag
// MUST still address the video as kind 30078, because the read side
// (zapTotals / mostZappedFeed) fetches totals by querying `30078:pubkey:d`.
// A kind-0 coordinate (`0:pubkey:d`) publishes fine but can never be matched,
// so zaps silently count as zero. This asserts the coordinate defaults to
// 30078 regardless of the source object's kind.
async function testZapPointerDefaultsToVideoKindWhenMissing() {
const previousOverride = globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__;
globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__ = 0;
const { deps } = createDeps();
const callbackUrl = "https://lnurl.example/api/callback";
const bech32Address = encodeLnurlBech32(callbackUrl);
const pubkey = "d".repeat(64);
for (const brokenKind of [0, undefined, "not-a-number"]) {
const videoEvent = {
id: "event-id",
pubkey,
lightningAddress: bech32Address,
tags: [["d", "1782939097186-elrw1x3fdvg"]],
kind: brokenKind,
};
const result = await splitAndZap(
{ videoEvent, amountSats: 500, comment: "PointerKind" },
deps
);
const parsedZap = JSON.parse(result.receipts[0].zapRequest);
const aTag = parsedZap.tags.find((tag) => Array.isArray(tag) && tag[0] === "a");
assert(aTag, `zap request should carry an 'a' coordinate (kind=${brokenKind})`);
assert.equal(
aTag[1],
`30078:${pubkey}:1782939097186-elrw1x3fdvg`,
`coordinate must address the video as kind 30078, not '${brokenKind}'`
);
}
if (previousOverride === undefined) {
delete globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__;
} else {
globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__ = previousOverride;
}
}
async function testBech32LightningAddressZapTag() {
const previousOverride = globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__;
globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__ = 0;
@@ -1046,6 +1093,7 @@ async function testSplitRoundingPreservesEverySat() {
await testCreatorEqualsPlatformCollapsesToOnePayment();
await testSplitRoundingPreservesEverySat();
await testSplitMath();
await testZapPointerDefaultsToVideoKindWhenMissing();
await testBech32LightningAddressZapTag();
await testUrlLightningAddressZapTag();
await testRequestInvoiceIncludesLnurlParam();