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:
+49
-1
@@ -20,7 +20,10 @@ function renderAnalytics(){
|
||||
(tot.purchase?`<div class="fact"><small>Confirmed joins</small><strong>${tot.purchase}</strong></div>`:'')+
|
||||
(tot.postback?`<div class="fact"><small>BeMob postbacks</small><strong>${tot.postback}</strong></div>`:'');
|
||||
const subs=state.submissions||[];
|
||||
document.getElementById('submissionRows').innerHTML=subs.map(s=>`<tr><td>${esc((s.ts||'').replace('T',' ').slice(0,16))}</td><td>${esc(s.memberName||'—')}</td><td><strong>${esc(s.newId)}</strong></td><td>ID ${esc(s.sponsorId)}</td><td>${esc(s.source)}</td></tr>`).join('')||'<tr><td colspan="5" class="empty">No submissions yet.</td></tr>';
|
||||
document.getElementById('submissionRows').innerHTML=subs.map(s=>{
|
||||
const oc=s.onchain?(s.onchain.registered?`<span style="color:var(--ok)">✓ ${esc(s.onchain.tier||'')} · ref #${esc(String(s.onchain.referrerId??'?'))}</span>`:'<span style="color:var(--danger)">✗ not found</span>'):'—';
|
||||
return `<tr><td>${esc((s.ts||'').replace('T',' ').slice(0,16))}</td><td>${esc(s.memberName||'—')}</td><td><strong>${esc(s.newId)}</strong></td><td>ID ${esc(s.sponsorId)}</td><td>${esc(s.source)}</td><td>${oc}</td></tr>`;
|
||||
}).join('')||'<tr><td colspan="6" class="empty">No submissions yet.</td></tr>';
|
||||
document.getElementById('trafficRows').innerHTML=list.map(r=>`<tr><td><strong>${esc(r.name)}</strong></td><td>${r.bridge}</td><td>${r.start}</td><td>${r.training}</td><td>${r.click}</td><td>${pct(r.click,r.start)}</td></tr>`).join('')||'<tr><td colspan="6" class="empty">No traffic recorded yet.</td></tr>';
|
||||
}
|
||||
function esc(s){return String(s??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c]))}
|
||||
@@ -38,4 +41,49 @@ rows.addEventListener('click',async e=>{const b=e.target.closest('button[data-ac
|
||||
document.getElementById('aiKeyForm').addEventListener('submit',async e=>{e.preventDefault();const inp=e.currentTarget.elements.key,key=inp.value.trim();if(!key){showToast('Paste a key first');return}try{const d=await api('/api/admin/openrouter-key',{method:'POST',body:JSON.stringify({key})});state.aiChat={...(state.aiChat||{}),configured:d.configured};inp.value='';render();showToast('AI chat enabled')}catch(x){showToast(x.message)}});
|
||||
document.getElementById('aiKeyClear').addEventListener('click',async()=>{if(!confirm('Turn off AI chat and go back to built-in answers?'))return;try{const d=await api('/api/admin/openrouter-key',{method:'POST',body:JSON.stringify({key:''})});state.aiChat={...(state.aiChat||{}),configured:d.configured};render();showToast('AI chat disabled')}catch(x){showToast(x.message)}});
|
||||
rows.addEventListener('change',async e=>{const sel=e.target.closest('select[data-action="level"]');if(!sel)return;try{const d=await api(`/api/admin/sponsors/${sel.dataset.id}`,{method:'PATCH',body:JSON.stringify({level:sel.value})});state.sponsors=d.sponsors;render();showToast('Level updated')}catch(x){showToast(x.message);render()}});
|
||||
document.getElementById('lookupForm').addEventListener('submit',async e=>{
|
||||
e.preventDefault();
|
||||
const id=document.getElementById('lookupId').value.trim(),out=document.getElementById('lookupResult');
|
||||
if(!/^\d+$/.test(id)){out.innerHTML='<div class="empty">Enter a numeric ID.</div>';return}
|
||||
out.innerHTML='<div class="empty">Reading the blockchain…</div>';
|
||||
try{
|
||||
const d=await api('/api/admin/member-lookup?id='+id);
|
||||
if(!d.registered){out.innerHTML=`<div class="empty" style="color:var(--danger)">ID ${esc(id)} is NOT registered on the contract.</div>`;return}
|
||||
const fmt=n=>Number(n).toLocaleString(undefined,{maximumFractionDigits:2});
|
||||
const date=ts=>ts?new Date(ts*1000).toISOString().replace('T',' ').slice(0,16):'—';
|
||||
const facts=`<div class="facts" style="grid-template-columns:repeat(3,1fr)">`+
|
||||
`<div class="fact"><small>Wallet</small><strong style="font-size:12px"><a style="color:var(--teal)" target="_blank" rel="noopener noreferrer" href="https://polygonscan.com/address/${esc(d.account)}">${esc(d.account.slice(0,10))}…${esc(d.account.slice(-6))} ↗</a></strong></div>`+
|
||||
`<div class="fact"><small>Joined (UTC)</small><strong>${date(d.joinedAt)}</strong></div>`+
|
||||
`<div class="fact"><small>Tier · Level</small><strong>${esc(d.tierName)} · ${esc(d.levelName)}</strong></div>`+
|
||||
`<div class="fact"><small>Referred by</small><strong>${d.referrerId?'#'+d.referrerId:'—'}</strong></div>`+
|
||||
`<div class="fact"><small>Matrix upline</small><strong>${d.uplineId?'#'+d.uplineId:'— (root)'}</strong></div>`+
|
||||
`<div class="fact"><small>Matrix children</small><strong>${d.matrix?`${d.matrix.left?'#'+d.matrix.left:'open'} · ${d.matrix.right?'#'+d.matrix.right:'open'}`:'—'}</strong></div>`+
|
||||
`<div class="fact"><small>Directs (${d.directCount})</small><strong style="font-size:13px">${d.directs&&d.directs.length?d.directs.map(x=>'#'+x).join(', '):'none yet'}</strong></div>`+
|
||||
`<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>';
|
||||
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>`}
|
||||
});
|
||||
document.getElementById('treeLoadBtn').addEventListener('click',async()=>{
|
||||
const out=document.getElementById('matrixTree');
|
||||
out.innerHTML='<div class="empty">Building tree from the on-chain snapshot…</div>';
|
||||
try{
|
||||
const d=await api('/api/admin/matrix-tree');
|
||||
if(!d.ready||!d.root){out.innerHTML='<div class="empty">Snapshot not ready yet — try again in a minute.</div>';return}
|
||||
const fmt=n=>Number(n).toLocaleString(undefined,{maximumFractionDigits:0});
|
||||
const node=(n,depth)=>{
|
||||
if(!n)return '';
|
||||
if(n.missing)return `<li class="mt-node"><span class="mt-id">#${n.id}</span> <span style="color:var(--danger)">(no data)</span></li>`;
|
||||
const badge=n.tier===2?'<span class="mt-badge mt-prem">P</span>':'<span class="mt-badge">S</span>';
|
||||
const kids=n.children&&n.children.length?`<ul class="mt-kids">${n.children.map(c=>node(c,depth+1)).join('')}</ul>`:'';
|
||||
const open=depth<4?' open':'';
|
||||
const label=`${badge} <span class="mt-id">#${n.id}</span> <span class="mt-meta">${esc(n.levelName)} · ${n.directCount}/2 directs · ${fmt(n.earnedPol)} POL earned${n.referrerId?` · ref #${n.referrerId}`:''}</span>`;
|
||||
return n.children&&n.children.length?`<li class="mt-node"><details${open}><summary>${label}</summary>${kids}</details></li>`:`<li class="mt-node mt-leaf">${label}</li>`;
|
||||
};
|
||||
const unplaced=d.unplaced?`<p class="micro" style="color:var(--danger)">Not reachable from root: ${d.unplaced.map(i=>'#'+i).join(', ')}</p>`:'';
|
||||
out.innerHTML=`<p class="micro" style="margin:0 0 8px">${d.memberCount} positions · snapshot ${new Date(d.snapshotAt).toISOString().replace('T',' ').slice(0,16)} UTC · P = Premium, S = Standard</p><ul class="mt-tree">${node(d.root,0)}</ul>${unplaced}`;
|
||||
}catch(x){out.innerHTML=`<div class="empty" style="color:var(--danger)">${esc(x.message)}</div>`}
|
||||
});
|
||||
loadState();
|
||||
|
||||
Reference in New Issue
Block a user