Add on-chain payment proof: live payout feed, ID verification, admin lineage tools

New chain.js reads the RM Circle contract (0x33Bd…2DAF, Polygon) via free
public RPCs — no API keys. Daily snapshot rebuilds complete payout history
from getIncomeHistory (log providers prune old history), and a 60s
eth_getLogs tail catches new payouts with tx hashes, upgrade context, and
passed-over upline IDs.

- Bridge page: "Live payment proof" feed + timed toast pop-ups for payouts
  seen in the last 15 min, every row linking to Polygonscan.
- /start: same toasts; ID submissions are now verified against the contract
  (result shown to the member, in Telegram notify, and in admin).
- Admin: on-chain member lookup (lineage to root, directs, matrix children,
  full income history) and a collapsible full matrix tree view.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-13 10:25:07 -05:00
parent 89a84aa7ca
commit 7c41499312
9 changed files with 546 additions and 8 deletions
+78
View File
@@ -0,0 +1,78 @@
// Live on-chain payout proof: feed section (if #payoutFeed exists) + timed
// toast pop-ups when payments are seen on the RM Circle contract.
(function(){
const SCAN='https://polygonscan.com/tx/';
const TOAST_FRESH_S=15*60; // toast anything seen on-chain in the last 15 min
const TOAST_SHOW_MS=8000, TOAST_GAP_MS=2500, TOAST_MAX_QUEUE=5;
let seen;
try{seen=new Set(JSON.parse(sessionStorage.getItem('ctb.seenPayouts')||'[]'))}catch(e){seen=new Set()}
function remember(k){seen.add(k);try{sessionStorage.setItem('ctb.seenPayouts',JSON.stringify([...seen].slice(-300)))}catch(e){}}
function esc(s){return String(s??'').replace(/[&<>'"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c]))}
function fmtPol(n){return n>=100?n.toFixed(1):n.toFixed(2)}
function timeAgo(ts){
if(!ts)return '';
const s=Math.max(0,Math.floor(Date.now()/1000-ts));
if(s<60)return 'just now';
if(s<3600)return Math.floor(s/60)+' min ago';
if(s<86400)return Math.floor(s/3600)+'h ago';
return Math.floor(s/86400)+'d ago';
}
function verifyHref(p){return p.tx?SCAN+p.tx:'https://polygonscan.com/address/'+(p.toAccount||'0x33BdAEEfd6d17D80aE53816c916dFb26c4fB2DAF')}
let stack=null;
function getStack(){
if(!stack){stack=document.createElement('div');stack.className='pp-stack';document.body.appendChild(stack)}
return stack;
}
const queue=[];let showing=false;
function enqueue(p){if(queue.length>=TOAST_MAX_QUEUE)return;queue.push(p);if(!showing)showNext()}
function showNext(){
const p=queue.shift();
if(!p){showing=false;return}
showing=true;
const el=document.createElement('a');
el.className='pp-toast';el.href=verifyHref(p);el.target='_blank';el.rel='noopener noreferrer';
const head=p.kind==='upline'&&p.upgrade
?`🚀 Member #${p.upgrade.id} upgraded to ${esc(p.upgrade.levelName)}`
:`💸 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`);
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'));
setTimeout(()=>{el.classList.remove('show');setTimeout(()=>{el.remove();setTimeout(showNext,TOAST_GAP_MS)},450)},TOAST_SHOW_MS);
}
function rowHtml(p){
const when=timeAgo(p.ts);
const verify=`<a class="pp-verify" href="${esc(verifyHref(p))}" target="_blank" rel="noopener noreferrer">Verify ↗</a>`;
if(p.kind==='upline'){
const up=p.upgrade?`Member #${p.upgrade.id} upgraded to ${esc(p.upgrade.levelName)} — `:'';
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}`;
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){
const feed=document.getElementById('payoutFeed');
if(!feed)return;
const totals=document.getElementById('payoutTotals');
if(totals&&d.totals)totals.innerHTML=`<div class="fact"><small>Members on-chain</small><strong>${d.totals.members}</strong></div><div class="fact"><small>Payouts recorded</small><strong>${d.totals.payouts}</strong></div><div class="fact"><small>POL paid to members</small><strong>${Math.round(d.totals.pol).toLocaleString()}</strong></div>`;
if(!d.payouts||!d.payouts.length){feed.innerHTML='<div class="empty">Reading the blockchain… check back in a minute.</div>';return}
feed.innerHTML=d.payouts.slice(0,12).map(rowHtml).join('');
}
async function poll(){
let d;
try{const r=await fetch('/api/public/payouts');d=await r.json()}catch(e){return}
if(!d||!d.payouts)return;
render(d);
const now=Date.now()/1000;
const fresh=d.payouts.filter(p=>p.ts&&(now-p.ts)<TOAST_FRESH_S&&!seen.has(p.key));
fresh.slice(0,TOAST_MAX_QUEUE).reverse().forEach(p=>{remember(p.key);enqueue(p)});
}
poll();
setInterval(poll,45000);
})();