Files
rm-circle-team-router/public/join-now.js
T
martbost d620555209 MoonPay card on-ramp for crypto-new members (zero custody)
- GET /api/public/moonpay-url: returns a MoonPay checkout link for POL on
  Polygon (pol_polygon). With partner keys in config (moonpayPublicKey/
  moonpaySecretKey, now PATCHable), the URL is HMAC-signed and prefilled
  with the member's own wallet address + shortfall amount; without keys it
  falls back to MoonPay's generic buy page. MoonPay is merchant of record -
  the site never touches funds.
- Join page: when the connected wallet can't cover the Premium entry, a
  funding box appears with the exact shortfall, a buy button (new tab), and
  a refresh-balance button.
- Training page: "buy POL with a card" callout after the funding video with
  the three things to get right (POL, Polygon network, own address).
- Start page: one-line card-buy pointer under the sponsor card.
- Chatbot canned answer + AI system prompt updated (funding guidance).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 09:44:01 -05:00

159 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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. 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 connect(){
try{
eth=await window.RMCWallet.pick();
if(!eth){ log('No wallet selected or detected. Install/unlock MetaMask, Trust, or SafePal and try again.','err'); return; }
attachWalletEvents();
log('Requesting wallet connection…');
var accs=await req('eth_requestAccounts'); account=accs[0];
await ensurePolygon(); 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';
log('Connected. Review the amount, then Join.','ok');
}catch(e){ log('Connect failed: '+(e.message||e),'err'); }
}
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){}
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){}
}
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(); });
});
})();