mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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:
1 parent
cf82978060
commit
96228a451b
2 files changed
+59
-15
No files matched your search
+11
-15
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user