// 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=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[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.desc||p.levelName||'payout'}`);
el.innerHTML=`${head}${esc(sub)}${timeAgo(p.ts)} · Verify on Polygonscan ↗`;
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=`Verify ↗`;
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?`
passed over ${p.passed.map(i=>'#'+i).join(', ')} (not yet eligible — needs 2 directs and the 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.desc||p.levelName)} — from #${p.fromId}`;
return `💸
Member #${p.toId} received ${fmtPol(p.pol)} POL${meta} · ${when}
${verify}
`;
}
// Paginated feed: keep a deduped store of payouts (by key), most-recent first.
// The live poll refreshes the top (recent) page; "Show more" pulls older pages.
const PAGE=12;
const store=new Map(); // key -> payout
let total=0, visible=PAGE, fetching=false;
const allSorted=()=>[...store.values()].sort((a,b)=>(b.ts||0)-(a.ts||0));
function ingest(d){
if(d&&Array.isArray(d.payouts))for(const p of d.payouts)store.set(p.key,p);
if(d&&typeof d.total==='number')total=Math.max(total,d.total);
}
function updateTotals(d){
const totals=document.getElementById('payoutTotals');
if(totals&&d&&d.totals)totals.innerHTML=`Members on-chain${d.totals.members}
Payouts recorded${d.totals.payouts}
POL paid to members${Math.round(d.totals.pol).toLocaleString()}
`;
}
function ensureMoreBtn(){
let btn=document.getElementById('ppMore');
if(!btn){
const feed=document.getElementById('payoutFeed');
if(!feed)return null;
btn=document.createElement('button');
btn.id='ppMore';btn.type='button';btn.className='btn btn-secondary pp-more';
btn.addEventListener('click',onMoreClick);
feed.parentNode.insertBefore(btn,feed.nextSibling);
}
return btn;
}
function renderMoreBtn(loadedLen){
const btn=ensureMoreBtn();if(!btn)return;
const cap=Math.max(total,loadedLen);
if(cap<=PAGE){btn.style.display='none';return}
btn.style.display='';
const showingNow=Math.min(visible,cap);
if(showingNowReading the blockchain… check back in a minute.';return}
feed.innerHTML=all.slice(0,visible).map(rowHtml).join('');
renderMoreBtn(all.length);
}
async function fetchPage(offset,limit){
if(fetching)return null;
fetching=true;
try{const r=await fetch(`/api/public/payouts?offset=${offset}&limit=${limit}`);const d=await r.json();ingest(d);updateTotals(d);return d}
catch(e){return null}
finally{fetching=false}
}
async function onMoreClick(){
const btn=document.getElementById('ppMore');
if(btn&&btn.dataset.mode==='less'){
visible=PAGE;renderFeed();
const sec=document.getElementById('proof');if(sec)sec.scrollIntoView({behavior:'smooth',block:'start'});
return;
}
visible+=PAGE;
if(visible>store.size&&store.sizep.ts&&(now-p.ts){remember(p.key);enqueue(p)});
}
poll();
setInterval(poll,45000);
})();