Chain index keeps the full event history (was a 600-event window); one-time rescan from the deploy block

Hugh's Earnings page showed no payouts, referrals or purchases: /api/my/activity read the last 600
events and the window had moved past his Sept 9-10 activity. Ten other readers (dashboard earned
total, leaderboard, holding-tank own-buy check, admin member view, growth snapshot, P&L, burner
match) treated the same list as complete. KEEP_EVENTS 600 -> 250000; chain.rescan() rebuilds from
deployBlock without re-firing onEvent (no repeat Telegram/email), keeps real ts for known events and
estimates ts by block height for backfilled ones; runs once at boot when a filled window is found;
admin routes POST /api/admin/chain/rescan and GET /api/admin/chain/status. State file only rewritten
when events changed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-16 10:36:28 -05:00
parent dda8c976b0
commit a85a198e95
2 changed files with 63 additions and 9 deletions
+14 -6
View File
@@ -656,7 +656,7 @@ async function emailOnEvent(ev) {
const PCT = { 1: 50, 2: 20, 3: 10 };
// the Purchase event of the same tx is already indexed (it precedes every payout log), so the
// dollar side of any share is that purchase's price times the tier percentage
const purchaseOf = tx => { try { return chain.recentEvents(600).find(e => e.tx === tx && e.type === 'Purchase'); } catch (e) { return null; } };
const purchaseOf = tx => { try { return chain.recentEvents(1e9).find(e => e.tx === tx && e.type === 'Purchase'); } catch (e) { return null; } };
const usdShare = (pur, pct) => pur ? (' (about ' + ('$' + (pur.priceCents * pct / 10000).toFixed(2)).replace(/\.00$/, '') + ')') : '';
const txUrlOf = tx => { const cc = chain.getConfig(); return (cc.explorer ? cc.explorer.replace(/\/+$/, '') : 'https://polygonscan.com') + '/tx/' + tx; };
if (ev.type === 'Purchase') {
@@ -1213,7 +1213,7 @@ const server = http.createServer(async (req, res) => {
out.credits = bal.total; out.creditedCredits = bal.credited; out.earnedCredits = bal.earned; out.inCampaigns = bal.inCampaigns; out.availableCredits = bal.available;
} catch (e) { out.chainReadError = true; }
let earned = 0n, n = 0;
for (const ev of chain.recentEvents(600)) {
for (const ev of chain.recentEvents(1e9)) { // whole history, not the last 600
if ((ev.type === 'TierPaid' && ev.recipientId === memberId) || (ev.type === 'AwardPaid' && ev.toId === memberId)) {
earned += BigInt(ev.amountWei); n += 1;
}
@@ -2353,12 +2353,12 @@ const server = http.createServer(async (req, res) => {
if (!s) return json(res, 401, { error: 'Sign in first.' });
const id = s.memberId || await auth.refreshMemberId(s);
if (!id) return json(res, 200, { memberId: 0, earnings: [], purchases: [], referrals: [] });
const evs = chain.recentEvents(600);
const evs = chain.recentEvents(1e9); // whole history (was the last 600 events: older members saw three empty boxes)
return json(res, 200, {
memberId: id,
earnings: await attachNames(evs.filter(e => (e.type === 'TierPaid' && e.recipientId === id) || (e.type === 'AwardPaid' && e.toId === id))),
purchases: await attachNames(evs.filter(e => e.type === 'Purchase' && e.buyerId === id)),
referrals: await attachNames(evs.filter(e => (e.type === 'MemberActivated' && e.sponsorId === id) || (e.type === 'BuyerCounted' && e.sponsorId === id)))
earnings: await attachNames(evs.filter(e => (e.type === 'TierPaid' && e.recipientId === id) || (e.type === 'AwardPaid' && e.toId === id)).slice(0, 300)),
purchases: await attachNames(evs.filter(e => e.type === 'Purchase' && e.buyerId === id).slice(0, 300)),
referrals: await attachNames(evs.filter(e => (e.type === 'MemberActivated' && e.sponsorId === id) || (e.type === 'BuyerCounted' && e.sponsorId === id)).slice(0, 300))
});
}
@@ -2554,6 +2554,14 @@ const server = http.createServer(async (req, res) => {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, await audit.run());
}
if (p === '/api/admin/chain/rescan' && req.method === 'POST') { // rebuild the full event history from the deploy block
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, await chain.rescan());
}
if (p === '/api/admin/chain/status' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, { events: chain.eventCount(), historyComplete: chain.historyComplete(), totals: chain.totals() });
}
if (p === '/api/admin/reports' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, { reports: await reports.list(200) });