// On-chain indexer + reader for the RM Circle contract (Polygon mainnet). // Streams payout events for the public proof feed, verifies member IDs, and // serves the admin lineage/income lookup. // // Architecture (free public RPCs only — no API keys): // 1. SNAPSHOT: the contract stores every member's full income history // (getIncomeHistory), so complete payout history = ~2 cheap eth_calls per // member. Re-run daily to self-heal any gap. // 2. LIVE TAIL: eth_getLogs over the recent window (publicnode keeps ~2 days // of logs) picks up new payouts within a minute, with tx hashes for // "Verify on Polygonscan" links plus upgrade/passed-over context. // State persists in DATA_DIR (mounted volume) across redeploys. const fs = require('fs'); const path = require('path'); const CONTRACT = '0x33bdaeefd6d17d80ae53816c916dfb26c4fb2daf'; const RPCS = [ 'https://polygon-bor-rpc.publicnode.com', 'https://1rpc.io/matic' ]; // keccak-256 hashes verified against live logs (tx 0x8a74c439…, 0x45e07f27…) const T_REGISTERED = '0xe4a74887d749eb048f14bfef37b204477f3a5ff67055908b7c8cc62c202aef17'; // MemberRegistered(uint48,address,uint48,uint8) const T_REFERRAL = '0xccd156c02c0b06d498576496d3d915e490750977240fbb8689055d88a4d9e231'; // ReferralRewarded(uint48,uint48,uint8,uint8,uint256) const T_UPLINE = '0x6cdacf6757bdea1fcf01918794f79ca1e71b088fbec8fde7b027a963cfab0ae9'; // UplineRewarded(uint48,uint48,uint8,uint256) const T_UPGRADED = '0xc0b79a9e133d4dcbb1a606a57591d98dce93c7d5c86197a5caae22c4a1480049'; // MemberUpgraded(uint48,uint8,uint8) // 4-byte selectors (keccak-256 of signature; getMember(address)=0x2ada2596 cross-checked with live dApp) const SEL = { members: '0xc92463fa', // members(uint48) totalMembers: '0x76e92559', // totalMembers() getIncomeHistory: '0x765d1209', // getIncomeHistory(uint48) getDirectReferrals: '0xcf00a645', // getDirectReferrals(uint48) getMatrixChildren: '0x04c8cc3d', // getMatrixChildren(uint48) getAllCosts: '0x735f87b9' // getAllCosts() }; const CHUNK = 9000; // publicnode getLogs range cap is 10k const TAIL_MAX_BEHIND = 60000; // never tail further back than ~1.5 days (log pruning) const POLL_MS = 60000; const SNAPSHOT_MS = 24 * 3600 * 1000; const KEEP_PAYOUTS = 400; const LEVELS = ['Scintilla','Ascensus','Fabrica','Culmen','Apex','Fastigium','Vertex','Corona']; const TIERS = { 1: 'Standard', 2: 'Premium' }; const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, 'data'); 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; } if (!state || state.v !== 2) { state = { v: 2, lastBlock: 0, snapshotAt: 0, members: {}, payouts: [], totals: { count: 0, pol: 0 }, updatedAt: null }; } // income rows saved before desc-classification existed: re-snapshot to backfill if (state.payouts.some(p => p.kind === 'income' && !p.desc)) state.snapshotAt = 0; // one-time true-up: earlier tail code didn't maintain directCount/matrix slots live if (!state.migratedLiveCounts) { state.snapshotAt = 0; state.migratedLiveCounts = true; } } function saveState() { try { const tmp = STATE_FILE + '.tmp'; fs.writeFileSync(tmp, JSON.stringify(state)); fs.renameSync(tmp, STATE_FILE); } catch (e) { console.error('chain: state save failed', e.message); } } // only publicnode supports ranged getLogs (1rpc caps at 50 blocks) const LOG_RPCS = ['https://polygon-bor-rpc.publicnode.com']; let rpcIdx = 0; async function rpc(method, params, timeoutMs = 15000, urls = RPCS) { let lastErr = new Error('no rpc'); for (let i = 0; i < urls.length; i++) { const url = urls[(rpcIdx + i) % urls.length]; try { const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), timeoutMs); const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), signal: ctrl.signal }); clearTimeout(t); const j = await r.json(); if (j.error) throw new Error(j.error.message || JSON.stringify(j.error)); if (urls === RPCS) rpcIdx = (rpcIdx + i) % urls.length; return j.result; } catch (e) { lastErr = e; } } throw lastErr; } const hexInt = h => parseInt(h, 16); const word = (data, i) => data.slice(2).slice(i * 64, (i + 1) * 64); const wInt = (data, i) => parseInt(word(data, i), 16); const wBig = (data, i) => BigInt('0x' + (word(data, i) || '0')); const wAddr = (data, i) => '0x' + word(data, i).slice(24); const topicInt = t => parseInt(t, 16); const topicAddr = t => '0x' + t.slice(26); const pol = wei => Number(wei / 1000000000000n) / 1e6; // 6-decimal POL const encU = v => BigInt(v).toString(16).padStart(64, '0'); async function ethCall(data) { return await rpc('eth_call', [{ to: CONTRACT, data }, 'latest']); } function decodeMemberStruct(r) { // members(uint48): account, joinedAt, referrerId, uplineId, tier, level, directCount, // totalEarned, totalPaid, referralEarned, uplineEarned if (!r || r === '0x' || r.length < 2 + 11 * 64) return null; const account = wAddr(r, 0); if (/^0x0{40}$/.test(account)) return null; return { account, joinedAt: wInt(r, 1), referrerId: wInt(r, 2), uplineId: wInt(r, 3), tier: wInt(r, 4), level: wInt(r, 5), directCount: wInt(r, 6), totalEarnedPol: pol(wBig(r, 7)), totalPaidPol: pol(wBig(r, 8)), referralEarnedPol: pol(wBig(r, 9)), uplineEarnedPol: pol(wBig(r, 10)) }; } async function fetchMember(id) { return decodeMemberStruct(await ethCall(SEL.members + encU(id))); } async function fetchIncome(id) { // getIncomeHistory(uint48) -> (uint48 fromId, uint8 atLevel, uint8 fromTier, uint256 amount, uint48 timestamp)[] const r = await ethCall(SEL.getIncomeHistory + encU(id)); const len = wInt(r, 1) || 0; const out = []; for (let i = 0; i < len && i < 1000; i++) { const o = 2 + i * 5; out.push({ fromId: wInt(r, o), level: wInt(r, o + 1), fromTier: wInt(r, o + 2), pol: pol(wBig(r, o + 3)), ts: wInt(r, o + 4) }); } return out; } function levelName(n) { return LEVELS[n - 1] || `Level ${n}`; } function tierName(n) { return TIERS[n] || `Tier ${n}`; } // Cost tables from getAllCosts(): standard[8], premium[8], stdUpgrade[8], premUpgrade[8]. let costs = null; async function fetchCosts() { if (costs) return costs; const r = await ethCall(SEL.getAllCosts); const arr = i => Array.from({ length: 8 }, (_, j) => pol(wBig(r, i * 8 + j))); costs = { reg: { 1: arr(0), 2: arr(1) }, up: { 1: arr(2), 2: arr(3) } }; return costs; } // The contract records income `atLevel` = the level the payer upgraded OUT of // (upgrade N→N+1 books atLevel=N), so raw atLevel reads misleadingly low. // Classify by amount against the cost tables: upgrade pass-ups arrive at FULL // upgrade cost; registration referral rewards arrive at slot cost minus the 5% // admin fee. function describeIncome(fromTier, atLevel, amountPol) { if (!costs || !costs.up[fromTier] || atLevel < 1 || atLevel > 8) return levelName(atLevel); if (Math.abs(amountPol - costs.up[fromTier][atLevel - 1]) < 0.02) return `Upgrade to ${levelName(atLevel + 1)}`; if (Math.abs(amountPol - costs.reg[fromTier][atLevel - 1] * 0.95) < 0.02) return `Entry — ${levelName(atLevel)}`; return levelName(atLevel); } function samePayout(a, b) { return a.toId === b.toId && a.fromId === b.fromId && Math.abs(a.pol - b.pol) < 1e-6 && Math.abs((a.ts || 0) - (b.ts || 0)) <= 5; } // full-history snapshot straight from contract storage (no logs needed) async function snapshot() { await fetchCosts().catch(() => {}); const total = wInt(await ethCall(SEL.totalMembers), 0); const members = {}, history = []; for (let id = 1; id <= total; id++) { let m = null; try { m = await fetchMember(id); } catch (e) { throw new Error(`snapshot member ${id}: ${e.message}`); } if (!m) continue; members[id] = { account: m.account, referrerId: m.referrerId, uplineId: m.uplineId, tier: m.tier, level: m.level, directCount: m.directCount, earnedPol: m.totalEarnedPol, joinedAt: m.joinedAt }; try { const r = await ethCall(SEL.getMatrixChildren + encU(id)); members[id].l = wInt(r, 0) || 0; members[id].r = wInt(r, 1) || 0; } catch (e) {} let inc = []; try { inc = await fetchIncome(id); } catch (e) { inc = []; } inc.forEach((p, i) => history.push({ key: `h${id}-${i}`, kind: 'income', toId: id, fromId: p.fromId, level: p.level, pol: p.pol, ts: p.ts, desc: describeIncome(p.fromTier, p.level, p.pol) })); await new Promise(r => setTimeout(r, 60)); } history.sort((a, b) => (a.ts || 0) - (b.ts || 0)); // keep richer log-sourced entries (they carry tx hashes / upgrade context) const logSourced = state.payouts.filter(p => p.tx); const merged = history.map(h => logSourced.find(l => samePayout(l, h)) || h); for (const l of logSourced) if (!merged.some(p => p === l || samePayout(p, l))) merged.push(l); merged.sort((a, b) => (a.ts || 0) - (b.ts || 0)); state.members = members; state.totals = { count: history.length, pol: +history.reduce((s, p) => s + p.pol, 0).toFixed(6) }; state.payouts = merged.slice(-KEEP_PAYOUTS); state.snapshotAt = Date.now(); console.log(`chain: snapshot done — ${total} members, ${history.length} payouts, ${state.totals.pol.toFixed(0)} POL total`); } // walk the stored upline chain from `fromId` up to `toId`; members in between // were passed over by that pass-up payment (not yet at that level) function passedOver(fromId, toId) { const out = []; const seen = new Set([fromId]); let cur = state.members[fromId] && state.members[fromId].uplineId; for (let i = 0; i < 40 && cur && !seen.has(cur); i++) { if (cur === toId) return out; seen.add(cur); out.push(cur); cur = state.members[cur] && state.members[cur].uplineId; } return []; } async function processRange(fromBlock, toBlock) { const logs = await rpc('eth_getLogs', [{ address: CONTRACT, fromBlock: '0x' + fromBlock.toString(16), toBlock: '0x' + toBlock.toString(16), topics: [[T_REGISTERED, T_REFERRAL, T_UPLINE, T_UPGRADED]] }], 25000, LOG_RPCS); if (!logs.length) return; logs.sort((a, b) => hexInt(a.blockNumber) - hexInt(b.blockNumber) || hexInt(a.logIndex) - hexInt(b.logIndex)); const blockTs = {}; for (const bn of [...new Set(logs.map(l => l.blockNumber))]) { try { const b = await rpc('eth_getBlockByNumber', [bn, false]); blockTs[bn] = hexInt(b.timestamp); } catch (e) { blockTs[bn] = null; } } const txUpgrades = {}; for (const l of logs) if (l.topics[0] === T_UPGRADED) txUpgrades[l.transactionHash] = { id: topicInt(l.topics[1]), newLevel: wInt(l.data, 0), tier: wInt(l.data, 1) }; const newIds = []; for (const l of logs) { const tx = l.transactionHash, blk = hexInt(l.blockNumber), ts = blockTs[l.blockNumber]; const key = tx.slice(2, 12) + ':' + hexInt(l.logIndex); if (l.topics[0] === T_REGISTERED) { const id = topicInt(l.topics[1]); if (!state.members[id]) { state.members[id] = { account: topicAddr(l.topics[2]), referrerId: topicInt(l.topics[3]), tier: wInt(l.data, 0), joinedAt: ts, level: 1, directCount: 0, earnedPol: 0 }; // keep the referrer's qualification progress live between snapshots const ref = state.members[topicInt(l.topics[3])]; if (ref) ref.directCount = (ref.directCount || 0) + 1; 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; const isRef = l.topics[0] === T_REFERRAL; const p = { key, kind: isRef ? 'referral' : 'upline', toId: topicInt(l.topics[1]), fromId: topicInt(l.topics[2]), level: wInt(l.data, 0), pol: pol(wBig(l.data, isRef ? 2 : 1)), tx, block: blk, ts }; if (!isRef) { if (txUpgrades[tx]) p.upgrade = txUpgrades[tx]; // a level-N payment structurally skips the first N-1 uplines (they're // never checked) — only list uplines that failed the eligibility test p.passed = passedOver(p.fromId, p.toId).slice(Math.max(0, p.level - 1)); } // 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); 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.level), upgrade: p.upgrade, tx, ts }); } } } for (const id of newIds) { try { const m = await fetchMember(id); if (m) { Object.assign(state.members[id], { uplineId: m.uplineId, level: m.level }); // wire the new member into the parent's matrix slots so trees update live const par = state.members[m.uplineId]; if (par) { if (!par.l) par.l = id; else if (!par.r && par.l !== id) par.r = id; } } } catch (e) { /* picked up by next snapshot */ } } state.payouts.sort((a, b) => (a.ts || 0) - (b.ts || 0)); if (state.payouts.length > KEEP_PAYOUTS) state.payouts = state.payouts.slice(-KEEP_PAYOUTS); } async function tick() { if (busy) return; busy = true; try { if (!state.snapshotAt || Date.now() - state.snapshotAt > SNAPSHOT_MS) { const latest = hexInt(await rpc('eth_blockNumber', [])); await snapshot(); if (!state.lastBlock) state.lastBlock = latest - 1000; // first run: small log overlap, dedup handles it saveState(); } const latest = hexInt(await rpc('eth_blockNumber', [])); let from = Math.max(state.lastBlock + 1, latest - TAIL_MAX_BEHIND); while (from <= latest) { const to = Math.min(from + CHUNK - 1, latest); try { await processRange(from, to); } catch (e) { if (/prun/i.test(e.message)) { from = to + 1; continue; } // pruned window — skip forward throw e; } state.lastBlock = to; from = to + 1; if (from <= latest) await new Promise(r => setTimeout(r, 150)); } state.updatedAt = new Date().toISOString(); saveState(); } catch (e) { console.error('chain tick error:', e.message); saveState(); } finally { busy = false; } } 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(offset = 0, limit = 40) { const off = Math.max(0, Number.isFinite(offset) ? Math.floor(offset) : 0); const lim = Math.min(100, Math.max(1, Number.isFinite(limit) ? Math.floor(limit) : 40)); const reversed = state ? state.payouts.slice().reverse() : []; // most recent first const page = reversed.slice(off, off + lim); return { updatedAt: state && state.updatedAt, ready: !!(state && state.snapshotAt), totals: state ? { payouts: state.totals.count, pol: +state.totals.pol.toFixed(2), members: Object.keys(state.members).length } : null, contract: CONTRACT, total: reversed.length, offset: off, limit: lim, hasMore: off + lim < reversed.length, payouts: page.map(p => ({ key: p.key, kind: p.kind, toId: p.toId, fromId: p.fromId, level: p.level, levelName: levelName(p.level), pol: p.pol, tx: p.tx, ts: p.ts, desc: p.desc, toAccount: state.members[p.toId] ? state.members[p.toId].account : undefined, upgrade: p.upgrade ? { id: p.upgrade.id, newLevel: p.upgrade.newLevel, levelName: levelName(p.upgrade.newLevel), tier: tierName(p.upgrade.tier) } : undefined, passed: p.passed && p.passed.length ? p.passed : undefined })) }; } async function verifyMember(id) { const m = await fetchMember(id); if (!m) return { registered: false }; if (state && !state.members[id]) state.members[id] = { account: m.account, referrerId: m.referrerId, uplineId: m.uplineId, tier: m.tier, level: m.level, joinedAt: m.joinedAt }; return { registered: true, ...m, tierName: tierName(m.tier), levelName: levelName(m.level) }; } async function memberLookup(id) { const m = await fetchMember(id); if (!m) return { registered: false, id }; const enc = encU(id); const out = { registered: true, id, ...m, tierName: tierName(m.tier), levelName: levelName(m.level) }; // upline chain to root (root's uplineId points at itself — visited set breaks the loop) out.uplineChain = []; const seen = new Set([id]); let cur = m.uplineId; for (let i = 0; i < 40 && cur && !seen.has(cur); i++) { seen.add(cur); let um = null; try { um = await fetchMember(cur); } catch (e) { break; } if (!um) break; out.uplineChain.push({ id: cur, tier: um.tier, tierName: tierName(um.tier), level: um.level, levelName: levelName(um.level), directCount: um.directCount }); cur = um.uplineId; } try { const r = await ethCall(SEL.getDirectReferrals + enc); // uint48[]: offset, length, ids const len = wInt(r, 1); out.directs = []; for (let i = 0; i < len && i < 512; i++) out.directs.push(wInt(r, 2 + i)); } catch (e) { out.directs = null; } try { const r = await ethCall(SEL.getMatrixChildren + enc); // two uint48 words out.matrix = { left: wInt(r, 0) || null, right: wInt(r, 1) || null }; } catch (e) { out.matrix = null; } try { await fetchCosts().catch(() => {}); const inc = await fetchIncome(id); out.income = inc.map(p => ({ ...p, levelName: levelName(p.level), desc: describeIncome(p.fromTier, p.level, p.pol) })).reverse(); } catch (e) { out.income = null; } return out; } // full matrix tree built from the snapshot (root id 1); cycle-guarded function getMatrixTree() { if (!state || !state.snapshotAt) return { ready: false }; const seen = new Set(); function node(id, depth) { if (!id || seen.has(id) || depth > 60) return null; seen.add(id); const m = state.members[id]; if (!m) return { id, missing: true }; const n = { id, tier: m.tier, tierName: tierName(m.tier), level: m.level, levelName: levelName(m.level), directCount: m.directCount || 0, earnedPol: m.earnedPol || 0, referrerId: m.referrerId, joinedAt: m.joinedAt }; n.left = node(m.l, depth + 1); n.right = node(m.r, depth + 1); // downline rollup: members below this position and the POL they earned n.downCount = 0; n.downPol = 0; for (const c of [n.left, n.right]) if (c && !c.missing) { n.downCount += 1 + (c.downCount || 0); n.downPol = +(n.downPol + (c.earnedPol || 0) + (c.downPol || 0)).toFixed(2); } return n; } const root = node(1, 0); const unplaced = Object.keys(state.members).map(Number).filter(id => !seen.has(id)); return { ready: true, snapshotAt: state.snapshotAt, memberCount: Object.keys(state.members).length, root, unplaced: unplaced.length ? unplaced : undefined }; } // subtree rooted at `id` from the snapshot: nodes to `showDepth`, rollups from the FULL subtree function getSubtree(id, showDepth = 3) { if (!state || !state.members[id]) return null; const seen = new Set(); function node(nid, depth) { if (!nid || seen.has(nid)) return null; seen.add(nid); const m = state.members[nid]; if (!m) return null; const l = node(m.l, depth + 1), r = node(m.r, depth + 1); const n = { id: nid, tier: m.tier, level: m.level, levelName: levelName(m.level || 1), directCount: m.directCount || 0, earnedPol: m.earnedPol || 0, referrerId: m.referrerId || 0, downCount: 0, downPol: 0 }; for (const c of [l, r]) if (c) { n.downCount += 1 + c.downCount; n.downPol = +(n.downPol + c.earnedPol + c.downPol).toFixed(2); } if (depth < showDepth) { n.left = l; n.right = r; } return n; } return node(id, 0); } // everything a member may see about their own position — chain data only async function memberPublic(id) { const m = await fetchMember(id); if (!m) return { registered: false, id }; await fetchCosts().catch(() => {}); const out = { registered: true, id, account: m.account, joinedAt: m.joinedAt, tier: m.tier, tierName: tierName(m.tier), level: m.level, levelName: levelName(m.level), directCount: m.directCount, totalEarnedPol: m.totalEarnedPol, totalPaidPol: m.totalPaidPol, referrerId: m.referrerId, uplineId: m.uplineId }; try { const inc = await fetchIncome(id); out.income = inc.map(p => ({ fromId: p.fromId, pol: p.pol, ts: p.ts, desc: describeIncome(p.fromTier, p.level, p.pol) })).reverse().slice(0, 50); } catch (e) { out.income = []; } out.subtree = getSubtree(id, 99); // full leg — the dashboard drills down client-side if (costs) out.upgradeCosts = costs.up; // {1:[8 std], 2:[8 prem]} POL — for the pipeline widget // moving-link automation: the first position below (breadth-first, left→right) // that still needs directs is where a qualified member's effort goes next if (out.subtree) { const q = [out.subtree.left, out.subtree.right].filter(Boolean); while (q.length) { const n = q.shift(); if ((n.directCount || 0) < 2) { out.nextInLine = { id: n.id, directCount: n.directCount || 0, levelName: n.levelName }; break; } if (n.left) q.push(n.left); if (n.right) q.push(n.right); } } const chain = []; const seen = new Set([id]); let cur = m.uplineId; for (let i = 0; i < 40 && cur && !seen.has(cur); i++) { seen.add(cur); chain.push(cur); cur = state && state.members[cur] ? state.members[cur].uplineId : 0; } out.uplineChain = chain; return out; } // For owned positions: detect when a leg member is ONE upgrade away from paying // the position, but the position isn't eligible yet (not qualified, or below the // required level) — i.e. "upgrade now or the payment passes you". Computed from // in-memory state, no RPC. A member M at depth D pays this position on M's // upgrade OUT of level D, so the trigger is M.level === D with the owner ineligible. function getOwnerUpgradeNeeds(ids) { if (!state || !state.snapshotAt || !costs) return { ready: false, needs: [] }; const needs = []; for (const id of ids) { const p = state.members[id]; const root = getSubtree(id, 99); if (!p || !root) continue; const pLevel = p.level || 1, pQual = (p.directCount || 0) >= 2; const items = []; (function walk(n, depth) { if (!n) return; if (depth >= 1 && (n.level || 1) === depth) { const eligible = pQual && pLevel >= depth; if (!eligible) items.push({ memberId: n.id, depth, amount: (costs.up[n.tier === 2 ? 2 : 1] || [])[depth - 1] || 0 }); } walk(n.left, depth + 1); walk(n.right, depth + 1); })(root, 0); if (items.length) { const minDepth = Math.min(...items.map(i => i.depth)); const atMin = items.filter(i => i.depth === minDepth); needs.push({ id, level: pLevel, levelName: levelName(pLevel), qualified: pQual, neededLevel: minDepth, neededLevelName: levelName(minDepth), members: atMin.map(i => i.memberId), amountAtRisk: +atMin.reduce((s, i) => s + i.amount, 0).toFixed(2), reason: pQual ? 'upgrade' : 'qualify' }); } } return { ready: true, needs }; } // How an organization rooted at `rootId` stacks up against the whole contract — // share of members and share of all POL paid. Computed from in-memory state. function getOrgShare(rootId) { if (!state || !state.snapshotAt) return { ready: false }; const totalMembers = Object.keys(state.members).length; let totalPol = 0; for (const m of Object.values(state.members)) totalPol += m.earnedPol || 0; const root = getSubtree(rootId, 99); const rm = state.members[rootId]; const base = { ready: true, rootId, totalMembers, totalPol: +totalPol.toFixed(2) }; if (!root || !rm) return { ...base, found: false }; const orgBelow = root.downCount || 0; const orgMembers = orgBelow + 1; const orgPol = (rm.earnedPol || 0) + (root.downPol || 0); let generations = 0; (function depth(n, dep) { if (!n) return; if (dep > generations) generations = dep; depth(n.left, dep + 1); depth(n.right, dep + 1); })(root, 0); return { ...base, found: true, orgBelow, orgMembers, generations, memberPct: +(100 * orgMembers / totalMembers).toFixed(1), belowPct: +(100 * orgBelow / totalMembers).toFixed(1), orgPol: +orgPol.toFixed(2), polPct: totalPol ? +(100 * orgPol / totalPol).toFixed(1) : 0 }; } // focused income read for one position — for the admin "my positions" income view async function getIncome(id) { const m = await fetchMember(id); if (!m) return { registered: false, id }; await fetchCosts().catch(() => {}); let inc = []; try { inc = await fetchIncome(id); } catch (e) { inc = []; } return { registered: true, id, levelName: levelName(m.level), tierName: tierName(m.tier), totalEarnedPol: m.totalEarnedPol, directCount: m.directCount, income: inc.map(p => ({ fromId: p.fromId, pol: p.pol, ts: p.ts, desc: describeIncome(p.fromTier, p.level, p.pol) })).reverse() }; } module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getOwnerUpgradeNeeds, getOrgShare, getMatrixTree, isInTeam, CONTRACT };