On-dashboard upgrades: qualified members upgrade their level right from /my/<id> - ownership enforced by matching the connected wallet to the position's account, exact BigInt cost read live via getAllCosts, MoonPay assist when underfunded, wallet-app deep links on mobile

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-19 15:30:50 -05:00
parent ac5d29fc6e
commit 70fc321c3e
4 changed files with 73 additions and 2 deletions
+70
View File
@@ -122,6 +122,7 @@
?`<div class="table-wrap"><table class="table" style="min-width:520px"><thead><tr><th>When</th><th>From member</th><th>For</th><th>Amount</th></tr></thead><tbody>${inc.map(p=>`<tr><td>${date(p.ts)}</td><td>#${p.fromId}</td><td>${esc(p.desc)}</td><td><strong>${fmt(p.pol)} POL</strong></td></tr>`).join('')}</tbody></table></div>`
:'<div class="empty">No payments yet — they appear here the moment they land on-chain.</div>';
renderNextStep(d);
renderUpgradeCTA(d);
renderGens(d);
renderPipeline(d);
renderCoach(d);
@@ -159,6 +160,75 @@
}).join('');
el.innerHTML=`<div style="border-top:1px solid var(--line);padding-top:12px"><small style="text-transform:uppercase;letter-spacing:.08em;color:var(--muted);font-size:11px">Team depth — members per generation</small><div style="margin-top:8px">${rows}</div><p class="micro" style="margin:8px 0 0">Full generations duplicate: each one can hold twice the last. A generation pays this position at exactly one level — stay qualified and at that level to catch it.</p></div>`;
}
// On-page upgrade: connect the wallet that OWNS this position, read the
// exact next-level cost from the contract (BigInt — never float-derived),
// send upgrade(). Wrong value would simply revert; ownership is enforced by
// matching the connected account to the position's on-chain account.
const UPG={CONTRACT:'0x33bdaeefd6d17d80ae53816c916dfb26c4fb2daf',POLYGON:'0x89',SEL_UPGRADE:'0xd55ec697',SEL_GETCOSTS:'0x735f87b9'};
let upEth=null;
function renderUpgradeCTA(d){
const el=document.getElementById('dUpgrade');if(!el)return;
const ns=d.nextStep||{};
if(ns.kind!=='upgrade'||d.directCount<2){el.innerHTML='';return;}
const name=LEVELS[ns.nextLevel-1]||('Level '+ns.nextLevel);
el.innerHTML=`<div class="table-card" style="margin:0 0 18px;border-color:rgba(240,197,109,.45)">
<h2 style="margin:0 0 4px">⬆️ Upgrade to ${esc(name)} — right from this page</h2>
<p style="color:var(--muted);font-size:13px;margin:0 0 12px">Exact cost <strong>${fmt(ns.cost)} POL</strong>, read live from the contract. Connect the wallet that owns position #${d.id}, confirm once, done. ${ns.funded?'<strong style="color:var(--ok)">Your wallet already covers it ✓</strong>':'Your wallet does not cover it yet — you can time it to your next catches, or top up below.'}</p>
<button id="upgGo" class="btn btn-primary">Connect &amp; upgrade to ${esc(name)} →</button>
${ns.funded?'':`<button id="upgFund" class="btn btn-secondary" style="margin-left:8px">💳 Buy POL with card</button>`}
<div id="upgHelp" style="display:none;margin-top:10px" class="micro">📱 On a phone with no wallet in this browser? Open this page inside your wallet app: <a id="upgMM" href="#">MetaMask</a> · <a id="upgTW" href="#">Trust Wallet</a></div>
<div id="upgLog" class="micro" style="margin-top:10px"></div>
<p class="micro" style="margin:10px 0 0">Upgrades are optional and self-paced — never use funds you can't afford to lose. If anything is off, the contract simply rejects it and you keep your POL (minus tiny gas).</p>
</div>`;
const go=document.getElementById('upgGo');if(go)go.addEventListener('click',function(){doUpgrade(d,ns);});
const fu=document.getElementById('upgFund');if(fu)fu.addEventListener('click',async function(){
try{const need=Math.max(62,Math.ceil(ns.cost- (0))+5);const r=await(await fetch('/api/public/moonpay-url?address='+encodeURIComponent(d.account||'')+'&pol='+need)).json();if(r&&r.url)window.open(r.url,'_blank','noopener');}catch(e){}
});
}
function upgLog(msg,bad){const l=document.getElementById('upgLog');if(l){l.innerHTML=msg;l.style.color=bad?'var(--danger)':'var(--muted)';}}
async function doUpgrade(d,ns){
try{
upgLog('Looking for your wallet…');
upEth=await window.RMCWallet.pick();
if(!upEth){
const h=document.getElementById('upgHelp');if(h){h.style.display='block';
const here=location.host+location.pathname+location.search;
document.getElementById('upgMM').href='https://metamask.app.link/dapp/'+here;
document.getElementById('upgTW').href='https://link.trustwallet.com/open_url?coin_id=966&url='+encodeURIComponent(location.href);}
upgLog('No wallet in this browser — use the wallet-app links above, or install MetaMask on desktop.',true);return;
}
const accs=await upEth.request({method:'eth_requestAccounts'});const account=(accs[0]||'').toLowerCase();
if(!d.account||account!==String(d.account).toLowerCase()){
upgLog('This wallet ('+account.slice(0,6)+'…'+account.slice(-4)+') does not own position #'+d.id+' — connect the wallet that registered it ('+String(d.account||'').slice(0,6)+'…'+String(d.account||'').slice(-4)+').',true);return;
}
// network
const cid=await upEth.request({method:'eth_chainId'});
if(cid!==UPG.POLYGON){
upgLog('Switching to Polygon…');
try{await upEth.request({method:'wallet_switchEthereumChain',params:[{chainId:UPG.POLYGON}]});}
catch(e){if(e&&e.code===4902){await upEth.request({method:'wallet_addEthereumChain',params:[{chainId:UPG.POLYGON,chainName:'Polygon Mainnet',nativeCurrency:{name:'POL',symbol:'POL',decimals:18},rpcUrls:['https://polygon-rpc.com'],blockExplorerUrls:['https://polygonscan.com']}]});}else{throw e;}}
}
// exact cost from the contract (BigInt)
upgLog('Reading the exact upgrade cost from the contract…');
const r=await upEth.request({method:'eth_call',params:[{to:UPG.CONTRACT,data:UPG.SEL_GETCOSTS},'latest']});
const words=[];for(let i=2;i<r.length;i+=64)words.push(BigInt('0x'+r.slice(i,i+64)));
const arr=(d.tier===2)?words.slice(24,32):words.slice(16,24);
const val=arr[(d.level||1)-1];
if(!val||val<=0n){upgLog('Could not read the upgrade cost — try again in a minute.',true);return;}
upgLog('Upgrading to '+esc(LEVELS[ns.nextLevel-1]||'')+' for '+(Number(val)/1e18).toFixed(4)+' POL — confirm in your wallet…');
const tx=await upEth.request({method:'eth_sendTransaction',params:[{from:account,to:UPG.CONTRACT,value:'0x'+val.toString(16),data:UPG.SEL_UPGRADE}]});
upgLog('Submitted — waiting for confirmation… <a href="https://polygonscan.com/tx/'+tx+'" target="_blank" rel="noopener">view tx</a>');
for(let i=0;i<60;i++){
try{const rc=await upEth.request({method:'eth_getTransactionReceipt',params:[tx]});
if(rc){if(rc.status==='0x0'){upgLog('⚠️ The upgrade reverted — your POL was returned (minus gas). Wait a moment and try again.',true);return;}
upgLog('🎉 <strong>Upgraded!</strong> Refreshing your dashboard…');setTimeout(function(){location.reload();},2500);return;}
}catch(e){}
await new Promise(function(s){setTimeout(s,3000);});
}
upgLog('Still pending — it will land shortly; refresh in a minute.');
}catch(e){upgLog('Upgrade failed / rejected: '+esc(e.message||String(e)),true);}
}
// Wallet-verified messaging: sign-in = one free personal_sign; identity is
// the wallet that owns a position; permissions follow the matrix lines.
function renderMessages(d){