From 110b9c9dfeb9032ab229713c262620a1b822836d Mon Sep 17 00:00:00 2001 From: martbost Date: Thu, 13 Aug 2026 14:18:12 -0500 Subject: [PATCH] Classify income rows as entry vs upgrade instead of raw atLevel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The contract books income atLevel = the level the payer upgraded OUT of (upgrade N->N+1 records atLevel=N), so an Ascensus upgrade pass-up was displaying as "Scintilla". Income rows are now classified against the contract's getAllCosts() tables — upgrade pass-ups arrive at full upgrade cost, entry referral rewards at slot cost minus the 5% admin fee — and display "Upgrade to Ascensus" / "Entry — Scintilla" in the admin income table, public feed, and toasts. Stale snapshot rows re-classify via a forced re-snapshot on boot. Co-Authored-By: Claude Fable 5 --- chain.js | 34 ++++++++++++++++++++++++++++++---- public/admin.js | 2 +- public/payouts.js | 4 ++-- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/chain.js b/chain.js index 2d7412b..d61061a 100644 --- a/chain.js +++ b/chain.js @@ -29,7 +29,8 @@ const SEL = { totalMembers: '0x76e92559', // totalMembers() getIncomeHistory: '0x765d1209', // getIncomeHistory(uint48) getDirectReferrals: '0xcf00a645', // getDirectReferrals(uint48) - getMatrixChildren: '0x04c8cc3d' // getMatrixChildren(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) @@ -52,6 +53,8 @@ function loadState() { 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; } function saveState() { try { @@ -126,12 +129,34 @@ async function fetchIncome(id) { 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++) { @@ -142,7 +167,7 @@ async function snapshot() { 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 })); + 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)); @@ -297,7 +322,7 @@ function getPayoutsPublic() { 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, + 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 @@ -340,8 +365,9 @@ async function memberLookup(id) { 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) })).reverse(); + 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; } diff --git a/public/admin.js b/public/admin.js index fbffba2..a532503 100644 --- a/public/admin.js +++ b/public/admin.js @@ -67,7 +67,7 @@ document.getElementById('lookupForm').addEventListener('submit',async e=>{ `
Total earned${fmt(d.totalEarnedPol)} POL
`+ `
Total paid in${fmt(d.totalPaidPol)} POL
`; const chain=d.uplineChain&&d.uplineChain.length?`

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?`
${d.income.map(p=>``).join('')}
When (UTC)From memberAt levelTierAmount
${date(p.ts)}#${p.fromId}${esc(p.levelName)}${p.fromTier===2?'Premium':'Standard'}${fmt(p.pol)} POL
`:'
No payments received yet.
'; + const income=d.income&&d.income.length?`
${d.income.map(p=>``).join('')}
When (UTC)From memberForTierAmount
${date(p.ts)}#${p.fromId}${esc(p.desc||p.levelName)}${p.fromTier===2?'Premium':'Standard'}${fmt(p.pol)} POL
`:'
No payments received yet.
'; out.innerHTML=facts+chain+`

Payments received by #${d.id} — ${d.income?d.income.length:0} total, newest first:

`+income; }catch(x){out.innerHTML=`
${esc(x.message)}
`} }); diff --git a/public/payouts.js b/public/payouts.js index 3c8894f..14d78de 100644 --- a/public/payouts.js +++ b/public/payouts.js @@ -37,7 +37,7 @@ :`💸 Member #${p.toId} just got paid`; const sub=p.kind==='upline' ?`${fmtPol(p.pol)} POL paid up to Member #${p.toId}` - :(p.kind==='referral'?`+${fmtPol(p.pol)} POL · direct referral reward`:`+${fmtPol(p.pol)} POL · ${p.levelName||'level'} payout`); + :(p.kind==='referral'?`+${fmtPol(p.pol)} POL · direct referral reward`:`+${fmtPol(p.pol)} POL · ${p.desc||p.levelName||'payout'}`); el.innerHTML=`${head}${esc(sub)}${timeAgo(p.ts)} · Verify on Polygonscan ↗`; getStack().appendChild(el); requestAnimationFrame(()=>el.classList.add('show')); @@ -52,7 +52,7 @@ const passed=p.passed&&p.passed.length?`
passed over ${p.passed.map(i=>'#'+i).join(', ')} (not yet at this level)
`:''; return `
🚀
${up}${fmtPol(p.pol)} POL paid up to Member #${p.toId}
Upgrade pass-up · ${esc(p.levelName)} · ${when}
${passed}
${verify}
`; } - const meta=p.kind==='referral'?`Direct referral reward from #${p.fromId}'s entry`:`${esc(p.levelName)} payout from #${p.fromId}`; + const meta=p.kind==='referral'?`Direct referral reward from #${p.fromId}'s entry`:`${esc(p.desc||p.levelName)} — from #${p.fromId}`; return `
💸
Member #${p.toId} received ${fmtPol(p.pol)} POL
${meta} · ${when}
${verify}
`; } function render(d){