Get exactly two
Use your position's link until two direct positions are placed beneath you and your position is qualified.
diff --git a/chain.js b/chain.js new file mode 100644 index 0000000..edc675a --- /dev/null +++ b/chain.js @@ -0,0 +1,344 @@ +// 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) +}; +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; + +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 }; + } +} +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}`; } + +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() { + 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 })); + 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 }; + newIds.push(id); + } + } 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]; + p.passed = passedOver(p.fromId, p.toId); + } + // upgrade a snapshot-sourced twin in place, else append + 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); } + } + } + for (const id of newIds) { + try { const m = await fetchMember(id); if (m) Object.assign(state.members[id], { uplineId: m.uplineId, level: m.level }); } + 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() { + loadState(); + tick(); + setInterval(tick, POLL_MS).unref(); +} + +function getPayoutsPublic() { + const recent = state ? state.payouts.slice(-40).reverse() : []; + 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, + payouts: recent.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, + 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 { + const inc = await fetchIncome(id); + out.income = inc.map(p => ({ ...p, levelName: levelName(p.level) })).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 + }; + const l = node(m.l, depth + 1), r = node(m.r, depth + 1); + if (l || r) n.children = [l, r].filter(Boolean); + 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 }; +} + +module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, getMatrixTree, CONTRACT }; diff --git a/public/admin.html b/public/admin.html index 6d4b39d..29c4821 100644 --- a/public/admin.html +++ b/public/admin.html @@ -3,7 +3,9 @@ diff --git a/public/admin.js b/public/admin.js index 1f8a6f7..18fc000 100644 --- a/public/admin.js +++ b/public/admin.js @@ -20,7 +20,10 @@ function renderAnalytics(){ (tot.purchase?`
Lineage up to root: #${d.id} → ${d.uplineChain.map(u=>`#${u.id} (${esc(u.levelName)}, ${u.directCount} directs)`).join(' → ')}
`:''; + const income=d.income&&d.income.length?`| When (UTC) | From member | At level | Tier | Amount |
|---|---|---|---|---|
| ${date(p.ts)} | #${p.fromId} | ${esc(p.levelName)} | ${p.fromTier===2?'Premium':'Standard'} | ${fmt(p.pol)} POL |
Payments received by #${d.id} — ${d.income?d.income.length:0} total, newest first:
`+income; + }catch(x){out.innerHTML=`Not reachable from root: ${d.unplaced.map(i=>'#'+i).join(', ')}
`:''; + out.innerHTML=`${d.memberCount} positions · snapshot ${new Date(d.snapshotAt).toISOString().replace('T',' ').slice(0,16)} UTC · P = Premium, S = Standard
The goal is not endless personal recruiting. Each position gets two directs, retires that referral link, then helps the next two positions repeat the process.
Use your position's link until two direct positions are placed beneath you and your position is qualified.
Retire the qualified link. Help each of your two directs use their links until they each have two.
Use earned POL to advance when practical. Active builders may also choose to self-fund, but only within their own risk tolerance.
Share the current position's referral link.
Place exactly two direct positions.
Stop creating extra shallow legs.
Shift the team effort to their links.
Keep the qualification wave moving down.
The first major team milestone is 30 correctly placed positions across the first four generations: 2 + 4 + 8 + 16.
Every payment in this program happens on a public smart contract on Polygon — nobody can fake, hide, or edit it. Below are the latest member payouts, read live from the contract. Tap any row to verify the transaction yourself on Polygonscan.
The onboarding page automatically shows the sponsor position the team is currently helping. Always use the sponsor shown there instead of an old screenshot or saved link.
Open Getting Started Instructions →