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:
@@ -3,6 +3,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { URL } = require('url');
|
||||
const chain = require('./chain');
|
||||
|
||||
const PORT = Number(process.env.PORT || 3000);
|
||||
const ROOT = __dirname;
|
||||
@@ -87,12 +88,25 @@ async function handleSubmitId(req, res) {
|
||||
const clickid = typeof b.clickid==='string' ? b.clickid.trim().slice(0,80).replace(/[^A-Za-z0-9._-]/g,'') : '';
|
||||
let subs = []; try { subs = readJson(SUBMISSIONS_FILE); } catch(e) {}
|
||||
if (subs.some(s=>s.newId===newId)) return json(res, 200, { ok: true, duplicate: true });
|
||||
subs.push({ newId, memberName, sponsorId, source: source||'(direct)', clickid, ts: new Date().toISOString() });
|
||||
// on-chain verification: does this ID actually exist on the contract?
|
||||
let onchain = null;
|
||||
try {
|
||||
onchain = await Promise.race([
|
||||
chain.verifyMember(Number(newId)),
|
||||
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 6000))
|
||||
]);
|
||||
} catch (e) { onchain = null; }
|
||||
subs.push({ newId, memberName, sponsorId, source: source||'(direct)', clickid, ts: new Date().toISOString(),
|
||||
onchain: onchain ? { registered: onchain.registered, tier: onchain.tierName, level: onchain.levelName, referrerId: onchain.referrerId, uplineId: onchain.uplineId } : undefined });
|
||||
writeJson(SUBMISSIONS_FILE, subs.slice(-1000));
|
||||
recordEvent('purchase', source);
|
||||
firePostback(clickid, `purchase-${clickid}`, source);
|
||||
sendTelegram(`🔔 RM Circle: NEW MEMBER CONFIRMED\nName: ${memberName}\nNew ID: ${newId}\nJoined under sponsor: ${sponsorId}\nSource: ${source||'(direct)'}\n→ Add ${memberName} (ID ${newId}) to the rotation queue.`);
|
||||
return json(res, 200, { ok: true });
|
||||
const chainLine = onchain === null ? '⏳ On-chain check unavailable — verify manually in admin.'
|
||||
: onchain.registered
|
||||
? `✅ VERIFIED ON-CHAIN: ${onchain.tierName} tier, level ${onchain.levelName}, referred by ID ${onchain.referrerId}${String(onchain.referrerId)!==sponsorId?` ⚠ (submitted sponsor was ${sponsorId})`:''}`
|
||||
: `❌ NOT FOUND ON-CHAIN — ID ${newId} has no registration on the contract yet.`;
|
||||
sendTelegram(`🔔 RM Circle: NEW MEMBER CONFIRMED\nName: ${memberName}\nNew ID: ${newId}\nJoined under sponsor: ${sponsorId}\nSource: ${source||'(direct)'}\n${chainLine}\n→ Add ${memberName} (ID ${newId}) to the rotation queue.`);
|
||||
return json(res, 200, { ok: true, onchain: onchain ? { registered: onchain.registered, tier: onchain.tierName, level: onchain.levelName, referrerId: onchain.referrerId } : null });
|
||||
}
|
||||
async function handleChat(req, res) {
|
||||
const ip = String(req.headers['x-forwarded-for']||req.socket.remoteAddress||'').split(',')[0].trim();
|
||||
@@ -205,6 +219,9 @@ async function handleApi(req,res,pathname){
|
||||
if(req.method==='GET'&&pathname==='/api/public/config'){
|
||||
const c=getConfig();return json(res,200,{siteName:c.siteName,programName:c.programName,bridgeHeadline:c.bridgeHeadline,bridgeSubheadline:c.bridgeSubheadline,premiumEntryPol:c.premiumEntryPol,telegramUrl:c.telegramUrl,supportLabel:c.supportLabel,showQueueProgress:c.showQueueProgress});
|
||||
}
|
||||
if(req.method==='GET'&&pathname==='/api/public/payouts'){
|
||||
return json(res,200,chain.getPayoutsPublic(),{'Cache-Control':'public, max-age=20'});
|
||||
}
|
||||
if(req.method==='GET'&&pathname==='/api/public/current-sponsor'){
|
||||
const sponsors=getSponsors(),c=getConfig(),a=activeSponsor(sponsors);if(!a)return json(res,404,{error:'No active sponsor is currently assigned.'});
|
||||
return json(res,200,{sponsor:publicSponsorPayload(a,c),waitingCount:sponsors.filter(s=>s.status==='waiting').length,message:'Always use the current sponsor shown on this page. Team placement rotates as members qualify.'});
|
||||
@@ -228,6 +245,17 @@ async function handleApi(req,res,pathname){
|
||||
const s=getSession(req);if(s)sessions.delete(s.token);return json(res,200,{ok:true},{'Set-Cookie':'ctb.sid=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0'});
|
||||
}
|
||||
if(pathname.startsWith('/api/admin/')&&!requireAdmin(req,res))return;
|
||||
if(req.method==='GET'&&pathname==='/api/admin/matrix-tree'){
|
||||
return json(res,200,chain.getMatrixTree());
|
||||
}
|
||||
if(req.method==='GET'&&pathname==='/api/admin/member-lookup'){
|
||||
const id=Number(new URL(req.url,'http://x').searchParams.get('id')||0);
|
||||
if(!Number.isInteger(id)||id<1||id>281474976710655)return json(res,400,{error:'Enter a numeric member ID.'});
|
||||
try{
|
||||
const r=await Promise.race([chain.memberLookup(id),new Promise((_,rej)=>setTimeout(()=>rej(new Error('Chain RPC timeout — try again.')),25000))]);
|
||||
return json(res,200,r);
|
||||
}catch(e){return json(res,502,{error:e.message||'Lookup failed'})}
|
||||
}
|
||||
if(req.method==='GET'&&pathname==='/api/admin/state'){let subs=[];try{subs=readJson(SUBMISSIONS_FILE).slice(-50).reverse()}catch(e){}return json(res,200,{sponsors:getSponsors(),config:getConfig(),analytics:getAnalytics(),submissions:subs,aiChat:{configured:!!getOpenRouterKey(),model:OPENROUTER_MODEL}});}
|
||||
if(req.method==='POST'&&pathname==='/api/admin/openrouter-key'){
|
||||
const b=await bodyJson(req);const key=typeof b.key==='string'?b.key.trim():null;
|
||||
@@ -270,3 +298,4 @@ const server=http.createServer(async(req,res)=>{
|
||||
}catch(e){console.error(e);json(res,500,{error:'Internal server error'});}
|
||||
});
|
||||
server.listen(PORT,()=>{console.log(`Crypto Team Build sponsor router running on http://localhost:${PORT}`);if(ADMIN_PASSWORD==='changeme')console.warn('WARNING: Set ADMIN_PASSWORD before production deployment.');});
|
||||
chain.startIndexer();
|
||||
|
||||
Reference in New Issue
Block a user