Classify income rows as entry vs upgrade instead of raw atLevel

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 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-13 14:18:12 -05:00
parent e332a4f1c2
commit 110b9c9dfe
3 changed files with 33 additions and 7 deletions
+30 -4
View File
@@ -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;
}
+1 -1
View File
@@ -67,7 +67,7 @@ document.getElementById('lookupForm').addEventListener('submit',async e=>{
`<div class="fact"><small>Total earned</small><strong style="color:var(--ok)">${fmt(d.totalEarnedPol)} POL</strong></div>`+
`<div class="fact"><small>Total paid in</small><strong>${fmt(d.totalPaidPol)} POL</strong></div></div>`;
const chain=d.uplineChain&&d.uplineChain.length?`<p style="font-size:13px;color:var(--muted);margin:12px 0 0;line-height:1.7"><strong style="color:var(--text)">Lineage up to root:</strong> #${d.id} → ${d.uplineChain.map(u=>`#${u.id} <span style="color:#8498aa">(${esc(u.levelName)}, ${u.directCount} directs)</span>`).join(' → ')}</p>`:'';
const income=d.income&&d.income.length?`<div class="table-wrap" style="margin-top:8px"><table class="table" style="min-width:560px"><thead><tr><th>When (UTC)</th><th>From member</th><th>At level</th><th>Tier</th><th>Amount</th></tr></thead><tbody>${d.income.map(p=>`<tr><td>${date(p.ts)}</td><td>#${p.fromId}</td><td>${esc(p.levelName)}</td><td>${p.fromTier===2?'Premium':'Standard'}</td><td><strong>${fmt(p.pol)} POL</strong></td></tr>`).join('')}</tbody></table></div>`:'<div class="empty" style="margin-top:8px">No payments received yet.</div>';
const income=d.income&&d.income.length?`<div class="table-wrap" style="margin-top:8px"><table class="table" style="min-width:560px"><thead><tr><th>When (UTC)</th><th>From member</th><th>For</th><th>Tier</th><th>Amount</th></tr></thead><tbody>${d.income.map(p=>`<tr><td>${date(p.ts)}</td><td>#${p.fromId}</td><td>${esc(p.desc||p.levelName)}</td><td>${p.fromTier===2?'Premium':'Standard'}</td><td><strong>${fmt(p.pol)} POL</strong></td></tr>`).join('')}</tbody></table></div>`:'<div class="empty" style="margin-top:8px">No payments received yet.</div>';
out.innerHTML=facts+chain+`<p style="font-size:12px;color:#8498aa;margin:14px 0 0">Payments received by #${d.id} — ${d.income?d.income.length:0} total, newest first:</p>`+income;
}catch(x){out.innerHTML=`<div class="empty" style="color:var(--danger)">${esc(x.message)}</div>`}
});
+2 -2
View File
@@ -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=`<span class="pp-toast-head">${head}</span><span class="pp-toast-sub">${esc(sub)}</span><span class="pp-toast-verify">${timeAgo(p.ts)} · Verify on Polygonscan ↗</span>`;
getStack().appendChild(el);
requestAnimationFrame(()=>el.classList.add('show'));
@@ -52,7 +52,7 @@
const passed=p.passed&&p.passed.length?`<div class="pp-passed">passed over ${p.passed.map(i=>'#'+i).join(', ')} (not yet at this level)</div>`:'';
return `<div class="pp-row"><div class="pp-icon">🚀</div><div class="pp-body"><strong>${up}${fmtPol(p.pol)} POL paid up to Member #${p.toId}</strong><div class="pp-meta">Upgrade pass-up · ${esc(p.levelName)} · ${when}</div>${passed}</div>${verify}</div>`;
}
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 `<div class="pp-row"><div class="pp-icon">💸</div><div class="pp-body"><strong>Member #${p.toId} received ${fmtPol(p.pol)} POL</strong><div class="pp-meta">${meta} · ${when}</div></div>${verify}</div>`;
}
function render(d){