Files
rm-circle-team-router/public/direct-join.js
T
martbost 5aceb84863 Add EIP-6963 multi-wallet picker to the join tools
Both /join-now and /direct-join now discover every injected wallet (Brave,
MetaMask, SafePal, Trust, Coinbase…) via EIP-6963 and, when more than one is
present, show a 'Choose your wallet' picker on connect — instead of blindly
grabbing window.ethereum (which silently hangs when e.g. Brave's built-in wallet
wins the slot). Shared public/rmc-wallet.js; provider resolved on connect and
wallet events bound once after. Falls back to window.ethereum for legacy
single-provider wallets. CSP-safe (6963 icons are data URIs).

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

171 lines
11 KiB
JavaScript

/* 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=null; // resolved via the wallet picker on connect
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(){
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();
// 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. Bound once, after the picker resolves the provider.
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=''; log('Wallet disconnected.','err'); }
else { $('wallet').textContent=account.slice(0,6)+'…'+account.slice(-4); refreshBalance(); }
});
eth.on('chainChanged',function(){ if(account) refreshBalance(); });
}
})();