Files
rm-circle-team-router/public/direct-join.js
T
martbost e14c541e5e Add dormant public direct-join fallback (/join-now) + post-join redirect
/join-now: public self-enroll page gated behind config.dappFallbackPublic
(default off -> redirects to /start, invisible until needed). Auto-assigns the
current rotation sponsor via /api/public/current-sponsor, registers the user's
own wallet, then redirects to /my/<newId> so new members land on their live
position instead of guessing their ID. HTML lives in private/, served only via
the flagged route. Reuses the proven contract engine. Admin /direct-join now
also shows a 'View position ->' link to /my/<newId> after a successful join.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-18 07:09:57 -05:00

169 lines
11 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 — direct on-chain enrollment (contingency tool).
Talks ONLY to the injected wallet provider (window.ethereum), so it needs no
external libraries and no network of its own — CSP-safe (connect-src 'self').
It builds transactions to the RM Circle contract that the USER signs and pays;
this page has zero custody and zero authority. If msg.value is ever wrong the
contract simply reverts and the user keeps their POL (minus trivial gas). */
(function(){
'use strict';
var CONTRACT='0x33bdaeefd6d17d80ae53816c916dfb26c4fb2daf';
var POLYGON_HEX='0x89'; // 137
// function selectors (decoded from live transactions)
var SEL_REGISTER='0x30de37e4'; // register(uint48 sponsorId, uint8 tier) payable
var SEL_UPGRADE ='0xd55ec697'; // upgrade() payable
var SEL_GETMEMBER='0x2ada2596';// getMember(address) view
var SEL_GETCOSTS='0x735f87b9'; // getAllCosts() view -> 4x uint256[8] (reg std, reg prem, up std, up prem)
var T_REGISTERED='0xe4a74887d749eb048f14bfef37b204477f3a5ff67055908b7c8cc62c202aef17'; // MemberRegistered(uint48 id,...)
var T_UPGRADED='0xc0b79a9e133d4dcbb1a606a57591d98dce93c7d5c86197a5caae22c4a1480049'; // MemberUpgraded(uint48 id,...)
var LEVELS=['','Scintilla','Ascensus','Fabrica','Culmen','Apex','Fastigium','Vertex','Corona'];
var $=function(id){return document.getElementById(id);};
var eth=window.ethereum;
var account=null, costs=null;
function log(msg,kind){var b=$('log');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');} // uint -> 32-byte hex word
function addrWord(a){return a.toLowerCase().replace(/^0x/,'').padStart(64,'0');}
function polStr(wei){return (Number(wei)/1e18).toFixed(4);}
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']); }
// ---- reads (via the wallet's own RPC) ----
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)));
// 4 fixed arrays of 8: [0-7] reg std, [8-15] reg prem, [16-23] up std, [24-31] up prem
costs={ regStd:w.slice(0,8), regPrem:w.slice(8,16), upStd:w.slice(16,24), upPrem:w.slice(24,32) };
return costs;
}
// registration price = base * 105/100 (5% on top — verified against live joins)
function regValue(tier){ var base=(tier===2?costs.regPrem:costs.regStd)[0]; return base*105n/100n; }
// upgrade price = the exact next-level cost (no add-on — verified against live upgrades)
function upValue(tier,level){ var arr=(tier===2?costs.upPrem:costs.upStd); return arr[level-1]; }
async function readPosition(addr){
try{
var r=await ethCall(SEL_GETMEMBER+addrWord(addr)); if(!r||r==='0x') return null;
var d=r.slice(2), tier=parseInt(d.slice(4*64,5*64),16), level=parseInt(d.slice(5*64,6*64),16);
var directs=parseInt(d.slice(6*64,7*64),16);
if(!level) return null; // unregistered
return {tier:tier,level:level,directs:directs};
}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){ // chain not added
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 refreshBalance(){
try{ var b=await req('eth_getBalance',[account,'latest']); $('bal').textContent=polStr(BigInt(b))+' POL'; }catch(e){}
}
// poll for the mined receipt (via the wallet's own RPC)
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;
}
// pull the member id from a contract event in the receipt (topics[1] = uint48 id)
function receiptEventId(rc,topic0){
var id=null; (rc.logs||[]).forEach(function(l){ if(l.address&&l.address.toLowerCase()===CONTRACT&&l.topics&&l.topics[0]===topic0) id=parseInt(l.topics[1],16); }); return id;
}
function showResult(big,sub,linkId){ var el=$('result'); if(!el) return; $('resultId').textContent=big; $('resultSub').textContent=sub; var lk=$('resultLink'); if(lk){ if(linkId){ lk.href='/my/'+linkId; lk.style.display='inline-flex'; } else lk.style.display='none'; } el.style.display='block'; try{el.scrollIntoView({behavior:'smooth',block:'center'});}catch(e){} }
async function connect(){
if(!eth){ log('No wallet detected. Open this page in a browser with MetaMask (extension or the MetaMask in-app browser).','err'); return; }
try{
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();
// premium join cost preview
$('joinCost').textContent=polStr(regValue(2))+' POL';
var pos=await readPosition(account);
if(pos){
$('posBox').style.display='block';
$('posInfo').innerHTML='This wallet already holds a position: <strong>'+LEVELS[pos.level]+'</strong> (level '+pos.level+'/8, '+(pos.tier===2?'Premium':'Standard')+' tier, '+pos.directs+'/2 directs).';
if(pos.level<8){ $('upBox').style.display='block'; $('upNext').textContent=LEVELS[pos.level+1]; $('upCost').textContent=polStr(upValue(pos.tier,pos.level))+' POL'; $('upgradeBtn').dataset.tier=pos.tier; $('upgradeBtn').dataset.level=pos.level; }
else $('upInfo').innerHTML='Already at the top level (Corona).';
log('Connected. This wallet is registered — Upgrade is available; Join a new position would need a fresh wallet.','ok');
} else {
log('Connected. This wallet has no position yet — you can Register a new position below.','ok');
}
}catch(e){ log('Connect failed: '+(e.message||e),'err'); }
}
async function doRegister(){
try{
var sponsor=($('sponsorId').value||'').trim();
if(!/^\d{1,15}$/.test(sponsor)){ log('Enter a valid numeric sponsor ID first.','err'); return; }
var tier=$('tier').value==='1'?1:2;
await ensurePolygon();
var val=regValue(tier);
var override=($('customPol').value||'').trim();
if(override){ if(!/^\d+(\.\d+)?$/.test(override)){log('Custom amount must be a number.','err');return;} val=BigInt(Math.round(parseFloat(override)*1e6))*(10n**12n); }
var data=SEL_REGISTER+word(sponsor)+word(tier);
log('Sending REGISTER — sponsor #'+sponsor+', '+(tier===2?'Premium':'Standard')+', paying '+polStr(val)+' POL. Confirm in your wallet…');
var tx=await req('eth_sendTransaction',[{from:account,to:CONTRACT,value:hexWei(val),data:data}]);
log('Submitted: <a href="https://polygonscan.com/tx/'+tx+'" target="_blank" rel="noopener">'+tx.slice(0,18)+'…</a> — waiting for confirmation…','ok');
var rc=await waitReceipt(tx);
if(!rc){ log('Still pending after a few minutes — your new ID will appear on Polygonscan once it mines.',''); return; }
if(rc.status==='0x0'){ log('⚠️ Reverted on-chain — no position created; your POL was returned (minus gas). Re-check the sponsor ID and amount.','err'); return; }
refreshBalance();
var newId=receiptEventId(rc,T_REGISTERED);
if(newId){ showResult('#'+newId,'Your new position under sponsor #'+sponsor+' is live. Save this ID.',newId); log('🎉 Confirmed — your new position is <strong>#'+newId+'</strong>.','ok'); }
else log('Confirmed on-chain, but could not read the ID from the receipt — check the tx on Polygonscan.','ok');
}catch(e){ log('Register failed / rejected: '+(e.message||e),'err'); }
}
async function doUpgrade(){
try{
await ensurePolygon();
var tier=parseInt($('upgradeBtn').dataset.tier,10), level=parseInt($('upgradeBtn').dataset.level,10);
var val=upValue(tier,level);
var override=($('customPol').value||'').trim();
if(override){ if(!/^\d+(\.\d+)?$/.test(override)){log('Custom amount must be a number.','err');return;} val=BigInt(Math.round(parseFloat(override)*1e6))*(10n**12n); }
var data=SEL_UPGRADE;
log('Sending UPGRADE '+LEVELS[level]+' → '+LEVELS[level+1]+', paying '+polStr(val)+' POL. Confirm in your wallet…');
var tx=await req('eth_sendTransaction',[{from:account,to:CONTRACT,value:hexWei(val),data:data}]);
log('Submitted: <a href="https://polygonscan.com/tx/'+tx+'" target="_blank" rel="noopener">'+tx.slice(0,18)+'…</a> — waiting for confirmation…','ok');
var rc=await waitReceipt(tx);
if(!rc){ log('Still pending — check the tx on Polygonscan.',''); return; }
if(rc.status==='0x0'){ log('⚠️ Upgrade reverted — your POL was returned (minus gas).','err'); return; }
refreshBalance();
showResult(LEVELS[level+1],'Upgraded to level '+(level+1)+' of 8.');
log('🎉 Confirmed — upgraded to <strong>'+LEVELS[level+1]+'</strong>.','ok');
}catch(e){ log('Upgrade failed / rejected: '+(e.message||e),'err'); }
}
document.addEventListener('DOMContentLoaded',function(){
$('connectBtn').addEventListener('click',connect);
$('registerBtn').addEventListener('click',doRegister);
var ub=$('upgradeBtn'); if(ub) ub.addEventListener('click',doUpgrade);
// Update in place on wallet events — do NOT location.reload() here: some wallets
// (Trust Wallet in Brave) fire chainChanged on load, and reloading re-arms the
// handler → infinite refresh loop that blocks the Connect button.
if(eth && eth.on){
eth.on('accountsChanged',function(accs){
account=(accs&&accs[0])||null;
if(!account){ $('connected').style.display='none'; $('connectBtn').style.display=''; log('Wallet disconnected.','err'); }
else { $('wallet').textContent=account.slice(0,6)+'…'+account.slice(-4); refreshBalance(); }
});
eth.on('chainChanged',function(){ if(account) refreshBalance(); });
}
if(!eth) log('No wallet detected in this browser. Use MetaMask or Trust Wallet (extension, or the wallet app’s in-app browser).','err');
});
})();