Announce each payout once, the same way registrations and upgrades already are

Marty saw #49's 4,971.22 POL Gen-4 pass-up from #222 posted twice in the
payments feed. The money is fine: the transaction has one transfer log to
#49's address and one contract payout event, 4971.219256933542 POL, paid
once. Only the announcement doubled.

Payouts were the one event type without the durable announce-once guard.
registered and upgraded are keyed into state.announced — 4,000 entries,
persisted with the rest of the index state. Payouts instead leaned on
state.payouts, which is a rolling window trimmed to KEEP_PAYOUTS (400),
and the announcement was emitted BEFORE that state was written. So a
restart in the gap between announcing and persisting, or any re-read of
the same block, announced the payment again — while an upgrade in the
very same transaction was correctly suppressed. That asymmetry is exactly
what Marty reported: the payout line twice, #222's Apex upgrade once.

The existing state.payouts check stays as the first line of defence; this
adds the durable one behind it, keyed on tx+logIndex, which identifies the
on-chain event exactly.

Not caused by today's deploy — that container started at 14:10 UTC and the
payout landed at 13:40 UTC. Checked before blaming it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-18 09:31:56 -05:00
parent 1284d28891
commit 85a6056354
+11
View File
@@ -352,10 +352,21 @@ async function processRange(fromBlock, toBlock) {
state.payouts.push(p); state.totals.count++; state.totals.pol = +(state.totals.pol + p.pol).toFixed(6);
const rcpt = state.members[p.toId];
if (rcpt) rcpt.earnedPol = +((rcpt.earnedPol || 0) + p.pol).toFixed(6);
// Payouts were the ONE event type without the durable announce-once guard.
// registered/upgraded are keyed into `state.announced` (4,000 entries, persisted);
// payouts leaned on `state.payouts` instead — a rolling window trimmed to
// KEEP_PAYOUTS (400) — and the announcement went out BEFORE that state was saved.
// So a restart in the gap between announcing and persisting, or any re-read of the
// same block, announced the payment a second time, while an upgrade in the very same
// transaction was correctly suppressed. That is exactly the shape Marty reported on
// 2026-09-18: #49's 4,971.22 POL pass-up posted twice, #222's Apex upgrade once.
// The key is already tx+logIndex, so it identifies the on-chain event exactly.
if (announceOnce('pay:' + key)) {
emit({ type: 'payout', kind: p.kind, toId: p.toId, fromId: p.fromId, pol: p.pol, levelName: levelName(p.kind === 'upline' ? p.level + 1 : p.level), upgrade: p.upgrade, gen: p.kind === 'upline' ? genBetween(p.fromId, p.toId) : undefined, tx, ts });
}
}
}
}
for (const id of newIds) {
try {
const m = await fetchMember(id);