diff --git a/chain.js b/chain.js index 6c68fd0..2d7412b 100644 --- a/chain.js +++ b/chain.js @@ -44,6 +44,8 @@ const STATE_FILE = path.join(DATA_DIR, 'chain-index.json'); let state = null; let busy = false; +let onEvent = null; // callback(evt) for NEW events seen by the live tail (never snapshot history) +function emit(evt) { if (onEvent) { try { onEvent(evt); } catch (e) { console.error('chain onEvent error', e.message); } } } function loadState() { try { state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8')); } catch (e) { state = null; } @@ -196,6 +198,13 @@ async function processRange(fromBlock, toBlock) { if (!state.members[id]) { state.members[id] = { account: topicAddr(l.topics[2]), referrerId: topicInt(l.topics[3]), tier: wInt(l.data, 0), joinedAt: ts }; newIds.push(id); + emit({ type: 'registered', id, referrerId: topicInt(l.topics[3]), tierName: tierName(wInt(l.data, 0)), tx, ts }); + } + } else if (l.topics[0] === T_UPGRADED) { + const id = topicInt(l.topics[1]); + if (!state.members[id] || (state.members[id].level || 0) < wInt(l.data, 0)) { + if (state.members[id]) state.members[id].level = wInt(l.data, 0); + emit({ type: 'upgraded', id, newLevel: wInt(l.data, 0), levelName: levelName(wInt(l.data, 0)), tx, ts }); } } else if (l.topics[0] === T_REFERRAL || l.topics[0] === T_UPLINE) { if (state.payouts.some(p => p.key === key)) continue; @@ -210,10 +219,13 @@ async function processRange(fromBlock, toBlock) { if (txUpgrades[tx]) p.upgrade = txUpgrades[tx]; p.passed = passedOver(p.fromId, p.toId); } - // upgrade a snapshot-sourced twin in place, else append + // upgrade a snapshot-sourced twin in place, else append (twin = already known from snapshot, so not a NEW event) const twinIdx = state.payouts.findIndex(x => !x.tx && samePayout(x, p)); if (twinIdx >= 0) state.payouts[twinIdx] = p; - else { state.payouts.push(p); state.totals.count++; state.totals.pol = +(state.totals.pol + p.pol).toFixed(6); } + else { + state.payouts.push(p); state.totals.count++; state.totals.pol = +(state.totals.pol + p.pol).toFixed(6); + emit({ type: 'payout', kind: p.kind, toId: p.toId, fromId: p.fromId, pol: p.pol, levelName: levelName(p.level), upgrade: p.upgrade, tx, ts }); + } } } for (const id of newIds) { @@ -255,12 +267,27 @@ async function tick() { } finally { busy = false; } } -function startIndexer() { +function startIndexer(eventCb) { + onEvent = eventCb || null; loadState(); tick(); setInterval(tick, POLL_MS).unref(); } +// true if `id` sits at or below `rootId` in the matrix (walks the upline chain) +function isInTeam(id, rootId) { + if (!state || !id || !rootId) return false; + if (id === rootId) return true; + const seen = new Set([id]); + let cur = state.members[id] && state.members[id].uplineId; + for (let i = 0; i < 60 && cur && !seen.has(cur); i++) { + if (cur === rootId) return true; + seen.add(cur); + cur = state.members[cur] && state.members[cur].uplineId; + } + return false; +} + function getPayoutsPublic() { const recent = state ? state.payouts.slice(-40).reverse() : []; return { @@ -341,4 +368,4 @@ function getMatrixTree() { return { ready: true, snapshotAt: state.snapshotAt, memberCount: Object.keys(state.members).length, root, unplaced: unplaced.length ? unplaced : undefined }; } -module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, getMatrixTree, CONTRACT }; +module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, getMatrixTree, isInTeam, CONTRACT }; diff --git a/public/admin.html b/public/admin.html index 99ef2fb..2f2a346 100644 --- a/public/admin.html +++ b/public/admin.html @@ -6,7 +6,7 @@
New members who confirmed their purchase on the start page. Each one was posted to your Hermes Telegram chat — add them to the rotation.
| When | Name / Handle | New ID | Joined under | Source | On-chain |
|---|
Enter an RM Circle ID to read its registration, lineage, and every payment it has received — live from the smart contract.
The entire on-chain matrix — who landed where, with tier, level, directs, and earnings per position. Click a position to drill down.
Paste an OpenRouter API key to switch the help chat from canned answers to AI (). Clear it to switch back.