Add sponsor contact emails and Telegram team-build activity alerts

Sponsors can carry an optional contact email (Add Sponsor form, ✉ inline
edit in the queue, shown under the name). New config teamRootId: any NEW
on-chain event at or below that member ID — registration, upgrade, or
payout seen by the live tail — posts to the Telegram group topic, with
the tx link and the sponsor's contact email when one is on file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-13 12:57:51 -05:00
parent 1e2b03c795
commit 6b17419646
4 changed files with 58 additions and 14 deletions
+31 -4
View File
@@ -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 };