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:
@@ -46,11 +46,16 @@ const SEL = {
|
||||
|
||||
const CHUNK = 9000;
|
||||
const POLL_MS = 30000;
|
||||
const KEEP_EVENTS = 600;
|
||||
// 2026-09-16: was 600. Eleven consumers (member Earnings page, dashboard earned total, leaderboard,
|
||||
// holding-tank own-buy check, admin member view, growth snapshot, P&L) read this list as if it
|
||||
// were the whole history; once the contract passed 600 events (Sept 12) every older member's
|
||||
// purchases and payouts silently vanished from all of them. Keep the full history; rescan() rebuilds it.
|
||||
const KEEP_EVENTS = 250000;
|
||||
|
||||
let cfg = null;
|
||||
let state = null;
|
||||
let busy = false;
|
||||
let dirty = false;
|
||||
let onEvent = null;
|
||||
|
||||
function getConfig() {
|
||||
@@ -229,11 +234,48 @@ async function tail() {
|
||||
}
|
||||
if (state.events.length > KEEP_EVENTS) state.events = state.events.slice(-KEEP_EVENTS);
|
||||
state.lastBlock = to;
|
||||
dirty = true;
|
||||
}
|
||||
saveState();
|
||||
if (dirty) { saveState(); dirty = false; }
|
||||
} catch (e) { console.error('chain tail', e.message); }
|
||||
busy = false;
|
||||
}
|
||||
// Full history rebuild from the deploy block. Does NOT fire onEvent (no repeat Telegram/email
|
||||
// posts) and keeps the real indexed-at ts of events already known; backfilled events get a ts
|
||||
// estimated from their block height so day/week windows stay honest enough.
|
||||
async function rescan() {
|
||||
let waited = 0; while (busy && waited < 120000) { await new Promise(r => setTimeout(r, 500)); waited += 500; }
|
||||
if (busy) return { error: 'indexer busy' };
|
||||
busy = true;
|
||||
try {
|
||||
const c = getConfig();
|
||||
const tip = await bestTip(); const latest = tip.block;
|
||||
const known = new Map(state.events.map(e => [e.tx + ':' + e.li, e]));
|
||||
const oldest = state.events.find(e => e.ts && e.block);
|
||||
const spb = oldest && latest > oldest.block ? Math.max(1000, Math.min(4000, (Date.now() - oldest.ts) / (latest - oldest.block))) : 2150; // ms per block
|
||||
const evs = [];
|
||||
for (let from = c.deployBlock; from <= latest; from += CHUNK) {
|
||||
const to = Math.min(from + CHUNK - 1, latest);
|
||||
const range = [{ address: c.contract, fromBlock: '0x' + from.toString(16), toBlock: '0x' + to.toString(16) }];
|
||||
let logs;
|
||||
try { logs = await rpcOnce(tip.url, 'eth_getLogs', range); } catch (e) { logs = await rpc('eth_getLogs', range); }
|
||||
for (const lg of logs) {
|
||||
const ev = decodeLog(lg); if (!ev) continue;
|
||||
const old = known.get(ev.tx + ':' + ev.li);
|
||||
ev.ts = old && old.ts ? old.ts : Math.round(Date.now() - (latest - ev.block) * spb);
|
||||
evs.push(ev);
|
||||
}
|
||||
}
|
||||
state.events = evs.slice(-KEEP_EVENTS);
|
||||
state.lastBlock = latest;
|
||||
state.totals = null; for (const ev of state.events) tally(ev);
|
||||
state.full = true;
|
||||
saveState();
|
||||
console.log('chain rescan: ' + evs.length + ' events, blocks ' + c.deployBlock + '-' + latest);
|
||||
return { ok: true, events: evs.length, fromBlock: c.deployBlock, toBlock: latest };
|
||||
} catch (e) { console.error('chain rescan', e.message); return { error: e.message }; }
|
||||
finally { busy = false; }
|
||||
}
|
||||
function recentEvents(n) { return state ? state.events.slice(-(n || 100)).reverse() : []; }
|
||||
|
||||
// running totals for the public counters — every number provable on-chain
|
||||
@@ -254,6 +296,9 @@ function init(opts) {
|
||||
getConfig(); loadState();
|
||||
tail();
|
||||
setInterval(tail, POLL_MS);
|
||||
// one-time migration: a state that filled the old 600-event window has lost history; rebuild it.
|
||||
// (Fresh/test servers have an empty state and never trigger this.)
|
||||
if (!state.full && state.events.length >= 600) setTimeout(() => { rescan().catch(() => {}); }, 8000);
|
||||
}
|
||||
|
||||
// Recommended EIP-1559 fees straight from the network. Polygon Amoy's Bor nodes
|
||||
@@ -273,4 +318,5 @@ async function suggestedFees() {
|
||||
}
|
||||
|
||||
module.exports = { init, getConfig, reloadConfig, memberIdByAccount, memberCount, member,
|
||||
product, productCount, quoteWei, creditBalance, catalog, recentEvents, totals, rpc, decodeLog, suggestedFees };
|
||||
product, productCount, quoteWei, creditBalance, catalog, recentEvents, totals, rpc, decodeLog, suggestedFees, rescan,
|
||||
eventCount: () => (state ? state.events.length : 0), historyComplete: () => !!(state && state.full) };
|
||||
|
||||
Reference in New Issue
Block a user