mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 17:02:28 +00:00
Third slice of docs/zap-tally-plan.md §5.3-5.5 — the publish side:
- zapSplit.js: surface receipt.preimage (NWC pay_invoice result.preimage).
- zapController.js: onZapSuccess now forwards per-share proof
{recipientType, amountSats, bolt11, preimage, zapRequest} for every settled
share with a preimage.
- zapTallyPublisher.js (new): buildTallyFromShare (pure — builds the addressable
tally from a share's bolt11/preimage/zap-request, d=payment_hash, pointers
from the zap request) + publishZapTallies (best-effort, flag-gated, never
throws).
- ModalManager.js: onZapSuccess keeps the instant optimistic bump AND publishes
the tallies via nostrClient.signAndPublishEvent to write relays (an
all-timeout publish is a soft success per #49). A publish failure is
non-fatal — the durable local ledger still shows the sender their zap.
With the counting slice (025ab348), a sent zap now: shows instantly (ledger),
publishes a verified tally, and every bitvid client counts it — surviving the
recipient never publishing a 9735. Tests: tests/zap-tally-publisher.test.mjs
(5). Lint + build green.
Co-Authored-By: Claude Fable 5 <[email protected]>
88 lines
3.2 KiB
JavaScript
88 lines
3.2 KiB
JavaScript
// js/payments/zapTallyPublisher.js
|
|
//
|
|
// Publishes bitvid-native zap tallies (kind ZAP_TALLY_KIND) after a successful
|
|
// zap, so the zapped video/profile's total is global + durable even when the
|
|
// recipient's LNURL server never publishes a NIP-57 9735 receipt
|
|
// (docs/zap-tally-plan.md §5.4-5.5). Each settled share (creator + platform)
|
|
// carries its own invoice/preimage, so each becomes one payer-signed,
|
|
// preimage-verified tally. Best-effort: a publish failure never affects the
|
|
// payment UX (the sats already moved).
|
|
|
|
import { buildZapTallyEvent } from "../nostrEventSchemas.js";
|
|
import { extractBolt11Fields } from "./zapReceiptValidator.js";
|
|
import { devLogger, userLogger } from "../utils/logger.js";
|
|
|
|
const tagFromReq = (req, name) => {
|
|
const tags = Array.isArray(req?.tags) ? req.tags : [];
|
|
const tag = tags.find((t) => Array.isArray(t) && t[0] === name && t[1] != null);
|
|
return tag ? String(tag[1]) : "";
|
|
};
|
|
|
|
// Pure: build the unsigned tally event for one settled zap share, or null when
|
|
// it can't be made verifiable (no decodable payment_hash / missing proof).
|
|
export function buildTallyFromShare({ share, pubkey, now = () => Math.floor(Date.now() / 1000) } = {}) {
|
|
if (!share || typeof share !== "object" || !pubkey) {
|
|
return null;
|
|
}
|
|
const bolt11 = typeof share.bolt11 === "string" ? share.bolt11.trim() : "";
|
|
const preimage = typeof share.preimage === "string" ? share.preimage.trim() : "";
|
|
const zapRequestJson =
|
|
typeof share.zapRequest === "string" ? share.zapRequest.trim() : "";
|
|
if (!bolt11 || !preimage || !zapRequestJson) {
|
|
return null;
|
|
}
|
|
|
|
const { paymentHash } = extractBolt11Fields(bolt11);
|
|
if (!paymentHash) {
|
|
return null; // needs payment_hash for the `d` tag + verification
|
|
}
|
|
|
|
let req = null;
|
|
try {
|
|
req = JSON.parse(zapRequestJson);
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
|
|
const amountSats = Math.max(0, Math.round(Number(share.amountSats) || 0));
|
|
|
|
return buildZapTallyEvent({
|
|
pubkey,
|
|
created_at: now(),
|
|
paymentHash,
|
|
recipientPubkey: tagFromReq(req, "p"),
|
|
eventId: tagFromReq(req, "e"),
|
|
coordinate: tagFromReq(req, "a"),
|
|
amountMsats: amountSats > 0 ? amountSats * 1000 : undefined,
|
|
bolt11,
|
|
preimage,
|
|
zapRequestJson,
|
|
});
|
|
}
|
|
|
|
// Publish a tally per settled share. `publish(event)` signs + broadcasts (best
|
|
// effort); `enabled` gates the whole thing (FEATURE_ZAP_TALLY). Never throws.
|
|
export async function publishZapTallies({ shares, pubkey, publish, enabled = true, now } = {}) {
|
|
if (!enabled || typeof publish !== "function" || !pubkey) {
|
|
return { published: 0 };
|
|
}
|
|
const list = Array.isArray(shares) ? shares : [];
|
|
let published = 0;
|
|
for (const share of list) {
|
|
const event = buildTallyFromShare({ share, pubkey, now });
|
|
if (!event) {
|
|
continue;
|
|
}
|
|
try {
|
|
await publish(event);
|
|
published += 1;
|
|
devLogger.log("[zapTally] Published tally for a settled share.");
|
|
} catch (error) {
|
|
// The payment already succeeded; a failed tally publish is non-fatal —
|
|
// the durable local ledger still shows the zap to the sender.
|
|
userLogger.warn("[zapTally] Failed to publish zap tally:", error);
|
|
}
|
|
}
|
|
return { published };
|
|
}
|