200 lines
12 KiB
JavaScript
200 lines
12 KiB
JavaScript
/* 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']); }
|
||
|
||
async function loadCosts(){
|
||
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) }; 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 ref=new URLSearchParams(location.search).get('ref');
|
||
if(ref&&/^\d{1,15}$/.test(ref)){
|
||
try{ var rm=await (await fetch('/api/public/member?id='+ref)).json(); 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;
|
||
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 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){ log('Connect failed: '+(e.message||e),'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){
|
||
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){}
|
||
setTimeout(function(){ location.href='/my/'+newId; }, 4500);
|
||
} 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():'');
|
||
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);
|
||
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();
|
||
$('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
|
||
});
|
||
})();
|