438b27ea3b
Contingency tool: if the official RM Circle dApp ever goes down, positions can still be created/upgraded straight from the member's own wallet. Vanilla JS talks only to window.ethereum (CSP-safe, no external libs), builds register() [0x30de37e4, sponsorId+tier, value=base*1.05] and upgrade() [0xd55ec697, value=next-level cost] calls decoded from live txs, reads prices via getAllCosts() and position via getMember(address). Page lives outside public/ and is served only through a getSession-gated route, so it's admin-only. The contract is public and immutable, so this cements a company-domain-independent path to enroll. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
136 lines
7.9 KiB
JavaScript
136 lines
7.9 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 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){}
|
|
}
|
|
|
|
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> — wait for it to confirm, then your position is live.','ok');
|
|
setTimeout(refreshBalance,4000);
|
|
}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>','ok');
|
|
setTimeout(refreshBalance,4000);
|
|
}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);
|
|
if(eth && eth.on){ eth.on('accountsChanged',function(){location.reload();}); eth.on('chainChanged',function(){location.reload();}); }
|
|
if(!eth) log('No wallet detected in this browser. Use MetaMask (desktop extension) or the MetaMask in-app browser on mobile.','err');
|
|
});
|
|
})();
|