fix(zap): collapse split when creator == platform; close #3 audit

Finishes the remaining #3 zap audit items.

- zapSplit: when the platform fee would pay the SAME Lightning address as the
  creator (e.g. the creator is the platform operator), collapse to a single
  full-amount payment instead of sending two invoices to one wallet — avoids an
  extra routing fee and a confusing duplicate receipt for the same total.
- Audited split correctness: floor(amount*fee/100) with the creator taking the
  remainder loses no sats; a sub-1-sat fee rounds to 0 and the whole zap goes to
  the creator (no zero-amount platform send); fee is clamped 0–100 and a junk
  override falls back to the default. The earlier "fee landed in my own wallet"
  report is expected behavior (sender is the platform operator, or a self-zap),
  not a bug.
- Cheat-resistant tests: testCreatorEqualsPlatformCollapsesToOnePayment and
  testSplitRoundingPreservesEverySat in tests/zap-split.test.mjs.
- Receipt validation (9735) and NWC budget/retry UX confirmed already solid;
  audit plan + TODO #3 closed.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-25 08:56:00 -04:00
1 parent b52a1b3fea
commit df99bdb65f
4 files changed
+147 -7

No files matched your search

+86
View File
@@ -959,6 +959,92 @@ function testExtractDescriptionHashFromBolt11() {
assert.equal(extracted, descriptionHashHex);
}
// When the platform fee would be paid to the SAME Lightning address as the
// creator, the split must collapse to a single full-amount payment instead of
// sending two invoices to one wallet (extra routing fee + duplicate receipt).
async function testCreatorEqualsPlatformCollapsesToOnePayment() {
globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__ = 30;
const { deps, getSendCalls, getValidatorCalls } = createDeps({
platformAddress: "[email protected]", // identical to the creator below
});
const videoEvent = {
id: "event-id",
pubkey: "c".repeat(64),
lightningAddress: "[email protected]", // mixed case → must still collapse
tags: [["d", "pointer"]],
kind: 30078,
};
const result = await splitAndZap(
{ videoEvent, amountSats: 1000, comment: "self-owned platform" },
deps
);
assert.equal(result.totalAmount, 1000);
assert.equal(result.creatorShare, 1000, "creator receives the full amount");
assert.equal(result.platformShare, 0, "no separate platform fee is split out");
assert.equal(result.receipts.length, 1, "exactly one payment is made");
const sendCalls = getSendCalls();
assert.equal(sendCalls.length, 1, "must not double-pay the same wallet");
assert.equal(sendCalls[0].amountSats, 1000);
assert.equal(
getValidatorCalls().length,
1,
"only one receipt to validate after collapsing"
);
}
// The split must never lose or invent sats: creatorShare + platformShare always
// equals the total, and a fee that rounds below 1 sat falls entirely to the
// creator rather than silently dropping value.
async function testSplitRoundingPreservesEverySat() {
// 33% of 1000 = 330 exactly-floored; creator gets the remainder.
globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__ = 33;
{
const { deps, getSendCalls } = createDeps();
const videoEvent = {
id: "e1",
pubkey: "c".repeat(64),
lightningAddress: "[email protected]",
tags: [["d", "p"]],
kind: 30078,
};
const result = await splitAndZap({ videoEvent, amountSats: 1000 }, deps);
assert.equal(result.platformShare, 330);
assert.equal(result.creatorShare, 670);
assert.equal(
result.creatorShare + result.platformShare,
result.totalAmount,
"no sats lost or invented"
);
const amounts = getSendCalls().map((c) => c.amountSats);
assert.deepEqual(amounts, [670, 330]);
}
// 10% of 5 sats floors to 0 → the whole zap goes to the creator as one payment.
globalThis.__BITVID_PLATFORM_FEE_OVERRIDE__ = 10;
{
const { deps, getSendCalls } = createDeps();
const videoEvent = {
id: "e2",
pubkey: "c".repeat(64),
lightningAddress: "[email protected]",
tags: [["d", "p"]],
kind: 30078,
};
const result = await splitAndZap({ videoEvent, amountSats: 5 }, deps);
assert.equal(result.platformShare, 0, "sub-1-sat fee is not charged");
assert.equal(result.creatorShare, 5);
const sendCalls = getSendCalls();
assert.equal(sendCalls.length, 1, "no zero-amount platform payment is sent");
assert.equal(sendCalls[0].amountSats, 5);
}
}
await testCreatorEqualsPlatformCollapsesToOnePayment();
await testSplitRoundingPreservesEverySat();
await testSplitMath();
await testBech32LightningAddressZapTag();
await testUrlLightningAddressZapTag();