8987451cf6
Auditing the rest of the prospect paths after Terry's leak. Every entry point funnels through /join/<id> — the Telegram Mini App redirects there, member page-builder pages link there, the public /my/<id> share panel links there — so the cookie covers all of them. One gap left: ?direct=1 is the inviter's explicit decision to take the join under themselves rather than route it down their leg, and only the id was being remembered. A prospect who opened a direct link and then wandered through the nav came back through the moving link instead — the opposite of what the shared link asked for. The flag now rides along in its own cookie, and both /start and /join-now prefer an explicit choice over the position's default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
271 lines
18 KiB
JavaScript
271 lines
18 KiB
JavaScript
fetch('/api/public/config').then(function(r){return r.json()}).then(function(c){if(c&&c.polUsd>0)window.__polUsd=c.polUsd;}).catch(function(){});
|
||
/* RM Circle — public direct-join engine (dormant fallback; served only when
|
||
config.dappFallbackPublic is on). Talks only to window.ethereum (CSP-safe).
|
||
Auto-assigns the current rotation sponsor, registers the member's own wallet,
|
||
then sends them to their live position page /my/<newId>. Zero custody. */
|
||
(function(){
|
||
'use strict';
|
||
var CONTRACT='0x33bdaeefd6d17d80ae53816c916dfb26c4fb2daf';
|
||
var POLYGON_HEX='0x89';
|
||
var SEL_REGISTER='0x30de37e4'; // register(uint48 sponsorId, uint8 tier) payable
|
||
var SEL_GETMEMBER='0x2ada2596'; // getMember(address)
|
||
var SEL_GETCOSTS='0x735f87b9'; // getAllCosts()
|
||
var T_REGISTERED='0xe4a74887d749eb048f14bfef37b204477f3a5ff67055908b7c8cc62c202aef17';
|
||
|
||
var $=function(id){return document.getElementById(id);};
|
||
var eth=null; // resolved via the wallet picker on connect
|
||
var account=null, costs=null, sponsorId=null;
|
||
|
||
function log(msg,kind){var b=$('log');if(!b)return;var d=document.createElement('div');d.className='dj-log '+(kind||'');d.innerHTML=msg;b.appendChild(d);b.scrollTop=b.scrollHeight;}
|
||
function word(n){return BigInt(n).toString(16).padStart(64,'0');}
|
||
function addrWord(a){return a.toLowerCase().replace(/^0x/,'').padStart(64,'0');}
|
||
function polStr(wei){return (Number(wei)/1e18).toFixed(2);}
|
||
function hexWei(wei){return '0x'+BigInt(wei).toString(16);}
|
||
async function req(method,params){ if(!eth) throw new Error('No wallet found'); return await eth.request({method:method,params:params||[]}); }
|
||
async function ethCall(data){ return await req('eth_call',[{to:CONTRACT,data:data},'latest']); }
|
||
|
||
// Immutable contract constant (getAllCosts premium[0], verified on-chain).
|
||
// Fallback when the WALLET's internal RPC hiccups (MetaMask iOS surfaces
|
||
// that as "Load failed") — prices can never change, so baking it is safe.
|
||
var REG_PREM_WEI=343406593406593400832n;
|
||
async function loadCosts(){
|
||
try{
|
||
var r=await ethCall(SEL_GETCOSTS); var d=r.slice(2), w=[];
|
||
for(var i=0;i<d.length;i+=64) w.push(BigInt('0x'+d.slice(i,i+64)));
|
||
costs={ regPrem:w.slice(8,16) };
|
||
}catch(e){ costs={ regPrem:[REG_PREM_WEI] }; }
|
||
return costs;
|
||
}
|
||
function regValue(){ return costs.regPrem[0]*105n/100n; } // premium base * 1.05 (verified)
|
||
|
||
async function readPosition(addr){
|
||
try{ var r=await ethCall(SEL_GETMEMBER+addrWord(addr)); if(!r||r==='0x') return null;
|
||
var d=r.slice(2), level=parseInt(d.slice(5*64,6*64),16); return level?{level:level}:null; }catch(e){ return null; }
|
||
}
|
||
async function ensurePolygon(){
|
||
var cid=await req('eth_chainId'); if(cid===POLYGON_HEX) return true;
|
||
try{ await req('wallet_switchEthereumChain',[{chainId:POLYGON_HEX}]); return true; }
|
||
catch(e){ if(e&&e.code===4902){ await req('wallet_addEthereumChain',[{chainId:POLYGON_HEX,chainName:'Polygon Mainnet',nativeCurrency:{name:'POL',symbol:'POL',decimals:18},rpcUrls:['https://polygon-rpc.com'],blockExplorerUrls:['https://polygonscan.com']}]); return true; } throw e; }
|
||
}
|
||
async function waitReceipt(tx){ for(var i=0;i<60;i++){ try{ var r=await req('eth_getTransactionReceipt',[tx]); if(r) return r; }catch(e){} await new Promise(function(s){setTimeout(s,3000);}); } return null; }
|
||
function receiptEventId(rc){ var id=null; (rc.logs||[]).forEach(function(l){ if(l.address&&l.address.toLowerCase()===CONTRACT&&l.topics&&l.topics[0]===T_REGISTERED) id=parseInt(l.topics[1],16); }); return id; }
|
||
|
||
async function loadSponsor(){
|
||
try{
|
||
// personal invite: ?ref=<id> -> place in that member's leg via the site's moving-link joinTarget
|
||
var qp=new URLSearchParams(location.search);
|
||
var ref=qp.get('ref');
|
||
// No ?ref on the URL? Fall back to the invite cookie set when this visitor landed on
|
||
// someone's /join/<id> page. Without this a prospect who wandered through the nav
|
||
// enrolled under the company rotation and the inviter lost the referral.
|
||
if(!ref){ try{ var ck=document.cookie.match(/(?:^|;\s*)rmc_ref=(\d{1,15})/); if(ck)ref=ck[1]; }catch(e){} }
|
||
// ...and the placement choice the invite link carried, for the same reason.
|
||
var directFlag=qp.get('direct');
|
||
if(directFlag!=='1'&&directFlag!=='0'){ try{ var cd=document.cookie.match(/(?:^|;\s*)rmc_direct=([01])/); if(cd)directFlag=cd[1]; }catch(e){} }
|
||
// Direct link (?direct=1): opt out of rotation — register straight under
|
||
// the inviter (spillover in their team), never the next-to-qualify member.
|
||
if(ref&&/^\d{1,15}$/.test(ref)&&directFlag==='1'){
|
||
sponsorId=String(ref); $('sponsorLine').innerHTML='You’ll join <strong>directly under #'+ref+'</strong> — a spillover placement in their team.'; return;
|
||
}
|
||
if(ref&&/^\d{1,15}$/.test(ref)){
|
||
try{ var rm=await (await fetch('/api/public/member?id='+ref)).json();
|
||
// position configured to default to direct, not explicitly overridden
|
||
if(rm&&rm.directDefault&&qp.get('direct')!=='0'){
|
||
sponsorId=String(ref); $('sponsorLine').innerHTML='You’ll join <strong>directly under #'+ref+'</strong> — a spillover placement in their team.'; return;
|
||
}
|
||
var t=rm&&rm.joinTarget;
|
||
if(t&&t.id){ sponsorId=String(t.id); $('sponsorLine').innerHTML='You’ll join under <strong>#'+t.id+'</strong>'+(String(t.id)!==String(ref)?' — the next open spot in #'+ref+'’s team':'')+'.'; return; }
|
||
}catch(e){}
|
||
}
|
||
var j=await (await fetch('/api/public/current-sponsor')).json(); var sp=j&&j.sponsor;
|
||
if(sp&&sp.id){ sponsorId=String(sp.id); $('sponsorLine').innerHTML='You’ll join the team under <strong>#'+sp.id+(sp.name?' ('+sp.name+')':'')+'</strong> — the current team placement.'; }
|
||
else $('sponsorLine').textContent='Loading the current team placement…';
|
||
}catch(e){ $('sponsorLine').textContent='Could not load the current sponsor — please refresh.'; }
|
||
}
|
||
|
||
var lastBal=null;
|
||
// One free signature straight after the join, while the wallet is still connected:
|
||
// it opens their inbox session so the member-profile gate can ask for a username
|
||
// and email on the dashboard. Best-effort — if they decline, the join still stands
|
||
// and they are asked again the first time they sign in.
|
||
async function signInNewMember(newId,reported){
|
||
var go=function(){ try{ location.href='/my/'+newId; }catch(e){} };
|
||
var bailout=setTimeout(go,120000); // wallet never answered: leave anyway
|
||
try{
|
||
// submit-id runs a LIVE verifyMember server-side, which seeds the index with
|
||
// this brand-new position. Let it finish before asking the server who we are.
|
||
try{ await reported; }catch(e){}
|
||
$('resultSub').innerHTML='Welcome to the team! One free signature sets up your member profile…';
|
||
var ch=await (await fetch('/api/public/msg-challenge',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({address:account})})).json();
|
||
if(!ch||!ch.message){ clearTimeout(bailout); setTimeout(go,2500); return; }
|
||
var hex='0x'; for(var i=0,b=new TextEncoder().encode(ch.message);i<b.length;i++) hex+=b[i].toString(16).padStart(2,'0');
|
||
var sig=await req('personal_sign',[hex,account]);
|
||
var v=await (await fetch('/api/public/msg-verify',{method:'POST',headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({address:account,signature:sig,id:String(newId)})})).json();
|
||
$('resultSub').innerHTML=(v&&v.ok)?'Signed in. Taking you to your position page…':'Welcome to the team! Taking you to your position page…';
|
||
if(!(v&&v.ok)) console.warn('post-join sign-in:', v&&v.error);
|
||
}catch(e){
|
||
try{ $('resultSub').innerHTML='Welcome to the team! Taking you to your position page…'; }catch(x){}
|
||
}
|
||
clearTimeout(bailout);
|
||
setTimeout(go,1800);
|
||
}
|
||
async function refreshBalance(){ try{ var b=await req('eth_getBalance',[account,'latest']); lastBal=BigInt(b); $('bal').textContent=polStr(lastBal)+' POL'; updateFundBox(); }catch(e){} }
|
||
|
||
// Card on-ramp: shown only when the connected wallet can't cover the entry.
|
||
function updateFundBox(){
|
||
var box=$('fundBox'); if(!box||!costs||lastBal===null) return;
|
||
var need=regValue();
|
||
if(lastBal>=need){ box.style.display='none'; return; }
|
||
var shortPol=Math.max(62,Math.ceil(Number(need-lastBal)/1e18)+5);
|
||
$('fundMsg').textContent='This wallet holds '+polStr(lastBal)+' POL; the Premium entry is '+polStr(need)+' POL plus a little gas (like a car, every blockchain transaction burns a bit of fuel - keep some in the tank). You can buy the remaining ~'+shortPol+' POL'+(window.__polUsd>0?' (about $'+Math.round(Number(shortPol.replace(/,/g,''))*window.__polUsd)+')':'')+' with a debit/credit card, Apple Pay, or Google Pay — delivered straight to this wallet.';
|
||
box.dataset.pol=shortPol;
|
||
box.style.display='block';
|
||
}
|
||
async function openMoonpay(){
|
||
try{
|
||
var box=$('fundBox');
|
||
var r=await (await fetch('/api/public/moonpay-url?address='+encodeURIComponent(account||'')+'&pol='+encodeURIComponent((box&&box.dataset.pol)||''))).json();
|
||
if(r&&r.url){ window.open(r.url,'_blank','noopener'); log(r.signed?'MoonPay opened in a new tab with your wallet address pre-filled — complete the purchase there, then come back and refresh your balance.':'MoonPay opened in a new tab. Choose POL on the Polygon network and paste YOUR wallet address ('+account+') as the destination, then come back and refresh your balance.','ok'); }
|
||
}catch(e){ log('Could not open MoonPay: '+(e.message||e),'err'); }
|
||
}
|
||
|
||
async function finishConnect(){
|
||
await loadCosts();
|
||
$('wallet').textContent=account.slice(0,6)+'…'+account.slice(-4);
|
||
$('connected').style.display='block'; $('connectBtn').style.display='none';
|
||
await refreshBalance();
|
||
$('joinCost').textContent=polStr(regValue())+' POL';
|
||
var pos=await readPosition(account);
|
||
if(pos){ $('already').style.display='block'; $('already').innerHTML='This wallet already holds position — <a href="/my">open your dashboard</a>. To create a <em>new</em> position, connect a different wallet.'; $('joinBtn').disabled=true; }
|
||
$('joinBox').style.display='block';
|
||
}
|
||
async function connect(){
|
||
try{
|
||
eth=await window.RMCWallet.pick();
|
||
if(!eth){ showWalletHelp(); log('No wallet detected in this browser. On a phone, use the "Open in your wallet" buttons above — on a computer, install/unlock MetaMask and refresh.','err'); return; }
|
||
attachWalletEvents();
|
||
log('Requesting wallet connection…');
|
||
var accs=await req('eth_requestAccounts'); account=accs[0];
|
||
await ensurePolygon();
|
||
await finishConnect();
|
||
log('Connected. Review the amount, then Join.','ok');
|
||
}catch(e){
|
||
var m=String((e&&e.message)||e);
|
||
if(/load failed|failed to fetch|network|timeout/i.test(m)){
|
||
log('Your wallet’s network connection hiccuped (this happens inside wallet apps sometimes) — tap <strong>Connect wallet</strong> once more and it usually goes right through.','err');
|
||
}else{
|
||
log('Connect failed: '+m,'err');
|
||
}
|
||
}
|
||
}
|
||
// Inside a wallet's in-app browser the wallet is often ALREADY connected —
|
||
// recognize it silently on load (eth_accounts never prompts) instead of
|
||
// greeting a connected user like a stranger. Skipped when multiple wallets
|
||
// are injected (desktop) so no picker UI ever pops uninvited.
|
||
async function autoDetect(){
|
||
try{
|
||
var list=window.RMCWallet.list();
|
||
var p=list.length===1?list[0].provider:(list.length===0?window.ethereum:null);
|
||
if(!p) return;
|
||
eth=p; attachWalletEvents();
|
||
var accs=await req('eth_accounts',[]);
|
||
if(!accs||!accs.length){ log('✅ Wallet detected in this browser — tap "Connect wallet" to continue.','ok'); return; }
|
||
account=accs[0];
|
||
var cid=await req('eth_chainId');
|
||
if(cid===POLYGON_HEX){
|
||
await finishConnect();
|
||
log('✅ Already connected as '+account.slice(0,6)+'…'+account.slice(-4)+'. Review the amount, then Join.','ok');
|
||
}else{
|
||
$('wallet').textContent=account.slice(0,6)+'…'+account.slice(-4);
|
||
$('connected').style.display='block';
|
||
log('Wallet connected ('+account.slice(0,6)+'…'+account.slice(-4)+') but on another network — tap "Connect wallet" to switch to Polygon and continue.','ok');
|
||
}
|
||
}catch(e){}
|
||
}
|
||
|
||
async function doJoin(){
|
||
try{
|
||
if(!sponsorId){ log('Still loading the current sponsor — one moment.','err'); await loadSponsor(); if(!sponsorId) return; }
|
||
await ensurePolygon();
|
||
var val=regValue();
|
||
var data=SEL_REGISTER+word(sponsorId)+word(2);
|
||
log('Joining under #'+sponsorId+', Premium — paying '+polStr(val)+' POL. Confirm in your wallet…');
|
||
$('joinBtn').disabled=true;
|
||
var tx=await req('eth_sendTransaction',[{from:account,to:CONTRACT,value:hexWei(val),data:data}]);
|
||
log('Submitted — waiting for confirmation… <a href="https://polygonscan.com/tx/'+tx+'" target="_blank" rel="noopener">view tx</a>','ok');
|
||
var rc=await waitReceipt(tx);
|
||
if(!rc){ log('Still pending. Once it mines, open your dashboard at /my.',''); $('joinBtn').disabled=false; return; }
|
||
if(rc.status==='0x0'){ log('⚠️ The transaction reverted — no position created, and your POL was returned (minus gas). Please try again.','err'); $('joinBtn').disabled=false; return; }
|
||
var newId=receiptEventId(rc);
|
||
if(newId){
|
||
var reported=reportJoin(newId);
|
||
$('result').style.display='block';
|
||
$('resultId').textContent='#'+newId;
|
||
$('resultSub').innerHTML='Welcome to the team! Taking you to your position page…';
|
||
$('resultLink').href='/my/'+newId; $('resultLink').style.display='inline-flex';
|
||
log('🎉 Confirmed — your position is <strong>#'+newId+'</strong>. Redirecting to your dashboard…','ok');
|
||
try{ el_scroll('result'); }catch(e){}
|
||
try{sessionStorage.setItem('rmc.fresh','1');}catch(e){}
|
||
signInNewMember(newId,reported); // redirects when it settles
|
||
} else {
|
||
log('Confirmed on-chain. Open your dashboard at <a href="/my">/my</a> and enter your new ID (also shown on Polygonscan).','ok');
|
||
$('joinBtn').disabled=false;
|
||
}
|
||
}catch(e){ log('Join failed / rejected: '+(e.message||e),'err'); $('joinBtn').disabled=false; }
|
||
}
|
||
function el_scroll(id){ var e=$(id); if(e&&e.scrollIntoView) e.scrollIntoView({behavior:'smooth',block:'center'}); }
|
||
|
||
// Auto-confirm the join server-side: records the submission (source + clickid
|
||
// attribution), fires the purchase event + BeMob postback, enqueues rotation
|
||
// joins, and alerts the team — same pipeline as the manual submit-ID form.
|
||
function reportJoin(newId){
|
||
try{
|
||
var nameEl=$('memberName');
|
||
var name=(nameEl&&nameEl.value?nameEl.value.trim().slice(0,60):'')||('Self-enrolled #'+newId);
|
||
var src=(window.ctbGetSource?window.ctbGetSource():'(direct)');
|
||
var cid=(window.ctbGetClickId?window.ctbGetClickId():'');
|
||
return fetch('/api/public/submit-id',{method:'POST',headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({newId:String(newId),memberName:name,sponsorId:String(sponsorId||'?'),source:src,clickid:cid})
|
||
}).catch(function(){});
|
||
}catch(e){}
|
||
}
|
||
|
||
// Mobile browsers have no injected wallet — offer deep links that reopen
|
||
// THIS page (query string included, so ?ref= survives) inside the wallet
|
||
// app's own browser, where window.ethereum exists.
|
||
function showWalletHelp(){
|
||
var box=$('walletHelp'); if(!box) return;
|
||
var here=location.host+location.pathname+location.search;
|
||
var mm=$('mmDeep'); if(mm) mm.href='https://metamask.app.link/dapp/'+here;
|
||
var tw=$('twDeep'); if(tw) tw.href='https://link.trustwallet.com/open_url?coin_id=966&url='+encodeURIComponent(location.href);
|
||
// Fill the "type this in your wallet's browser" hint with the sponsor's
|
||
// real join path so SafePal/Coinbase/etc. users land on the right page.
|
||
var wb=$('wbId'); if(wb){ var ref=new URLSearchParams(location.search).get('ref'); wb.parentNode.textContent='rmcircle.team'+(ref&&/^\d{1,15}$/.test(ref)?'/join/'+ref:'/join-now'); }
|
||
box.style.display='block';
|
||
}
|
||
|
||
function attachWalletEvents(){
|
||
if(!eth||!eth.on||eth.__rmcBound) return; eth.__rmcBound=true;
|
||
eth.on('accountsChanged',function(accs){ account=(accs&&accs[0])||null; if(!account){ $('connected').style.display='none'; $('connectBtn').style.display=''; } else { $('wallet').textContent=account.slice(0,6)+'…'+account.slice(-4); refreshBalance(); } });
|
||
eth.on('chainChanged',function(){ if(account) refreshBalance(); });
|
||
}
|
||
document.addEventListener('DOMContentLoaded',function(){
|
||
loadSponsor();
|
||
// Telegram Mini App webview: no wallet can ever be injected here, so lead
|
||
// with the deep links immediately — the join finishes in the wallet app's
|
||
// own browser (same page, sponsor + attribution carried in the URL).
|
||
if(window.__rmcInTg){
|
||
showWalletHelp();
|
||
var hd=$('walletHelp')&&$('walletHelp').firstElementChild;
|
||
if(hd)hd.textContent='📱 One tap to finish in your wallet app.';
|
||
log('You’re in Telegram — joining happens in your wallet app’s secure browser. Tap your wallet above; this page reopens there with your sponsor already set.','ok');
|
||
}
|
||
$('connectBtn').addEventListener('click',connect);
|
||
$('joinBtn').addEventListener('click',doJoin);
|
||
var fb=$('fundBtn'); if(fb)fb.addEventListener('click',openMoonpay);
|
||
var fr=$('fundRefresh'); if(fr)fr.addEventListener('click',function(){ log('Checking your balance…'); refreshBalance(); });
|
||
setTimeout(autoDetect,350); // let EIP-6963 announcements land first
|
||
});
|
||
})();
|