From 85a6056354020260d6d24952f8d9fbb7b840effb Mon Sep 17 00:00:00 2001 From: martbost Date: Fri, 18 Sep 2026 09:31:56 -0500 Subject: [PATCH] Announce each payout once, the same way registrations and upgrades already are MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- chain.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/chain.js b/chain.js index a9d459f..73b3618 100644 --- a/chain.js +++ b/chain.js @@ -352,7 +352,18 @@ 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); - 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 }); + // 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 }); + } } } }