diff --git a/.gitignore b/.gitignore index ea588d4e23..6a2393064c 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,11 @@ cmd/web/explorer/.idea rag node_modules +plugin/go/tutorial/test_config.json +Frontend_backup.html +sidecar/main.go.bak +sidecar/praxis-sidecar +ui/index.html.bak # Hardhat -cache/solidity-files-cache.json \ No newline at end of file +cache/solidity-files-cache.json diff --git a/Frontend/app.js b/Frontend/app.js new file mode 100644 index 0000000000..e8153d5b69 --- /dev/null +++ b/Frontend/app.js @@ -0,0 +1,2855 @@ + +// ═══════════════════════════════════════════ +// BLS +// ═══════════════════════════════════════════ +let bls12_381 = null; +(async () => { + for (const url of ['https://esm.sh/@noble/curves@1.4.2/bls12-381','https://cdn.skypack.dev/@noble/curves@1.4.2/bls12-381']) { + try { const m = await import(url); bls12_381 = m.bls12_381; break; } catch {} + } + if (!bls12_381) toast('BLS library failed to load — check internet', true); +})(); + +// ═══════════════════════════════════════════ +// CONFIG & STATE +// ═══════════════════════════════════════════ +const getRPCHost = () => localStorage.getItem('praxis_rpc_host') || 'localhost'; +const getRPC = () => `http://${getRPCHost()}:50002`; + +let currentHeight = 0; +let currentNetworkID = 1; +let currentChainID = 1; +let selectedOut = true; +let propOut = true; +let revOut = true; +let signerPrivKey = null, signerPubKey = null, signerAddress = null; + +// ═══════════════════════════════════════════ +// PROTO ENCODER +// ═══════════════════════════════════════════ +function encV(value) { + const out = []; let v = typeof value==='bigint'?value:BigInt(value); + while(v>127n){out.push(Number((v&0x7fn)|0x80n));v>>=7n;}out.push(Number(v));return new Uint8Array(out); +} +function cat(...a){const t=a.reduce((s,x)=>s+x.length,0);const o=new Uint8Array(t);let off=0;for(const x of a){o.set(x,off);off+=x.length;}return o;} +function tag(f,w){return encV((BigInt(f)<<3n)|BigInt(w));} +function vf(f,v){const x=typeof v==='bigint'?v:BigInt(v);if(x===0n)return new Uint8Array(0);return cat(tag(f,0),encV(x));} +function bf(f,b){if(!b||!b.length)return new Uint8Array(0);return cat(tag(f,2),encV(b.length),b);} +function sf(f,s){if(!s||!s.length)return new Uint8Array(0);const e=new TextEncoder().encode(s);return cat(tag(f,2),encV(e.length),e);} +function ef(f,m){if(!m||!m.length)return new Uint8Array(0);return cat(tag(f,2),encV(m.length),m);} +function boolF(f,v){return cat(tag(f,0),new Uint8Array([v?1:0]));} +function hexToBytes(hex){const b=new Uint8Array(hex.length/2);for(let i=0;ix.toString(16).padStart(2,"0")).join("");} + +// ═══════════════════════════════════════════ +// HELPERS +// ═══════════════════════════════════════════ +function h2b(hex){hex=hex.trim().toLowerCase();if(hex.length%2)throw new Error('Odd hex');const o=new Uint8Array(hex.length/2);for(let i=0;ix.toString(16).padStart(2,'0')).join('');} +function fmtA(n){if(!n&&n!==0)return'—';const x=Number(n);if(x>=1e9)return(x/1e9).toFixed(2)+'B';if(x>=1e6)return(x/1e6).toFixed(2)+'M';if(x>=1e3)return(x/1e3).toFixed(1)+'k';return String(x);} +function fmtPRX(n){if(!n&&n!==0)return'—';const x=Number(n)/1_000_000;if(x>=1e9)return(x/1e9).toFixed(2)+'B';if(x>=1e6)return(x/1e6).toFixed(2)+'M';if(x>=1000)return(x/1000).toFixed(2)+'k';if(x>=1)return x.toFixed(2);return x.toFixed(6);} +function esc(s){return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,''');} +function addr40(s,label){if(!s||s.length!==40)throw new Error(`${label||'Address'} must be 40 hex chars`);} +function mid40(s){addr40(s,'Market ID');} + +// ═══════════════════════════════════════════ +// google.protobuf.Any +// ═══════════════════════════════════════════ +function encAny(typeUrl,inner){return cat(sf(1,typeUrl),bf(2,inner));} + +function getSelectedCat() { + const el = document.querySelector('#c_cat_pick .cpick.active'); + return el ? el.getAttribute('data-cat') : 'other'; +} + +// ═══════════════════════════════════════════ +// INNER MESSAGE ENCODERS — field numbers match tx.proto +// ═══════════════════════════════════════════ +function encSend(from,to,amt){return cat(bf(1,h2b(from)),bf(2,h2b(to)),vf(3,amt));} +function encCreate(creator,b0,expiry,nonce,question,rules){return cat(bf(1,h2b(creator)),vf(2,b0),vf(3,expiry),vf(4,nonce),sf(5,question),sf(6,rules||''));} +function encPredict(mid,bettor,outcome,shares,maxcost){return cat(bf(1,h2b(mid)),bf(2,h2b(bettor)),boolF(3,outcome),vf(4,shares),vf(5,maxcost));} +function encResolve(mid,resolver,outcome){return cat(bf(1,h2b(mid)),bf(2,h2b(resolver)),boolF(3,outcome));} +function encClaim(mid,claimant){return cat(bf(1,h2b(mid)),bf(2,h2b(claimant)));} +function encReclaim(mid,claimant){return cat(bf(1,h2b(mid)),bf(2,h2b(claimant)));} +function encRegister(addr,stake){return cat(bf(1,h2b(addr)),vf(2,stake));} +function encPropose(mid,resolver,outcome,bond){return cat(bf(1,h2b(mid)),bf(2,h2b(resolver)),boolF(3,outcome),vf(4,bond));} +function encDispute(mid,addr,bond){return cat(bf(1,h2b(mid)),bf(2,h2b(addr)),vf(3,bond));} +function encCommit(mid,voter,hash){return cat(bf(1,h2b(mid)),bf(2,h2b(voter)),bf(3,h2b(hash)));} +function encReveal(mid,voter,vote,nonce){return cat(bf(1,h2b(mid)),bf(2,h2b(voter)),boolF(3,vote),bf(4,h2b(nonce)));} +function encTally(mid,addr){return cat(bf(1,h2b(mid)),bf(2,h2b(addr)));} +function encFinalize(mid,addr){return cat(bf(1,h2b(mid)),bf(2,h2b(addr)));} +function encSlash(mid,addr){return cat(bf(1,h2b(mid)),bf(2,h2b(addr)));} +function encForfeit(mid,resolver){return cat(bf(1,h2b(mid)),bf(2,h2b(resolver)));} +function encUnstakeResolver(addr,amount){return cat(bf(1,h2b(addr)),vf(2,amount));} +function encClaimUnbonded(addr){return cat(bf(1,h2b(addr)));} + +// ═══════════════════════════════════════════ +// TX SIGN BYTES ENCODER +// ═══════════════════════════════════════════ +function encSignBytes(msgType,typeUrl,inner,{txTime,fee,height,memo,netId,chainId}){ + const any=encAny(typeUrl,inner); + return cat( + sf(1,msgType),ef(2,any), + vf(4,height||currentHeight),vf(5,txTime),vf(6,fee||10000), + memo?sf(7,memo):new Uint8Array(0), + vf(8,netId||1),vf(9,chainId||1), + ); +} + +// ═══════════════════════════════════════════ +// BLS SIGN +// ═══════════════════════════════════════════ +async function blsSign(msg){ + if(!signerPrivKey)throw new Error('No key loaded — go to Signer'); + if(!bls12_381)throw new Error('BLS library not loaded'); + return await bls12_381.sign(msg,signerPrivKey); +} + +// ═══════════════════════════════════════════ +// BASE64 HELPER (for proto JSON encoding) +// ═══════════════════════════════════════════ +function b2b64(bytes){ + let s='';for(let i=0;iel.style.display='none',5000); +}; + +// ═══════════════════════════════════════════ +// NAVIGATION +// ═══════════════════════════════════════════ +window.showPage=function(id,btn){ + document.querySelectorAll('.page').forEach(p=>p.classList.remove('active')); + document.getElementById('page-'+id).classList.add('active'); + document.querySelectorAll('#deskNav .ni').forEach(b=>b.classList.remove('active')); + const dm=document.querySelector(`#deskNav [data-p="${id}"]`);if(dm)dm.classList.add('active'); + document.querySelectorAll('#bnav .btab').forEach(b=>b.classList.remove('active')); + const bm=document.querySelector(`#bnav [data-p="${id}"]`);if(bm)bm.classList.add('active'); + if(id==='markets')loadMarkets(); + if(id==='wallet'){refreshBalance();loadMyPredictions();} + if(id==='create'){updateCreateBreakdown();setTimeout(initExpiryDate,50);} + if(id==='predict')updatePredictBreakdown(); + if(id==='resolvers')loadResolvers(); + closeNav(); +}; + +// ═══════════════════════════════════════════ +// MOBILE NAV +// ═══════════════════════════════════════════ +window.openNav=function(){document.getElementById('deskNav').classList.add('open');document.getElementById('mobNav').classList.add('open');};window.closeNav=function(e){if(!e||e.target===document.getElementById('mobNav')||e.currentTarget===document.getElementById('mobNav')){document.getElementById('deskNav').classList.remove('open');document.getElementById('mobNav').classList.remove('open');}}; + +function buildMobNav(){ + const body=document.getElementById('mobNavBody'); + if(!body)return; + body.innerHTML=document.getElementById('deskNav').innerHTML; + body.querySelectorAll('.ni').forEach(item=>{ + const p=item.getAttribute('data-p'); + if(p)item.setAttribute('onclick',`showPage('${p}',this)`); + }); +} + +// ═══════════════════════════════════════════ +// THEME +// ═══════════════════════════════════════════ +window.toggleTheme=function(){ + const html=document.documentElement; + const d=html.getAttribute('data-theme')==='dark'; + html.setAttribute('data-theme',d?'light':'dark'); + localStorage.setItem('praxis_theme',d?'light':'dark'); + updateTL(); +}; +function updateTL(){ + const d=document.documentElement.getAttribute('data-theme')==='dark'; + const lbl=d?'Light mode':'Dark mode'; + ['tlD','tlM'].forEach(id=>{const e=document.getElementById(id);if(e)e.textContent=lbl;}); +} +const st=localStorage.getItem('praxis_theme'); +if(st)document.documentElement.setAttribute('data-theme',st); +updateTL(); + +// ═══════════════════════════════════════════ +// RPC STATUS +// ═══════════════════════════════════════════ +window.checkRPC=async function(){ + try{ + const d=await rpc('/v1/query/height',{});currentHeight=d.height||0; + currentNetworkID=d.network_id||d.networkID||currentNetworkID; + currentChainID=d.chain_id||d.chainID||currentChainID; + ['rpcDot','rpcDotM'].forEach(id=>{const e=document.getElementById(id);if(e)e.className='dot live';}); + const el=document.getElementById('rpcStatus');if(el)el.textContent='live'; + const hb=document.getElementById('hBadge');if(hb)hb.textContent=`block ${currentHeight}`; + const hm=document.getElementById('hbM');if(hm)hm.textContent=`#${currentHeight}`; + ['ni_height'].forEach(id=>{const e=document.getElementById(id);if(e)e.textContent=currentHeight;}); + const ns=document.getElementById('ni_status');if(ns)ns.textContent='connected'; + const nr=document.getElementById('ni_rpc');if(nr)nr.textContent=getRPC(); + const sh=document.getElementById('sb_h');if(sh)sh.textContent=currentHeight; + updateExpiryFromDate(); + const nonceEl=document.getElementById('c_nonce');if(nonceEl&&!nonceEl.value)nonceEl.value=BigInt(Date.now())*1000n; + }catch{ + ['rpcDot','rpcDotM'].forEach(id=>{const e=document.getElementById(id);if(e)e.className='dot';}); + const el=document.getElementById('rpcStatus');if(el)el.textContent='offline'; + const ns=document.getElementById('ni_status');if(ns)ns.textContent='offline'; + } +}; +window.applyHost=function(){const h=document.getElementById('ni_host').value.trim();if(h)localStorage.setItem('praxis_rpc_host',h);checkRPC();toast('Connecting to '+h+'…');}; + +// ═══════════════════════════════════════════ +// OUTCOME TOGGLES +// ═══════════════════════════════════════════ +window.setOut=function(v){selectedOut=v;document.getElementById('btn_yes').className='obtn yes'+(v?' active':'');document.getElementById('btn_no').className='obtn no'+(!v?' active':'');}; +window.setPropOut=function(v){propOut=v;document.getElementById('pbtn_yes').className='obtn yes'+(v?' active':'');document.getElementById('pbtn_no').className='obtn no'+(!v?' active':'');}; +window.setRevOut=function(v){revOut=v;document.getElementById('rvbtn_yes').className='obtn yes'+(v?' active':'');document.getElementById('rvbtn_no').className='obtn no'+(!v?' active':'');}; + +// ═══════════════════════════════════════════ +// SIGNER +// ═══════════════════════════════════════════ +window.loadKey=async function(){ + const hex=document.getElementById('sk_input').value.trim().toLowerCase(); + if(hex.length!==64)return toast('Private key must be exactly 64 hex chars',true); + try{ + if(!bls12_381)throw new Error('BLS library not loaded'); + signerPrivKey=h2b(hex); + signerPubKey=bls12_381.getPublicKey(signerPrivKey); + const hb=await crypto.subtle.digest('SHA-256',signerPubKey); + signerAddress=b2h(new Uint8Array(hb).slice(0,20)); + document.getElementById('keyStatus').className='kstat loaded'; + document.getElementById('keyStatus').textContent='✓ loaded — '+signerAddress.slice(0,16)+'…'; + document.getElementById('sk_derived').style.display='block'; + document.getElementById('sk_pub').textContent=b2h(signerPubKey); + document.getElementById('sk_addr').textContent=signerAddress; + ['c_creator','p_bettor','r_resolver','cl_addr','s_from','w_addr','ft_addr', + 'reg_addr','pr_resolver','dis_addr','cv_voter','rv_voter','tal_addr','fin_addr','sl_addr', + 'fo_resolver','rc_addr','ccf_addr','can_addr','unst_addr','cub_addr'].forEach(id=>{ + const el=document.getElementById(id);if(el&&!el.value)el.value=signerAddress; + }); + const _ski=document.getElementById('sk_input');if(_ski)_ski.value=''; + refreshBalance(); + loadMyPredictions(); + toast('Key loaded — '+signerAddress); + }catch(e){signerPrivKey=signerPubKey=signerAddress=null;toast('Key load failed: '+e.message,true);} +}; +window.clearKey=function(){ + localStorage.removeItem('praxis_keystore'); + signerPrivKey=signerPubKey=signerAddress=null; + document.getElementById('keyStatus').className='kstat'; + document.getElementById('keyStatus').textContent='○ No key loaded'; + document.getElementById('sk_derived').style.display='none'; + const _ski=document.getElementById('sk_input');if(_ski)_ski.value=''; + toast('Key cleared'); +}; + +// ═══════════════════════════════════════════ +// ACCOUNT QUERY +// ═══════════════════════════════════════════ +window.queryAccount=async function(){ + const addr=document.getElementById('w_addr').value.trim().toLowerCase(); + addr40(addr,'Address'); + try{ + const d=await rpc('/v1/query/account',{address:addr}); + document.getElementById('w_result').style.display='block'; + document.getElementById('w_balance').textContent=fmtPRX(d.amount||0); + document.getElementById('w_addrD').textContent=addr; + }catch(e){toast('Query failed: '+e.message,true);} +}; + +// ═══════════════════════════════════════════ +// FAILED TX +// ═══════════════════════════════════════════ +window.checkFailedTxs=async function(){ + const addr=document.getElementById('ft_addr').value.trim().toLowerCase(); + addr40(addr,'Address'); + try{ + const d=await rpc('/v1/query/failed-txs',{address:addr,perPage:20}); + const c=d.totalCount||0;const el=document.getElementById('ft_result');el.style.display='block'; + if(c===0){el.innerHTML=`
✓ No failed transactions for ${addr.slice(0,12)}…
`;return;} + const rows=(d.results||[]).map(r=>`
${esc(r.error?.msg||'?')} (${r.error?.code})
${r.txHash?.slice(0,24)}…
`).join(''); + el.innerHTML=`
⚠ ${c} failed tx(s)
${rows}`; + }catch(e){toast('Query failed: '+e.message,true);} +}; + +// ═══════════════════════════════════════════ +// PENDING HELPER +// ═══════════════════════════════════════════ +function setPend(btnId,pendId,on){ + const b=document.getElementById(btnId);const p=document.getElementById(pendId); + if(b)b.disabled=on;if(p)p.style.display=on?'flex':'none'; +} + +async function doSubmit(msgType,typeUrl,inner,meta,btnId,pendId){ + if(!signerPrivKey)return toast('Load a private key in Signer first',true); + if(!currentHeight)return toast('Node not connected',true); + setPend(btnId,pendId,true); + try{ + const tx=await buildSigned(msgType,typeUrl,inner,meta); + const hash=await submitTxRPC(tx); + toast('⏳ Broadcasting — confirming in ~25s…'); + checkRPC(); + if(msgType==='create_market')setTimeout(loadMarkets,3000); + setTimeout(async()=>{ + try{ + const d=await rpc('/v1/query/failed-txs',{address:signerAddress,perPage:20}); + const failed=(d.results||[]).find(r=>r.txHash===hash); + if(failed){ + const code=failed.error?.code; + const msg=failed.error?.msg||'Transaction failed'; + toast('✗ Failed — '+friendlyError(code,msg),true); + } else { + toast('✓ Confirmed — '+(hash.length>20?hash.slice(0,20)+'…':hash)); + if(msgType==='create_market'||msgType==='finalize_market')loadMarkets(); + } + }catch(e){toast('✓ Submitted — could not confirm status',false);} + },25000); + }catch(e){toast(friendlyError(null,e.message),true);} + finally{setPend(btnId,pendId,false);} +} + +function showPL(outId,payId,tx){ + document.getElementById(outId).style.display='block'; + document.getElementById(payId).value=JSON.stringify(tx,null,2); +} + +// ═══════════════════════════════════════════ +// MY PREDICTIONS +// ═══════════════════════════════════════════ +async function refreshBalance(){ + if(!signerAddress)return; + try{ + const d=await rpc('/v1/query/account',{address:signerAddress}); + const bal=Number(d.amount||0); + const wbal=document.getElementById('w_balance');if(wbal)wbal.textContent=fmtPRX(bal); + const wres=document.getElementById('w_result');if(wres)wres.style.display='block'; + const wadr=document.getElementById('w_addrD');if(wadr)wadr.textContent=signerAddress; + const waddr=document.getElementById('w_addr');if(waddr&&!waddr.value)waddr.value=signerAddress; + }catch{} +} + +window.loadMyPredictions = async function () { + const el = document.getElementById('myPredictions'); + if (!signerAddress) { + el.innerHTML = '
Load wallet to see predictions
'; + return; + } + el.innerHTML = '
▪▪▪ loading predictions
'; + try { + const data = await rpc('/v1/query/txs-by-sender', { address: signerAddress, perPage: 200 }); + const results = data.results || []; + const seen = {}; + const predictions = []; + + for (const tx of results) { + const t = tx.transaction || tx; + const type = t.type || t.messageType || ''; + if (type !== 'submit_prediction') continue; + const msg = t.msg || t; + let marketId = '', outcome = false, shares = 0n, maxCost = 0n; + if (t.msgBytes) { + const bytes = h2b(t.msgBytes); + let pos = 0; + while (pos < bytes.length) { + const { v: tagV, p: p1 } = decVarint(bytes, pos); pos = p1; + const fn = Number(tagV >> 3n), wt = Number(tagV & 7n); + if (fn === 3 && wt === 0) { const { v, p: p2 } = decVarint(bytes, pos); pos = p2; outcome = v === 1n; } + else if (wt === 0) { const { v: _, p: p2 } = decVarint(bytes, pos); pos = p2; if (fn === 4) shares = _; if (fn === 5) maxCost = _; } + else if (wt === 2) { const { v: lenV, p: p2 } = decVarint(bytes, pos); pos = p2 + Number(lenV); if (fn === 1) marketId = b2h(bytes.slice(p2 - Number(lenV), pos)); } + else if (wt === 1) { pos += 8; } else if (wt === 5) { pos += 4; } else break; + } + } else { + marketId = msg.marketId || ''; + outcome = msg.outcome === true || msg.outcome === 'true' || msg.outcome === 1; + shares = BigInt(msg.shares || 0); + maxCost = BigInt(msg.maxCost || msg.max_cost || 0); + } + const key = marketId || tx.txHash; + if (!seen[key]) { + seen[key] = true; + predictions.push({ marketId: marketId || tx.txHash, outcome, shares, maxCost, height: tx.height || 0 }); + } + } + + if (predictions.length === 0) { + el.innerHTML = '
No predictions yet
'; + return; + } + + el.innerHTML = predictions.map(p => { + const m = _allMarkets.find(x => x.id === p.marketId); + let payoutHtml = ''; + if (m && m.status === 6) { + // finalized — compute expected payout + const totalPool = m.qYes + m.qNo; + const winPool = p.outcome ? m.qYes : m.qNo; + const won = m.proposedOutcome === p.outcome; + if (won && winPool > 0n) { + const payout = totalPool * p.shares / winPool; + payoutHtml = '
✓ Est. payout: ' + fmtPRX(payout) + ' PRX
'; + } else if (!won) { + payoutHtml = '
✗ Lost
'; + } + } else if (m && m.status === 4) { + payoutHtml = '
⏳ Awaiting finalization
'; + } + return '
' + + '
' + + '
' + + '
MKT ' + p.marketId.slice(0,12) + '…
' + + '
' + + '' + (p.outcome ? 'YES' : 'NO') + '' + + 'Shares: ' + fmtPRX(p.shares) + '' + + 'Max: ' + fmtPRX(p.maxCost) + ' PRX' + + '
' + + '
' + + '#' + p.height + '' + + '
' + + payoutHtml + + '
'; + }).join(''); + } catch (e) { + el.innerHTML = '
Error: ' + esc(e.message) + '
'; + } +}; + +// ═══════════════════════════════════════════ +// RENDER MARKET CARDS — Premium Design +// ═══════════════════════════════════════════ +function resolverTier(addr) { + const r = _resolverRegistry.get(addr); + if (!r) return null; + const estRRS = Math.min(r.proposalCount * 10, 999); + if (estRRS >= 200) return {label:'Gold', color:'#FFD700', icon:'★'}; + if (estRRS >= 50) return {label:'Silver', color:'#C0C0C0', icon:'◆'}; + if (estRRS >= 1) return {label:'Bronze', color:'#CD7F32', icon:'▲'}; + return {label:'Registered', color:'var(--text3)', icon:'○'}; +} + +window.renderMarketCards = function(markets) { + if (!markets || markets.length === 0) return '
No markets found
'; + + // category filter — tag-based first, keyword fallback + let filtered = markets; + if (window._activeCat && window._activeCat !== 'all') { + const cat = window._activeCat; + filtered = markets.filter(m => { + const tag = extractCat(m.rules || ''); + if (tag && tag !== 'other') return tag === cat; + // keyword fallback for untagged markets + const q = (m.question || '').toLowerCase(); + if (cat === 'crypto') return /btc|eth|crypto|bitcoin|ethereum|solana|token|defi|nft|blockchain/.test(q); + if (cat === 'sports') return /nba|nfl|fifa|soccer|football|tennis|golf|sports|league|match|game|win/.test(q); + if (cat === 'politics') return /election|president|vote|congress|senate|government|policy|law|bill/.test(q); + if (cat === 'finance') return /stock|market|fed|rate|gdp|inflation|s&p|nasdaq|economy|oil|gold/.test(q); + return cat === 'other'; + }); + } + + if (filtered.length === 0) return '
No markets in this category
'; + + return '
' + filtered.map(m => { + const total = m.qYes + m.qNo; + const yesPct = total > 0n ? Number((m.qYes * 100n) / total) : 50; + const noPct = 100 - yesPct; + const mid = m.marketId || m.txHash || ''; + const vol = total > 0n ? fmtPRX(total) : '—'; + const exp = m.expiry ? '#' + m.expiry.toString() : '—'; + const creator = (m.creator || '').slice(0,8) + '…'; + + let statusHtml = ''; + let cardClass = ''; + if (m.status === 0) { + statusHtml = 'LIVE'; + } else if (m.status === 4) { + statusHtml = 'PROPOSED'; + cardClass = 'mexp'; + } else if (m.status === 5) { + statusHtml = 'DISPUTED'; + cardClass = 'mexp'; + } else if (m.status === 6) { + statusHtml = 'FINALIZED'; + cardClass = 'mfin'; + } else if (m.status === 1) { + statusHtml = 'CANCELLED'; + cardClass = 'mcan'; + } else if (m.status === 8) { + statusHtml = 'EXPIRED'; + cardClass = 'mexp'; + } + + const showBtns = m.status === 0; + + const catIcon = {'crypto':'🪙','sports':'⚽','politics':'🗳','finance':'📈','other':'◈'}[extractCat(m.rules||'')] || '◈'; + const catName = (CAT_LABELS[extractCat(m.rules||'')] || 'Other').replace(/[🪙⚽🗳📈◈]\s*/,''); + const hasBanner = !!extractImg(m.rules||''); + const yesMulti = m.qYes > 0n ? (Number(m.qYes + m.qNo) / Number(m.qYes)).toFixed(2) : '—'; + const noMulti = m.qNo > 0n ? (Number(m.qYes + m.qNo) / Number(m.qNo)).toFixed(2) : '—'; + return `
+ ${mkBannerImg(m.rules)}
+
${catIcon} ${catName}  ${statusHtml}
+
${esc(m.question || '(no question)')}
+
+ ${yesPct}% ${yesMulti}x + · + ${noPct}% ${noMulti}x +
+
+
+
+
Vol${vol}
+
Exp${exp}
+
Creator${creator}
+
`; + }).join('') + '
'; +}; + +// ── Volume chip updater ── + +// store markets globally for detail view +let _allMarkets = []; +let _resolverRegistry = new Map(); +let _detailMarketId = null; // address -> {stake, proposalCount} + +window.showDetail = function(marketId) { + const m = _allMarkets.find(x => x.marketId === marketId || x.txHash === marketId); + if (!m) return; + const open = m.status === 0; + const expired = m.status === 8; + const cancelled = m.status === 1; + const proposed = m.status === 4; + const disputed = m.status === 5; + const finalized = m.status === 6; + const voided = m.status === 7; + const resolved = m.status === 2; + const total = m.qYes + m.qNo; + const yesPct = total > 0n ? Number(m.qYes * 100n / total) : 50; + const noPct = 100 - yesPct; + const mid = m.marketId || m.txHash; + + document.getElementById('det-question').textContent = m.question; + document.getElementById('det-qyes').textContent = fmtPRX(m.qYes) + ' PRX'; + document.getElementById('det-qno').textContent = fmtPRX(m.qNo) + ' PRX'; + document.getElementById('det-yes-pct').textContent = yesPct + '%'; + document.getElementById('det-no-pct').textContent = noPct + '%'; + document.getElementById('det-bar').style.width = yesPct + '%'; + document.getElementById('det-mid').textContent = mid; + document.getElementById('det-creator').textContent = m.creator || '—'; + document.getElementById('det-total').textContent = fmtPRX(m.qYes + m.qNo) + ' PRX'; + if (m.expiry) { + const blk = Number(m.expiry); + const blocksLeft = blk - currentHeight; + const msLeft = blocksLeft * 5000; + const expDate = new Date(Date.now() + msLeft); + const dateStr = expDate.toLocaleDateString('en-US', {month:'short', day:'numeric', year:'numeric'}); + const timeStr = expDate.toLocaleTimeString('en-US', {hour:'2-digit', minute:'2-digit'}); + document.getElementById('det-expiry').textContent = 'blk #' + blk + ' (' + dateStr + ' ' + timeStr + ')'; + } else { + document.getElementById('det-expiry').textContent = '—'; + } + // Banner image + const imgUrl = extractImg(m.rules || ''); + const bannerDiv = document.getElementById('det-img-banner'); + const bannerImg = document.getElementById('det-img-banner-img'); + if (bannerDiv && bannerImg) { + if (imgUrl) { + bannerImg.src = imgUrl; + bannerDiv.style.display = ''; + bannerImg.onerror = () => { bannerDiv.style.display = 'none'; }; + } else { + bannerDiv.style.display = 'none'; + } + } + + const rulesRow = document.getElementById('det-rules-row'); + const rulesEl = document.getElementById('det-rules'); + const catBadge = document.getElementById('det-cat-badge'); + if (rulesRow && rulesEl) { + const rawRules = m.rules || ''; + const cat = extractCat(rawRules); + const stripped = stripImgTag(stripCatPrefix(rawRules)).trim(); + const displayRules = stripped || (cat !== 'other' ? 'No resolution criteria specified.' : ''); + if (displayRules || cat !== 'other') { + rulesEl.textContent = displayRules || 'No resolution criteria specified.'; + rulesRow.style.display = ''; + if (catBadge) { + catBadge.textContent = CAT_LABELS[cat] || '◈ Other'; + const catColors = { crypto:'#f7931a', sports:'#22c55e', politics:'#3b82f6', finance:'#a855f7', other:'var(--text3)' }; + catBadge.style.background = 'var(--surf2)'; + catBadge.style.color = catColors[cat] || 'var(--text3)'; + catBadge.style.border = '1px solid var(--border)'; + } + } else { + rulesRow.style.display = 'none'; + } + } + + const resolverRow = document.getElementById('det-resolver-row'); + if (m.resolver) { + resolverRow.style.display = ''; + const tier = m.resolver ? resolverTier(m.resolver) : null; + const tierHtml = tier ? ' ' + tier.icon + ' ' + tier.label + '' : ''; + document.getElementById('det-resolver').innerHTML = (m.resolver || '—') + tierHtml + (m.proposedOutcome !== undefined ? ' → proposed ' + (m.proposedOutcome ? 'YES' : 'NO') : ''); + } else { + resolverRow.style.display = 'none'; + } + + const statusLabels = {0:'Open',1:'Cancelled',2:'Resolved',3:'Expired',4:'Proposed',5:'Disputed',6:'Finalized',7:'Voided',8:'Expired'}; + const statusClasses = {0:'sp-o',1:'sp-d',2:'sp-f',3:'sp-e',4:'sp-e',5:'sp-d',6:'sp-f',7:'sp-e',8:'sp-e'}; + document.getElementById('det-status-pill').innerHTML = '
' + (statusLabels[m.status]||'Closed') + '
'; + + const yesBtn = document.getElementById('det-bet-yes'); + const noBtn = document.getElementById('det-bet-no'); + if (open) { + yesBtn.removeAttribute('disabled'); yesBtn.setAttribute('onclick', 'fillP(' + JSON.stringify(mid) + ', true)'); + noBtn.removeAttribute('disabled'); noBtn.setAttribute('onclick', 'fillP(' + JSON.stringify(mid) + ', false)'); + } else { + yesBtn.setAttribute('disabled',''); noBtn.setAttribute('disabled',''); + } + + const proposeBtn = document.getElementById('det-propose-btn'); + const claimBtn = document.getElementById('det-claim-btn'); + if (proposeBtn) { + if (m.status === 8) { + // COI-1: hide propose if signer is market creator + const signerIsCreator = signerAddress && m.creator && signerAddress.toLowerCase() === m.creator.toLowerCase(); + // COI-2: hide propose if signer holds a position in this market + const signerHasPosition = (() => { + try { + const txs = JSON.parse(localStorage.getItem('praxis_tx_cache') || '[]'); + return txs.some(tx => + tx.messageType === 'submit_prediction' && + tx.sender && tx.sender.toLowerCase() === (signerAddress||'').toLowerCase() && + tx.transaction && tx.transaction.msg && + (() => { try { return b2h(Uint8Array.from(atob(tx.transaction.msg.marketId||''), c=>c.charCodeAt(0))) === mid; } catch { return false; } })() + ); + } catch { return false; } + })(); + if (signerIsCreator) { + proposeBtn.style.display = ''; + proposeBtn.disabled = true; + proposeBtn.title = 'Market creators cannot propose outcomes for their own markets'; + proposeBtn.textContent = '⚖ Cannot Propose (Creator)'; + } else if (signerHasPosition) { + proposeBtn.style.display = ''; + proposeBtn.disabled = true; + proposeBtn.title = 'Forfeit your position before proposing'; + proposeBtn.textContent = '⚖ Forfeit Position First'; + } else { + proposeBtn.style.display = ''; + proposeBtn.disabled = false; + proposeBtn.textContent = '⚖ Propose Outcome'; + proposeBtn.setAttribute('onclick', 'fillPropose(' + JSON.stringify(mid) + ')'); + } + } else { + proposeBtn.style.display = 'none'; + proposeBtn.disabled = false; + proposeBtn.textContent = '⚖ Propose Outcome'; + } + } + if (claimBtn) { + if (m.status === 6) { + claimBtn.style.display = ''; + claimBtn.textContent = '◎ Claim Winnings'; + claimBtn.setAttribute('onclick', 'fillC(' + JSON.stringify(mid) + ')'); + } else if (m.status === 1) { + claimBtn.style.display = ''; + claimBtn.textContent = '◎ Claim Refund'; + claimBtn.setAttribute('onclick', 'fillC(' + JSON.stringify(mid) + ')'); + } + } + + const reclaimBtn = document.getElementById('det-reclaim-btn'); + if (reclaimBtn) { + if (m.status === 8 && currentHeight > Number(m.expiry) + 300) { + reclaimBtn.style.display = ''; + reclaimBtn.setAttribute('onclick', 'fillReclaim(' + JSON.stringify(mid) + ')'); + } else { + reclaimBtn.style.display = 'none'; + } + } + + const forfeitBtn = document.getElementById('det-forfeit-btn'); + if (forfeitBtn) { + if (m.status === 0 && signerAddress && signerAddress !== m.creator) { + forfeitBtn.style.display = ''; + forfeitBtn.setAttribute('onclick', 'fillForfeit(' + JSON.stringify(mid) + ')'); + } else { + forfeitBtn.style.display = 'none'; + } + } + + const bannerCard = document.getElementById('det-banner-card'); + if (m.status === 8) { + bannerCard.style.display = ''; + bannerCard.innerHTML = '
Awaiting resolver proposal
'; + } else if (m.status === 4) { + bannerCard.style.display = ''; + bannerCard.innerHTML = '
🔎 Resolver: ' + (m.resolver ? m.resolver.slice(0,8) + '…' : '?') + ' — proposed ' + (m.proposedOutcome ? 'YES' : 'NO') + '
'; + } else if (m.status === 1) { + bannerCard.style.display = ''; + bannerCard.innerHTML = '
Market cancelled — reclaim your stake
'; + } else if (m.status === 7) { + bannerCard.style.display = ''; + bannerCard.innerHTML = '
Market voided — full refund available
'; + } else { + bannerCard.style.display = 'none'; + } + + _detailMarketId = mid; + showPage('detail', null); + setTimeout(()=>switchDetailTab('activity'), 50); +}; +window.openDetail = window.showDetail; + +window.fillP = (id, outcome) => { + document.getElementById('p_mid').value = id; + if (outcome !== undefined) { setOut(outcome); } + showPage('predict', null); +}; +window.fillC = id => { document.getElementById('cl_mid').value = id; showPage('claim', null); }; + +// ═══════════════════════════════════════════ +// MARKETS PAGE +// ═══════════════════════════════════════════ +function decVarint(buf,pos){let r=0n,s=0n;while(pos b.classList.remove('active')); + const btn = document.getElementById('tab-' + tab); + if (btn) btn.classList.add('active'); + renderCurrentTab(); +}; + +function renderCurrentTab() { + const el = document.getElementById('marketsList'); + if (!_allMarkets.length) return; + + let markets; + if (_activeTab === 'live') { + markets = _allMarkets.filter(m => m.status === 0); + } else if (_activeTab === 'proposed') { + markets = _allMarkets.filter(m => m.status === 4 || m.status === 5); + } else { + // closed — rolling window of last CLOSED_WINDOW blocks + markets = _allMarkets.filter(m => + (m.status === 8 || m.status === 1 || m.status === 6 || m.status === 7 || m.status === 2 || m.status === 3) && + m.expiry && Number(m.expiry) >= (currentHeight - CLOSED_WINDOW) + ); + } + + const countEl = document.getElementById('sb_c'); + if (countEl) countEl.textContent = _allMarkets.filter(m => m.status === 0).length; + + if (markets.length === 0) { + const labels = {live:'No open markets yet', proposed:'No markets awaiting resolution', closed:'No recently closed markets'}; + el.innerHTML = '
' + (labels[_activeTab] || 'No markets') + '
'; + return; + } + el.innerHTML = window.renderMarketCards(markets); +} + +window.loadMarkets = async function () { + const el = document.getElementById('marketsList'); + const countEl = document.getElementById('sb_c'); + el.innerHTML = '
▪ ▪ ▪  loading markets from chain
'; + try { + await checkRPC(); + + const heightResp = await rpc('/v1/query/height', {}); + const tipHeight = Number(heightResp.height || currentHeight || 1); + const BATCH = 100; + const CACHE_KEY = 'praxis_tx_cache'; + const CACHE_HEIGHT_KEY = 'praxis_scan_height'; + + // load cache + let allTxs = []; + let scanFrom = 1; + try { + const cached = localStorage.getItem(CACHE_KEY); + const cachedHeight = parseInt(localStorage.getItem(CACHE_HEIGHT_KEY) || '0'); + if (cached && cachedHeight > 0) { + allTxs = JSON.parse(cached); + scanFrom = cachedHeight + 1; + el.innerHTML = '
▪ ▪ ▪  Cache loaded to block ' + cachedHeight + ' — scanning new blocks…
'; + } + } catch(e) { allTxs = []; scanFrom = 1; } + + // scan only new blocks + if (scanFrom <= tipHeight) { + for (let h = scanFrom; h <= tipHeight; h += BATCH) { + const pct = Math.round(((h - scanFrom) / Math.max(tipHeight - scanFrom, 1)) * 100); + el.innerHTML = '
▪ ▪ ▪  Scanning blocks ' + h + ' / ' + tipHeight + '  ' + pct + '%
'; + const batchPromises = []; + for (let bh = h; bh < h + BATCH && bh <= tipHeight; bh++) { + batchPromises.push( + rpc('/v1/query/txs-by-height', { height: bh, perPage: 50 }) + .then(d => allTxs.push(...(d.results || []))) + .catch(() => {}) + ); + } + await Promise.all(batchPromises); + } + // save cache + try { + localStorage.setItem(CACHE_KEY, JSON.stringify(allTxs)); + localStorage.setItem(CACHE_HEIGHT_KEY, String(tipHeight)); + } catch(e) {} + } + + const marketsMap = new Map(); + + for (const tx of allTxs) { + if (tx.messageType !== 'create_market') continue; + const msg = (tx.transaction && tx.transaction.msg) || {}; + const question = msg.question || ''; + const rules = msg.rules || ''; + const creator = tx.sender || ''; + const b0 = BigInt(msg.b0 || 0); + const expiry = BigInt(msg.expiryTime || msg.expiry_time || 0); + const nonce = BigInt(msg.nonce || 0); + const lmsrSeed = b0 > 50000000n ? b0 - 50000000n : b0; + + let marketId = tx.txHash || (creator + String(nonce)); + try { + const creatorBytes = /^[0-9a-fA-F]{40}$/.test(creator) + ? h2b(creator) + : (() => { const bin = atob(creator); return new Uint8Array([...bin].map(c => c.charCodeAt(0))); })(); + const nonceBytes = new Uint8Array(8); + let n = nonce; + for (let i = 7; i >= 0; i--) { nonceBytes[i] = Number(n & 0xffn); n >>= 8n; } + const input = new Uint8Array(creatorBytes.length + 8); + input.set(creatorBytes); input.set(nonceBytes, 20); + const hash = await crypto.subtle.digest('SHA-256', input); + marketId = b2h(new Uint8Array(hash).slice(0, 20)); + } catch (e) {} + + if (!marketsMap.has(marketId)) { + marketsMap.set(marketId, { + txHash: tx.txHash || '', + marketId, + question: question || '(no question)', + rules: rules || '', + creator, + b0, + lmsrSeed, + expiry, + nonce, + status: 0, + qYes: lmsrSeed / 2n, + qNo: lmsrSeed / 2n, + }); + } + } + + for (const tx of allTxs) { + if (tx.messageType !== 'submit_prediction') continue; + const msg = (tx.transaction && tx.transaction.msg) || {}; + const rawMid = msg.marketId || msg.market_id || ''; + let marketId = rawMid; + try { const b = Uint8Array.from(atob(rawMid), c => c.charCodeAt(0)); marketId = b2h(b); } catch(e) {} + const outcome = msg.outcome === true || msg.outcome === 'true' || msg.outcome === 1; + const amount = BigInt(msg.shares || msg.amount || 0); + if (!marketId || !marketsMap.has(marketId)) continue; + const m = marketsMap.get(marketId); + if (outcome) { m.qYes += amount; } else { m.qNo += amount; } + } + + for (const tx of allTxs) { + if (tx.messageType !== 'propose_outcome') continue; + const msg = (tx.transaction && tx.transaction.msg) || {}; + const rawMid = msg.marketId || ''; + let marketId = rawMid; + try { const b = Uint8Array.from(atob(rawMid), c => c.charCodeAt(0)); marketId = b2h(b); } catch(e) {} + if (!marketId || !marketsMap.has(marketId)) continue; + const m = marketsMap.get(marketId); + let resolver = tx.sender || ''; + try { const rb = Uint8Array.from(atob(msg.resolverAddress || ''), c => c.charCodeAt(0)); resolver = b2h(rb); } catch(e) {} + m.status = 4; + m.resolver = resolver; + m.proposedOutcome = msg.proposedOutcome; + } + + // Build resolver registry + for (const tx of allTxs) { + if (tx.messageType !== 'register_resolver') continue; + const msg = (tx.transaction && tx.transaction.msg) || {}; + const addr = tx.sender || ''; + let stake = BigInt(msg.stakeAmount || msg.stake_amount || 0); + if (!_resolverRegistry.has(addr)) { + _resolverRegistry.set(addr, { stake, proposalCount: 0, rrs: 10 }); + } else { + _resolverRegistry.get(addr).stake = stake; + } + } + for (const tx of allTxs) { + if (tx.messageType !== 'propose_outcome') continue; + const addr = tx.sender || ''; + if (_resolverRegistry.has(addr)) { + _resolverRegistry.get(addr).proposalCount++; + } + } + // sync rrs estimate into registry + _resolverRegistry.forEach(r => { r.rrs = Math.min(10 + r.proposalCount * 10, 999); }); + + for (const tx of allTxs) { + if (tx.messageType !== 'finalize_market') continue; + const msg = (tx.transaction && tx.transaction.msg) || {}; + const rawMid = msg.marketId || ''; + let marketId = rawMid; + try { const b = Uint8Array.from(atob(rawMid), c => c.charCodeAt(0)); marketId = b2h(b); } catch(e) {} + if (!marketId || !marketsMap.has(marketId)) continue; + marketsMap.get(marketId).status = 6; + } + + for (const tx of allTxs) { + if (tx.messageType !== 'file_dispute') continue; + const msg = (tx.transaction && tx.transaction.msg) || {}; + const rawMid = msg.marketId || ''; + let marketId = rawMid; + try { const b = Uint8Array.from(atob(rawMid), c => c.charCodeAt(0)); marketId = b2h(b); } catch(e) {} + if (!marketId || !marketsMap.has(marketId)) continue; + marketsMap.get(marketId).status = 5; + } + + for (const tx of allTxs) { + if (tx.messageType !== 'cancel_market') continue; + const msg = (tx.transaction && tx.transaction.msg) || {}; + const rawMid = msg.marketId || ''; + let marketId = rawMid; + try { const b = Uint8Array.from(atob(rawMid), c => c.charCodeAt(0)); marketId = b2h(b); } catch(e) {} + if (!marketId || !marketsMap.has(marketId)) continue; + marketsMap.get(marketId).status = 1; + } + + const markets = [...marketsMap.values()]; + for (const m of markets) { + if (m.expiry && currentHeight > Number(m.expiry) && m.status === 0) m.status = 8; + } + + _allMarkets = markets; + checkRoles(); + renderCurrentTab(); + + } catch (e) { + el.innerHTML = '
⚠ Cannot reach node at ' + getRPC() + '
' + esc(e.message) + '
'; + } +}; + +// ═══════════════════════════════════════════ +// ── SEND +// ═══════════════════════════════════════════ +window.build_send=function(){try{ + const from=document.getElementById('s_from').value.trim().toLowerCase(); + const to=document.getElementById('s_to').value.trim().toLowerCase(); + const amt=parseInt(document.getElementById('s_amount').value)*1000000; + const fee=parseInt(document.getElementById('s_fee').value)||10000; + addr40(from,'From');addr40(to,'To');if(!amt||amt<=0)throw new Error('Amount > 0 required'); + showPL('so','sp',buildUnsigned('send','type.googleapis.com/types.MessageSend',encSend(from,to,amt),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.signAndSubmit_send=async function(){try{ + const from=document.getElementById('s_from').value.trim().toLowerCase(); + const to=document.getElementById('s_to').value.trim().toLowerCase(); + const amt=parseInt(document.getElementById('s_amount').value)*1000000; + const fee=parseInt(document.getElementById('s_fee').value)||10000; + addr40(from,'From');addr40(to,'To');if(!amt||amt<=0)throw new Error('Amount > 0'); + await doSubmit('send','type.googleapis.com/types.MessageSend',encSend(from,to,amt),{fee},'btn_send','pend_send'); +}catch(e){toast(e.message,true);}}; + +// ── CREATE MARKET +window.build_create=function(){try{ + const _cat=getSelectedCat(); + const q=document.getElementById('c_question').value.trim(); + const cr=document.getElementById('c_creator').value.trim().toLowerCase(); + const b0=parseInt(document.getElementById('c_b0').value)*1000000; + const exp=parseInt(document.getElementById('c_expiry').value)||currentHeight+1000; + const fee=parseInt(document.getElementById('c_fee').value)||10000; + let nonce=document.getElementById('c_nonce').value; + if(!nonce)nonce=BigInt(Date.now())*1000n; + else nonce=parseInt(nonce); + const rules=document.getElementById('c_rules').value.trim(); + const _imgUrl=document.getElementById('c_img')?.value.trim()||''; + if(!q)throw new Error('Question required');addr40(cr,'Creator'); + showPL('co','cp',buildUnsigned('create_market','type.googleapis.com/types.MessageCreateMarket',encCreate(cr,b0,exp,nonce,q,buildRulesWithImg(buildRulesWithCat(_cat,rules),_imgUrl)),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.updateCreateBreakdown=function(){ + const b0=parseInt(document.getElementById('c_b0')?.value||0); + const fee=parseInt(document.getElementById('c_fee')?.value||10000); + const bond=5000; + const total=b0+bond+(fee/1000000); + const el=document.getElementById('create_breakdown'); + if(!el)return; + el.innerHTML= + '
B0 liquidity seed'+b0.toLocaleString()+' PRX
'+ + '
Creator bond (locked)5,000 PRX
'+ + '
TX fee'+fee.toLocaleString()+' uPRX
'+ + '
Total deducted'+(b0+bond).toLocaleString()+' PRX
'; +}; +window.signAndSubmit_create=async function(){try{ + const _cat=getSelectedCat(); + const q=document.getElementById('c_question').value.trim(); + const cr=document.getElementById('c_creator').value.trim().toLowerCase(); + const b0=parseInt(document.getElementById('c_b0').value)*1000000; + const exp=parseInt(document.getElementById('c_expiry').value)||currentHeight+1000; + const fee=parseInt(document.getElementById('c_fee').value)||10000; + let nonce=document.getElementById('c_nonce').value; + if(!nonce)nonce=BigInt(Date.now())*1000n; + else nonce=parseInt(nonce); + const rules=document.getElementById('c_rules').value.trim(); + const _imgUrl=document.getElementById('c_img')?.value.trim()||''; + if(!q)throw new Error('Question required');addr40(cr,'Creator'); + await doSubmit('create_market','type.googleapis.com/types.MessageCreateMarket',encCreate(cr,b0,exp,nonce,q,buildRulesWithImg(buildRulesWithCat(_cat,rules),_imgUrl)),{fee},'btn_create','pend_create'); +}catch(e){toast(e.message,true);}}; + +// ── SUBMIT PREDICTION +window.build_predict=function(){try{ + const mid=document.getElementById('p_mid').value.trim().toLowerCase();mid40(mid); + const bettor=document.getElementById('p_bettor').value.trim().toLowerCase();addr40(bettor,'Bettor'); + const sharesInput=parseInt(document.getElementById("p_shares").value); + const shares=sharesInput*1000000; + const mc=parseInt(document.getElementById('p_maxcost').value)*1000000; + const fee=parseInt(document.getElementById('p_fee').value)||10000; + if(sharesInput<1)throw new Error("Shares min 1 PRX"); + showPL('po','pp',buildUnsigned('submit_prediction','type.googleapis.com/types.MessageSubmitPrediction',encPredict(mid,bettor,selectedOut,shares,mc),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.updatePredictBreakdown=function(){ + const shares=parseInt(document.getElementById('p_shares')?.value||0); + const fee=parseInt(document.getElementById('p_fee')?.value||10000); + const slipPct=parseFloat(document.getElementById('p_slippage')?.value||5); + const el=document.getElementById('predict_breakdown'); + const slipLbl=document.getElementById('p_slip_lbl'); + if(slipLbl)slipLbl.textContent=slipPct.toFixed(1)+'%'; + if(!el)return; + const tradeCost=shares; + const creatorFee=Math.ceil(shares*0.01); + const resolverFee=Math.ceil(shares*0.01); + const total=tradeCost+creatorFee+resolverFee; + const maxCost=Math.ceil(total*(1+slipPct/100)); + const mcEl=document.getElementById('p_maxcost'); + if(mcEl)mcEl.value=maxCost; + el.innerHTML= + '
Trade cost'+tradeCost.toLocaleString()+' PRX
'+ + '
Market fee (2%)'+(creatorFee+resolverFee).toLocaleString()+' PRX
'+ + '
TX fee'+fee.toLocaleString()+' uPRX
'+ + '
Max cost ('+slipPct.toFixed(1)+'% slippage)'+maxCost.toLocaleString()+' PRX
'; +}; +window.signAndSubmit_predict=async function(){try{ + const mid=document.getElementById('p_mid').value.trim().toLowerCase();mid40(mid); + const bettor=document.getElementById('p_bettor').value.trim().toLowerCase();addr40(bettor,'Bettor'); + const sharesInput=parseInt(document.getElementById("p_shares").value); + const shares=sharesInput*1000000; + const mc=parseInt(document.getElementById('p_maxcost').value)*1000000; + const fee=parseInt(document.getElementById('p_fee').value)||10000; + if(sharesInput<1)throw new Error("Shares min 1 PRX"); + await doSubmit('submit_prediction','type.googleapis.com/types.MessageSubmitPrediction',encPredict(mid,bettor,selectedOut,shares,mc),{fee},'btn_predict','pend_predict'); +}catch(e){toast(e.message,true);}}; + +// ── CLAIM WINNINGS +window.build_claim=function(){try{ + const mid=document.getElementById('cl_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('cl_addr').value.trim().toLowerCase();addr40(addr,'Claimant'); + const fee=parseInt(document.getElementById('cl_fee').value)||10000; + showPL('clo','clp',buildUnsigned('claim_winnings','type.googleapis.com/types.MessageClaimWinnings',encClaim(mid,addr),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.signAndSubmit_claim=async function(){try{ + const mid=document.getElementById('cl_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('cl_addr').value.trim().toLowerCase();addr40(addr,'Claimant'); + const fee=parseInt(document.getElementById('cl_fee').value)||10000; + await doSubmit('claim_winnings','type.googleapis.com/types.MessageClaimWinnings',encClaim(mid,addr),{fee},'btn_claim','pend_claim'); +}catch(e){toast(e.message,true);}}; + +// ── REGISTER RESOLVER +window.build_register=function(){try{ + const addr=document.getElementById('reg_addr').value.trim().toLowerCase();addr40(addr,'Resolver'); + const stake=parseInt(document.getElementById('reg_stake').value)*1000000; + const fee=parseInt(document.getElementById('reg_fee').value)||10000; + if(stake<500000000000)throw new Error('Stake min 500,000 PRX'); + showPL('rego','regp',buildUnsigned('register_resolver','type.googleapis.com/types.MessageRegisterResolver',encRegister(addr,stake),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.signAndSubmit_register=async function(){try{ + const addr=document.getElementById('reg_addr').value.trim().toLowerCase();addr40(addr,'Resolver'); + const stake=parseInt(document.getElementById('reg_stake').value)*1000000; + const fee=parseInt(document.getElementById('reg_fee').value)||10000; + if(stake<500000000000)throw new Error('Stake min 500,000 PRX'); + await doSubmit('register_resolver','type.googleapis.com/types.MessageRegisterResolver',encRegister(addr,stake),{fee},'btn_register','pend_register'); +}catch(e){toast(e.message,true);}}; + +// ── PROPOSE OUTCOME +window.build_propose=function(){try{ + const mid=document.getElementById('pr_mid').value.trim().toLowerCase();mid40(mid); + const res=document.getElementById('pr_resolver').value.trim().toLowerCase();addr40(res,'Resolver'); + const bond=parseInt(document.getElementById('pr_bond').value)*1000000; + const fee=parseInt(document.getElementById('pr_fee').value)||10000; + showPL('propo','propp',buildUnsigned('propose_outcome','type.googleapis.com/types.MessageProposeOutcome',encPropose(mid,res,propOut,bond),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.signAndSubmit_propose=async function(){try{ + const mid=document.getElementById('pr_mid').value.trim().toLowerCase();mid40(mid); + const res=document.getElementById('pr_resolver').value.trim().toLowerCase();addr40(res,'Resolver'); + const bond=parseInt(document.getElementById('pr_bond').value)*1000000; + const fee=parseInt(document.getElementById('pr_fee').value)||10000; + await doSubmit('propose_outcome','type.googleapis.com/types.MessageProposeOutcome',encPropose(mid,res,propOut,bond),{fee},'btn_propose','pend_propose'); +}catch(e){toast(e.message,true);}}; + +// ── FILE DISPUTE +window.build_dispute=function(){try{ + const mid=document.getElementById('dis_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('dis_addr').value.trim().toLowerCase();addr40(addr,'Disputer'); + const bond=parseInt(document.getElementById('dis_bond').value)*1000000; + const fee=parseInt(document.getElementById('dis_fee').value)||10000; + showPL('diso','disp',buildUnsigned('file_dispute','type.googleapis.com/types.MessageFileDispute',encDispute(mid,addr,bond),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.signAndSubmit_dispute=async function(){try{ + const mid=document.getElementById('dis_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('dis_addr').value.trim().toLowerCase();addr40(addr,'Disputer'); + const bond=parseInt(document.getElementById('dis_bond').value)*1000000; + const fee=parseInt(document.getElementById('dis_fee').value)||10000; + await doSubmit('file_dispute','type.googleapis.com/types.MessageFileDispute',encDispute(mid,addr,bond),{fee},'btn_dispute','pend_dispute'); +}catch(e){toast(e.message,true);}}; + +// ── COMMIT VOTE +window.build_commit=function(){try{ + const mid=document.getElementById('cv_mid').value.trim().toLowerCase();mid40(mid); + const voter=document.getElementById('cv_voter').value.trim().toLowerCase();addr40(voter,'Voter'); + const hash=document.getElementById('cv_hash').value.trim().toLowerCase();if(hash.length!==64)throw new Error('Commit hash must be 64 hex chars'); + const fee=parseInt(document.getElementById('cv_fee').value)||10000; + showPL('cvo','cvp',buildUnsigned('commit_vote','type.googleapis.com/types.MessageCommitVote',encCommit(mid,voter,hash),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.signAndSubmit_commit=async function(){try{ + const mid=document.getElementById('cv_mid').value.trim().toLowerCase();mid40(mid); + const voter=document.getElementById('cv_voter').value.trim().toLowerCase();addr40(voter,'Voter'); + const hash=document.getElementById('cv_hash').value.trim().toLowerCase();if(hash.length!==64)throw new Error('Commit hash must be 64 hex chars'); + const fee=parseInt(document.getElementById('cv_fee').value)||10000; + await doSubmit('commit_vote','type.googleapis.com/types.MessageCommitVote',encCommit(mid,voter,hash),{fee},'btn_commit','pend_commit'); +}catch(e){toast(e.message,true);}}; + +// ── REVEAL VOTE +window.build_reveal=function(){try{ + const mid=document.getElementById('rv_mid').value.trim().toLowerCase();mid40(mid); + const voter=document.getElementById('rv_voter').value.trim().toLowerCase();addr40(voter,'Voter'); + const nonce=document.getElementById('rv_nonce').value.trim().toLowerCase();if(nonce.length!==64)throw new Error('Nonce must be 64 hex chars'); + const fee=parseInt(document.getElementById('rv_fee').value)||10000; + showPL('rvo','rvp',buildUnsigned('reveal_vote','type.googleapis.com/types.MessageRevealVote',encReveal(mid,voter,revOut,nonce),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.signAndSubmit_reveal=async function(){try{ + const mid=document.getElementById('rv_mid').value.trim().toLowerCase();mid40(mid); + const voter=document.getElementById('rv_voter').value.trim().toLowerCase();addr40(voter,'Voter'); + const nonce=document.getElementById('rv_nonce').value.trim().toLowerCase();if(nonce.length!==64)throw new Error('Nonce must be 64 hex chars'); + const fee=parseInt(document.getElementById('rv_fee').value)||10000; + await doSubmit('reveal_vote','type.googleapis.com/types.MessageRevealVote',encReveal(mid,voter,revOut,nonce),{fee},'btn_reveal','pend_reveal'); +}catch(e){toast(e.message,true);}}; + +// ── TALLY VOTES +window.build_tally=function(){try{ + const mid=document.getElementById('tal_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('tal_addr').value.trim().toLowerCase();addr40(addr,'Caller'); + const fee=parseInt(document.getElementById('tal_fee').value)||10000; + showPL('talo','talp',buildUnsigned('tally_votes','type.googleapis.com/types.MessageTallyVotes',encTally(mid,addr),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.signAndSubmit_tally=async function(){try{ + const mid=document.getElementById('tal_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('tal_addr').value.trim().toLowerCase();addr40(addr,'Caller'); + const fee=parseInt(document.getElementById('tal_fee').value)||10000; + await doSubmit('tally_votes','type.googleapis.com/types.MessageTallyVotes',encTally(mid,addr),{fee},'btn_tally','pend_tally'); +}catch(e){toast(e.message,true);}}; + +// ── FINALIZE MARKET +window.build_finalize=function(){try{ + const mid=document.getElementById('fin_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('fin_addr').value.trim().toLowerCase();addr40(addr,'Caller'); + const fee=parseInt(document.getElementById('fin_fee').value)||10000; + showPL('fino','finp',buildUnsigned('finalize_market','type.googleapis.com/types.MessageFinalizeMarket',encFinalize(mid,addr),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.signAndSubmit_finalize=async function(){try{ + const mid=document.getElementById('fin_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('fin_addr').value.trim().toLowerCase();addr40(addr,'Caller'); + const fee=parseInt(document.getElementById('fin_fee').value)||10000; + await doSubmit('finalize_market','type.googleapis.com/types.MessageFinalizeMarket',encFinalize(mid,addr),{fee},'btn_finalize','pend_finalize'); +}catch(e){toast(e.message,true);}}; + +// ── CLAIM SLASH +window.build_slash=function(){try{ + const mid=document.getElementById('sl_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('sl_addr').value.trim().toLowerCase();addr40(addr,'Claimant'); + const fee=parseInt(document.getElementById('sl_fee').value)||10000; + showPL('slo','slp',buildUnsigned('claim_slash','type.googleapis.com/types.MessageClaimSlash',encSlash(mid,addr),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.signAndSubmit_slash=async function(){try{ + const mid=document.getElementById('sl_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('sl_addr').value.trim().toLowerCase();addr40(addr,'Claimant'); + const fee=parseInt(document.getElementById('sl_fee').value)||10000; + await doSubmit('claim_slash','type.googleapis.com/types.MessageClaimSlash',encSlash(mid,addr),{fee},'btn_slash','pend_slash'); +}catch(e){toast(e.message,true);}}; + +// ═══════════════════════════════════════════ +// MAINNET POLISH — UI ONLY, NO CHAIN LOGIC +// ═══════════════════════════════════════════ + +// PRX denomination — 1 PRX = 1 PRX (no micro conversion) + +// Copy to clipboard +window.copyText = async function(text, btn) { + try { + await navigator.clipboard.writeText(text); + if (btn) { btn.textContent = '✓'; btn.classList.add('ok'); setTimeout(() => { btn.textContent = '⎘'; btn.classList.remove('ok'); }, 1800); } + toast('Copied'); + } catch { toast('Copy failed', true); } +}; + +// Wire copy buttons to derived address and pubkey after key load +function wireCopyBtns() { + const pairs = [ + ['sk_addr', 'copy_sk_addr'], + ['sk_pub', 'copy_sk_pub'], + ]; + pairs.forEach(([srcId, btnId]) => { + const btn = document.getElementById(btnId); + if (!btn) return; + btn.onclick = function() { + const el = document.getElementById(srcId); + copyText(el ? el.textContent.trim() : '', this); + }; + }); + // payload boxes + document.querySelectorAll('.payload-box textarea').forEach(ta => { + const box = ta.closest('.payload-box'); + if (!box || box.querySelector('.copy-payload-btn')) return; + const b = document.createElement('button'); + b.className = 'btn bg bsm copy-payload-btn'; + b.style.cssText = 'margin-top:6px;font-size:10px'; + b.textContent = '⎘ Copy payload'; + b.onclick = function() { copyText(ta.value, this); }; + box.appendChild(b); + }); +} + +// Inject copy buttons into derived key display +function injectKeyboardCopyBtns() { + const addrEl = document.getElementById('sk_addr'); + const pubEl = document.getElementById('sk_pub'); + if (addrEl && !document.getElementById('copy_sk_addr')) { + const wrap = document.createElement('div'); + wrap.className = 'cwrap'; + addrEl.parentNode.insertBefore(wrap, addrEl); + wrap.appendChild(addrEl); + const btn = document.createElement('button'); + btn.id = 'copy_sk_addr'; btn.className = 'cbtn'; btn.textContent = '⎘'; + btn.title = 'Copy address'; + wrap.appendChild(btn); + } + if (pubEl && !document.getElementById('copy_sk_pub')) { + const wrap = document.createElement('div'); + wrap.className = 'cwrap'; + pubEl.parentNode.insertBefore(wrap, pubEl); + wrap.appendChild(pubEl); + const btn = document.createElement('button'); + btn.id = 'copy_sk_pub'; btn.className = 'cbtn'; btn.textContent = '⎘'; + btn.title = 'Copy pubkey'; + wrap.appendChild(btn); + } + wireCopyBtns(); +} + +// Confirm modal +let _confirmResolve = null; +window.closeConfirm = function() { + document.getElementById('confOverlay').classList.remove('open'); + if (_confirmResolve) { _confirmResolve(false); _confirmResolve = null; } +}; +document.getElementById('confOk').onclick = function() { + document.getElementById('confOverlay').classList.remove('open'); + if (_confirmResolve) { _confirmResolve(true); _confirmResolve = null; } +}; +document.getElementById('confOverlay').addEventListener('click', function(e) { + if (e.target === this) closeConfirm(); +}); + +function showConfirm(title, rows) { + return new Promise(resolve => { + _confirmResolve = resolve; + document.getElementById('confTitle').textContent = title; + document.getElementById('confSub').textContent = 'review before signing · canopy network'; + const rowsEl = document.getElementById('confRows'); + rowsEl.innerHTML = rows.map(([l, v, cls]) => + `
${l}${v}
` + ).join(''); + document.getElementById('confOverlay').classList.add('open'); + }); +} + +// Patch signAndSubmit_* functions with confirm gate +// We wrap — originals are preserved, just called after confirmation +(function() { + const v = id => parseInt(document.getElementById(id)?.value)||0; + const patches = { + signAndSubmit_create: () => [ + 'Create Market', [ + ['Question', document.getElementById('c_question')?.value || '—', ''], + ['B0 Liquidity', v('c_b0').toLocaleString()+' PRX', 'g'], + ['Fee', v('c_fee')+' PRX', ''], + ] + ], + signAndSubmit_predict: () => [ + 'Submit Prediction', [ + ['Market ID', (document.getElementById('p_mid')?.value||'').slice(0,16)+'…', ''], + ['Outcome', (window._selectedOut!==false?'YES':'NO'), window._selectedOut!==false?'green':'red'], + ['Shares', v('p_shares').toLocaleString()+' PRX', ''], + ['Max Cost', v('p_maxcost').toLocaleString()+' PRX', ''], + ] + ], + signAndSubmit_claim: () => [ + 'Claim Winnings', [ + ['Market ID', (document.getElementById('cl_mid')?.value||'').slice(0,16)+'…', ''], + ['Claimant', (document.getElementById('cl_addr')?.value||'').slice(0,16)+'…', ''], + ] + ], + signAndSubmit_register: () => [ + 'Register Resolver', [ + ['Address', (document.getElementById('reg_addr')?.value||'').slice(0,16)+'…', ''], + ['Stake', (parseInt(document.getElementById('reg_stake')?.value||0)).toLocaleString()+' PRX', 'g'], + ] + ], + signAndSubmit_propose: () => [ + 'Propose Outcome', [ + ['Market ID', (document.getElementById('pr_mid')?.value||'').slice(0,16)+'…', ''], + ['Outcome', (window._propOut!==false?'YES':'NO'), window._propOut!==false?'green':'red'], + ['Bond', v('prop_bond').toLocaleString()+' PRX', ''], + ] + ], + signAndSubmit_dispute: () => [ + 'File Dispute', [ + ['Market ID', (document.getElementById('dis_mid')?.value||'').slice(0,16)+'…', ''], + ['Bond', v('dis_bond').toLocaleString()+' PRX', ''], + ] + ], + signAndSubmit_commit: () => [ + 'Commit Vote', [ + ['Market ID', (document.getElementById('cv_mid')?.value||'').slice(0,16)+'…', ''], + ['Commit Hash', (document.getElementById('cv_hash')?.value||'').slice(0,16)+'…', ''], + ] + ], + signAndSubmit_reveal: () => [ + 'Reveal Vote', [ + ['Market ID', (document.getElementById('rv_mid')?.value||'').slice(0,16)+'…', ''], + ['Vote', (window._revOut!==false?'YES':'NO'), window._revOut!==false?'green':'red'], + ] + ], + signAndSubmit_tally: () => [ + 'Tally Votes', [ + ['Market ID', (document.getElementById('tal_mid')?.value||'').slice(0,16)+'…', ''], + ] + ], + signAndSubmit_finalize: () => [ + 'Finalize Market', [ + ['Market ID', (document.getElementById('fin_mid')?.value||'').slice(0,16)+'…', ''], + ] + ], + signAndSubmit_slash: () => [ + 'Claim Slash', [ + ['Market ID', (document.getElementById('sl_mid')?.value||'').slice(0,16)+'…', ''], + ['Claimant', (document.getElementById('sl_addr')?.value||'').slice(0,16)+'…', ''], + ] + ], + signAndSubmit_claimcreator: () => [ + 'Claim Creator Fee', [ + ['Market ID', (document.getElementById('ccf_mid')?.value||'').slice(0,16)+'…', ''], + ['Creator', (document.getElementById('ccf_addr')?.value||'').slice(0,16)+'…', ''], + ] + ], + signAndSubmit_cancel: () => [ + 'Cancel Market', [ + ['Market ID', (document.getElementById('can_mid')?.value||'').slice(0,16)+'…', ''], + ['Creator', (document.getElementById('can_addr')?.value||'').slice(0,16)+'…', ''], + ] + ], + signAndSubmit_unstake_resolver: () => [ + 'Unstake Resolver', [ + ['Resolver', (document.getElementById('unst_addr')?.value||'').slice(0,16)+'…', ''], + ['Amount', (parseInt(document.getElementById('unst_amount')?.value||0)).toLocaleString()+' PRX (0 = full exit)', ''], + ] + ], + signAndSubmit_claim_unbonded: () => [ + 'Claim Unbonded Stake', [ + ['Resolver', (document.getElementById('cub_addr')?.value||'').slice(0,16)+'…', ''], + ] + ], + signAndSubmit_send: () => [ + 'Send $PRX', [ + ['To', (document.getElementById('s_to')?.value||'').slice(0,16)+'…', ''], + ['Amount', v('s_amount').toLocaleString()+' PRX', 'g'], + ] + ], + }; + + // Expose outcome vars so patches can read them + // (they already exist as module-level vars; we shadow-expose via a getter trick) + Object.defineProperty(window, '_selectedOut', { get: () => typeof selectedOut !== 'undefined' ? selectedOut : true }); + Object.defineProperty(window, '_resolveOut', { get: () => typeof resolveOut !== 'undefined' ? resolveOut : true }); + Object.defineProperty(window, '_propOut', { get: () => typeof propOut !== 'undefined' ? propOut : true }); + Object.defineProperty(window, '_revOut', { get: () => typeof revOut !== 'undefined' ? revOut : true }); + + Object.keys(patches).forEach(name => { + const orig = window[name]; + if (!orig) return; + window[name] = async function() { + const [title, rows] = patches[name](); + const ok = await showConfirm(title, rows); + if (ok) await orig(); + }; + }); +})(); + +// Offline banner wired to RPC status +const _origCheckRPC = window.checkRPC; +window.checkRPC = async function() { + try { + await _origCheckRPC(); + document.getElementById('offBanner').classList.remove('show'); + } catch { + document.getElementById('offBanner').classList.add('show'); + } +}; + +// Session badge visibility +const _origLoadKey = window.loadKey; +window.loadKey = async function() { + await _origLoadKey(); + const badge = document.getElementById('sessBadge'); + if (badge) badge.classList.remove('hidden'); + injectKeyboardCopyBtns(); + setTimeout(wireCopyBtns, 100); +}; +const _origClearKey = window.clearKey; +window.clearKey = function() { + _origClearKey(); + const badge = document.getElementById('sessBadge'); + if (badge) badge.classList.add('hidden'); +}; + +// Wire payload copy buttons when pages are shown +const _origShowPage = window.showPage; +window.showPage = function(id, btn) { + _origShowPage(id, btn); + setTimeout(wireCopyBtns, 50); +}; + +// Init copy btn injection +injectKeyboardCopyBtns(); + +// ═══════════════════════════════════════════ +// INIT +// ═══════════════════════════════════════════ +const _niHost=document.getElementById('ni_host');if(_niHost)_niHost.value=getRPCHost(); +buildMobNav(); +checkRPC(); +setInterval(checkRPC,12000); + +// ═══════════════════════════════════════════ +// KEYSTORE — AES-GCM + Argon2id (Canopy official format) +// Uses argon2-bundled.min.js (must be served alongside app.js) +// ═══════════════════════════════════════════ + +// Argon2id params matching Canopy CLI keystore +const ARGON2_TIME = 3; +const ARGON2_MEM = 65536; // 64 MB +const ARGON2_THREADS = 4; +const ARGON2_KEYLEN = 32; + +async function deriveKeyArgon2(password, salt) { + // argon2-bundled exposes window.argon2 + if (!window.argon2) throw new Error('Argon2 library not loaded — ensure argon2-bundled.min.js is present'); + const result = await window.argon2.hash({ + pass: password, + salt: salt, // Uint8Array + time: window._argon2Override?.time || ARGON2_TIME, + mem: window._argon2Override?.mem || ARGON2_MEM, + hashLen: window._argon2Override?.keylen || ARGON2_KEYLEN, + parallelism: window._argon2Override?.threads || ARGON2_THREADS, + type: window.argon2.ArgonType.Argon2id, + }); + // result.hash is Uint8Array of 32 bytes — import as AES-GCM key + return crypto.subtle.importKey('raw', result.hash, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']); +} + +async function encryptKey(privKeyBytes, password) { + const salt = crypto.getRandomValues(new Uint8Array(16)); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const key = await deriveKeyArgon2(password, salt); + const enc = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, privKeyBytes); + return { + kdf: 'argon2id', + salt: b2h(salt), + iv: b2h(iv), + encrypted: b2h(new Uint8Array(enc)), + argon2: { time: ARGON2_TIME, mem: ARGON2_MEM, threads: ARGON2_THREADS, keylen: ARGON2_KEYLEN }, + }; +} + +async function decryptKey(encrypted, iv, salt, password, kdf) { + let key, nonce; + if (kdf === 'canopy') { + // Canopy CLI format: Argon2i (not id), mem=32MB, keyLen=32, nonce=key[:12] + if (!window.argon2) throw new Error('Argon2 library not loaded'); + const result = await window.argon2.hash({ + pass: password, salt: h2b(salt), + time: 3, mem: 32768, hashLen: 32, + parallelism: 4, type: window.argon2.ArgonType.Argon2i, + }); + const keyBytes = result.hash; // 32 bytes + nonce = keyBytes.slice(0, 12); // nonce = key[:12] + key = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['decrypt']); + } else if (!kdf || kdf === 'argon2id') { + key = await deriveKeyArgon2(password, h2b(salt)); + nonce = h2b(iv); + } else { + // legacy PBKDF2 fallback + const enc = new TextEncoder(); + const km = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveKey']); + key = await crypto.subtle.deriveKey( + { name: 'PBKDF2', salt: h2b(salt), iterations: 200000, hash: 'SHA-256' }, + km, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'] + ); + nonce = h2b(iv); + } + const dec = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: nonce }, key, h2b(encrypted)); + return new Uint8Array(dec); +} + +window.createKeystore = async function() { + const pw = document.getElementById('ks_new_pw').value; + const pw2 = document.getElementById('ks_new_pw2').value; + if (!pw) return toast('Enter a password', true); + if (pw !== pw2) return toast('Passwords do not match', true); + if (pw.length < 8) return toast('Password must be at least 8 characters', true); + + try { + // generate new BLS private key (valid scalar) + const privBytes = bls12_381.utils.randomPrivateKey(); + const pubKey = bls12_381.getPublicKey(privBytes); + const hash = await crypto.subtle.digest('SHA-256', pubKey); + const address = b2h(new Uint8Array(hash).slice(0, 20)); + + const { salt, iv, encrypted } = await encryptKey(privBytes, pw); + + const keystore = { + version: 1, + kdf: 'argon2id', + publicKey: b2h(pubKey), + keyAddress: address, + salt, iv, encrypted, + argon2: { time: ARGON2_TIME, mem: ARGON2_MEM, threads: ARGON2_THREADS, keylen: ARGON2_KEYLEN }, + }; + + // download + const blob = new Blob([JSON.stringify(keystore, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; a.download = 'praxis-keystore-' + address.slice(0,8) + '.json'; + a.click(); URL.revokeObjectURL(url); + + // auto-load into session + signerPrivKey = privBytes; + signerPubKey = pubKey; + signerAddress = address; + updateSignerUI(); + toast('Keystore created and loaded'); + document.getElementById('ks_new_pw').value = ''; + document.getElementById('ks_new_pw2').value = ''; + } catch(e) { toast('Create failed: ' + e.message, true); } +}; + +window.checkSavedKeystore = function() { + const saved = localStorage.getItem('praxis_keystore'); + const wrap = document.getElementById('ks_quick_wrap'); + if (!wrap) return; + if (saved) { + const raw = JSON.parse(saved); + const addr = raw.keyAddress || '?'; + document.getElementById('ks_quick_addr').textContent = addr.slice(0,8) + '…' + addr.slice(-6); + wrap.style.display = ''; + } else { + wrap.style.display = 'none'; + } +}; + +window.quickUnlock = async function() { + const pw = document.getElementById('ks_quick_pw').value; + if (!pw) return toast('Enter password', true); + const saved = localStorage.getItem('praxis_keystore'); + if (!saved) return toast('No saved keystore', true); + try { + const raw = JSON.parse(saved); + if (!raw.encrypted || !raw.salt || !raw.iv || !raw.publicKey) throw new Error('Invalid saved keystore'); + if (raw.argon2) { window._argon2Override = raw.argon2; } else { window._argon2Override = null; } + let privBytes; + try { + privBytes = await decryptKey(raw.encrypted, raw.iv, raw.salt, pw, raw.kdf || 'argon2id'); + } catch(e) { + privBytes = await decryptKey(raw.encrypted, raw.iv, raw.salt, pw, 'pbkdf2'); + } + let pubKey = bls12_381.getPublicKey(privBytes); + if (b2h(pubKey) !== raw.publicKey) { + try { privBytes = await decryptKey(raw.encrypted, raw.iv, raw.salt, pw, 'pbkdf2'); pubKey = bls12_381.getPublicKey(privBytes); } catch(e2) {} + } + if (b2h(pubKey) !== raw.publicKey) throw new Error('Wrong password'); + const hash = await crypto.subtle.digest('SHA-256', pubKey); + const address = b2h(new Uint8Array(hash).slice(0, 20)); + signerPrivKey = privBytes; + signerPubKey = pubKey; + signerAddress = address; + updateSignerUI(); + toast('Session restored — ' + address.slice(0,8) + '…'); + document.getElementById('ks_quick_pw').value = ''; + } catch(e) { toast('Unlock failed: ' + e.message, true); } +}; + +window.importKeystore = async function() { + const pw = document.getElementById('ks_imp_pw').value; + const file = document.getElementById('ks_imp_file').files[0]; + if (!file) return toast('Select a keystore file', true); + if (!pw) return toast('Enter password', true); + + try { + const text = await file.text(); + const raw = JSON.parse(text); + + // Praxis flat format + if (!raw.encrypted || !raw.salt || !raw.iv || !raw.publicKey) throw new Error('Invalid keystore file'); + if (raw.argon2) { window._argon2Override = raw.argon2; } else { window._argon2Override = null; } + let privBytes; + try { + privBytes = await decryptKey(raw.encrypted, raw.iv, raw.salt, pw, raw.kdf || 'argon2id'); + } catch(e) { + privBytes = await decryptKey(raw.encrypted, raw.iv, raw.salt, pw, 'pbkdf2'); + } + let pubKey = bls12_381.getPublicKey(privBytes); + if (b2h(pubKey) !== raw.publicKey) { + // try pbkdf2 fallback + try { + privBytes = await decryptKey(raw.encrypted, raw.iv, raw.salt, pw, 'pbkdf2'); + pubKey = bls12_381.getPublicKey(privBytes); + } catch(e2) {} + } + if (b2h(pubKey) !== raw.publicKey) throw new Error('Wrong password or corrupted keystore'); + const hash = await crypto.subtle.digest('SHA-256', pubKey); + const address = b2h(new Uint8Array(hash).slice(0, 20)); + signerPrivKey = privBytes; + signerPubKey = pubKey; + signerAddress = address; + updateSignerUI(); + toast('Keystore unlocked — ' + address.slice(0,8) + '…'); + localStorage.setItem('praxis_keystore', JSON.stringify(raw)); + document.getElementById('ks_imp_pw').value = ''; + document.getElementById('ks_imp_file').value = ''; + checkSavedKeystore(); + } catch(e) { console.error('Import failed full error:', e); toast('Import failed: ' + e.message, true); } +}; + +function updateSignerUI() { + document.getElementById('keyStatus').className = 'kstat loaded'; + document.getElementById('keyStatus').textContent = '✓ loaded — ' + signerAddress.slice(0,16) + '…'; + document.getElementById('sk_derived').style.display = 'block'; + document.getElementById('sk_pub').textContent = b2h(signerPubKey); + document.getElementById('sk_addr').textContent = signerAddress; + ['c_creator','p_bettor','r_resolver','cl_addr','s_from','w_addr','ft_addr', + 'reg_addr','pr_resolver','dis_addr','cv_voter','rv_voter','tal_addr','fin_addr','sl_addr', + 'fo_resolver','rc_addr','ccf_addr','can_addr','unst_addr','cub_addr'].forEach(id => { + const el = document.getElementById(id); if (el && !el.value) el.value = signerAddress; + }); + const badge = document.getElementById('sessBadge'); + if (badge) badge.classList.remove('hidden'); + refreshBalance(); + loadMyPredictions(); + injectKeyboardCopyBtns(); + setTimeout(wireCopyBtns, 100); +} + +// ═══════════════════════════════════════════ +// METAMASK +// ═══════════════════════════════════════════ +let mmAddress = null; + +window.connectMetaMask = async function() { + if (!window.ethereum) return toast('MetaMask not detected — install MetaMask first', true); + try { + const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); + if (!accounts || accounts.length === 0) return toast('No accounts returned', true); + mmAddress = accounts[0].toLowerCase(); + updateMMUI(true); + toast('MetaMask connected — ' + mmAddress.slice(0,8) + '…'); + } catch(e) { + toast('MetaMask connection failed: ' + e.message, true); + } +}; + +window.disconnectMetaMask = function() { + mmAddress = null; + updateMMUI(false); + toast('MetaMask disconnected'); +}; + +function updateMMUI(connected) { + const disc = document.getElementById('mm_disconnected'); + const conn = document.getElementById('mm_connected'); + const addr = document.getElementById('mm_addr'); + const status = document.getElementById('mm_status'); + if (!disc || !conn) return; + if (connected) { + disc.style.display = 'none'; + conn.style.display = 'block'; + if (addr) addr.textContent = mmAddress; + if (status) status.textContent = '✓ connected — ' + mmAddress.slice(0,8) + '…'; + } else { + disc.style.display = 'block'; + conn.style.display = 'none'; + } +} + +// auto-reconnect if already authorized +(async () => { + if (!window.ethereum) return; + try { + const accounts = await window.ethereum.request({ method: 'eth_accounts' }); + if (accounts && accounts.length > 0) { + mmAddress = accounts[0].toLowerCase(); + updateMMUI(true); + } + } catch {} +})(); + +// listen for account changes +if (window.ethereum) { + window.ethereum.on('accountsChanged', (accounts) => { + if (accounts.length === 0) { mmAddress = null; updateMMUI(false); } + else { mmAddress = accounts[0].toLowerCase(); updateMMUI(true); } + }); +} + +// ═══════════════════════════════════════════ +// SCAN CACHE +// ═══════════════════════════════════════════ +window.clearScanCache = function() { + localStorage.removeItem('praxis_tx_cache'); + localStorage.removeItem('praxis_scan_height'); + toast('Cache cleared — full rescan on next refresh'); + loadMarkets(); +}; + +// ═══════════════════════════════════════════ +// ERROR CODES +// ═══════════════════════════════════════════ +const PRAXIS_ERRORS = { + 124: 'Market has not expired yet — propose_outcome is only callable after expiry.', + 181: 'Cannot finalize — dispute window is still open. Wait for the dispute period to close.', + 4001: 'Resolver has an open position in this market. Use Forfeit Position before proposing.', + 4002: 'Market creator cannot act as resolver for their own market.', + 4003: 'This prediction exceeds the 20% position cap for one side. Try a smaller amount.', + 4010: 'Storage error — please try again or contact support.', + 195: 'Dispute panel could not be formed', + 196: 'This market is not eligible for reclaim', + 197: "Reclaim window hasn't opened yet — wait 300 blocks after expiry", + 198: 'Nothing to reclaim for this wallet', + 199: 'You hold a position in this market and cannot act as resolver. Transfer or forfeit your shares first.', + 200: 'The market creator cannot resolve their own market.', + 201: 'This prediction would exceed the 20% per-address position cap for this market. Try a smaller amount.', + 202: 'Resolver stake below minimum — 500,000 PRX required.', + 203: 'Cooldown period has not elapsed yet.', + 204: 'Pool is empty — nothing to claim.', + 205: 'Market is not finalized.', + 207: 'Resolver RRS is zero — not eligible for rewards.', + 208: 'No successful resolutions in this epoch.', + 210: 'Active proposal exists — unstake not allowed.', + 211: 'Resolver is not active.', + 212: 'No unbonding stake to claim.', + 213: 'Unbonding period not complete.', + 214: 'Resolver record not found.', + 215: 'Market has expired.', + 216: 'Market has positions — cannot cancel.', + 217: 'Unbonding already pending.', +}; + +function friendlyError(code, msg) { + if (!code && msg) { const m = msg.match(/"code":(\d+)/); if (m) code = parseInt(m[1]); } + if (code && PRAXIS_ERRORS[code]) return PRAXIS_ERRORS[code]; + return msg || 'Unknown error'; +} + +// ═══════════════════════════════════════════ +// RECLAIM STAKE +// ═══════════════════════════════════════════ +window.build_reclaim = function() { + try { + const mid = document.getElementById('rc_mid').value.trim().toLowerCase(); + const addr = document.getElementById('rc_addr').value.trim().toLowerCase(); + const fee = parseInt(document.getElementById('rc_fee').value) || 10000; + addr40(mid, 'Market ID'); addr40(addr, 'Claimant Address'); + showPL('rco','rcp', buildUnsigned('reclaim_stake','type.googleapis.com/types.MessageReclaimStake', encReclaim(mid,addr),{fee})); + toast('Payload built'); + } catch(e) { toast(e.message, true); } +}; + +window.signAndSubmit_reclaim = async function() { + const mid = document.getElementById('rc_mid').value.trim().toLowerCase(); + const addr = document.getElementById('rc_addr').value.trim().toLowerCase(); + const fee = parseInt(document.getElementById('rc_fee').value) || 10000; + try { + addr40(mid,'Market ID'); addr40(addr,'Claimant Address'); + } catch(e) { return toast(e.message, true); } + await doSubmit('reclaim_stake','type.googleapis.com/types.MessageReclaimStake', encReclaim(mid,addr),{fee},'btn_reclaim','pend_reclaim'); +}; + +window.fillReclaim = function(id) { + document.getElementById('rc_mid').value = id; + if (signerAddress) document.getElementById('rc_addr').value = signerAddress; + showPage('reclaim', null); +}; + +// ═══════════════════════════════════════════ +// ROLE-BASED SIDEBAR +// ═══════════════════════════════════════════ +async function checkRoles() { + if (!signerAddress) return; + + // Superadmin — full access regardless of role + const SUPERADMIN = '8e14dc0ce537f1c75036f11d7495d60882aa6731'; + if (signerAddress.toLowerCase() === SUPERADMIN) { + document.getElementById('nav-resolver-section').style.display = ''; + document.querySelectorAll('.nav-resolver-item').forEach(el => el.style.display = ''); + document.getElementById('nav-admin-section').style.display = ''; + document.querySelectorAll('.nav-admin-item').forEach(el => el.style.display = ''); + return; + } + + // Admin section — superadmin only (handled above) + document.getElementById('nav-admin-section').style.display = 'none'; + document.querySelectorAll('.nav-admin-item').forEach(el => el.style.display = 'none'); + + // Check RESOLVER — has a register_resolver tx in scanned data + const isResolver = _allMarkets.length >= 0 && (() => { + const cache = localStorage.getItem('praxis_tx_cache'); + if (!cache) return false; + try { + const txs = JSON.parse(cache); + return txs.some(tx => + tx.messageType === 'register_resolver' && + tx.sender && tx.sender.toLowerCase() === signerAddress.toLowerCase() + ); + } catch { return false; } + })(); + + document.getElementById('nav-resolver-section').style.display = isResolver ? '' : 'none'; + document.querySelectorAll('.nav-resolver-item').forEach(el => el.style.display = isResolver ? '' : 'none'); +} + +// Run role check after key load and after markets load +const _origUpdateSignerUI = updateSignerUI; +updateSignerUI = function() { + _origUpdateSignerUI(); + checkRoles(); +}; + +// ═══════════════════════════════════════════ +// COI-3 POSITION CAP CHECK +// ═══════════════════════════════════════════ +window.checkPositionCap = async function() { + const mid = document.getElementById('p_mid').value.trim().toLowerCase(); + const bettor = document.getElementById('p_bettor').value.trim().toLowerCase(); + const mc = (parseInt(document.getElementById('p_maxcost').value) || 0)*1000000; + const capEl = document.getElementById('cap_indicator'); + const btn = document.getElementById('btn_predict'); + + if (!capEl) return; + if (!mid || mid.length !== 40 || !bettor || bettor.length !== 40) { + capEl.style.display = 'none'; + return; + } + + // find market in _allMarkets + const m = _allMarkets.find(x => x.marketId === mid || x.txHash === mid); + if (!m) { capEl.style.display = 'none'; return; } + + const pool = Number(m.qYes + m.qNo); + const cap = Math.floor(pool * 2000 / 10000); // 20% + + // try to get user's current cost paid from chain + let costPaid = 0; + try { + const d = await rpc('/v1/query/account', { address: bettor }); + // costPaid not available without plugin query — use 0 for now + costPaid = 0; + } catch {} + + const newTotal = costPaid + mc; + const remaining = cap - costPaid; + const pct = pool > 0 ? Math.round((newTotal / pool) * 100) : 0; + const over = newTotal > cap; + + capEl.style.display = ''; + if (over) { + capEl.style.background = 'rgba(255,61,90,.08)'; + capEl.style.border = '1px solid rgba(255,61,90,.3)'; + capEl.style.color = 'var(--red)'; + capEl.textContent = '⚠ Exceeds 20% position cap — max ' + fmtPRX(remaining) + ' PRX remaining'; + if (btn) btn.setAttribute('disabled', ''); + } else { + capEl.style.background = 'rgba(0,232,122,.05)'; + capEl.style.border = '1px solid rgba(0,232,122,.15)'; + capEl.style.color = 'var(--text2)'; + capEl.textContent = 'Position: ' + fmtPRX(newTotal) + ' PRX / Cap: ' + fmtPRX(cap) + ' PRX (' + pct + '% of pool)'; + if (btn) btn.removeAttribute('disabled'); + } +}; + +// ═══════════════════════════════════════════ +// FORFEIT POSITION +// ═══════════════════════════════════════════ +window.build_forfeit = function() { + const mid = document.getElementById('fo_mid').value.trim().toLowerCase(); + const resolver = document.getElementById('fo_resolver').value.trim().toLowerCase(); + const fee = parseInt(document.getElementById('fo_fee').value) || 10000; + mid40(mid); addr40(resolver, 'Resolver Address'); + const inner = encForfeit(mid, resolver); + showPL('foo','fop', buildUnsigned('forfeit_position','type.googleapis.com/types.MessageForfeitPosition',inner,{fee})); + toast('Payload built'); +}; + +window.signAndSubmit_forfeit = async function() { + const mid = document.getElementById('fo_mid').value.trim().toLowerCase(); + const resolver = document.getElementById('fo_resolver').value.trim().toLowerCase(); + const fee = parseInt(document.getElementById('fo_fee').value) || 10000; + try { mid40(mid); addr40(resolver, 'Resolver Address'); } catch(e) { return toast(e.message, true); } + const inner = encForfeit(mid, resolver); + await doSubmit('forfeit_position','type.googleapis.com/types.MessageForfeitPosition',inner,{fee},'btn_forfeit','pend_forfeit'); +}; + +window.fillForfeit = function(id) { + document.getElementById('fo_mid').value = id; + if (signerAddress) document.getElementById('fo_resolver').value = signerAddress; + showPage('forfeit', null); +}; +window.fillPropose = function(id) { + document.getElementById('pr_mid').value = id; + if (signerAddress) document.getElementById('pr_resolver').value = signerAddress; + showPage('propose', null); + setTimeout(() => { updateMinBondHint(); updateProposeRisk(); }, 50); +}; + +window.updateMinBondHint = function() { + const mid = document.getElementById('pr_mid').value.trim().toLowerCase(); + const hint = document.getElementById('prop_bond_hint'); + const bondEl = document.getElementById('pr_bond'); + if (!hint) return; + if (!mid || mid.length !== 40) { + hint.textContent = 'Enter Market ID to compute min bond'; + hint.style.color = ''; + return; + } + const m = _allMarkets.find(x => x.marketId === mid || x.txHash === mid); + if (!m) { + hint.textContent = 'Market not found in cache — browse Markets first'; + hint.style.color = 'var(--red)'; + return; + } + // BEff = current pool size (qYes + qNo) + const beff = Number(m.qYes + m.qNo) / 1_000_000; + const onePct = beff * 0.01; + const minBond = Math.max(onePct, 60); + hint.textContent = 'Min bond: ' + minBond.toFixed(2) + ' PRX (max(1% of pool, 60 PRX) — deducted from resolver stake)'; + hint.style.color = 'var(--amber)'; + const bondEl2 = document.getElementById('pr_bond'); if (bondEl2 && parseFloat(bondEl2.value) < minBond) bondEl2.value = Math.ceil(minBond); +}; +checkSavedKeystore(); + + +// ═══════════════════════════════════════════ +// ELEVATED RISK / PANEL SIZE INDICATOR +// ═══════════════════════════════════════════ +const ELEVATED_RISK_THRESHOLD = 25_000_000_000n; // 25,000 PRX in uPRX +const STANDARD_PANEL_SIZE = 5; +const ELEVATED_PANEL_SIZE = 7; + +function getRiskInfo(mid) { + if (!mid || mid.length !== 40) return null; + const m = (_allMarkets || []).find(x => x.marketId === mid || x.txHash === mid); + if (!m) return null; + const pool = m.qYes + m.qNo; + const elevated = pool >= ELEVATED_RISK_THRESHOLD; + const poolPRX = (Number(pool) / 1_000_000).toFixed(2); + return { elevated, pool, poolPRX, panelSize: elevated ? ELEVATED_PANEL_SIZE : STANDARD_PANEL_SIZE }; +} + +function renderRiskBox(boxEl, info) { + if (!info) { + boxEl.style.display = 'none'; + return; + } + const { elevated, poolPRX, panelSize } = info; + boxEl.style.display = ''; + if (elevated) { + boxEl.style.background = 'rgba(255,64,96,.08)'; + boxEl.style.border = '1px solid rgba(255,64,96,.25)'; + boxEl.style.color = 'var(--red)'; + boxEl.innerHTML = '⚠ ELEVATED RISK MARKET
Pool: ' + poolPRX + ' PRX (>= 25,000 PRX threshold)
Panel size: ' + panelSize + ' resolvers (extended panel)'; + } else { + boxEl.style.background = 'var(--gdim)'; + boxEl.style.border = '1px solid rgba(0,232,122,.15)'; + boxEl.style.color = 'var(--text2)'; + boxEl.innerHTML = '✓ Standard market
Pool: ' + poolPRX + ' PRX
Panel size: ' + panelSize + ' resolvers (standard panel)'; + } +} + +window.updateProposeRisk = function() { + const mid = (document.getElementById('pr_mid')?.value || '').trim().toLowerCase(); + const box = document.getElementById('pr_risk_box'); + if (!box) return; + renderRiskBox(box, getRiskInfo(mid)); + // also update bond hint + if (typeof updateMinBondHint === 'function') updateMinBondHint(); +}; + +window.updateDisputeRisk = function() { + const mid = (document.getElementById('di_mid')?.value || '').trim().toLowerCase(); + const box = document.getElementById('di_risk_box'); + if (!box) return; + renderRiskBox(box, getRiskInfo(mid)); +}; + + + + +// ═══════════════════════════════════════════ +// MARKET BANNER IMAGE SYSTEM +// ═══════════════════════════════════════════ + +function mkBannerImg(rules) { + const u = extractImg(rules||''); + if (!u) return ''; + return ''; +} + +function extractImg(rules) { + if (!rules) return ''; + const m = rules.match(/\[IMG:([^\]]+)\]/); + return m ? m[1].trim() : ''; +} + +function stripImgTag(rules) { + if (!rules) return ''; + return rules.replace(/\[IMG:[^\]]+\]\s*/g, '').trim(); +} + +function buildRulesWithImg(rules, imgUrl) { + const stripped = stripImgTag(rules); + if (!imgUrl) return stripped; + return stripped + (stripped ? ' ' : '') + '[IMG:' + imgUrl.trim() + ']'; +} + +window.previewBanner = function() { + const url = (document.getElementById('c_img')?.value || '').trim(); + const preview = document.getElementById('c_img_preview'); + const img = document.getElementById('c_img_preview_img'); + const hint = document.getElementById('c_img_hint'); + if (!preview || !img) return; + if (url && (url.startsWith('http') || url.startsWith('ipfs'))) { + img.src = url; + preview.style.display = ''; + img.onload = () => { if (hint) { hint.textContent = '✓ Image loaded'; hint.style.color = 'var(--green)'; } }; + img.onerror = () => { + preview.style.display = 'none'; + if (hint) { hint.textContent = '✗ Could not load image — check URL or CORS policy'; hint.style.color = 'var(--red)'; } + }; + } else { + preview.style.display = 'none'; + if (hint) { hint.textContent = 'Image will be stored on-chain via IPFS or direct URL. Recommended: 16:9, min 800x450px.'; hint.style.color = ''; } + } +}; + +// ═══════════════════════════════════════════ +// EXPIRY DATE → BLOCK HEIGHT CONVERTER +// ═══════════════════════════════════════════ +const BLOCK_TIME_MS = 5000; // 5s per block + +function blocksFromNow(ms) { + return Math.ceil(ms / BLOCK_TIME_MS); +} + +function fmtDuration(ms) { + const s = Math.floor(ms / 1000); + const d = Math.floor(s / 86400); + const h = Math.floor((s % 86400) / 3600); + const m = Math.floor((s % 3600) / 60); + if (d > 0) return d + 'd ' + h + 'h'; + if (h > 0) return h + 'h ' + m + 'm'; + return m + 'm'; +} + +window.updateExpiryFromDate = function() { + const dtEl = document.getElementById('c_expiry_dt'); + const hidden = document.getElementById('c_expiry'); + const hint = document.getElementById('c_expiry_hint'); + if (!dtEl || !hidden || !hint) return; + + const val = dtEl.value; + if (!val) { + hint.textContent = 'Select a date to compute block height'; + hint.style.color = ''; + hidden.value = ''; + return; + } + + const targetMs = new Date(val).getTime(); + const nowMs = Date.now(); + const diffMs = targetMs - nowMs; + + if (diffMs <= 0) { + hint.textContent = 'Date must be in the future'; + hint.style.color = 'var(--red)'; + hidden.value = ''; + return; + } + + const blocksNeeded = blocksFromNow(diffMs); + const blockHeight = currentHeight + blocksNeeded; + hidden.value = blockHeight; + + const dur = fmtDuration(diffMs); + hint.textContent = 'Block #' + blockHeight + ' (~' + dur + ' from now, ' + blocksNeeded + ' blocks)'; + hint.style.color = blocksNeeded < 100 ? 'var(--red)' : 'var(--amber)'; +}; + +// Set default expiry to 7 days from now when page loads +window.initExpiryDate = function() { + const dtEl = document.getElementById('c_expiry_dt'); + if (!dtEl || dtEl.value) return; + const d = new Date(Date.now() + 7 * 24 * 3600 * 1000); + // Format: YYYY-MM-DDTHH:MM + const pad = n => String(n).padStart(2, '0'); + dtEl.value = d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) + 'T' + pad(d.getHours()) + ':' + pad(d.getMinutes()); + updateExpiryFromDate(); +}; + +// ═══════════════════════════════════════════ +// CATEGORY SYSTEM +// ═══════════════════════════════════════════ +const CAT_LABELS = { + crypto: '🪙 Crypto', sports: '⚽ Sports', politics: '🗳 Politics', + finance: '📈 Finance', other: '◈ Other' +}; + +function extractCat(rules) { + if (!rules) return 'other'; + const m = rules.match(/^\[CAT:(\w+)\]/); + return m ? m[1] : 'other'; +} + +function stripCatPrefix(rules) { + if (!rules) return ''; + return rules.replace(/^\[CAT:\w+\]\s*/, ''); +} + +function buildRulesWithCat(cat, rules) { + const stripped = stripCatPrefix(rules); + return '[CAT:' + cat + '] ' + stripped; +} + +window.pickCat = function(el) { + document.querySelectorAll('#c_cat_pick .cpick').forEach(e => e.classList.remove('active')); + el.classList.add('active'); +}; + + + +// ═══════════════════════════════════════════ +// CLAIM CREATOR FEE +// ═══════════════════════════════════════════ +function encClaimCreatorFee(mid,creator){return cat(bf(1,h2b(mid)),bf(2,h2b(creator)));} + +window.build_claimcreator=function(){ + try{ + const mid=document.getElementById('ccf_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('ccf_addr').value.trim().toLowerCase();addr40(addr,'Creator'); + const fee=parseInt(document.getElementById('ccf_fee').value)||10000; + showPL('ccfo','ccfp',buildUnsigned('claim_creator_fee','type.googleapis.com/types.MessageClaimCreatorFee',encClaimCreatorFee(mid,addr),{fee})); + toast('Payload built'); + }catch(e){toast(e.message,true);} +}; +window.signAndSubmit_claimcreator=async function(){ + try{ + const mid=document.getElementById('ccf_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('ccf_addr').value.trim().toLowerCase();addr40(addr,'Creator'); + const fee=parseInt(document.getElementById('ccf_fee').value)||10000; + await doSubmit('claim_creator_fee','type.googleapis.com/types.MessageClaimCreatorFee',encClaimCreatorFee(mid,addr),{fee},'btn_claimcreator','pend_claimcreator'); + }catch(e){toast(e.message,true);} +}; +window.fillClaimCreator=function(id){ + document.getElementById('ccf_mid').value=id; + if(signerAddress)document.getElementById('ccf_addr').value=signerAddress; + showPage('claimcreator',null); +}; + +// ═══════════════════════════════════════════ +// CANCEL MARKET +// ═══════════════════════════════════════════ +function encCancelMarket(mid,creator){return cat(bf(1,h2b(mid)),bf(2,h2b(creator)));} + +window.build_cancel=function(){ + try{ + const mid=document.getElementById('can_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('can_addr').value.trim().toLowerCase();addr40(addr,'Creator'); + const fee=parseInt(document.getElementById('can_fee').value)||10000; + showPL('cano','canp',buildUnsigned('cancel_market','type.googleapis.com/types.MessageCancelMarket',encCancelMarket(mid,addr),{fee})); + toast('Payload built'); + }catch(e){toast(e.message,true);} +}; +window.signAndSubmit_cancel=async function(){ + try{ + const mid=document.getElementById('can_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('can_addr').value.trim().toLowerCase();addr40(addr,'Creator'); + const fee=parseInt(document.getElementById('can_fee').value)||10000; + await doSubmit('cancel_market','type.googleapis.com/types.MessageCancelMarket',encCancelMarket(mid,addr),{fee},'btn_cancel','pend_cancel'); + }catch(e){toast(e.message,true);} +}; + +// ═══════════════════════════════════════════ +// UNSTAKE RESOLVER +// ═══════════════════════════════════════════ +window.build_unstake_resolver=function(){ + try{ + const addr=document.getElementById('unst_addr').value.trim().toLowerCase();addr40(addr,'Resolver'); + const amount=parseInt(document.getElementById('unst_amount').value||'0'); + const amountU=BigInt(amount)*1000000n; + const fee=parseInt(document.getElementById('unst_fee').value)||10000; + showPL('unsto','unstp',buildUnsigned('unstake_resolver','type.googleapis.com/types.MessageUnstakeResolver',encUnstakeResolver(addr,amountU),{fee})); + toast('Payload built'); + }catch(e){toast(e.message,true);} +}; +window.signAndSubmit_unstake_resolver=async function(){ + try{ + const addr=document.getElementById('unst_addr').value.trim().toLowerCase();addr40(addr,'Resolver'); + const amount=parseInt(document.getElementById('unst_amount').value||'0'); + const amountU=BigInt(amount)*1000000n; + const fee=parseInt(document.getElementById('unst_fee').value)||10000; + await doSubmit('unstake_resolver','type.googleapis.com/types.MessageUnstakeResolver',encUnstakeResolver(addr,amountU),{fee},'btn_unstake_resolver','pend_unstake_resolver'); + }catch(e){toast(e.message,true);} +}; + +// ═══════════════════════════════════════════ +// CLAIM UNBONDED STAKE +// ═══════════════════════════════════════════ +window.build_claim_unbonded=function(){ + try{ + const addr=document.getElementById('cub_addr').value.trim().toLowerCase();addr40(addr,'Resolver'); + const fee=parseInt(document.getElementById('cub_fee').value)||10000; + showPL('cubo','cubp',buildUnsigned('claim_unbonded_stake','type.googleapis.com/types.MessageClaimUnbondedStake',encClaimUnbonded(addr),{fee})); + toast('Payload built'); + }catch(e){toast(e.message,true);} +}; +window.signAndSubmit_claim_unbonded=async function(){ + try{ + const addr=document.getElementById('cub_addr').value.trim().toLowerCase();addr40(addr,'Resolver'); + const fee=parseInt(document.getElementById('cub_fee').value)||10000; + await doSubmit('claim_unbonded_stake','type.googleapis.com/types.MessageClaimUnbondedStake',encClaimUnbonded(addr),{fee},'btn_claim_unbonded','pend_claim_unbonded'); + }catch(e){toast(e.message,true);} +}; + +// ═══════════════════════════════════════════ +// RESOLVER RECORD STATE QUERY +// prefix 0x16 + len + addr bytes +// ═══════════════════════════════════════════ +function buildResolverKey(addrHex){ + const addr=h2b(addrHex); + const key=new Uint8Array(1+1+addr.length); + key[0]=0x16; key[1]=addr.length; key.set(addr,2); + return b2h(key); +} + +function decodeResolverRecord(hexData){ + const buf=h2b(hexData); + let pos=0; + const rec={stake:0n,rrs:0n,registeredAt:0n,successfulResolutions:0n,lastClaimedEpoch:0n}; + while(pos>3n),wt=Number(tagV&7n); + if(wt===2){const {v:lenV,p:p2}=decVarint(buf,pos);pos=p2+Number(lenV);} + else if(wt===0){ + const {v,p:p2}=decVarint(buf,pos);pos=p2; + if(fn===2)rec.stake=v; + if(fn===3)rec.rrs=v; + if(fn===4)rec.registeredAt=v; + if(fn===5)rec.successfulResolutions=v; + if(fn===6)rec.lastClaimedEpoch=v; + } else if(wt===1){pos+=8;} else if(wt===5){pos+=4;} else break; + } + return rec; +} + +async function fetchResolverRecord(addrHex){ + try{ + const key=buildResolverKey(addrHex); + const resp=await fetch(getRPC()+'/v1/query/state',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({key})}); + if(!resp.ok)return null; + const data=await resp.json(); + const hex=data.value||data.result||''; + if(!hex)return null; + return decodeResolverRecord(hex); + }catch{return null;} +} + +// ═══════════════════════════════════════════ +// BROWSE RESOLVERS +// ═══════════════════════════════════════════ +window.loadResolvers=async function(){ + const el=document.getElementById('resolversList'); + if(!el)return; + el.innerHTML='
▪ ▪ ▪  loading resolvers
'; + try{ + // auto-scan if no cache or registry empty + if(!localStorage.getItem('praxis_tx_cache')||_resolverRegistry.size===0){ + await loadMarkets(); + } + const cache=localStorage.getItem('praxis_tx_cache'); + if(!cache){el.innerHTML='
No chain data — node may be offline
';return;} + const txs=JSON.parse(cache); + const resolvers=new Map(); + for(const tx of txs){ + if(tx.messageType!=='register_resolver')continue; + const msg=(tx.transaction&&tx.transaction.msg)||{}; + const addr=tx.sender||''; + const stake=BigInt(msg.stakeAmount||msg.stake_amount||0); + if(!resolvers.has(addr)){ + // use rrs from _resolverRegistry if available (already computed in loadMarkets) + const regEntry=_resolverRegistry.get(addr); + const rrs=regEntry?regEntry.rrs:10; + resolvers.set(addr,{addr,stake,proposals:regEntry?regEntry.proposalCount:0,height:tx.height||0,rrs}); + } else { + const r=resolvers.get(addr); + r.stake=stake; + if((tx.height||0)>r.height)r.height=tx.height; + } + } + for(const tx of txs){ + if(tx.messageType!=='propose_outcome')continue; + const addr=tx.sender||''; + if(resolvers.has(addr)&&!_resolverRegistry.has(addr))resolvers.get(addr).proposals++; + } + if(resolvers.size===0){el.innerHTML='
No registered resolvers found
';return;} + const list=[...resolvers.values()]; + // if registry has data, rrs already set; otherwise estimate + list.forEach(r=>{ if(!_resolverRegistry.has(r.addr)) r.rrs=Math.min(10+r.proposals*10,999); }); + list.sort((a,b)=>b.rrs-a.rrs); + el.innerHTML=list.map(r=>{ + const rrs=r.rrs||0; + let tier,tcolor,ticon; + if(rrs<10){tier='Suspended';tcolor='var(--red)';ticon='✕';} + else if(rrs>=100){tier='Gold';tcolor='#FFD700';ticon='★';} + else if(rrs>=50){tier='Silver';tcolor='#C0C0C0';ticon='◆';} + else{tier='Bronze';tcolor='#CD7F32';ticon='▲';} + return '
'+ + '
'+ + '
'+r.addr.slice(0,8)+'\u2026'+r.addr.slice(-6)+'
'+ + ''+ticon+' '+tier+''+ + '
'+ + '
'+ + '
Stake
'+fmtPRX(r.stake)+' PRX
'+ + '
Est. RRS
'+rrs+'
'+ + '
Proposals
'+r.proposals+'
'+ + '
Since block
#'+r.height+'
'+ + '
'+ + '
'+r.addr+'
'+ + '
'; + }).join(''); + }catch(e){el.innerHTML='
Error: '+esc(e.message)+'
';} +}; +// ═══════════════════════════════════════════ +// MARKET DETAIL — ACTIVITY FEED + TOP HOLDERS +// ═══════════════════════════════════════════ +window.switchDetailTab = function(tab) { + ['activity','holders'].forEach(t => { + const btn = document.getElementById('dtab-'+t); + const pane = document.getElementById('dpane-'+t); + if(btn) { + btn.style.borderBottomColor = t===tab ? 'var(--green)' : 'transparent'; + btn.style.color = t===tab ? 'var(--text2)' : 'var(--text3)'; + } + if(pane) pane.style.display = t===tab ? '' : 'none'; + }); + if(tab==='activity') renderActivityFeed(_detailMarketId); + if(tab==='holders') renderTopHolders(_detailMarketId); +}; + +window.renderActivityFeed = function(mid) { + const el = document.getElementById('dpane-activity'); + if(!el||!mid) return; + try { + const txs = JSON.parse(localStorage.getItem('praxis_tx_cache')||'[]'); + const relevant = txs.filter(tx => { + const msg = (tx.transaction&&tx.transaction.msg)||{}; + const rawMid = msg.marketId||msg.market_id||''; + if(!rawMid && tx.messageType==='create_market') { + // match by derived marketId + return false; // handled below + } + if(!rawMid) return false; + let txMid = rawMid; + try { txMid = b2h(Uint8Array.from(atob(rawMid),c=>c.charCodeAt(0))); } catch{} + return txMid === mid; + }); + + // also include the create_market TX for this market + const createTx = txs.find(tx => tx.messageType==='create_market' && _allMarkets.find(m=>m.marketId===mid&&m.txHash===tx.txHash)); + if(createTx && !relevant.includes(createTx)) relevant.unshift(createTx); + + relevant.sort((a,b)=>(b.height||0)-(a.height||0)); + + if(!relevant.length){ + el.innerHTML='
No activity found
'; + return; + } + + const typeIcon = {create_market:'◎',submit_prediction:'⚡',propose_outcome:'⚖',finalize_market:'✓',cancel_market:'✕',claim_winnings:'◈',forfeit_position:'↩',resolve_market:'⚑'}; + const typeColor = {create_market:'var(--text2)',submit_prediction:'var(--green)',propose_outcome:'#FFD700',finalize_market:'var(--green)',cancel_market:'var(--red)',claim_winnings:'var(--green)',forfeit_position:'var(--red)',resolve_market:'#C0C0C0'}; + + el.innerHTML = relevant.map(tx => { + const msg = (tx.transaction&&tx.transaction.msg)||{}; + const type = tx.messageType||'unknown'; + const icon = typeIcon[type]||'▪'; + const color = typeColor[type]||'var(--text3)'; + const sender = tx.sender||''; + const height = tx.height||0; + let detail = ''; + if(type==='submit_prediction'){ + const outcome = msg.outcome===true||msg.outcome==='true'||msg.outcome===1; + const shares = BigInt(msg.shares||msg.amount||0); + detail = ''+(outcome?'YES':'NO')+'  '+fmtPRX(shares)+' PRX'; + } else if(type==='propose_outcome'){ + const outcome = msg.proposedOutcome===true||msg.proposedOutcome==='true'||msg.proposedOutcome===1; + detail = 'Proposed '+( outcome?'YES':'NO')+''; + } else if(type==='create_market'){ + const b0=BigInt(msg.b0||0); + detail='Market created · B0 '+fmtPRX(b0)+' PRX'; + } else if(type==='finalize_market'){detail='Market finalized';} + else if(type==='cancel_market'){detail='Market cancelled';} + else if(type==='claim_winnings'){detail='Claimed winnings';} + else if(type==='forfeit_position'){detail='Position forfeited';} + return '
'+ + '
'+icon+'
'+ + '
'+ + '
'+ + ''+type.replace(/_/g,' ')+''+ + 'blk #'+height+''+ + '
'+ + '
'+ + (sender?sender.slice(0,8)+'…'+sender.slice(-6):'')+ + '
'+ + (detail?'
'+detail+'
':'')+ + '
'+ + '
'; + }).join(''); + } catch(e) { + el.innerHTML='
Error: '+esc(e.message)+'
'; + } +}; + +window.renderTopHolders = function(mid) { + const el = document.getElementById('dpane-holders'); + if(!el||!mid) return; + try { + const txs = JSON.parse(localStorage.getItem('praxis_tx_cache')||'[]'); + const holders = new Map(); + for(const tx of txs){ + if(tx.messageType!=='submit_prediction') continue; + const msg=(tx.transaction&&tx.transaction.msg)||{}; + const rawMid=msg.marketId||msg.market_id||''; + let txMid=rawMid; + try{txMid=b2h(Uint8Array.from(atob(rawMid),c=>c.charCodeAt(0)));}catch{} + if(txMid!==mid) continue; + const addr=tx.sender||''; + const outcome=msg.outcome===true||msg.outcome==='true'||msg.outcome===1; + const shares=BigInt(msg.shares||msg.amount||0); + if(!holders.has(addr)) holders.set(addr,{addr,yes:0n,no:0n,txCount:0}); + const h=holders.get(addr); + if(outcome) h.yes+=shares; else h.no+=shares; + h.txCount++; + } + if(!holders.size){ + el.innerHTML='
No positions yet
'; + return; + } + const list=[...holders.values()].sort((a,b)=>Number((b.yes+b.no)-(a.yes+a.no))); + const totalYes=list.reduce((s,h)=>s+h.yes,0n); + const totalNo=list.reduce((s,h)=>s+h.no,0n); + const grand=totalYes+totalNo; + el.innerHTML= + '
'+ + '
'+ + '
TOTAL YES
'+ + '
'+fmtPRX(totalYes)+' PRX
'+ + '
'+ + '
TOTAL NO
'+ + '
'+fmtPRX(totalNo)+' PRX
'+ + '
'+ + list.map((h,i)=>{ + const total=h.yes+h.no; + const pct=grand>0n?Number(total*100n/grand):0; + const yesPct=total>0n?Number(h.yes*100n/total):0; + const bias=h.yes>h.no?'YES':h.no>h.yes?'NO':'EVEN'; + const bc=bias==='YES'?'var(--green)':bias==='NO'?'var(--red)':'var(--text3)'; + return '
'+ + '
'+ + '
'+ + '#'+(i+1)+''+ + ''+h.addr.slice(0,8)+'…'+h.addr.slice(-6)+''+ + '
'+ + ''+bias+''+ + '
'+ + '
'+ + '
YES
'+ + '
'+fmtPRX(h.yes)+'
'+ + '
NO
'+ + '
'+fmtPRX(h.no)+'
'+ + '
SHARE
'+ + '
'+pct+'%
'+ + '
'+ + '
'+ + '
'+ + '
'+ + '
'; + }).join(''); + } catch(e){ + el.innerHTML='
Error: '+esc(e.message)+'
'; + } +}; + +// ═══════════════════════════════════════════ +// PRIS REWARD PAGES +// ═══════════════════════════════════════════ + +const EPOCH_BLOCKS = 1000; +const AUTHORIZED_BUILDER = '954378ba109c5ca45b23bfa284f3ac70e2671b87'; +const AUTHORIZED_COMMUNITY = '15e658698d2510799339273f6fccb0484c4f4b6f'; +const AUTHORIZED_INVESTOR = '125c1bb803a2dd9194dca40d77445cf75647cb12'; +const AUTHORIZED_PROTOCOL = 'c1764f10ad672558afe1a3b666185fd141ae1ea8'; + +// Encoding +function encRewardResolver(addr, epoch){ return cat(bf(1,h2b(addr)), vf(2, BigInt(epoch))); } +function encRewardBuilder(addr){ return cat(bf(1,h2b(addr))); } +function encRewardCommunity(addr){ return cat(bf(1,h2b(addr))); } +function encRewardInvestor(addr){ return cat(bf(1,h2b(addr))); } +function encRewardProtocol(addr){ return cat(bf(1,h2b(addr))); } + +// Auth guard helper +function checkRewardAuth(pageId, contentId, unauthId, authorizedAddr) { + const authed = !authorizedAddr || (signerAddress && signerAddress.toLowerCase() === authorizedAddr.toLowerCase()); + document.getElementById(contentId).style.display = authed ? '' : 'none'; + document.getElementById(unauthId).style.display = authed ? 'none' : ''; +} + +// Auto-fill address fields when reward page opens + +// Generic pool stat loader (reads from chain via admin RPC) +async function loadPoolStat(elId, key) { + try { + const el = document.getElementById(elId); + if (!el) return; + // Pool data comes from plugin state — show epoch estimate for now + const epoch = currentHeight ? Math.floor(currentHeight / EPOCH_BLOCKS) : 0; + el.textContent = 'Epoch #' + epoch; + } catch(e) {} +} + +// Resolver reward data +async function loadResolverRewardData() { + try { + const epoch = currentHeight ? Math.floor(currentHeight / EPOCH_BLOCKS) : 0; + document.getElementById('rrw-pool').textContent = 'Epoch #' + epoch; + + // Pull resolver info from the resolvers map if already loaded + if (signerAddress && window._resolvers) { + const r = window._resolvers.get(signerAddress.toLowerCase()); + if (r) { + const rrs = r.rrs || 10; + const proposals = r.proposalCount || 0; + document.getElementById('rrw-rrs').textContent = rrs; + document.getElementById('rrw-rrs2').textContent = rrs; + document.getElementById('rrw-resolutions').textContent = proposals; + + // Tier + let tier = 'bronze', tierLabel = '🥉 Bronze', tierClass = 'rrs-bronze'; + if (rrs >= 100) { tier = 'gold'; tierLabel = 'Gold'; tierClass = 'rrs-gold'; } + else if (rrs >= 50) { tier = 'silver'; tierLabel = 'Silver'; tierClass = 'rrs-silver'; } + const weight = rrs >= 100 ? 3 : rrs >= 50 ? 2 : 1; + + const badge = document.getElementById('rrw-tier-badge'); + badge.className = 'rrs-badge ' + tierClass; + badge.innerHTML = tierLabel + ' — RRS ' + rrs + ''; + document.getElementById('rrw-share').textContent = weight + '× weight'; + + // Epoch history table (last 5 epochs) + let rows = ''; + for (let i = Math.max(0, epoch - 4); i <= epoch; i++) { + const isCurrent = i === epoch; + rows += ` + #${i} + ${isCurrent ? 'In progress' : '—'} + — + ${isCurrent ? 'Current' : 'Claimable'} + `; + } + document.querySelector('#rrw-history tbody').innerHTML = rows; + } + } + } catch(e) {} +} + +// Builder reward data +async function loadBuilderRewardData() { + try { + const epoch = currentHeight ? Math.floor(currentHeight / EPOCH_BLOCKS) : 0; + document.getElementById('brw-pool').textContent = 'Epoch #' + epoch; + let rows = ''; + for (let i = Math.max(0, epoch - 4); i <= epoch; i++) { + const isCurrent = i === epoch; + rows += ` + #${i} + ${isCurrent ? 'In progress' : '—'} + — + ${isCurrent ? 'Current' : 'Claimable'} + `; + } + document.querySelector('#brw-history tbody').innerHTML = rows; + } catch(e) {} +} + +// ── Submit handlers ── + +window.signAndSubmit_rewardResolver = async function() { + try { + const addr = document.getElementById('rrw-addr').value.trim(); + const epochVal = document.getElementById('rrw-epoch').value.trim(); + const epoch = epochVal ? parseInt(epochVal) : Math.floor((currentHeight||0) / EPOCH_BLOCKS); + const fee = BigInt(document.getElementById('rrw-fee').value||10000); + if (!addr || addr.length !== 40) return toast('Invalid resolver address', true); + await doSubmit('claim_resolver_reward','type.googleapis.com/types.MessageClaimResolverReward',encRewardResolver(addr,epoch),{fee},'btn_rrw','pend_rrw'); + } catch(e) { toast(friendlyError(null,e.message),true); } +}; + +window.build_rewardResolver = function() { + try { + const addr = document.getElementById('rrw-addr').value.trim(); + const epochVal = document.getElementById('rrw-epoch').value.trim(); + const epoch = epochVal ? parseInt(epochVal) : Math.floor((currentHeight||0) / EPOCH_BLOCKS); + const fee = BigInt(document.getElementById('rrw-fee').value||10000); + if (!addr || addr.length !== 40) return toast('Invalid resolver address', true); + showPL('rrwo','rrwp',buildUnsigned('claim_resolver_reward','type.googleapis.com/types.MessageClaimResolverReward',encRewardResolver(addr,epoch),{fee})); + toast('Payload built'); + } catch(e) { toast(friendlyError(null,e.message),true); } +}; + +window.signAndSubmit_rewardBuilder = async function() { + try { + const addr = document.getElementById('brw-addr').value.trim(); + const fee = BigInt(document.getElementById('brw-fee').value||10000); + if (!addr || addr.length !== 40) return toast('Invalid address', true); + await doSubmit('claim_builder_reward','type.googleapis.com/types.MessageClaimBuilderReward',encRewardBuilder(addr),{fee},'btn_brw','pend_brw'); + } catch(e) { toast(friendlyError(null,e.message),true); } +}; + +window.build_rewardBuilder = function() { + try { + const addr = document.getElementById('brw-addr').value.trim(); + const fee = BigInt(document.getElementById('brw-fee').value||10000); + if (!addr || addr.length !== 40) return toast('Invalid address', true); + showPL('brwo','brwp',buildUnsigned('claim_builder_reward','type.googleapis.com/types.MessageClaimBuilderReward',encRewardBuilder(addr),{fee})); + toast('Payload built'); + } catch(e) { toast(friendlyError(null,e.message),true); } +}; + +window.signAndSubmit_rewardCommunity = async function() { + try { + const addr = document.getElementById('crw-addr').value.trim(); + const fee = BigInt(document.getElementById('crw-fee').value||10000); + if (!addr || addr.length !== 40) return toast('Invalid address', true); + await doSubmit('claim_community_reward','type.googleapis.com/types.MessageClaimCommunityReward',encRewardCommunity(addr),{fee},'btn_crw','pend_crw'); + } catch(e) { toast(friendlyError(null,e.message),true); } +}; + +window.build_rewardCommunity = function() { + try { + const addr = document.getElementById('crw-addr').value.trim(); + const fee = BigInt(document.getElementById('crw-fee').value||10000); + if (!addr || addr.length !== 40) return toast('Invalid address', true); + showPL('crwo','crwp',buildUnsigned('claim_community_reward','type.googleapis.com/types.MessageClaimCommunityReward',encRewardCommunity(addr),{fee})); + toast('Payload built'); + } catch(e) { toast(friendlyError(null,e.message),true); } +}; + +window.signAndSubmit_rewardInvestor = async function() { + try { + const addr = document.getElementById('irw-addr').value.trim(); + const fee = BigInt(document.getElementById('irw-fee').value||10000); + if (!addr || addr.length !== 40) return toast('Invalid address', true); + await doSubmit('claim_investor_reward','type.googleapis.com/types.MessageClaimInvestorReward',encRewardInvestor(addr),{fee},'btn_irw','pend_irw'); + } catch(e) { toast(friendlyError(null,e.message),true); } +}; + +window.build_rewardInvestor = function() { + try { + const addr = document.getElementById('irw-addr').value.trim(); + const fee = BigInt(document.getElementById('irw-fee').value||10000); + if (!addr || addr.length !== 40) return toast('Invalid address', true); + showPL('irwo','irwp',buildUnsigned('claim_investor_reward','type.googleapis.com/types.MessageClaimInvestorReward',encRewardInvestor(addr),{fee})); + toast('Payload built'); + } catch(e) { toast(friendlyError(null,e.message),true); } +}; + +window.signAndSubmit_rewardProtocol = async function() { + try { + const addr = document.getElementById('prw-addr').value.trim(); + const fee = BigInt(document.getElementById('prw-fee').value||10000); + if (!addr || addr.length !== 40) return toast('Invalid address', true); + await doSubmit('claim_protocol_reward','type.googleapis.com/types.MessageClaimProtocolReward',encRewardProtocol(addr),{fee},'btn_prw','pend_prw'); + } catch(e) { toast(friendlyError(null,e.message),true); } +}; + +window.build_rewardProtocol = function() { + try { + const addr = document.getElementById('prw-addr').value.trim(); + const fee = BigInt(document.getElementById('prw-fee').value||10000); + if (!addr || addr.length !== 40) return toast('Invalid address', true); + showPL('prwo','prwp',buildUnsigned('claim_protocol_reward','type.googleapis.com/types.MessageClaimProtocolReward',encRewardProtocol(addr),{fee})); + toast('Payload built'); + } catch(e) { toast(friendlyError(null,e.message),true); } +}; + +// expose resolvers map for reward page +const _origBrowseResolvers = window.loadResolvers; +if (typeof loadResolvers === 'function') { + const __orig = loadResolvers; + window.loadResolvers = async function() { + await __orig(); + // cache resolvers map reference + window._resolvers = resolvers; + }; +} + +// ═══════════════════════════════════════════ +// SEARCH PAGE +// ═══════════════════════════════════════════ +let _srchCat = 'all'; + +window.srchCat = function(el) { + document.querySelectorAll('#srch-cats .cpick').forEach(e => e.classList.remove('active')); + el.classList.add('active'); + _srchCat = el.getAttribute('data-cat') || 'all'; + runSearch(); +}; + +window.runSearch = function() { + const q = (document.getElementById('srch-input')?.value || '').trim().toLowerCase(); + const out = document.getElementById('srch-results'); + if (!out) return; + const markets = _allMarkets || []; + if (!q && _srchCat === 'all') { + out.innerHTML = '
Type to search markets
'; + return; + } + let filtered = markets.filter(m => { + const catMatch = _srchCat === 'all' || extractCat(m.rules || '') === _srchCat; + const textMatch = !q || + (m.question || '').toLowerCase().includes(q) || + (m.marketId || '').toLowerCase().includes(q) || + (m.creator || '').toLowerCase().includes(q) || + stripCatPrefix(m.rules || '').toLowerCase().includes(q); + return catMatch && textMatch; + }); + if (filtered.length === 0) { + out.innerHTML = '
No markets found
'; + return; + } + out.innerHTML = '
' + filtered.map(m => { + const mid = m.marketId || m.txHash || ''; + const total = m.qYes + m.qNo; + const yesPct = total > 0n ? Number((m.qYes * 100n) / total) : 50; + const noPct = 100 - yesPct; + const vol = fmtPRX(total) + ' PRX'; + const exp = m.expiry ? '#' + m.expiry.toString() : '—'; + const catIcon = {'crypto':'🪙','sports':'⚽','politics':'🗳','finance':'📈','other':'◈'}[extractCat(m.rules||'')] || '◈'; + const catName = (CAT_LABELS[extractCat(m.rules||'')] || 'Other').replace(/[🪙⚽🗳📈◈]\s*/,''); + const statusMap = {0:'● LIVE',2:'◆ PROPOSED',3:'⚠ DISPUTED',4:'✓ FINALIZED',1:'✕ CANCELLED',8:'⏱ EXPIRED'}; + const statusHtml = statusMap[m.status] || ''; + const hasBanner = !!extractImg(m.rules||''); + const yesMulti = m.qYes > 0n ? (Number(m.qYes + m.qNo) / Number(m.qYes)).toFixed(2) : '—'; + const noMulti = m.qNo > 0n ? (Number(m.qYes + m.qNo) / Number(m.qNo)).toFixed(2) : '—'; + return `
+ ${mkBannerImg(m.rules)}
+
${catIcon} ${catName}  ${statusHtml}
+
${esc(m.question || '(no question)')}
+
+ ${yesPct}% ${yesMulti}x + · + ${noPct}% ${noMulti}x +
+
+
+
+
Vol${vol}
+
Exp${exp}
+
`; + }).join('') + '
'; +}; diff --git a/Frontend/index.html b/Frontend/index.html new file mode 100644 index 0000000000..b0535a50bb --- /dev/null +++ b/Frontend/index.html @@ -0,0 +1,1573 @@ + + + + + + + + + +Praxis — Prediction Markets + + + + + + +
⚠   Node offline — check Settings
+
+ + +
+ + +
+ +
+
+ +
+
+ + +
+
+ +
+
+
+ + +
+ + + + + +
+
+
Live on Canopy
+
Prediction Markets
+
Browse open markets and place your predictions using $PRX
+
+ +
+
Markets
+
Block
+
Volume
+ + +
+ +
+
All
+
🪙 Crypto
+
⚽ Sports
+
🗳 Politics
+
📈 Finance
+
◈ Other
+
+ +
+ + + +
+ +
▪ ▪ ▪  loading markets
+
+ + +
+
+
Market Detail
+
+
+
+ + +
+
+ +
+
+
+
YES
+
+
+
+
+
NO
+
+
+
+
+ + + + + +
+
+ + +
+
+ +
+
+ +
+ +
+
+
+
+ + +
+
+ + + + +
+
+
+ + +
+
+
Market Info
+
Market ID
+
Creator
+
Total Pool
+
Expiry
+ +
Resolver
+
+
+
+
+
+ + +
+
Resolver Action
Forfeit Position
Exit your position before proposing an outcome — required for COI-1 compliance
+
⚠ Forfeiting permanently exits your shares in this market. This cannot be undone.
+
+
// forfeit_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
Place Bet
Submit Prediction
Buy YES or NO shares — cost determined by LMSR pricing
+
+
// prediction_parameters
+
+
YES
NO
+
+
Min 1 PRX
+
1%10%
+
+
+
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
Collect Payout
Claim Winnings
Proportional payout from losing pool after finalization
+
+
// claim_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
Recover Funds
Reclaim Stake
Recover funds from expired markets with no resolver
+
Only available if no resolver proposed an outcome within the resolution window.
+
+
// reclaim_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
Network
Browse Resolvers
Active resolvers staking $PRX to guarantee market outcomes
+
▪ ▪ ▪  loading resolvers
+
+ + +
+
Creator
Claim Creator Fee
Collect accumulated fees from markets you created
+
+
// creator_fee_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
Admin
Cancel Market
Cancel an open market before expiry — creator bond returned
+
⚠ Cancellation is irreversible. All bettors can reclaim their stakes.
+
+
// cancel_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
Account
Wallet
Check balances, send $PRX, and view your predictions
+
+
// metamask_identity
+
+
Connect MetaMask to verify your identity. Your BLS keystore handles transaction signing.
+
+
+ +
+
+
// balance_lookup
+
+
+ +
+
+
// send_prx
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+
// my_predictions
+
Load wallet to see predictions
+
+
+ + +
+
BLS12-381
Signer
Encrypted keystore — private key held in RAM only, never stored or sent
+ +
+
// session_status
+
○ No key loaded — transactions cannot be signed
+ + +
+ + + +
+
// import_keystore
+
+ + +
Upload your encrypted Praxis keystore JSON
+
+
+ + +
+
+ + +
+
+ +
+
// create_keystore
+
Generates a new BLS12-381 keypair and downloads an encrypted keystore file.
+
+ + +
+
+ + +
+
+ +
+
+ +
+
// signing_spec
+
+ 1. sign_bytes = proto.Marshal(Transaction{…, signature:nil})
+ 2. time = BigInt(Date.now()) × 1000n — microseconds
+ 3. BLS12-381 G2 signature (96 bytes) — @noble/curves
+ 4. address = SHA256(pubKey).slice(0,20)
+ 5. Keystore = AES-GCM + PBKDF2 (200k iterations) +
+
+
+ + +
+
Resolver
Register as Resolver
Stake $PRX to earn resolution fees — minimum 500,000 PRX
+
+
// register_parameters
+
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Resolver
Propose Outcome
Submit your resolution after market expiry
+
+
// propose_parameters
+
+
YES
NO
+ +
Enter Market ID to compute min bond
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Resolver
File Dispute
Challenge a proposed outcome during the dispute window
+
+
// dispute_parameters
+
+ +
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Resolver
Commit Vote
Submit a blinded commitment during the voting phase
+
+
// commit_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Resolver
Reveal Vote
Reveal your committed vote during the reveal phase
+
+
// reveal_parameters
+
+
YES
NO
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Resolver
Tally Votes
Trigger vote tallying after the reveal phase ends
+
+
// tally_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Resolver
Claim Slash
Claim slashed stake from a penalized resolver
+
+
// slash_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Resolver
Unstake Resolver
Begin 120,960-block unbonding period — partial or full exit
+
+
// unstake_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Resolver
Claim Unbonded Stake
Release tokens after the 120,960-block unbonding period
+
+
// claim_unbonded_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
Admin
Create Market
Deploy a new prediction market on Praxis
+
+
// market_parameters
+
+
+
🪙 Crypto
+
⚽ Sports
+
🗳 Politics
+
📈 Finance
+
◈ Other
+
+
+
+
+
+
+ + +
Select a date to compute block height
+ +
+
+
+
+
+ + + +
Image will be stored on-chain via IPFS or direct URL. Recommended: 16:9, min 800x450px.
+
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Admin
Finalize Market
Collect finalization bounty after the dispute window closes
+
+
// finalize_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + + +
+
Rewards
Resolver Rewards
Epoch-weighted rewards based on your RRS tier and resolution activity
+
+
Claimable Now
PRX
+
Total Earned
PRX all time
+
Current Epoch Pool
PRX this epoch
+
+
+
// resolver_status
+
Bronze — RRS 10
+
+
Resolutions 0
+
RRS Score 10
+
Weight 1x
+
+
+ Share formula: your_weight / total_weight x epoch_pool
+ Weight = 1x Bronze (RRS 1-49) / 3x Silver (RRS 50-199) / 7x Gold (RRS 200+) +
+
+
+
// epoch_history
+
+ + + +
EpochPool (PRX)Your ShareStatus
Connect signer to load history
+
+
+
+
// claim_resolver_reward
+
+
+
+
+
+
+
... broadcasting...
+
// unsigned_payload
+
+
+ + +
+
Rewards
Builder Rewards
Protocol rewards for market creators — 120,960 block vesting window
+ +
+
+
Claimable Now
PRX
+
Total Earned
PRX all time
+
Current Pool
PRX this epoch
+
+
Vesting window: 120,960 blocks (~14 days). Rewards vest linearly.
+
+
// epoch_history
+
+ + + +
EpochPool (PRX)Your ShareStatus
Connect signer to load history
+
+
+
+
// claim_builder_reward
+
+
+
+
... broadcasting...
+
// unsigned_payload
+
+
+
+ + +
+
Rewards
Community Rewards
Participation rewards for active predictors
+ +
+
Claimable Now
PRX
+
Total Earned
PRX all time
+
Current Pool
PRX this epoch
+
+
+
// claim_community_reward
+
+
+
+
... broadcasting...
+
// unsigned_payload
+
+
+
+ + +
+
Rewards
Investor Rewards
Liquidity provision rewards — 241,920 block vesting window
+ +
+
Claimable Now
PRX
+
Total Earned
PRX all time
+
Current Pool
PRX this epoch
+
+
Vesting window: 241,920 blocks (~28 days). Rewards vest linearly.
+
+
// claim_investor_reward
+
+
+
+
... broadcasting...
+
// unsigned_payload
+
+
+
+ + +
+
Rewards
Protocol Rewards
Governance and staking rewards from protocol treasury
+ +
+
+
Claimable Now
PRX
+
Total Earned
PRX all time
+
Current Pool
PRX this epoch
+
+
+
// claim_protocol_reward
+
+
+
+
... broadcasting...
+
// unsigned_payload
+
+
+
+ +
+
Admin
Node Settings
Configure RPC endpoint and network connection
+
+
// rpc_configuration
+
+
+ +
+
+
// admin_unlock
+
Enter your admin address to unlock Creator and Resolver sections.
+
+
+
+
+ + + + +
+
+
Confirm Transaction
Review before signing
+
+
+ + +
+
+
+ +
+ + + + diff --git a/Frontend/style.css b/Frontend/style.css new file mode 100644 index 0000000000..11a23f96a9 --- /dev/null +++ b/Frontend/style.css @@ -0,0 +1,401 @@ +/* TOKENS */ +:root{ + --bg:#07090d;--bg2:#0b0e16;--surface:#0f1320;--surf2:#141929; + --border:#1c2236;--border2:#263048; + --text:#c4d4ec;--text2:#6a7d9c;--text3:#2e3d58; + --green:#00e87a;--gdim:rgba(0,232,122,.07);--gglow:rgba(0,232,122,.22); + --red:#ff3d5a;--rdim:rgba(255,61,90,.07); + --amber:#f59e0b;--adim:rgba(245,158,11,.08); + --cyan:#22d3ee;--cdim:rgba(34,211,238,.07); + --purple:#a78bfa;--pdim:rgba(167,139,250,.08); + --shadow:rgba(0,0,0,.55); + --font-mono:'JetBrains Mono',monospace; + --font-ui:'Space Grotesk',sans-serif; + --font-d:'Unbounded',sans-serif; +} +[data-theme="light"]{ + --bg:#eef1f8;--bg2:#e4e8f2;--surface:#fff;--surf2:#f5f7fc; + --border:#dae0ee;--border2:#c4cedf; + --text:#18253a;--text2:#526070;--text3:#a0b0c8; + --green:#00a352;--gdim:rgba(0,163,82,.07);--gglow:rgba(0,163,82,.18); + --red:#e02040;--rdim:rgba(224,32,64,.07); + --amber:#b45309;--adim:rgba(180,83,9,.07); + --cyan:#0891b2;--cdim:rgba(8,145,178,.07); + --purple:#7c3aed;--pdim:rgba(124,58,237,.07); + --shadow:rgba(0,0,0,.1); +} +*{box-sizing:border-box;margin:0;padding:0} +html{scroll-behavior:smooth} +body{background:var(--bg);color:var(--text);font-family:var(--font-ui);font-size:14px;line-height:1.6;min-height:100vh;overflow-x:hidden;transition:background .25s,color .25s} +[data-theme="dark"] body::after{content:'';position:fixed;inset:0;background-image:linear-gradient(rgba(0,232,122,.01) 1px,transparent 1px),linear-gradient(90deg,rgba(0,232,122,.01) 1px,transparent 1px);background-size:44px 44px;pointer-events:none;z-index:0} +.shell{display:flex;min-height:100vh;position:relative;z-index:1} + +/* SIDEBAR */ + +.logo{padding:18px 16px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:11px} +.logo-hex{width:36px;height:36px;flex-shrink:0;display:flex;align-items:center;justify-content:center} +.logo-hex span{font-family:var(--font-d);font-size:10px;font-weight:900;color:#000;letter-spacing:-1px} +.logo-text .wm{font-family:var(--font-d);font-size:13px;font-weight:900;letter-spacing:2px;color:var(--green);text-shadow:0 0 20px var(--gglow)} +.logo-text .tg{font-family:var(--font-mono);font-size:8px;color:var(--text3);letter-spacing:2px;text-transform:uppercase;margin-top:1px} +.nav{padding:4px 0;flex:1;overflow-y:auto} +.nav::-webkit-scrollbar{width:3px} +.nav::-webkit-scrollbar-thumb{background:var(--border2)} +.ns{font-family:var(--font-mono);font-size:8px;letter-spacing:3px;text-transform:uppercase;color:var(--text3);padding:13px 16px 4px;display:flex;align-items:center;gap:8px} +.ns::after{content:'';flex:1;height:1px;background:var(--border)} +.ni{display:flex;align-items:center;gap:8px;padding:7px 16px;cursor:pointer;color:var(--text2);font-size:12px;font-weight:500;transition:all .12s;border-left:2px solid transparent;user-select:none} +.ni:hover{color:var(--text);background:var(--gdim)} +.ni.active{color:var(--green);border-left-color:var(--green);background:var(--gdim)} +.ni-ic{width:14px;text-align:center;font-size:13px;flex-shrink:0;opacity:.7} +.ni.active .ni-ic{opacity:1} +.rb{font-family:var(--font-mono);font-size:7px;letter-spacing:1px;text-transform:uppercase;padding:1px 5px;margin-left:auto;flex-shrink:0} +.rb-r{background:var(--adim);color:var(--amber);border:1px solid rgba(245,158,11,.2)} +.rb-a{background:var(--pdim);color:var(--purple);border:1px solid rgba(167,139,250,.2)} +.sbfoot{padding:11px 14px;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:7px} +.conn-row{display:flex;align-items:center;gap:6px;font-family:var(--font-mono);font-size:10px;color:var(--text2)} +.dot{width:6px;height:6px;border-radius:50%;background:var(--text3);flex-shrink:0;transition:all .4s} +.dot.live{background:var(--green);box-shadow:0 0 8px var(--green);animation:pulse 2s ease-in-out infinite} +@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}} +#hBadge{font-family:var(--font-mono);font-size:9px;color:var(--text3);margin-left:auto} +.theme-btn{display:flex;align-items:center;gap:7px;cursor:pointer;padding:5px 9px;background:var(--bg2);border:1px solid var(--border);font-family:var(--font-mono);font-size:9px;color:var(--text2);transition:all .15s} +.theme-btn:hover{border-color:var(--green);color:var(--green)} +.ttrack{width:24px;height:12px;background:var(--border2);border-radius:6px;position:relative;flex-shrink:0;transition:background .2s} +[data-theme="light"] .ttrack{background:var(--green)} +.tthumb{position:absolute;top:2px;left:2px;width:8px;height:8px;border-radius:50%;background:var(--text2);transition:transform .2s,background .2s} +[data-theme="light"] .tthumb{transform:translateX(12px);background:#fff} + +/* MOBILE */ +.mobhead{display:flex;width:100%;position:sticky;top:0;z-index:100;background:var(--surface);border-bottom:1px solid var(--border);padding:11px 16px;align-items:center;justify-content:space-between} +.moblogo{font-family:var(--font-d);font-size:13px;font-weight:900;letter-spacing:2px;color:var(--green)} +.ham{background:none;border:none;cursor:pointer;color:var(--text);font-size:20px;padding:2px} +.mobnav{display:none;position:fixed;inset:0;z-index:200;background:rgba(0,0,0,.7)} +.mobnav.open{display:block} +.mobpanel{position:absolute;left:0;top:0;bottom:0;width:260px;background:var(--surface);border-right:1px solid var(--border);overflow-y:auto;transform:translateX(-100%);transition:transform .25s} +.mobnav.open .mobpanel{transform:translateX(0)} +.mobclose{position:absolute;top:13px;right:13px;background:none;border:none;cursor:pointer;font-size:18px;color:var(--text2)} + +/* SECTION ROLE HEADERS */ +.role-banner{padding:9px 16px;font-family:var(--font-mono);font-size:9px;letter-spacing:2px;text-transform:uppercase;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px} +.rb-resolver{background:rgba(245,158,11,.04);color:var(--amber)} +.rb-admin-b{background:rgba(167,139,250,.04);color:var(--purple)} + +/* MAIN */ +.main{flex:1;overflow-y:auto;padding:26px 32px 80px;max-width:950px} + +/* PAGE HEADER */ +.ph{margin-bottom:22px;padding-bottom:16px;border-bottom:1px solid var(--border)} +.ph-eye{font-family:var(--font-mono);font-size:9px;letter-spacing:3px;text-transform:uppercase;margin-bottom:7px;display:flex;align-items:center;gap:9px} +.ph-eye::before{content:'';width:18px;height:1px} +.ph-g .ph-eye{color:var(--green)}.ph-g .ph-eye::before{background:var(--green)} +.ph-r .ph-eye{color:var(--amber)}.ph-r .ph-eye::before{background:var(--amber)} +.ph-a .ph-eye{color:var(--purple)}.ph-a .ph-eye::before{background:var(--purple)} +.ph-title{font-family:var(--font-d);font-size:20px;font-weight:900;letter-spacing:-.5px;line-height:1.2} +.ph-sub{font-size:13px;color:var(--text2);margin-top:5px} + +/* STAT BAR */ +.sbar{display:flex;gap:8px;margin-bottom:20px;flex-wrap:wrap;align-items:center} +.chip{background:var(--surface);border:1px solid var(--border);padding:5px 12px;font-family:var(--font-mono);font-size:10px;color:var(--text2);display:flex;align-items:center;gap:5px} +.chip b{color:var(--text);font-family:var(--font-d);font-size:13px;font-weight:700} + +/* CARD */ +.card{background:var(--surface);border:1px solid var(--border);margin-bottom:13px;position:relative;overflow:hidden;transition:background .25s} +[data-theme="dark"] .card::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,var(--green),transparent);opacity:.15} +[data-theme="dark"] .ph-r ~ .card::before,[data-theme="dark"] .card.r-card::before{background:linear-gradient(90deg,var(--amber),transparent)!important} +[data-theme="dark"] .ph-a ~ .card::before,[data-theme="dark"] .card.a-card::before{background:linear-gradient(90deg,var(--purple),transparent)!important} +.ci{padding:18px} +.ct{font-family:var(--font-mono);font-size:9px;color:var(--text3);letter-spacing:2px;text-transform:uppercase;margin-bottom:15px;padding-bottom:10px;border-bottom:1px solid var(--border)} + +/* FORM */ +.field{margin-bottom:12px} +.field label{display:block;font-family:var(--font-mono);font-size:9px;color:var(--text2);letter-spacing:2px;text-transform:uppercase;margin-bottom:5px} +.field input,.field textarea,.field select{width:100%;background:var(--bg);border:1px solid var(--border2);padding:8px 11px;color:var(--text);font-family:var(--font-mono);font-size:12px;outline:none;transition:border-color .15s,box-shadow .15s;appearance:none} +.field input:focus,.field textarea:focus{border-color:var(--green);box-shadow:0 0 0 3px var(--gdim)} +.field textarea{resize:vertical;min-height:70px;line-height:1.6} +.hint{font-family:var(--font-mono);font-size:9px;color:var(--text3);margin-top:3px;line-height:1.5} +.r2{display:grid;grid-template-columns:1fr 1fr;gap:12px} +.r3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px} + +/* BUTTONS */ +.btn{display:inline-flex;align-items:center;gap:6px;padding:9px 17px;border:none;cursor:pointer;font-family:var(--font-ui);font-size:13px;font-weight:600;letter-spacing:.2px;transition:all .15s;position:relative} +.bp{background:var(--green);color:#000} +.bp:hover{filter:brightness(1.1);transform:translateY(-1px);box-shadow:0 4px 20px var(--gglow)} +.bp:active{transform:translateY(0)} +.bp:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none} +.bsec{background:transparent;border:1px solid var(--border2);color:var(--text2)} +.bsec:hover{border-color:var(--green);color:var(--green);background:var(--gdim)} +.bamb{background:var(--amber);color:#000} +.bamb:hover{filter:brightness(1.08);transform:translateY(-1px);box-shadow:0 4px 16px rgba(245,158,11,.3)} +.bamb:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none} +.bsm{padding:6px 11px;font-size:11px} +.brow{display:flex;gap:8px;flex-wrap:wrap;margin-top:15px;align-items:center} + +/* OUTCOME TOGGLE */ +.otog{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-bottom:12px} +.obtn{padding:10px;border:1px solid var(--border2);background:transparent;color:var(--text2);cursor:pointer;font-family:var(--font-d);font-size:9px;font-weight:700;letter-spacing:2px;transition:all .15s;text-align:center} +.obtn.yes:hover{border-color:var(--green);color:var(--green)} +.obtn.no:hover{border-color:var(--red);color:var(--red)} +.obtn.yes.active{background:var(--gdim);border-color:var(--green);color:var(--green)} +.obtn.no.active{background:var(--rdim);border-color:var(--red);color:var(--red)} + +/* ALERTS */ +.alert{padding:10px 13px;font-family:var(--font-mono);font-size:10px;margin-bottom:13px;line-height:1.7} +.ag{background:var(--gdim);border:1px solid rgba(0,232,122,.15);color:var(--green)} +.ar{background:var(--rdim);border:1px solid rgba(255,61,90,.15);color:var(--red)} +.ay{background:var(--adim);border:1px solid rgba(245,158,11,.2);color:var(--amber)} +.ac{background:var(--cdim);border:1px solid rgba(34,211,238,.15);color:var(--cyan)} +.ap{background:var(--pdim);border:1px solid rgba(167,139,250,.2);color:var(--purple)} + +/* PAYLOAD */ +.pbox{display:none;margin-top:13px} +.plbl{font-family:var(--font-mono);font-size:9px;color:var(--text3);letter-spacing:2px;text-transform:uppercase;margin-bottom:4px} +.pbox textarea{width:100%;background:var(--bg);border:1px solid var(--border2);padding:10px;color:var(--text2);font-family:var(--font-mono);font-size:10px;resize:vertical;min-height:120px;outline:none;line-height:1.7} + +/* PENDING */ +.pend{display:none;align-items:center;gap:8px;font-family:var(--font-mono);font-size:10px;color:var(--amber);padding:8px 12px;background:var(--adim);border:1px solid rgba(245,158,11,.2);margin-top:11px} +.blink{animation:blink 1.2s step-end infinite} +@keyframes blink{0%,100%{opacity:1}50%{opacity:.1}} +.div{height:1px;background:var(--border);margin:15px 0} +.loading{padding:52px;text-align:center;color:var(--text3);font-family:var(--font-mono);font-size:11px;letter-spacing:1px} + +/* TOAST */ +#toast{position:fixed;bottom:20px;right:20px;background:var(--surf2);border:1px solid var(--border2);padding:10px 15px;font-family:var(--font-mono);font-size:11px;color:var(--text);z-index:9999;display:none;max-width:360px;box-shadow:0 12px 40px var(--shadow);letter-spacing:.3px;line-height:1.5} +#toast.ok{border-color:var(--green);color:var(--green)} +#toast.err{border-color:var(--red);color:var(--red)} + +/* PAGES */ +.page{display:none}.page.active{display:block;animation:fu .18s ease} +@keyframes fu{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:translateY(0)}} + +/* INFO GRID */ +.igrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:8px} +.icell{background:var(--bg);border:1px solid var(--border);padding:12px} +.ilbl{font-family:var(--font-mono);font-size:8px;color:var(--text3);letter-spacing:2px;text-transform:uppercase;margin-bottom:4px} +.ival{font-family:var(--font-mono);font-size:14px;color:var(--text)} + +/* KEY */ +.kstat{display:flex;align-items:center;gap:8px;padding:9px 12px;border:1px solid var(--border2);font-family:var(--font-mono);font-size:11px;margin-bottom:13px;color:var(--text2)} +.kstat.loaded{border-color:var(--green);background:var(--gdim);color:var(--green)} +.addr-mono{font-family:var(--font-mono);font-size:10px;color:var(--cyan);word-break:break-all;padding:8px 11px;background:var(--bg);border:1px solid var(--border2);margin-top:8px} +.bal-wrap{padding:16px;background:var(--bg);border:1px solid var(--border);margin-bottom:13px} +.bal-num{font-family:var(--font-d);font-size:30px;font-weight:900;color:var(--green);line-height:1;margin-bottom:2px} +.bal-unit{font-family:var(--font-mono);font-size:8px;color:var(--text3);letter-spacing:2px} +.sinfo{font-family:var(--font-mono);font-size:10px;color:var(--text2);line-height:2} +.sinfo em{color:var(--green);font-style:normal} +code{font-family:var(--font-mono);font-size:11px;background:var(--bg2);padding:1px 5px;border:1px solid var(--border);color:var(--cyan)} +.cwrap{display:flex;align-items:center;gap:5px} +.cbtn{background:var(--bg2);border:1px solid var(--border);color:var(--text3);padding:3px 7px;font-size:11px;cursor:pointer;transition:all .15s} +.cbtn:hover{border-color:var(--green);color:var(--green)} +.cbtn.ok{color:var(--green);border-color:var(--green)} +.sbadge{display:inline-flex;align-items:center;gap:5px;padding:4px 11px;background:var(--rdim);border:1px solid rgba(255,61,90,.2);font-family:var(--font-mono);font-size:9px;color:var(--red);cursor:pointer;transition:all .15s;margin-top:7px} +.sbadge:hover{background:var(--red);color:#fff} +.sbadge.hidden{display:none} + +/* OFFLINE */ +#offBanner{display:none;position:sticky;top:0;z-index:50;background:rgba(255,61,90,.07);border-bottom:1px solid rgba(255,61,90,.2);padding:7px 18px;font-family:var(--font-mono);font-size:10px;color:var(--red);text-align:center} +#offBanner.show{display:block} + +/* CONFIRM */ +#confOverlay{display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.75);backdrop-filter:blur(4px);align-items:center;justify-content:center} +#confOverlay.open{display:flex} +.cm{background:var(--surface);border:1px solid var(--border2);width:min(420px,90vw);max-height:80vh;overflow-y:auto} +.cm-h{padding:18px;border-bottom:1px solid var(--border)} +.cm-t{font-family:var(--font-d);font-size:15px;font-weight:900} +.cm-s{font-family:var(--font-mono);font-size:8px;color:var(--text3);letter-spacing:2px;text-transform:uppercase;margin-top:3px} +.cm-rows{padding:14px 18px} +.cm-row{display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--border);font-family:var(--font-mono);font-size:11px} +.cm-row:last-child{border:none} +.cm-l{color:var(--text3)}.cm-v{color:var(--text);word-break:break-all;text-align:right;max-width:60%} +.cm-v.g{color:var(--green)}.cm-v.r{color:var(--red)} +.cm-btns{display:flex;border-top:1px solid var(--border)} +.cm-btns button{flex:1;padding:13px;border:none;cursor:pointer;font-family:var(--font-d);font-size:13px;font-weight:700;transition:all .15s} +#confCancel{background:transparent;color:var(--text2);border-right:1px solid var(--border)} +#confCancel:hover{color:var(--text);background:var(--bg2)} +#confOk{background:var(--green);color:#000} +#confOk:hover{filter:brightness(1.08)} + +/* MARKET CARDS */ +.mgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px} +.mcard{background:var(--surface);border:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden;transition:border-color .25s,box-shadow .25s,transform .2s;position:relative;border-radius:2px} +[data-theme="dark"] .mcard::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,var(--green),transparent);opacity:.4} +[data-theme="dark"] .mcard.sp-live::after{content:'';position:absolute;inset:0;border-radius:2px;box-shadow:0 0 0 1px rgba(0,232,122,.06) inset;pointer-events:none} +.mcard:hover{border-color:var(--green);box-shadow:0 8px 32px rgba(0,0,0,.6),0 0 0 1px rgba(0,232,122,.08);transform:translateY(-2px)} +.mcard.mexp::before{background:linear-gradient(90deg,var(--amber),transparent)!important} +.mcard.mexp:hover{border-color:var(--amber);box-shadow:0 8px 32px rgba(0,0,0,.6),0 0 0 1px rgba(245,158,11,.08)} +.mcard.mfin{opacity:.65}.mcard.mfin::before{background:linear-gradient(90deg,#3a3a3a,transparent)!important} +.mcard.mfin:hover{border-color:var(--border2);box-shadow:0 4px 16px rgba(0,0,0,.4);transform:none} +.mcard.mcan{opacity:.8}.mcard.mcan::before{background:linear-gradient(90deg,var(--red),transparent)!important} +.mcard.mcan:hover{border-color:var(--red);box-shadow:0 8px 32px rgba(0,0,0,.6),0 0 0 1px rgba(255,61,90,.08)} +.mc-head{padding:14px 15px 12px;display:flex;align-items:flex-start;justify-content:space-between;gap:10px;cursor:pointer;transition:background .15s} +.mc-head:hover{background:rgba(255,255,255,.02)} +.mc-q{font-family:var(--font-d);font-size:13px;font-weight:700;color:var(--text);line-height:1.45;flex:1;letter-spacing:-.01em} +.spill{flex-shrink:0;display:flex;align-items:center;gap:4px;padding:3px 8px;font-size:8px;font-weight:700;letter-spacing:.15em;text-transform:uppercase;font-family:var(--font-mono);border-radius:1px} +.sp-o{background:rgba(0,232,122,.1);color:var(--green);border:1px solid rgba(0,232,122,.25)} +.sp-o .dot{background:var(--green);box-shadow:0 0 6px var(--green);animation:pulse 2s infinite} +.sp-e{background:rgba(245,158,11,.1);color:var(--amber);border:1px solid rgba(245,158,11,.25)} +.sp-e .dot{background:var(--amber)} +.sp-d{background:rgba(255,61,90,.1);color:var(--red);border:1px solid rgba(255,61,90,.25)} +.sp-d .dot{background:var(--red)} +.sp-f{background:rgba(100,100,100,.08);color:#666;border:1px solid rgba(100,100,100,.15)} +.sp-f .dot{background:#555} +.mc-prob{padding:16px 15px 12px} +.mc-prob-nums{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:10px} +.mc-prob-side{display:flex;flex-direction:column} +.pvy{font-family:var(--font-d);font-size:36px;font-weight:900;color:var(--green);letter-spacing:-.04em;line-height:1;text-shadow:0 0 24px rgba(0,232,122,.35)} +.pvn{font-family:var(--font-d);font-size:36px;font-weight:900;color:var(--red);letter-spacing:-.04em;line-height:1;text-shadow:0 0 24px rgba(255,61,90,.25)} +.pvl{font-family:var(--font-mono);font-size:8px;color:var(--text3);letter-spacing:2px;text-transform:uppercase;display:block;margin-top:4px} +.btrack{height:4px;background:rgba(255,255,255,.04);overflow:hidden;display:flex;border-radius:2px} +.byes{height:100%;background:var(--green);box-shadow:0 0 8px rgba(0,232,122,.5);transition:width .5s cubic-bezier(.4,0,.2,1)} +.bno{height:100%;background:var(--red);flex:1} +.mc-pools{display:flex;border-top:1px solid var(--border)} +.pc{flex:1;padding:10px 14px;font-family:var(--font-mono)} +.pc:first-child{border-right:1px solid var(--border)} +.pc-lbl{font-size:8px;color:var(--text3);letter-spacing:1.5px;text-transform:uppercase;margin-bottom:3px} +.pc-val{font-size:13px;font-weight:600;letter-spacing:-.01em} +.pcy .pc-val{color:var(--green)}.pcn .pc-val{color:var(--red)} +.mc-banner{padding:8px 14px;font-family:var(--font-mono);font-size:9px;display:flex;align-items:center;gap:8px;border-top:1px solid var(--border);letter-spacing:.03em} +.bnr{background:rgba(245,158,11,.06);color:var(--amber);border-left:2px solid var(--amber)} +.bnd{background:rgba(255,61,90,.06);color:var(--red);border-left:2px solid var(--red)} +.bnf{background:rgba(0,232,122,.06);color:var(--green);border-left:2px solid var(--green)} +.mc-acts{display:flex;padding:10px 14px;gap:8px;border-top:1px solid var(--border)} +.byes2{flex:1;padding:9px 8px;background:var(--gdim);border:1px solid rgba(0,232,122,.2);color:var(--green);font-family:var(--font-d);font-size:9px;font-weight:700;letter-spacing:2px;cursor:pointer;transition:all .15s;border-radius:1px} +.byes2:hover{background:var(--green);color:#000;box-shadow:0 0 16px rgba(0,232,122,.3);border-color:var(--green)} +.byes2:disabled{opacity:.25;cursor:not-allowed} +.bno2{flex:1;padding:9px 8px;background:var(--rdim);border:1px solid rgba(255,61,90,.2);color:var(--red);font-family:var(--font-d);font-size:9px;font-weight:700;letter-spacing:2px;cursor:pointer;transition:all .15s;border-radius:1px} +.bno2:hover{background:var(--red);color:#fff;box-shadow:0 0 16px rgba(255,61,90,.3);border-color:var(--red)} +.bno2:disabled{opacity:.25;cursor:not-allowed} +.mc-foot{padding:9px 15px;display:flex;justify-content:space-between;align-items:center;border-top:1px solid var(--border);background:var(--bg2)} +.mc-foot-item{display:flex;flex-direction:column;gap:2px} +.mc-propose-btn{flex:1;padding:9px 8px;background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.25);color:var(--amber);font-family:var(--font-d);font-size:9px;font-weight:700;letter-spacing:2px;cursor:pointer;transition:all .15s;border-radius:1px} +.mc-propose-btn:hover{background:var(--amber);color:#000;box-shadow:0 0 16px rgba(245,158,11,.3)} +.mitems{display:flex;gap:14px;flex-wrap:wrap} +.mitem{display:flex;flex-direction:column} +.mlbl{font-family:var(--font-mono);font-size:7px;color:var(--text3);letter-spacing:2px;text-transform:uppercase;margin-bottom:2px} +.mval{font-family:var(--font-mono);font-size:10px;color:var(--text2)} +.mval.g{color:var(--green)} +.midb{font-family:var(--font-mono);font-size:8px;color:var(--text3)} + +/* PREDICTIONS */ +.pitem{background:var(--bg);border:1px solid var(--border);padding:11px 13px;margin-bottom:7px;display:flex;justify-content:space-between;align-items:center;transition:border-color .15s} +.pitem:hover{border-color:var(--border2)} +.pmid{font-family:var(--font-mono);font-size:10px;color:var(--text3);margin-bottom:3px} +.pmeta{display:flex;gap:12px;align-items:center} +.poc{font-family:var(--font-mono);font-size:11px;font-weight:500} +.pst{font-family:var(--font-mono);font-size:10px;color:var(--text2)} +.pht{font-family:var(--font-mono);font-size:9px;color:var(--text3)} + +/* BOTTOM NAV */ +.bnav{display:none;position:fixed;bottom:0;left:0;right:0;background:var(--surface);border-top:1px solid var(--border);z-index:100;padding:0 4px;justify-content:space-around;align-items:center;height:54px} +.bni{display:flex;flex-direction:column;align-items:center;gap:2px;padding:4px 5px;cursor:pointer;color:var(--text3);font-size:8px;font-family:var(--font-mono);letter-spacing:.3px;transition:color .15s;flex:1;text-align:center;user-select:none} +.bni.active{color:var(--green)} +.bni svg{width:17px;height:17px} + +@media(max-width:900px){ + .mobhead{display:flex}.main{padding:14px 14px 70px;width:100%;min-width:0}.bnav{display:flex} + .r2,.r3{grid-template-columns:1fr}.mgrid{grid-template-columns:1fr}.ph-title{font-size:17px} + .sbar{flex-wrap:wrap;gap:6px;padding:8px 0;justify-content:flex-start} + .hide-mob{display:none} + .sbar .chip{font-size:10px} + .sbar .bsm{font-size:9px;padding:4px 10px;flex:1} + .mtabs{padding:0 0 12px;gap:6px} + .mtab{flex:1;text-align:center;padding:6px 8px} + .shell{display:block} +} + +/* Market Tabs */ +.mtabs{display:flex;gap:8px;padding:0 16px 12px;border-bottom:1px solid var(--border);margin-bottom:16px} +.mtab{padding:6px 16px;font-family:var(--font-mono);font-size:10px;letter-spacing:1px;border:1px solid var(--border);background:transparent;color:var(--text3);cursor:pointer;transition:all .15s;border-radius:2px} +.mtab:hover{border-color:var(--green);color:var(--green)} +.mtab.active{background:var(--gdim);border-color:var(--green);color:var(--green)} + +/* Keystore account card selector */ +.ks-acct-card:hover { border-color: var(--green) !important; } +.ks-acct-active { border-color: var(--green) !important; } +.ks-acct-active div:last-child { background: var(--green) !important; } + +/* NEW CARD DESIGN — inline script classes */ +.spill.sp-live{background:rgba(0,232,122,.1);color:var(--green);border:1px solid rgba(0,232,122,.25)} +.spill.sp-live .sp-dot{background:var(--green);box-shadow:0 0 6px var(--green);animation:pulse 2s infinite} +.spill.sp-proposed{background:rgba(245,158,11,.1);color:var(--amber);border:1px solid rgba(245,158,11,.25)} +.spill.sp-disputed{background:rgba(255,61,90,.1);color:var(--red);border:1px solid rgba(255,61,90,.25)} +.spill.sp-finalized{background:rgba(100,100,100,.08);color:#666;border:1px solid rgba(100,100,100,.15)} +.spill.sp-cancelled{background:rgba(255,61,90,.1);color:var(--red);border:1px solid rgba(255,61,90,.25)} +.spill.sp-expired{background:rgba(245,158,11,.1);color:var(--amber);border:1px solid rgba(245,158,11,.25)} +.sp-dot{width:6px;height:6px;border-radius:50%;background:currentColor;flex-shrink:0;display:inline-block} + +.mcard-top{padding:16px 15px 12px;display:flex;flex-direction:column;gap:10px;cursor:pointer} +.mcard-cat{font-family:var(--font-mono);font-size:8px;letter-spacing:2px;color:var(--text3);display:flex;align-items:center;gap:8px} +.mcard-cat-dot{width:5px;height:5px;border-radius:50%;background:var(--green);flex-shrink:0} +.mcard-q{font-family:var(--font-d);font-size:13px;font-weight:700;color:var(--text);line-height:1.45;letter-spacing:-.01em} +.mcard-probs{display:flex;align-items:flex-end;justify-content:space-between} +.prob-yes{font-family:var(--font-d);font-size:36px;font-weight:900;color:var(--green);letter-spacing:-.04em;line-height:1;text-shadow:0 0 24px rgba(0,232,122,.35)} +.prob-no{font-family:var(--font-d);font-size:36px;font-weight:900;color:var(--red);letter-spacing:-.04em;line-height:1;text-shadow:0 0 24px rgba(255,61,90,.25)} +.prob-labels{display:flex;gap:4px;margin-top:4px} +.prob-lbl{font-family:var(--font-mono);font-size:8px;color:var(--text3);letter-spacing:2px;text-transform:uppercase} +.prob-divider{width:1px;background:var(--border);align-self:stretch;margin:0 8px} + +.mcard-meta{display:flex;justify-content:space-between;padding:9px 15px;border-top:1px solid var(--border);background:var(--bg2)} +.meta-item{display:flex;flex-direction:column;gap:2px} +.meta-lbl{font-family:var(--font-mono);font-size:7px;color:var(--text3);letter-spacing:2px;text-transform:uppercase} +.meta-val{font-family:var(--font-mono);font-size:10px;color:var(--text2)} +.meta-val.g{color:var(--green)} + +.mcard-actions{display:flex;gap:8px;padding:10px 14px;border-top:1px solid var(--border)} +.mcard-btn{flex:1;padding:9px 8px;font-family:var(--font-d);font-size:9px;font-weight:700;letter-spacing:2px;cursor:pointer;transition:all .15s;border-radius:1px;border:1px solid} +.mcard-btn-yes{background:var(--gdim);border-color:rgba(0,232,122,.2);color:var(--green)} +.mcard-btn-yes:hover{background:var(--green);color:#000;box-shadow:0 0 16px rgba(0,232,122,.3);border-color:var(--green)} +.mcard-btn-no{background:var(--rdim);border-color:rgba(255,61,90,.2);color:var(--red)} +.mcard-btn-no:hover{background:var(--red);color:#fff;box-shadow:0 0 16px rgba(255,61,90,.3);border-color:var(--red)} + +/* RESOLVER CARDS */ +.res-card{display:flex;align-items:center;gap:14px;padding:14px 16px;border-bottom:1px solid var(--border);transition:background .15s} +.res-card:hover{background:var(--gdim)} +.res-avatar{width:38px;height:38px;border-radius:50%;background:var(--surf2);border:1px solid var(--border2);display:flex;align-items:center;justify-content:center;font-family:var(--font-d);font-size:13px;font-weight:900;color:var(--green);flex-shrink:0} +.res-info{flex:1;min-width:0} +.res-addr{font-family:var(--font-mono);font-size:10px;color:var(--text2);margin-bottom:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.res-stats{display:flex;gap:16px} +.res-stat{display:flex;flex-direction:column;gap:1px} +.res-stat-lbl{font-family:var(--font-mono);font-size:7px;color:var(--text3);letter-spacing:2px;text-transform:uppercase} +.res-stat-val{font-family:var(--font-mono);font-size:11px;color:var(--text)} +.tier-badge{font-family:var(--font-mono);font-size:8px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;padding:3px 8px;border-radius:2px;flex-shrink:0} +.tier-gold{background:rgba(255,215,0,.1);color:#FFD700;border:1px solid rgba(255,215,0,.25)} +.tier-silver{background:rgba(192,192,192,.1);color:#C0C0C0;border:1px solid rgba(192,192,192,.25)} +.tier-bronze{background:rgba(205,127,50,.1);color:#CD7F32;border:1px solid rgba(205,127,50,.25)} + +/* DETAIL PAGE */ +.det-grid{display:grid;grid-template-columns:1fr 340px;gap:16px;align-items:start} +.det-main{display:flex;flex-direction:column;gap:14px} +.det-sidebar{display:flex;flex-direction:column;gap:14px;position:sticky;top:16px} +.prob-big{display:grid;grid-template-columns:1fr 1fr;gap:1px;background:var(--border);border:1px solid var(--border);margin-bottom:12px} +.prob-side{padding:20px 18px;background:var(--surface);display:flex;flex-direction:column;gap:4px} +.prob-side-yes{border-right:none} +.prob-side-no{text-align:right} +.prob-side-pct{font-family:var(--font-d);font-size:42px;font-weight:900;letter-spacing:-.04em;line-height:1} +.prob-side-yes .prob-side-pct{color:var(--green);text-shadow:0 0 30px rgba(0,232,122,.4)} +.prob-side-no .prob-side-pct{color:var(--red);text-shadow:0 0 30px rgba(255,61,90,.3)} +.prob-side-lbl{font-family:var(--font-mono);font-size:8px;letter-spacing:3px;text-transform:uppercase;color:var(--text3)} +.prob-side-pool{font-family:var(--font-mono);font-size:11px;color:var(--text2);margin-top:6px} + +@media(max-width:768px){ + .det-grid{grid-template-columns:1fr} + .det-sidebar{position:static} + .prob-side-pct{font-size:32px} +} + +/* MOBILE CARD OVERRIDES */ +@media(max-width:768px){ + .mgrid{grid-template-columns:1fr;gap:10px} + .mcard-top{padding:12px 12px 10px} + .prob-yes,.prob-no{font-size:28px} + .mcard-q{font-size:12px} + .mcard-meta{padding:7px 12px} + .meta-lbl{font-size:7px} + .meta-val{font-size:10px} + .mcard-actions{padding:8px 12px;gap:6px} + .mcard-btn{padding:8px 6px;font-size:8px} + .sbar .chip{font-size:9px;padding:4px 8px} + .cat{font-size:9px;padding:5px 10px} + .ph-title{font-size:22px} + .ph-sub{font-size:12px} +} + + + diff --git a/Frontend_backup.html b/Frontend_backup.html new file mode 100644 index 0000000000..55cb554241 --- /dev/null +++ b/Frontend_backup.html @@ -0,0 +1,2152 @@ + + + + + +PRAXIS — Prediction Markets + + + + + + + +
+ ⚠   Node offline — configure endpoint via Node settings +
+ + +
+
+
Confirm Transaction
+
review before signing · canopy network
+
+
⚡ This will broadcast a signed transaction to the Canopy network. Irreversible.
+
+ + +
+
+
+ + +
+
+ + + + + +
+
+
+ +
+
+ + +
+
+ +
+ +
+ +
$PRX · CANOPY
+
+
+
+
+
+
+ Light mode +
+
+
+
+ + +
+ + + + + +
+ + + + +
+
+
Live chain state
+
Prediction Markets
+
Browse all active markets — click Bet YES / NO to place a position
+
+
+
Block
+
Markets
+
+ +
+
+
▪ ▪ ▪  loading markets
+
+ + +
+
+
Take a position
+
Place Prediction
+
Stake $PRX on YES or NO — LMSR pricing, cost computed on-chain
+
+
+
// prediction_parameters
+
+
+
+
+
+
+
YES
+
NO
+
+
+
+
min 1 PRX
+
slippage guard
+
+
+
+ + +
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
+
Your positions
+
My Predictions
+
All prediction transactions submitted from your loaded address
+
+ +
+
+ Load your key in Signer to view predictions +
+
+
+ + +
+
+
Collect payout
+
Claim Winnings
+
Pro-rata payout from the market pool after finalization
+
+
+
// claim_parameters
+
+
+
+
+
+
+ + +
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
+
Account
+
Wallet
+
Query balance and send $PRX tokens
+
+
+
// balance_lookup
+
+ +
+ +
+
+
+ +
+
+
// send_tokens
+
+
+
+
+
+
+
+
+
+ + +
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + + + +
+
+
Resolver · Registration
+
Register Resolver
+
Stake $PRX to become eligible to propose outcomes on expired markets
+
+
Resolvers must maintain stake and a strong reputation score (RRS) to be assigned to markets. Dishonest proposals risk slash.
+
+
// resolver_registration
+
+
+
min 800,000 uPRX
+
+
+
+ + +
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
+
Resolver · Resolution
+
Propose Outcome
+
Post a bonded outcome proposal after market expiry — opens dispute window
+
+
⚠ Proposal bond is at risk if your outcome is successfully disputed. Ensure you have strong evidence before proposing.
+
+
// proposal_parameters
+
+
+
+
+
+
+
YES
+
NO
+
+
+
+
min 1% of BEff
+
+
+
+ + +
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
+
Resolver · Dispute
+
File Dispute
+
Challenge a proposed outcome within the dispute window
+
+
⚠ Dispute bond must match the proposal bond. If the panel votes against your dispute, your bond is slashed.
+
+
// dispute_parameters
+
+
+
+
+
+
must match proposal bond
+
+
+
+ + +
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
+
Resolver · Panel voting
+
Commit Vote
+
Submit a blinded vote hash during the commit phase
+
+
Compute: commit_hash = SHA256(vote_byte ∥ nonce ∥ voter_addr)  vote_byte: 0x01 = YES, 0x00 = NO
+
+
// commit_parameters
+
+
+
+
+
+
+
+
+
+ + +
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
+
Resolver · Panel voting
+
Reveal Vote
+
Reveal your blinded vote after the commit phase closes
+
+
+
// reveal_parameters
+
+
+
+
+
+
+
YES
+
NO
+
+
+
+
+
+
+
+ + +
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
+
Resolver · Panel voting
+
Tally Votes
+
Trigger vote tally after the reveal phase ends — anyone can call this
+
+
+
// tally_parameters
+
+
+
+
+
+
+ + +
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
+
Resolver · Enforcement
+
Claim Slash
+
Claim the slash reward from a dishonest resolver's forfeited bond
+
+
+
// slash_parameters
+
+
+
+
+
+
+ + +
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + + + +
+
+
Admin · Markets
+
Create Market
+
Deploy a new YES/NO prediction market on Canopy — B0 sets initial LMSR liquidity
+
+
MIN_B0 is 60,000,000 uPRX (60 PRX). Of this, 50 PRX is reserved as finalization bounty — the LMSR pool seeds with 10 PRX.
+
+
// market_parameters
+
+
+
min 60,000,000
+
current + N blocks
+
+
+ + +
+ + +
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
+
Admin · Markets
+
Finalize Market
+
Seal the market after PORS vote tally — unlocks claim_winnings for all positions
+
+
⚠ Only callable after vote tally or when dispute window has passed without challenge. Caller receives the 50 PRX finalization bounty.
+
+
// finalize_parameters
+
+
+
+
+
+
+ + +
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
+
Admin · Key management
+
Signer
+
Load your BLS12-381 private key — held in RAM only, never stored or transmitted
+
+
+
// session_key
+
○ No key loaded — transactions cannot be signed
+
+ + +
⚠ Only enter on a trusted, private instance. Key is wiped on page reload.
+
+
+ + +
+ + +
+
+
// signing_specification
+
+ 1. sign_bytes = proto.Marshal(Transaction{…fields, signature: nil})
+ 2. time = BigInt(Date.now()) × 1000n — microseconds, computed once
+ 3. same txTime in sign bytes AND body JSON — never recompute
+ 4. BLS12-381 G2 signature (96 bytes) via @noble/curves bls12_381.sign()
+ 5. address = SHA256(pubKey).slice(0, 20) — first 20 bytes
+ 6. Plugin format: msgTypeUrl + msgBytes (hex) for all message types +
+
+
+ + +
+
+
Admin · System
+
Node Status
+
RPC endpoint configuration and live chain diagnostics
+
+
+
// rpc_connection
+
+ + +
Public RPC → :50002  ·  Admin RPC → :50003 (local only)
+
+
+
+
+
// chain_status
+
+
Block Height
+
Connection
+
RPC Endpoint
+
+
+
+
// failed_transactions
+
+
+ +
+
+ +
+
+ + +
+
+ + Markets +
+
+ + Predict +
+
+ + Positions +
+
+ + Wallet +
+
+ + Signer +
+
+ +
+ + + + diff --git a/README.md b/README.md index 45ed2881b7..cbcdaaf0c2 100644 --- a/README.md +++ b/README.md @@ -1,108 +1,239 @@ -Canopy Logo +# PRAXIS -_Official golang implementation of the Canopy Network Protocol_ +
-[![GoDoc](https://img.shields.io/badge/godoc-reference-white.svg)](https://godoc.org/github.com/canopy-network/canopy) -[![Getting Started](https://img.shields.io/badge/getting%20started-guide-white)](https://canopynetwork.org) -[![Go Version](https://img.shields.io/badge/golang-v1.21-white.svg)](https://golang.org) -[![Next.js Version](https://img.shields.io/badge/next%20js-v14.2.3-white.svg)](https://nextjs.org/) +``` +██████╗ ██████╗ █████╗ ██╗ ██╗██╗███████╗ +██╔══██╗██╔══██╗██╔══██╗╚██╗██╔╝██║██╔════╝ +██████╔╝██████╔╝███████║ ╚███╔╝ ██║███████╗ +██╔═══╝ ██╔══██╗██╔══██║ ██╔██╗ ██║╚════██║ +██║ ██║ ██║██║ ██║██╔╝ ██╗██║███████║ +╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝ +``` +**On-Chain Prediction Markets on the Canopy Network** -# Overview +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![Go](https://img.shields.io/badge/Go-1.24-00ADD8?logo=go)](https://go.dev) +[![Canopy](https://img.shields.io/badge/Canopy-Betanet-00ff88)](https://canopynetwork.org) +[![Plugin](https://img.shields.io/badge/Plugin-Go-00d4ff)](plugin/go) +[![Status](https://img.shields.io/badge/Status-Betanet-ffc940)](https://canopynetwork.org) -[![License](https://img.shields.io/badge/License-MIT-white.svg)](https://opensource.org/licenses/MIT) -[![Testing](https://img.shields.io/badge/testing-docker%20compose-white)](https://docs.docker.com/compose/) -[![Platform](https://img.shields.io/badge/platform-linux%20%7C%20macos-white.svg)](https://github.com/canopy-network/canopy/releases) -[![Status](https://img.shields.io/badge/status-alphanet-white)](https://docs.docker.com/compose/) +
-### ⫸ **Welcome to the Network that Powers the Peer-to-Peer Launchpad for New Chains** +--- -Built on a recursive architecture, chains bootstrap each other into independence — -forming an `unstoppable` web of utility and security. +## Overview -**Here you'll find:** +Praxis is a sovereign prediction market protocol built as a Canopy Nested Chain. -➪ A recursive framework to build blockchains. +Praxis ($PRX) combines the **ADLMSR** logarithmic market scoring rule (LMSR) with the **PORS** optimistic resolution system. It supports 13 on-chain transaction types — creating markets, submitting predictions, resolving markets via a resolver/panel/dispute flow, and claiming winnings or slashed bonds. The plugin runs as an application-specific blockchain with its own state, token, and custom transaction logic. -➪ The seed chain that started the recursive cycle. +[![architecture](https://img.shields.io/badge/architecture-appchain-00ff88)]() +[![consensus](https://img.shields.io/badge/consensus-NestBFT-00d4ff)]() +[![signing](https://img.shields.io/badge/signing-BLS12--381-b48eff)]() +[![state](https://img.shields.io/badge/state-key--value-ffc940)]() -For more information on the Canopy Network Protocol visit [https://canopynetwork.org](https://canopynetwork.org) +--- -## Network Status +## Transaction Types -⪢ Canopy is in `Betanet` 🚀 ➝ learn more about the [road-to-mainnet](https://www.canopynetwork.org/learn-more/road-to-mainnet) +| # | Transaction | Description | +|---|-------------|-------------| +| 0 | `send` | Transfer $PRX between accounts | +| 1 | `create_market` | Open a YES/NO prediction market with LMSR liquidity | +| 2 | `submit_prediction` | Purchase shares on YES or NO using LMSR pricing | +| 3 | `resolve_market` | (ADLMSR – deprecated in combined flow) | +| 4 | `claim_winnings` | Claim pro-rata payout from a finalised or cancelled/voided market | +| 5 | `register_resolver` | Stake $PRX to become a registered resolver | +| 6 | `propose_outcome` | Propose the winning outcome after a market expires (replaces `resolve_market`) | +| 7 | `file_dispute` | Challenge a proposed outcome with a bond | +| 8 | `commit_vote` | Panel members submit a blinded vote | +| 9 | `reveal_vote` | Panel members reveal their vote | +| 10 | `tally_votes` | Tally revealed votes and determine dispute outcome | +| 11 | `finalize_market` | Finalise a market — caller receives a 50 PRX bounty | +| 12 | `claim_slash` | Claim the slashed bond of a losing disputer | -## Protocol Documentation +--- -➪ Check out the Canopy Network wiki: [https://canopy-network.gitbook.io/docs](https://canopy-network.gitbook.io/docs) +## Architecture — Combined ADLMSR + PORS -## Repository Documentation +The original ADLMSR LMSR market-maker is augmented by the **Praxis Optimistic Resolution System (PORS)**. After a market expires, a registered resolver proposes the outcome. A dispute window opens and any other resolver can challenge the proposal by posting a bond. A panel of independent resolvers is randomly selected and commits-reveals votes. Once tallied, finalisation slashes the losing party and rewards the winner. -Welcome to the Canopy Network reference implementation. This repository can be well understood reading about the core modules: +File-descriptor registration is handled by `z_descriptor.go`, which decompresses `file_tx_proto_rawDescGZIP()` and provides all proto definitions to the Canopy node during the handshake. -- [Controller](controller/README.md): Coordinates communication between all the major parts of the Canopy blockchain, like a central hub or "bus" that connects the system together. -- [Finite State Machine (FSM)](fsm/README.md): Defines the logic for how transactions change the blockchain's state — it decides what’s valid and how state transitions happen from one block to the next. -- [Byzantine Fault Tolerant (BFT) Consensus](bft/README.md): A consensus mechanism that allows the network to agree on new blocks even if some nodes are unreliable or malicious. -- [Peer-to-Peer Networking](p2p/README.md): A secure and encrypted communication system that lets nodes talk directly to each other without needing a central server. -- [Persistence](store/README.md): Manages the blockchain’s storage — it saves the current state (ledger), indexes past transactions, and ensures fast and reliable data verification. +--- -## How to Run It +## State Model -➪ To run the Canopy binary, use the following commands: +Praxis state is stored in the Canopy KV store with byte-prefixed keys: -```bash -make build/canopy-full -canopy start +| Prefix | Type | Description | +|--------|------|-------------| +| `0x10` | `MarketState` | Per-market record (status, LMSR quantities, creator, expiry) | +| `0x11` | `PositionState` | Per-bettor position (shares YES/NO, cost paid) | +| `0x12` | `OutcomeState` | Winning outcome and resolution height | +| `0x13` | `ResolverState` | Per-market assigned resolver | +| `0x14` | `TreasuryReserve` | Locked PRX for bounties and bonds | +| `0x16` | `ResolverRecord` | Global resolver profile (stake, RRS score) | +| `0x17` | `ProposalRecord` | Outcome proposal for a market | +| `0x18` | `DisputeRecord` | Dispute details, panel, vote status | +| `0x19` | `VoteCommit` | Blinded vote commitment per panel member | +| `0x1A` | `VoteReveal` | Revealed vote per panel member | +| `0x1B` | `SlashRecord` | Record of a slashed resolver | +| `0x1C` | `PanelEntropyAccum` | Rolling entropy accumulator for panel selection | + +Built-in prefixes (`0x01` Account, `0x02` Pool, `0x07` FeeParams) are preserved. + +--- + +## Key ADLMSR Parameters + +| Constant | Value | Meaning | +|----------|-------|---------| +| `PRECISION_SCALE` | 1,000,000 | Fixed-point scaling for LMSR quantities | +| `MIN_B0` | 1,000,000 | Minimum initial liquidity (1 PRX) | +| `ELEVATED_RISK_THRESHOLD` | 25,000,000,000 | Pool size at which markets become elevated-risk | +| `RESOLUTION_DELAY_BLOCKS` | 100 | Blocks after expiry before resolution can begin | +| `GRACE_PERIOD_BLOCKS` | 200 | Resolution window | +| `CLAIM_GRACE_PERIOD` | 1000 | Time window to claim winnings | + +**LMSR cost function:** + +``` +C(qYes, qNo) = bEff · ln( exp(qYes/bEff) + exp(qNo/bEff) ) ``` -## How to Run It with 🐳 Docker +**Payout:** overflow-safe pro-rata via `quot·winnerShares + rem·winnerShares/totalWinShares` -➪ To run a Canopy `Localnet` in a *containerized* environment, use the following commands: -```bash -make docker/build -make docker/up-fast -make docker/logs +--- + +## Payout Example + +``` +YES pool: 600,000 μPRX (bettor contributed 200,000) +NO pool: 400,000 μPRX (losing side) + +Bettor share of YES pool: 200,000 / 600,000 = 33.3% +Bettor payout: 200,000 + (400,000 × 33.3%) = 333,333 μPRX +``` -or simply +--- -make docker/up && make docker/logs +## Repository Layout + +``` +plugin/go/ +├── main.go +├── chain.json +├── Makefile +├── pluginctl.sh +├── AGENTS.md +├── README.md +├── go.mod / go.sum +│ +├── contract/ +│ ├── contract.go ← ContractConfig + lifecycle methods +│ ├── constants.go ← All named constants +│ ├── error.go ← Praxis error codes 100-199 +│ ├── helpers.go ← mulDiv, DeriveMarketId, ComputeCommitHash +│ ├── keys.go ← All state key constructors +│ ├── lmsr.go ← LMSR cost, trade, payout, min bond +│ ├── height.go ← Global height with RWMutex +│ ├── plugin.go ← Socket protocol (never modify) +│ ├── z_descriptor.go ← Proto file descriptor registration +│ ├── handler_*.go (17 files) ← CheckTx+DeliverTx for all tx types +│ ├── tx.pb.go ← Generated (never edit) +│ ├── event.pb.go ← Generated +│ ├── account.pb.go ← Generated +│ └── plugin.pb.go ← Generated +│ +├── crypto/ +│ ├── bls.go ← BLS12-381 signing (kyber/bdn) +│ └── signing.go ← GetSignBytes helper +│ +├── proto/ +│ ├── tx.proto ← Message & state definitions +│ ├── account.proto +│ ├── plugin.proto +│ ├── event.proto +│ └── _generate.sh +│ +└── tutorial/ + ├── main.go + ├── go.mod / go.sum + ├── praxis_test.go ← Integration test for send + create_market + └── contract/ ← (tutorial-local generated copies) + +frontend/ +└── index.html ← Single-file HTML/JS dashboard ``` -## Running Tests +--- + +## Getting Started -➪ To run Canopy unit tests, use the Go testing tools: +### Build ```bash -make test +git clone https://github.com/Makaveli912/canopy.git +cd canopy +git checkout feat/praxis-prediction-markets + +# Build Canopy node +go build -o ~/go/bin/canopy ./cmd/main + +# Build Praxis plugin +cd plugin/go +GOTOOLCHAIN=local go build -o go-plugin . ``` -## How to Contribute +### Run + +```bash +cd ~/canopy +canopy start +``` + +Watch for: + +``` +Plugin go started: go-plugin started successfully +Plugin service listening on socket: /tmp/plugin/plugin.sock +``` + +### Test + +```bash +cd plugin/go/tutorial +GOTOOLCHAIN=local go test -v -run TestCreateMarket -timeout 120s +``` -➪ Canopy is an open-source project, and we welcome contributions from the community. Here's how to get involved: +--- -1. **Fork** the repository and clone it locally. -2. **Code** your improvements or fixes. -3. **Submit a Pull Request** (PR) for review. +## Token -➣ Please follow these [guidelines](CONTRIBUTING.md) to maintain high-quality contributions: +| Property | Value | +|----------|-------| +| Name | Praxis | +| Symbol | $PRX | +| Denomination | μPRX (micro-PRX) | +| Chain ID | 1 | +| Network ID | 1 | -### High Impact or Architectural Changes +--- -➪ Before making large changes, discuss them with the Canopy team on [Discord](https://discord.gg/pNcSJj7Wdh) to ensure alignment. +## Error Codes -### Coding Style +| Range | Description | +|-------|-------------| +| 1–14 | Standard Canopy built-in errors | +| 100–199 | Praxis-specific errors (market, position, resolution, dispute, etc.) | -- Code must adhere to official Go formatting (use [`gofmt`](https://golang.org/cmd/gofmt)). -- (Optional) Use [EditorConfig](https://editorconfig.org) for consistent formatting. -- All code should follow Go documentation/commentary guidelines. -- PRs should be opened against the `development` branch. +See `contract/error.go` for the full list. -[![Pre-Release](https://img.shields.io/github/release-pre/canopy-network/canopy.svg)](https://github.com/canopy-network/canopy/releases) -[![Go Report Card](https://goreportcard.com/badge/github.com/canopy-network/canopy)](https://goreportcard.com/report/github.com/canopy-network/canopy) -[![Contributors](https://img.shields.io/github/contributors/canopy-network/canopy.svg)](https://github.com/canopy-network/canopy/pulse) -[![Last Commit](https://img.shields.io/github/last-commit/canopy-network/canopy.svg)](https://github.com/canopy-network/canopy/pulse) +--- -## Contact +## License -[![Twitter](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://x.com/CNPYNetwork) -[![Discord](https://img.shields.io/badge/discord-online-blue.svg)](https://discord.gg/pNcSJj7Wdh) +MIT — see [LICENSE](LICENSE) diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000000..80f47fc279 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,95 @@ +# Praxis V2 Roadmap + +19 items · 3 waves · Active development + +--- + +## Wave 1 — Critical (Build Now) + +### Security & Integrity + +**1. Dispute Window After Resolution** +Add a `dispute_resolution` transaction. After a market is resolved, open a 24-hour (N block) dispute window. Any address can challenge the outcome by staking a dispute bond. If the dispute succeeds, the original resolver is slashed and the market is re-opened. +Tags: `Security` `New Tx Type` + +**2. Resolution Window Enforcement** +Block `resolve_market` calls before the market's `resolution_height` is reached. Currently any resolver can finalise early. Add a height check in `DeliverResolveMarket`. +Tags: `Security` `Critical` + +**3. Creator Self-Resolution Restriction** +Prevent the market creator from also being the resolver. This is the single most obvious manipulation vector. One-line check in `CheckCreateMarket`. +Tags: `Security` `Critical` + +**4. Invalid Market Handling — Void + Refund** +Add an `INVALID` resolution outcome. When a market resolves as INVALID, return all stakes to the original forecasters with no fee deduction. Required for markets where the question becomes unanswerable. +Tags: `Security` `New Tx Type` + +**5. Resolver Stake Slashing** +Require resolvers to bond a stake when designated. If a dispute succeeds against their resolution, slash the bond and redistribute to the disputing address. Aligns resolver incentives with honest outcomes. +Tags: `Security` `Protocol` + +### Revenue + +**6. Losing Pool Cut — Protocol Treasury Fee** +Take 2–3% of every losing pool before paying out winners. Route it to a hardcoded treasury address in `contract.go`. This is how Polymarket makes most of its revenue. At scale this is the highest ROI change in the entire roadmap. +Tags: `Revenue` `High Priority` + +**7. Volume Fee on Predictions** +Charge a small percentage (0.5–1%) of every prediction stake on submission, separate from the transaction fee. Prediction volume is always much higher than market creation volume. Compounds revenue every time anyone bets on any market. +Tags: `Revenue` + +--- + +## Wave 2 — Core Features + +**8. Treasury Address in contract.go** +Hardcode a protocol treasury address as a constant in `contract.go`. All protocol fee cuts route here automatically. Should be a multisig or governance-controlled account for transparency. +Tags: `Revenue` `Protocol` + +**9. Market Creation Bond** +Charge a returnable bond on market creation — returned to the creator if the market resolves cleanly, slashed if the resolver misbehaves or the market is abandoned. Deters spam and aligns creator incentives with good market hygiene. +Tags: `Revenue` `Security` + +**10. Position Withdrawal — Exit Before Resolution** +Add a `withdraw_prediction` transaction that lets forecasters exit their position before the market resolves. Apply a withdrawal penalty (e.g. 10%) to discourage gaming — penalty goes to the remaining pool. Polymarket's most-used feature after betting itself. +Tags: `New Tx Type` `UX` + +**11. Market Categories and Tags** +Add a `category` string field to `MessageCreateMarket`. Categories: Crypto, Politics, Sports, Science, Entertainment, Other. Enables filtering on the Markets page and makes large market sets navigable. +Tags: `Proto Change` `UX` + +--- + +## Wave 3 — Growth & Polish + +**12. Leaderboard — Top Forecasters** +Rank addresses by total profit, accuracy rate, and number of markets predicted. Query all prediction transactions, aggregate by address, and display on a dedicated Leaderboard page. Builds competition and social proof. +Tags: `Growth` `Frontend` + +**13. My Positions Page** +A personal dashboard showing all open predictions, pending claims, resolved markets, and total P&L. Query transactions by the loaded address and display a personal history. Makes the protocol feel like a real trading interface. +Tags: `UX` `Frontend` + +**14. Market Search** +Client-side search over loaded markets by question text, category, or creator address. No new RPC endpoint needed — filter the already-loaded market list. Simple but essential once there are more than 20 markets. +Tags: `UX` `Frontend` + +**15. Rate Limiting — Cap Market Creation Per Address** +Limit each address to N market creations per block or per epoch. Prevents a single actor from flooding the chain with spam markets. Implement as a counter in `DeliverCreateMarket` state. +Tags: `Security` `Protocol` + +--- + +## Competitive Analysis + +| Protocol | Sovereign Chain | No Oracle | Position Exit | Dispute | Revenue Model | +|---|---|---|---|---|---| +| **Praxis v1** | ✓ | ✓ | ✗ | ✗ | Fees only | +| Polymarket | ✗ | ✗ | ✓ | ✓ | 2% volume fee | +| Augur | ✗ | ✗ | ✓ | ✓ | Reporter fees | +| Manifold | ✗ | ✓ | ✓ | ✗ | Token model | +| **Praxis v2 target** | ✓ | ✓ | ✓ | ✓ | Pool cut + volume | + +--- + +*Praxis ($PRX) · Canopy Nested Chain · Prediction Markets Protocol* diff --git a/plugin/go/AGENTS.md b/plugin/go/AGENTS.md index e89a413e14..46f2f2cae1 100644 --- a/plugin/go/AGENTS.md +++ b/plugin/go/AGENTS.md @@ -1,214 +1,111 @@ -# Agent Instructions for Go Plugin +# Praxis Plugin — AI Agent Context -This document provides context for AI agents working with the Canopy Go plugin codebase. +This file provides context for AI coding assistants working on the Praxis +prediction-market plugin for the Canopy Network. -## Overview +## What This Is -This is a **Go plugin for the Canopy blockchain** that communicates with the Canopy FSM (Finite State Machine) via Unix sockets. The plugin implements custom transaction types and state management for a nested blockchain. +Praxis is an on-chain YES/NO prediction market protocol built as a Canopy +Nested Chain plugin. It is written in Go and implements the Canopy plugin +interface (the five lifecycle methods: Genesis, BeginBlock, CheckTx, +DeliverTx, EndBlock). -## Architecture +## Repository Layout -### Communication Pattern -- **Protocol**: Length-prefixed protobuf messages over Unix socket (`/tmp/plugin/plugin.sock`) -- **Flow**: FSM ↔ Plugin via `FSMToPlugin` and `PluginToFSM` protobuf messages -- **Lifecycle**: Plugin connects → Handshake → Receives tx requests → Responds with results - -### Key Components - -| Directory | Purpose | -|-----------|---------| -| `contract/` | Core contract logic, protobuf generated code, plugin communication | -| `crypto/` | BLS12-381 signing utilities | -| `proto/` | Protobuf definitions (`.proto` files) | -| `tutorial/` | Test project with pre-built faucet/reward transaction examples | - -### Important Files - -- `contract/contract.go` - Main contract logic with `CheckTx` and `DeliverTx` handlers -- `contract/plugin.go` - Socket communication and plugin lifecycle -- `contract/*.pb.go` - Generated protobuf code (do not edit manually) -- `proto/tx.proto` - Transaction message definitions -- `main.go` - Entry point - -## Transaction Flow - -1. **CheckTx**: Stateless validation (fee check, address validation, returns authorized signers) -2. **DeliverTx**: Stateful execution (reads state, applies changes, writes state) - -## Adding New Transaction Types - -Follow the pattern in `TUTORIAL.md`: - -1. Add message to `proto/tx.proto` -2. Run `proto/_generate.sh` to regenerate Go code -3. Register in `ContractConfig.SupportedTransactions` and `TransactionTypeUrls` -4. Add `case` in `CheckTx` switch → implement `CheckMessage` -5. Add `case` in `DeliverTx` switch → implement `DeliverMessage` - -## State Management - -### Key Prefixes -- `[]byte{1}` - Account storage -- `[]byte{2}` - Pool storage -- `[]byte{7}` - Governance parameters - -### State Operations -```go -// Read state -c.plugin.StateRead(c, &PluginStateReadRequest{Keys: []*PluginKeyRead{...}}) - -// Write state -c.plugin.StateWrite(c, &PluginStateWriteRequest{Sets: [...], Deletes: [...]}) ``` - -## Cryptography - -- **Signature scheme**: BLS12-381 (not Ed25519) -- **Address derivation**: First 20 bytes of SHA256(publicKey) -- **Sign bytes**: Deterministic protobuf marshaling of Transaction (without signature field) - -## Building - -```bash -cd plugin/go -make build # Builds to plugin/go/go-plugin +plugin/go/ +├── main.go # Entry point — calls contract.StartPlugin(). Do not modify. +├── chain.json # Chain metadata (name, symbol, chainId, networkId) +├── Makefile # Build targets — run `make build` before `canopy start` +├── pluginctl.sh # Plugin lifecycle script (start/stop/restart/status) +├── crypto/ # BLS12-381 signing helpers (key loading, sign-bytes, address derivation) +│ +├── contract/ +│ ├── plugin.go # Socket protocol, StartPlugin(), StateRead/StateWrite. Do not modify. +│ ├── contract.go # ALL application logic lives here — this is the file to edit +│ └── error.go # Plugin error codes. Built-in: 1–14. Praxis: 15–29. +│ +└── proto/ + ├── tx.proto # Transaction and state message definitions + ├── account.proto # Account and Pool types (built-in, do not modify) + ├── plugin.proto # FSM communication protocol (do not modify) + ├── event.proto # Event types (do not modify) + └── _generate.sh # Run this after editing tx.proto to regenerate Go structs ``` -## Running with Docker +## Transaction Types -The Go plugin can be run in a Docker container that includes both Canopy and the plugin. +| Name | Type URL | Signer | +|---------------------|---------------------------------------------------|-------------------| +| `send` | `type.googleapis.com/types.MessageSend` | from_address | +| `create_market` | `type.googleapis.com/types.MessageCreateMarket` | creator_address | +| `submit_prediction` | `type.googleapis.com/types.MessageSubmitPrediction`| forecaster_address| +| `resolve_market` | `type.googleapis.com/types.MessageResolveMarket` | resolver_address | +| `claim_winnings` | `type.googleapis.com/types.MessageClaimWinnings` | claimer_address | -### Build the Docker Image +## State Key Prefixes -From the repository root: +| Prefix | Type | Key function | +|--------|---------------|-------------------------| +| 0x01 | Account | KeyForAccount(addr) | +| 0x02 | Pool | KeyForFeePool(chainId) | +| 0x07 | FeeParams | KeyForFeeParams() | +| 0x10 | Market | KeyForMarket(id) | +| 0x11 | MarketCounter | KeyForMarketCounter() | +| 0x12 | Prediction | KeyForPrediction(addr, marketId) | -```bash -make docker/plugin PLUGIN=go -``` +**Important**: All keys are built with `JoinLenPrefix` which length-prefixes +each segment. This means `KeyForMarket(1)` starts with bytes `[0x01, 0x10, ...]` +not `[0x10, ...]`. When querying state by prefix, use `0110` (hex), not `10`. -This builds a Docker image named `canopy-go` that contains: -- The Canopy binary -- The Go plugin binary and control script -- Pre-configured `config.json` with `"plugin": "go"` +## Critical Rules (from Canopy Plugin Spec) -### Run the Container +1. `SupportedTransactions[i]` MUST match `TransactionTypeUrls[i]` exactly. + A mismatch causes silent transaction misrouting. -```bash -make docker/run-go -``` - -Or manually with volume mount for persistent data: +2. `CheckTx` is stateless. It CANNOT write state. It may read state minimally + (the fee params read is acceptable). Do not add state reads for business + logic — those belong in `DeliverTx`. -```bash -docker run -v ~/.canopy:/root/.canopy canopy-go -``` +3. Sign bytes = `proto.Marshal(txWithSignatureFieldNil)`. Never sign JSON. -### Expose Ports for Testing +4. If `DeliverTx` returns an error, the transaction is still included in the + block and the fee is still charged. `CheckTx` must catch everything + recoverable. -To run tests against the containerized Canopy, expose the RPC ports: - -```bash -docker run -p 50002:50002 -p 50003:50003 -v ~/.canopy:/root/.canopy canopy-go -``` +5. `PluginDeliverRequest` does not carry a block height. Capture it in + `BeginBlock` via `c.currentHeight = req.Height`. -| Port | Service | -|------|---------| -| 50002 | RPC API (transactions, queries) | -| 50003 | Admin RPC (keystore operations) | +6. Always batch-read all required keys in a single `StateRead` call, then + batch-write all changes in a single `StateWrite` call. -Now you can run tests from your host machine that connect to `localhost:50002`. +7. When building composite state keys with `append`, always allocate a new + slice. `append(existingSlice, ...)` may mutate the original if it has + spare capacity. -### View Logs - -```bash -# Get the container ID -docker ps - -# View Canopy logs -docker exec -it tail -f /root/.canopy/logs/log - -# View plugin logs -docker exec -it tail -f /tmp/plugin/go-plugin.log -``` +## Adding a New Transaction Type -## Running with Canopy - -1. Add `"plugin": "go"` to `~/.canopy/config.json` -2. Start Canopy: `~/go/bin/canopy start` -3. Plugin auto-starts and connects via Unix socket - -## Testing - -```bash -cd plugin/go/tutorial -go test -v -run TestPluginTransactions -timeout 120s -``` - -Requires Canopy running with the plugin enabled and faucet/reward transactions implemented. - -## Code Conventions - -- **Error handling**: Return `*PluginError` structs, use error functions from `error.go` -- **Protobuf**: Use `Marshal`/`Unmarshal` helpers from `contract/plugin.go` -- **Logging**: Use `log.Printf` for debugging (logs to `/tmp/plugin/go-plugin.log`) -- **QueryIds**: Use `rand.Uint64()` to correlate batch state read requests - -## Common Patterns - -### CheckTx Template -```go -func (c *Contract) CheckMessage(msg *Message) *PluginCheckResponse { - // Validate addresses (must be 20 bytes) - if len(msg.Address) != 20 { - return &PluginCheckResponse{Error: ErrInvalidAddress()} - } - // Validate amount - if msg.Amount == 0 { - return &PluginCheckResponse{Error: ErrInvalidAmount()} - } - // Return authorized signers - return &PluginCheckResponse{ - Recipient: msg.RecipientAddress, - AuthorizedSigners: [][]byte{msg.SignerAddress}, - } -} -``` - -### DeliverTx Template -```go -func (c *Contract) DeliverMessage(msg *Message, fee uint64) *PluginDeliverResponse { - // 1. Generate query IDs - queryId := rand.Uint64() - - // 2. Read state - response, err := c.plugin.StateRead(c, &PluginStateReadRequest{...}) - - // 3. Unmarshal accounts - account := new(Account) - Unmarshal(bytes, account) - - // 4. Apply business logic - account.Amount += msg.Amount - - // 5. Marshal and write state - bytes, _ = Marshal(account) - c.plugin.StateWrite(c, &PluginStateWriteRequest{...}) - - return &PluginDeliverResponse{} -} -``` +1. Add the message definition to `proto/tx.proto` +2. Run `proto/_generate.sh` to regenerate Go structs +3. Add the name to `ContractConfig.SupportedTransactions` at index N +4. Add the type URL to `ContractConfig.TransactionTypeUrls` at index N (same index) +5. Add a `case *MessageXxx:` to the `CheckTx` switch and implement `CheckMessageXxx` +6. Add a `case *MessageXxx:` to the `DeliverTx` switch and implement `DeliverMessageXxx` +7. Add a `KeyForXxx` function using an unused byte prefix (> 0x12 for Praxis) +8. If you need a new error, add it to `error.go` starting at code 30+ -## Debugging +## Prompts That Work Well -- **Plugin logs**: `tail -f /tmp/plugin/go-plugin.log` -- **Canopy logs**: `tail -f ~/.canopy/logs/log` -- **Common errors**: - - `"message name X is unknown"` → Transaction not registered in `ContractConfig` - - `"invalid signature"` → Sign bytes mismatch, check protobuf serialization - - Balance not updating → Check `DeliverTx` is being called, wait for block finalization +**Adding a transaction type:** +"Using the Canopy Go plugin pattern in contract.go, add a [tx_name] transaction. +The proto message fields are: [list fields]. Validate [conditions] in CheckTx. +In DeliverTx, read [keys], apply [logic], write [keys] back. Use prefix 0x13." -## Dependencies +**State schema:** +"I need state keys for [type] in a Canopy plugin. Existing prefixes in use: +0x01–0x02, 0x07, 0x10–0x12. Design JoinLenPrefix-based keys that don't collide." -Key external packages: -- `google.golang.org/protobuf` - Protobuf serialization -- `github.com/drand/kyber` + `github.com/drand/kyber-bls12381` - BLS signing +**Frontend encoding:** +"Write a JavaScript hand-encoder for [MessageName] with fields [list with types]. +Field numbers must match tx.proto exactly. Use the varintField/bytesField/stringField +helpers already in the frontend." diff --git a/plugin/go/README.md b/plugin/go/README.md index 6d6df8cf32..38d389164e 100644 --- a/plugin/go/README.md +++ b/plugin/go/README.md @@ -1,178 +1,291 @@ -# Send Transaction Flow Analysis +# PRAXIS +[![Status](https://img.shields.io/badge/status-live-brightgreen)](https://github.com/Makaveli912/canopy) + + +
+ +``` +██████╗ ██████╗ █████╗ ██╗ ██╗██╗███████╗ +██╔══██╗██╔══██╗██╔══██╗╚██╗██╔╝██║██╔════╝ +██████╔╝██████╔╝███████║ ╚███╔╝ ██║███████╗ +██╔═══╝ ██╔══██╗██╔══██║ ██╔██╗ ██║╚════██║ +██║ ██║ ██║██║ ██║██╔╝ ██╗██║███████║ +╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝ +``` + +**On-Chain Prediction Markets on the Canopy Network** + +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![Go](https://img.shields.io/badge/Go-1.24-00ADD8?logo=go)](https://go.dev) +[![Canopy](https://img.shields.io/badge/Canopy-Betanet-00ff88)](https://canopynetwork.org) +[![Plugin](https://img.shields.io/badge/Plugin-Go-00d4ff)](plugin/go) +[![Status](https://img.shields.io/badge/Status-Betanet-ffc940)](https://canopynetwork.org) + +
+ +--- + +## ▶ Praxis is a sovereign prediction market protocol built as a Canopy Nested Chain + +Praxis ($PRX) lets anyone create YES/NO prediction markets, stake on outcomes, and claim proportional winnings — entirely on-chain, with no platform extraction and no central authority. It is implemented as a Go plugin on the Canopy Network, meaning it runs as an application-specific blockchain with its own state, its own token, and its own transaction types. + +[![architecture](https://img.shields.io/badge/architecture-appchain-00ff88)]() +[![consensus](https://img.shields.io/badge/consensus-NestBFT-00d4ff)]() +[![signing](https://img.shields.io/badge/signing-BLS12--381-b48eff)]() +[![state](https://img.shields.io/badge/state-key--value-ffc940)]() + +--- ## Overview -This document analyzes the complete flow of a send transaction in the Canopy blockchain plugin template. The plugin implements a Unix socket-based communication system between smart contracts and the Canopy FSM (Finite State Machine). +Praxis implements four on-chain transaction types: -## Architecture Components +| Transaction | Description | +|---|---| +| `create_market` | Open a new YES/NO prediction market with a question, resolver, and resolution height | +| `submit_prediction` | Stake tokens on a YES or NO outcome for an open market | +| `resolve_market` | The designated resolver finalises a market with the winning outcome | +| `claim_winnings` | Winners claim their proportional payout from the resolved market pool | -### Key Files -- `main.go`: Entry point starting the plugin with graceful shutdown -- `contract/plugin.go`: Socket communication and plugin lifecycle management -- `contract/contract.go`: Core contract logic and transaction processing -- `contract/error.go`: Plugin-specific error definitions -- `proto/*.proto`: Protocol buffer definitions for communication +All state is stored on-chain in the plugin's key-value store. No database, no backend, no off-chain oracle required for basic operation. -## Complete Transaction Flow +--- -### 1. Plugin Initialization (main.go:11-18) +## Architecture + +Praxis follows the standard Canopy plugin architecture. The plugin runs as a separate process alongside the Canopy node and communicates over a Unix socket using Protocol Buffers. ``` -main() → StartPlugin(DefaultConfig()) → Socket connection to FSM +┌─────────────────────────────────────────┐ +│ CANOPY NODE PROCESS │ +│ │ +│ ┌──────────┐ ┌────────────────────┐ │ +│ │ NestBFT │ │ FSM / Controller │ │ +│ │ Consensus│ │ (block lifecycle) │ │ +│ └──────────┘ └────────┬───────────┘ │ +│ │ Unix socket │ +└─────────────────────────┼───────────────┘ + │ + ┌───────────────▼──────────────┐ + │ PRAXIS PLUGIN │ + │ │ + │ Genesis() │ + │ BeginBlock() │ + │ CheckTx() ← validate │ + │ DeliverTx() ← execute │ + │ EndBlock() │ + │ │ + │ Transactions: │ + │ - create_market │ + │ - submit_prediction │ + │ - resolve_market │ + │ - claim_winnings │ + └──────────────────────────────┘ + ▲ + │ HTTP RPC :50002 / :50003 + │ + ┌───────────────┴──────────────┐ + │ PRAXIS FRONTEND │ + │ Single-file HTML/JS │ + │ BLS12-381 signing │ + │ Hand-encoded protobuf │ + └──────────────────────────────┘ ``` -The plugin connects to the Canopy FSM via Unix socket (`plugin.sock`): -- Attempts connection every second until successful -- Creates Plugin instance with configuration -- Starts background listener for FSM messages -- Performs handshake with FSM +--- -### 2. Socket Communication Setup (plugin.go:34-68) +## Repository Layout ``` -StartPlugin() → net.Dial("unix", sockPath) → ListenForInbound() → Handshake() +plugin/go/ +├── main.go # Entry point — calls contract.StartPlugin() +├── chain.json # Chain metadata: name, symbol, chainId, networkId +├── Makefile # Build targets +├── pluginctl.sh # Plugin lifecycle (start/stop/restart/status) +├── AGENTS.md # AI assistant context for this plugin +│ +├── contract/ +│ ├── contract.go # Application logic — all transaction handlers +│ ├── error.go # Error codes (built-in 1–14, Praxis 15–16) +│ ├── plugin.go # Socket protocol — do not modify +│ └── tx.pb.go # Generated Go structs from tx.proto +│ +└── proto/ + ├── tx.proto # Transaction and state message definitions + ├── account.proto # Account and Pool types + ├── plugin.proto # FSM communication protocol + └── _generate.sh # Regenerates Go structs from .proto files + +frontend/ +└── index.html # Single-file frontend — no build step required ``` -- **Socket Path**: `/tmp/plugin/plugin.sock` (default data directory) -- **Protocol**: Length-prefixed protobuf messages -- **Handshake**: Exchanges plugin configuration with FSM -- **Concurrent Handling**: Each message processed in separate goroutine +--- -### 3. Transaction Reception (plugin.go:122-167) +## State Model -``` -FSM → Unix Socket → ListenForInbound() → Route by message type -``` +Praxis stores all on-chain data in the Canopy key-value store using byte-prefixed keys: -When FSM sends a transaction: -1. Plugin receives length-prefixed protobuf message -2. Creates new Contract instance with FSM context -3. Routes message based on type (`FSMToPlugin_Check` or `FSMToPlugin_Deliver`) -4. Processes request concurrently in goroutine +| Prefix | Type | Description | +|---|---|---| +| `0x10` | `Market` | One record per prediction market | +| `0x11` | `MarketCounter` | Singleton — tracks the next market ID | +| `0x12` | `Prediction` | One record per (forecaster, market) pair | -### 4. Transaction Validation - CheckTx (contract.go:38-72) +Built-in Canopy prefixes (`0x01` Account, `0x02` Pool, `0x07` FeeParams) are preserved unchanged. -``` -CheckTx() → Validate Fee → Parse Message → CheckMessageSend() -``` +--- -**Fee Validation**: -- Reads fee parameters from state: `KeyForFeeParams()` (contract.go:42) -- Verifies `tx.fee >= minFees.SendFee` (contract.go:57) -- Returns error if fee too low: `ErrTxFeeBelowStateLimit()` +## Transaction Types -**Message Parsing**: -- Deserializes `tx.msg` from protobuf Any type (contract.go:61) -- Type-switches to handle `MessageSend` (contract.go:66-68) +### create_market -**Send Message Validation** (contract.go:96-111): -- **From Address**: Must be exactly 20 bytes (contract.go:98) -- **To Address**: Must be exactly 20 bytes (contract.go:102) -- **Amount**: Must be greater than 0 (contract.go:106) -- Returns authorized signers: `[][]byte{msg.FromAddress}` +Opens a new YES/NO prediction market. The creator bonds a stake amount and designates a resolver address. The market remains open for predictions until the resolution height is reached. + +```protobuf +message MessageCreateMarket { + bytes creator_address = 1; + string question = 2; + string description = 3; + bytes resolver_address = 4; + uint64 resolution_height = 5; + uint64 stake_amount = 6; +} +``` -### 5. Transaction Execution - DeliverTx (contract.go:74-88) +### submit_prediction +Stakes tokens on a YES (outcome=1) or NO (outcome=2) outcome. Each forecaster may only submit one prediction per market. The staked amount is added to the corresponding pool. + +```protobuf +message MessageSubmitPrediction { + bytes forecaster_address = 1; + uint64 market_id = 2; + uint32 outcome = 3; + uint64 amount = 4; +} ``` -DeliverTx() → Parse Message → DeliverMessageSend() + +### resolve_market + +Finalises the market. Only the designated resolver address may call this. Sets the winning outcome and closes the market to further predictions. + +```protobuf +message MessageResolveMarket { + bytes resolver_address = 1; + uint64 market_id = 2; + uint32 winning_outcome = 3; +} ``` -Routes to `DeliverMessageSend()` with fee parameter for state modifications. +### claim_winnings + +Pays out a winner's original stake plus their proportional share of the losing pool. Each prediction can only be claimed once. -### 6. Send Transaction Processing (contract.go:114-212) +```protobuf +message MessageClaimWinnings { + bytes claimer_address = 1; + uint64 market_id = 2; +} +``` -**State Reading** (contract.go:124-147): +Payout formula: ``` -StateRead() → FSM via Socket → Returns account balances and fee pool +payout = stake + (stake × losing_pool) / winning_pool ``` -Batch read operation for: -- **Fee Pool**: `KeyForFeePool(chainId)` with prefix `[]byte{2}` -- **From Account**: `KeyForAccount(fromAddress)` with prefix `[]byte{1}` -- **To Account**: `KeyForAccount(toAddress)` with prefix `[]byte{1}` +--- + +## Getting Started + +### Prerequisites + +- Go 1.24 or later +- `protoc` and `protoc-gen-go` (for proto regeneration only) +- A running Canopy node + +See the [Canopy Builder Docs](https://canopynetwork.org) for full prerequisites. -**Balance Validation** (contract.go:149-164): -- Calculates total deduction: `amount + fee` -- Checks sender balance: `from.Amount >= amountToDeduct` -- Returns `ErrInsufficientFunds()` if insufficient +### Build -**Balance Updates** (contract.go:166-187): -- **Self-transfer optimization**: Uses same account object if `fromKey == toKey` -- **Sender**: `from.Amount -= (msg.Amount + fee)` -- **Recipient**: `to.Amount += msg.Amount` -- **Fee Pool**: `feePool.Amount += fee` +```bash +# Clone and switch to the Praxis branch +git clone https://github.com/Makaveli912/canopy.git +cd canopy +git checkout feat/praxis-prediction-markets -**State Writing** (contract.go:189-211): +# Build the Canopy node binary +go build -o ~/go/bin/canopy ./cmd/main + +# Build the Praxis plugin binary +cd plugin/go +go build -o go-plugin . ``` -StateWrite() → FSM via Socket → Commits state changes + +### Run + +```bash +# From repo root +canopy start ``` -Two write patterns: -- **Account Deletion**: If sender balance reaches 0, delete sender account (contract.go:191-198) -- **Normal Update**: Update all three entities (contract.go:199-207) +Watch for: +``` +Plugin go started: go-plugin started successfully +Plugin service listening on socket: /tmp/plugin/plugin.sock +``` -### 7. State Key Structure +### Frontend -**Account Storage**: -- Prefix: `[]byte{1}` -- Key: `JoinLenPrefix(accountPrefix, address)` -- 20-byte addresses only +```bash +python3 -m http.server 8080 --directory frontend +``` + +Open `http://localhost:8080`. Go to **Node** → set host to `localhost` → Apply. The green dot confirms connection. -**Fee Pool Storage**: -- Prefix: `[]byte{2}` -- Key: `JoinLenPrefix(poolPrefix, formatUint64(chainId))` +Go to **Signer** → paste your BLS12-381 private key → Load Key. Your address will be auto-derived and filled into all transaction forms. -**Fee Parameters**: -- Prefix: `[]byte{7}` -- Key: `JoinLenPrefix(paramsPrefix, []byte("/f/"))` +--- -### 8. Socket Communication Protocol (plugin.go:239-292) +## Payout Model + +Praxis uses an AMM-style proportional payout. Winners split the entire losing pool in proportion to their contribution to the winning pool. -**Message Format**: ``` -[4-byte length prefix][protobuf message bytes] +Example: + YES pool: 600,000 μPRX (forecaster contributed 200,000) + NO pool: 400,000 μPRX (losing side) + + Forecaster share of YES pool: 200,000 / 600,000 = 33.3% + Forecaster payout: 200,000 + (400,000 × 33.3%) = 333,333 μPRX ``` -**Request/Response Pattern**: -- **Sync Requests**: Plugin waits for FSM response with 10-second timeout -- **Async Handling**: FSM requests processed concurrently -- **Request Correlation**: Unique request IDs track pending operations -- **Error Handling**: Timeout cleanup and error propagation +If no one bet on the losing side, the winner's original stake is returned unchanged. -### 9. Error Handling +--- -**Validation Errors** (error.go): -- `ErrInvalidAddress()` - Code 12: Non-20-byte addresses -- `ErrInvalidAmount()` - Code 13: Zero amounts -- `ErrTxFeeBelowStateLimit()` - Code 14: Insufficient fees -- `ErrInsufficientFunds()` - Code 9: Balance too low +## Error Codes -**System Errors**: -- Socket communication failures -- Protobuf serialization errors -- State read/write timeouts -- Plugin response correlation errors +| Code | Name | Description | +|---|---|---| +| 1–14 | Built-in | Standard Canopy plugin errors | +| 15 | `ErrWrongOutcome` | Claimer's prediction did not match the winning outcome | +| 16 | `ErrDuplicatePrediction` | Forecaster already submitted a prediction for this market | -## Transaction Lifecycle Summary +--- -1. **Plugin Startup**: Connect to FSM via Unix socket -2. **Transaction Receipt**: FSM sends transaction via socket -3. **Validation Phase**: CheckTx validates fee, addresses, and amount -4. **Execution Phase**: DeliverTx reads state, validates balance, updates accounts -5. **State Persistence**: Write updated balances and fee pool to FSM -6. **Response**: Return success/error to FSM via socket +## Token -## Key Features +| Property | Value | +|---|---| +| Name | Praxis | +| Symbol | $PRX | +| Denomination | μPRX (micro-PRX) | +| Chain ID | 1 | +| Network ID | 1 | -- **Concurrent Processing**: Multiple transactions handled simultaneously -- **State Management**: Efficient batch reads/writes with FSM -- **Error Recovery**: Comprehensive error handling and timeouts -- **Account Lifecycle**: Automatic cleanup of zero-balance accounts -- **Fee Collection**: Transparent fee pooling for network sustainability +--- -## Performance Characteristics +## License -- **Socket Communication**: Low-latency Unix domain sockets -- **Batch Operations**: Multiple state operations in single FSM call -- **Memory Efficiency**: Length-prefixed messaging avoids buffering issues -- **Concurrent Safety**: Thread-safe request correlation and state management \ No newline at end of file +MIT — see [LICENSE](LICENSE) diff --git a/plugin/go/chain.json b/plugin/go/chain.json index c702b8cdb3..7069fbf8a5 100644 --- a/plugin/go/chain.json +++ b/plugin/go/chain.json @@ -1,16 +1,6 @@ { + "name": "Praxis", + "symbol": "PRX", "chainId": 1, - "name": "canopy", - "symbol": "CNPY", - "website": "https://canopynetwork.org", - "logoURI": "https://", - "bannerURI": "https://", - "explorerLogoURI": "https://", - "walletLogoURI": "https://", - "color1Hex": "#2c9b5a", - "color2Hex": "#16502e", - "description": "Canopy is a recursive, progressive-sovereignty framework for blockchains", - "consensusPreset": 1, - "initialMintPerBlock": 80000000, - "blocksPerHalvening": 3150000 -} \ No newline at end of file + "networkId": 1 +} diff --git a/plugin/go/contract/account.pb.go b/plugin/go/contract/account.pb.go index 15ca5f467b..330669fe8c 100644 --- a/plugin/go/contract/account.pb.go +++ b/plugin/go/contract/account.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.11 // protoc v5.29.3 // source: account.proto @@ -145,7 +145,7 @@ const file_account_proto_rawDesc = "" + "\x06amount\x18\x02 \x01(\x04R\x06amount\".\n" + "\x04Pool\x12\x0e\n" + "\x02id\x18\x01 \x01(\x04R\x02id\x12\x16\n" + - "\x06amount\x18\x02 \x01(\x04R\x06amountB.Z,github.com/canopy-network/go-plugin/contractb\x06proto3" + "\x06amount\x18\x02 \x01(\x04R\x06amountB5Z3github.com/canopy-network/canopy/plugin/go/contractb\x06proto3" var ( file_account_proto_rawDescOnce sync.Once diff --git a/plugin/go/contract/constants.go b/plugin/go/contract/constants.go new file mode 100644 index 0000000000..d5f2cd9537 --- /dev/null +++ b/plugin/go/contract/constants.go @@ -0,0 +1,167 @@ +package contract + +import "os" + +// ═══════════════════════════════════════════════════════════════════════════════ +// Praxis Prediction Market — Named Constants +// Spec authority: +// ADLMSR v5.6.6-r2-CORRECTED +// PORS v1.0-r2-CORRECTED +// ═══════════════════════════════════════════════════════════════════════════════ + +// ───────────────────────────────────────────────────────────────────────────── +// MARKET STATUS CONSTANTS +// CRIT-2: STATUS_FINALIZED (= 6) is the ClaimWinnings gate — never STATUS_RESOLVED. +// NF-6: STATUS_EXPIRED is never persisted — propose_outcome transitions inline. +// ───────────────────────────────────────────────────────────────────────────── + +const ( +STATUS_OPEN uint32 = 0 +STATUS_CANCELLED uint32 = 1 +STATUS_RESOLVED uint32 = 2 +STATUS_EXPIRED uint32 = 3 +STATUS_PROPOSED uint32 = 4 +STATUS_DISPUTED uint32 = 5 +STATUS_FINALIZED uint32 = 6 +STATUS_VOIDED uint32 = 7 +) + +// ───────────────────────────────────────────────────────────────────────────── +// PROPOSAL STATUS CONSTANTS +// ───────────────────────────────────────────────────────────────────────────── + +const ( +PROPOSAL_OPEN uint32 = 0 +PROPOSAL_DISPUTED uint32 = 1 +PROPOSAL_FINALIZED uint32 = 2 +) + +// ───────────────────────────────────────────────────────────────────────────── +// VOTE STATUS CONSTANTS +// ───────────────────────────────────────────────────────────────────────────── + +const ( +VOTE_PENDING uint32 = 0 +VOTE_COMMITTED uint32 = 1 +VOTE_REVEALED uint32 = 2 +VOTE_TALLIED uint32 = 3 +) + +// ───────────────────────────────────────────────────────────────────────────── +// LMSR PRICING CONSTANTS +// ───────────────────────────────────────────────────────────────────────────── + +const ( +PRECISION_SCALE uint64 = 1_000_000 +MIN_B0 uint64 = 60_000_000 +ELEVATED_RISK_THRESHOLD uint64 = 25_000_000_000 +FIBONACCI_HASH_CONSTANT uint64 = 0x9e3779b97f4a7c15 +) + +// ───────────────────────────────────────────────────────────────────────────── +// TIMING CONSTANTS — ADLMSR (all values in blocks) +// MAX_EXPIRY_TIME: R7 fix — parenthesised expression. +// AUDIT-11: guards uint64 overflow in all post-expiry arithmetic. +// ───────────────────────────────────────────────────────────────────────────── + +const ( +RESOLUTION_DELAY_BLOCKS uint64 = 100 +GRACE_PERIOD_BLOCKS uint64 = 200 +CLAIM_GRACE_PERIOD uint64 = 1000 +) + +const MAX_EXPIRY_TIME uint64 = (^uint64(0) - +RESOLUTION_DELAY_BLOCKS - GRACE_PERIOD_BLOCKS - CLAIM_GRACE_PERIOD - 1) + +// ───────────────────────────────────────────────────────────────────────────── +// TIMING CONSTANTS — PORS (all values in blocks) +// Issue-18: DISPUTE_BLOCKS is block-count based, not wall-clock. +// At 5s/block the floor is ~48h. If block time deviates (e.g. 4s/block +// during validator churn), the wall-clock window shrinks proportionally. +// Future: anchor to wall-clock via a protocol time parameter. +// MIN_DISPUTE_BLOCKS = 34,560 ≈ 48h at ~5s block time (P5) +// ───────────────────────────────────────────────────────────────────────────── + +const ( +MIN_DISPUTE_BLOCKS uint64 = 34_560 +COMMIT_PHASE_BLOCKS uint64 = 17_280 +REVEAL_PHASE_BLOCKS uint64 = 17_280 +) + +// ───────────────────────────────────────────────────────────────────────────── +// PORS ECONOMIC CONSTANTS +// ───────────────────────────────────────────────────────────────────────────── + +const ( +MIN_RRS_TO_PROPOSE uint64 = 10 + MAX_OPEN_MARKETS_PER_CREATOR uint64 = 50 +FINALIZATION_BOUNTY uint64 = 50_000_000 +CREATOR_BOND uint64 = 5_000_000_000 +RRS_INITIAL uint64 = 100 +MIN_RESOLVER_STAKE uint64 = 500_000_000_000 +) + +// ───────────────────────────────────────────────────────────────────────────── +// PANEL CONSTANTS +// ───────────────────────────────────────────────────────────────────────────── + +const ( +MIN_PANEL_SIZE uint32 = 3 +ELEVATED_RISK_PANEL_SIZE uint32 = 7 +) + +// ───────────────────────────────────────────────────────────────────────────── +// RRS VOTE WEIGHT TIERS (Layer 2 — anti-whale panel protection) +// Bronze: RRS 10–49 → weight 1 +// Silver: RRS 50–199 → weight 2 +// Gold: RRS 200+ → weight 3 +// Hard cap at 3 prevents infinite influence via staking. +// ───────────────────────────────────────────────────────────────────────────── +const ( +RRS_SILVER_THRESHOLD uint64 = 50 +RRS_GOLD_THRESHOLD uint64 = 200 +VOTE_WEIGHT_BRONZE uint32 = 1 +VOTE_WEIGHT_SILVER uint32 = 2 +VOTE_WEIGHT_GOLD uint32 = 3 +) + +// ───────────────────────────────────────────────────────────────────────────── +// PROTOCOL TREASURY +// PRAXIS_TREASURY_ID: destination pool ID for surplus sweeps (R2) and slashes. +// []byte cannot be const in Go — defined as var. +// ───────────────────────────────────────────────────────────────────────────── + +var PRAXIS_TREASURY_ID = []byte{ +0xe7, 0xc7, 0xda, 0xd1, 0x31, 0xa0, 0x3f, 0x7e, +0xa0, 0xcc, 0x09, 0xa6, 0x37, 0xad, 0x09, 0x6e, +0xb3, 0x49, 0x5f, 0x77, +} + +// ───────────────────────────────────────────────────────────────────────────── +// PANEL ENTROPY KEY +// Singleton state key for the 0x1C rolling entropy accumulator. +// Initialised in contract.go init() via JoinLenPrefix. +// ───────────────────────────────────────────────────────────────────────────── + +var panelEntropyPrefix = []byte{0x1C} +var PANEL_ENTROPY_KEY []byte + +// TEST_MODE — enables compressed timing windows for local testing. +// Controlled by the PRAXIS_TEST_MODE environment variable. +// Defaults to false — safe for mainnet. +// To enable: PRAXIS_TEST_MODE=true ./go-plugin +// COI-3: maximum fractional share of the winning side any single address may hold. +// Expressed in basis points: 2000 = 20%. +// Cap is enforced on shares (not CostPaid) so early cheap buyers face the same +// limit as late entrants — prevents single-address dominant share accumulation. +// Limitations (by design): +// - Does not prevent multi-address (Sybil) wash trading — on-chain identity +// is not enforced; two addresses can each hold up to 20%. +// - Cap is share-based so pool growth does not progressively loosen it. +const MAX_POSITION_BPS uint64 = 2000 + +var TEST_MODE = os.Getenv("PRAXIS_TEST_MODE") == "true" +const TEST_DISPUTE_BLOCKS uint64 = 20 +const TEST_RESOLUTION_DELAY uint64 = 2 +const TEST_GRACE_PERIOD uint64 = 2 +const TEST_CLAIM_GRACE_PERIOD uint64 = 50 diff --git a/plugin/go/contract/constants_pris.go b/plugin/go/contract/constants_pris.go new file mode 100644 index 0000000000..d7ffa19127 --- /dev/null +++ b/plugin/go/contract/constants_pris.go @@ -0,0 +1,71 @@ +package contract + +// ───────────────────────────────────────────────────────────────────────────── +// PRIS v1.0-r3 CONSTANTS +// Spec authority: PRIS v1.0-r3 +// ───────────────────────────────────────────────────────────────────────────── + +const ( +// Epoch timing +PRIS_EPOCH_BLOCKS uint64 = 1_000 // ~83 minutes at 5s/block +PRIS_BUILDER_EPOCH_BLOCKS uint64 = 120_960 // ~7 days at 5s/block +PRIS_INVESTOR_VESTING_BLOCKS uint64 = 241_920 // ~14 days at 5s/block + +// Treasury distribution BPS (basis points, 10000 = 100%) +PRIS_RESOLVER_SHARE_BPS uint64 = 2_000 // 20% +PRIS_BUILDER_SHARE_BPS uint64 = 2_000 // 20% +PRIS_COMMUNITY_SHARE_BPS uint64 = 2_000 // 20% +PRIS_INVESTOR_SHARE_BPS uint64 = 2_000 // 20% +PRIS_PROTOCOL_SHARE_BPS uint64 = 2_000 // 20% + +// Fee BPS +CREATOR_FEE_BPS uint64 = 100 // 1% of tradeCost — charged on top +RESOLVER_FEE_BPS uint64 = 100 // 1% of tradeCost — charged on top +TX_TREASURY_SPLIT_BPS uint64 = 5_000 // 50% of TX fees to treasury + +// RRS +// r3 fix R3-1: RRS_INITIAL reduced from 100 to 10. +// New resolvers start at Bronze — no Sybil recycling advantage. +PRIS_RRS_INITIAL uint64 = 10 +PRIS_RRS_FLOOR uint64 = 0 // Slashed resolvers lose all tier weight +) + +// ComputeBps applies basis points to an amount. +// ComputeBps(amount, 2000) = amount * 20 / 100 +func ComputeBps(amount, bps uint64) uint64 { +return amount * bps / 10_000 +} + +// ───────────────────────────────────────────────────────────────────────────── +// PRIS AUTHORIZED WALLET ADDRESSES +// Hardcoded protocol constants — all claim TXs validate signer against these. +// ───────────────────────────────────────────────────────────────────────────── + +var ( +PRAXIS_BUILDER_ADDR = []byte{ +0x95, 0x43, 0x78, 0xba, 0x10, 0x9c, 0x5c, 0xa4, +0x5b, 0x23, 0xbf, 0xa2, 0x84, 0xf3, 0xac, 0x70, +0xe2, 0x67, 0x1b, 0x87, +} +PRAXIS_COMMUNITY_ADDR = []byte{ +0x15, 0xe6, 0x58, 0x69, 0x8d, 0x25, 0x10, 0x79, +0x93, 0x39, 0x27, 0x3f, 0x6f, 0xcc, 0xb0, 0x48, +0x4c, 0x4f, 0x4b, 0x6f, +} +PRAXIS_INVESTOR_ADDR = []byte{ +0x12, 0x5c, 0x1b, 0xb8, 0x03, 0xa2, 0xdd, 0x91, +0x94, 0xdc, 0xa4, 0x0d, 0x77, 0x44, 0x5c, 0xf7, +0x56, 0x47, 0xcb, 0x12, +} +PRAXIS_PROTOCOL_ADDR = []byte{ +0xc1, 0x76, 0x4f, 0x10, 0xad, 0x67, 0x25, 0x58, +0xaf, 0xe1, 0xa3, 0xb6, 0x66, 0x18, 0x5f, 0xd1, +0x41, 0xae, 0x1e, 0xa8, +} +) + +// Unstake constants +const ( +PRIS_UNSTAKE_UNBONDING_BLOCKS uint64 = 120_960 // 7 days at 5s/block +PRIS_UNSTAKE_PARTIAL_RRS_HIT uint64 = 10 // RRS penalty for partial unstake +) diff --git a/plugin/go/contract/contract.go b/plugin/go/contract/contract.go index 559b5e2b03..bac0fe6327 100644 --- a/plugin/go/contract/contract.go +++ b/plugin/go/contract/contract.go @@ -1,291 +1,284 @@ package contract import ( - "bytes" - "encoding/binary" - "log" - "math/rand" +"crypto/sha256" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protodesc" - "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/types/known/anypb" +"google.golang.org/protobuf/types/known/anypb" ) -/* This file contains the base contract implementation that overrides the basic 'transfer' functionality */ +var ( +PREFIX_MARKET_STATE = []byte{0x10} +PREFIX_POSITION_STATE = []byte{0x11} +PREFIX_OUTCOME_STATE = []byte{0x12} +PREFIX_RESOLVER_STATE = []byte{0x13} +PREFIX_TREASURY = []byte{0x14} +PREFIX_RESOLVER_RECORD = []byte{0x16} +PREFIX_PROPOSAL_RECORD = []byte{0x17} +PREFIX_DISPUTE_RECORD = []byte{0x18} +PREFIX_VOTE_COMMIT = []byte{0x19} +PREFIX_VOTE_REVEAL = []byte{0x1A} +PREFIX_SLASH_RECORD = []byte{0x1B} +) -// PluginConfig: the configuration of the contract -var ContractConfig = &PluginConfig{ - Name: "go_plugin_contract", - Id: 1, - Version: 1, - SupportedTransactions: []string{"send"}, - TransactionTypeUrls: []string{ - "type.googleapis.com/types.MessageSend", - }, - EventTypeUrls: nil, -} +var ( +PREFIX_ACCOUNT = []byte{0x01} +PREFIX_FEE_POOL = []byte{0x02} +) -// init sets FileDescriptorProtos after ensuring .pb.go files are initialized +// Issue-12: assert MIN_B0 > FINALIZATION_BOUNTY at startup. +// If this ever fails, create_market would seed a TreasuryReserve that cannot +// cover the finalization bounty, silently breaking permissionless finalization. func init() { - // Explicitly initialize the proto files first to ensure File_*_proto are set - file_account_proto_init() - file_event_proto_init() - file_plugin_proto_init() - file_tx_proto_init() +if MIN_B0 <= FINALIZATION_BOUNTY { +panic("invariant violated: MIN_B0 must be greater than FINALIZATION_BOUNTY") +} +} - var fds [][]byte - // Include google/protobuf/any.proto first as it's a dependency of event.proto and tx.proto - for _, file := range []protoreflect.FileDescriptor{ - anypb.File_google_protobuf_any_proto, - File_account_proto, File_event_proto, File_plugin_proto, File_tx_proto, - } { - fd, _ := proto.Marshal(protodesc.ToFileDescriptorProto(file)) - fds = append(fds, fd) - } - ContractConfig.FileDescriptorProtos = fds +var ContractConfig = &PluginConfig{ +Name: "praxis_prediction_market", +Id: 1, +Version: 1, +SupportedTransactions: []string{ +"create_market", +"submit_prediction", +"claim_winnings", +"register_resolver", +"propose_outcome", +"file_dispute", +"commit_vote", +"reveal_vote", +"tally_votes", +"finalize_market", +"claim_slash", +"reclaim_stake", +"forfeit_position", + "claim_builder_reward", + "claim_creator_fee", + "claim_resolver_reward", + "claim_community_reward", + "claim_investor_reward", + "claim_protocol_reward", + "unstake_resolver", + "cancel_market", + "claim_unbonded_stake", +}, +TransactionTypeUrls: []string{ + "type.googleapis.com/types.MessageCreateMarket", + "type.googleapis.com/types.MessageSubmitPrediction", + "type.googleapis.com/types.MessageClaimWinnings", + "type.googleapis.com/types.MessageRegisterResolver", + "type.googleapis.com/types.MessageProposeOutcome", + "type.googleapis.com/types.MessageFileDispute", + "type.googleapis.com/types.MessageCommitVote", + "type.googleapis.com/types.MessageRevealVote", + "type.googleapis.com/types.MessageTallyVotes", + "type.googleapis.com/types.MessageFinalizeMarket", + "type.googleapis.com/types.MessageClaimSlash", + "type.googleapis.com/types.MessageReclaimStake", + "type.googleapis.com/types.MessageForfeitPosition", + "type.googleapis.com/types.MessageClaimBuilderReward", + "type.googleapis.com/types.MessageClaimCreatorFee", + "type.googleapis.com/types.MessageClaimResolverReward", + "type.googleapis.com/types.MessageClaimCommunityReward", + "type.googleapis.com/types.MessageClaimInvestorReward", + "type.googleapis.com/types.MessageClaimProtocolReward", + "type.googleapis.com/types.MessageUnstakeResolver", + "type.googleapis.com/types.MessageCancelMarket", + "type.googleapis.com/types.MessageClaimUnbondedStake", +}, } -// Contract() defines the smart contract that implements the extended logic of the nested chain + + type Contract struct { - Config Config - FSMConfig *PluginFSMConfig // fsm configuration - plugin *Plugin // plugin connection - fsmId uint64 // the id of the requesting fsm +Config Config +FSMConfig *PluginFSMConfig +plugin *Plugin +fsmId uint64 } -// Genesis() implements logic to import a json file to create the state at height 0 and export the state at any height -func (c *Contract) Genesis(_ *PluginGenesisRequest) *PluginGenesisResponse { - return &PluginGenesisResponse{} // TODO map out original token holders +func marketKey(prefix, marketId []byte) []byte { +return append(append([]byte{}, prefix...), marketId...) +} +func positionKey(marketId, addr []byte) []byte { +k := append(append([]byte{}, PREFIX_POSITION_STATE...), marketId...) +return append(k, addr...) +} +func addrKey(prefix, addr []byte) []byte { +return append(append([]byte{}, prefix...), addr...) } -// BeginBlock() is code that is executed at the start of `applying` the block -func (c *Contract) BeginBlock(_ *PluginBeginRequest) *PluginBeginResponse { - return &PluginBeginResponse{} +func (c *Contract) Genesis(req *PluginGenesisRequest) *PluginGenesisResponse { +return &PluginGenesisResponse{} } -// CheckTx() is code that is executed to statelessly validate a transaction -func (c *Contract) CheckTx(request *PluginCheckRequest) *PluginCheckResponse { - // validate fee - resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ - Keys: []*PluginKeyRead{ - {QueryId: rand.Uint64(), Key: KeyForFeeParams()}, - }}) - if err == nil { - err = resp.Error - } - // handle error - if err != nil { - return &PluginCheckResponse{Error: err} - } - // convert bytes into fee parameters - minFees := new(FeeParams) - if err = Unmarshal(resp.Results[0].Entries[0].Value, minFees); err != nil { - return &PluginCheckResponse{Error: err} - } - // check for the minimum fee - if request.Tx.Fee < minFees.SendFee { - return &PluginCheckResponse{Error: ErrTxFeeBelowStateLimit()} - } - // get the message - msg, err := FromAny(request.Tx.Msg) - if err != nil { - return &PluginCheckResponse{Error: err} - } - // handle the message - switch x := msg.(type) { - case *MessageSend: - return c.CheckMessageSend(x) - default: - return &PluginCheckResponse{Error: ErrInvalidMessageCast()} - } +func (c *Contract) BeginBlock(req *PluginBeginRequest) *PluginBeginResponse { +SetGlobalHeight(req.Height) + +entropyQId := nextQueryId() +entropyResp, readErr := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: entropyQId, Key: PANEL_ENTROPY_KEY}, +}, +}) + +// Issue-17 fix: replace XOR accumulator with SHA256 hash chain. +// XOR with a deterministic height function is fully predictable by chain +// observers — they can compute the accumulator value at any future block and +// time a file_dispute call to influence panel selection. +// SHA256(prev || height_bytes) is a one-way function: knowing the output +// does not allow computing a height that produces a desired panel seed. +var prev [8]byte +if readErr == nil && entropyResp != nil { +for _, r := range entropyResp.Results { +if r.QueryId == entropyQId && len(r.Entries) > 0 && len(r.Entries[0].Value) >= 8 { +copy(prev[:], r.Entries[0].Value[:8]) +} +} } -// DeliverTx() is code that is executed to apply a transaction -func (c *Contract) DeliverTx(request *PluginDeliverRequest) *PluginDeliverResponse { - // get the message - msg, err := FromAny(request.Tx.Msg) - if err != nil { - return &PluginDeliverResponse{Error: err} - } - // handle the message - switch x := msg.(type) { - case *MessageSend: - return c.DeliverMessageSend(x, request.Tx.Fee) - default: - return &PluginDeliverResponse{Error: ErrInvalidMessageCast()} - } +heightBytes := make([]byte, 8) +for i := 7; i >= 0; i-- { +heightBytes[i] = byte(req.Height & 0xFF) +req.Height >>= 8 } +input := append(prev[:], heightBytes...) +hash := sha256.Sum256(input) +buf := hash[:8] -// EndBlock() is code that is executed at the end of 'applying' a block -func (c *Contract) EndBlock(_ *PluginEndRequest) *PluginEndResponse { - return &PluginEndResponse{} +wr, writeErr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: PANEL_ENTROPY_KEY, Value: buf}, +}, +}) +if writeErr != nil || (wr != nil && wr.Error != nil) { +_ = wr } -// CheckMessageSend() statelessly validates a 'send' message -func (c *Contract) CheckMessageSend(msg *MessageSend) *PluginCheckResponse { - // check sender address - if len(msg.FromAddress) != 20 { - return &PluginCheckResponse{Error: ErrInvalidAddress()} - } - // check recipient address - if len(msg.ToAddress) != 20 { - return &PluginCheckResponse{Error: ErrInvalidAddress()} - } - // check amount - if msg.Amount == 0 { - return &PluginCheckResponse{Error: ErrInvalidAmount()} - } - // return the authorized signers - return &PluginCheckResponse{Recipient: msg.ToAddress, AuthorizedSigners: [][]byte{msg.FromAddress}} +return &PluginBeginResponse{} } -// DeliverMessageSend() handles a 'send' message -func (c *Contract) DeliverMessageSend(msg *MessageSend, fee uint64) *PluginDeliverResponse { - log.Printf("DeliverMessageSend called: from=%x to=%x amount=%d fee=%d", msg.FromAddress, msg.ToAddress, msg.Amount, fee) - var ( - fromKey, toKey, feePoolKey []byte - fromBytes, toBytes, feePoolBytes []byte - fromQueryId, toQueryId, feeQueryId = rand.Uint64(), rand.Uint64(), rand.Uint64() - from, to, feePool = new(Account), new(Account), new(Pool) - ) - // calculate the from key and to key - fromKey, toKey, feePoolKey = KeyForAccount(msg.FromAddress), KeyForAccount(msg.ToAddress), KeyForFeePool(c.Config.ChainId) - log.Printf("Keys: fromKey=%x toKey=%x feePoolKey=%x", fromKey, toKey, feePoolKey) - // get the from and to account - response, err := c.plugin.StateRead(c, &PluginStateReadRequest{ - Keys: []*PluginKeyRead{ - {QueryId: feeQueryId, Key: feePoolKey}, - {QueryId: fromQueryId, Key: fromKey}, - {QueryId: toQueryId, Key: toKey}, - }}) - // check for internal error - if err != nil { - log.Printf("StateRead error: %v", err) - return &PluginDeliverResponse{Error: err} - } - // ensure no error fsm error - if response.Error != nil { - log.Printf("StateRead FSM error: %v", response.Error) - return &PluginDeliverResponse{Error: response.Error} - } - log.Printf("StateRead returned %d results", len(response.Results)) - // get the from bytes and to bytes - for _, resp := range response.Results { - log.Printf("Result QueryId=%d Entries=%d", resp.QueryId, len(resp.Entries)) - if len(resp.Entries) == 0 { - log.Printf("WARNING: No entries for QueryId=%d", resp.QueryId) - continue - } - switch resp.QueryId { - case fromQueryId: - fromBytes = resp.Entries[0].Value - log.Printf("fromBytes len=%d", len(fromBytes)) - case toQueryId: - toBytes = resp.Entries[0].Value - log.Printf("toBytes len=%d", len(toBytes)) - case feeQueryId: - feePoolBytes = resp.Entries[0].Value - log.Printf("feePoolBytes len=%d", len(feePoolBytes)) - } - } - // add fee to 'amount to deduct' - amountToDeduct := msg.Amount + fee - // convert the bytes to account structures - if err = Unmarshal(fromBytes, from); err != nil { - return &PluginDeliverResponse{Error: err} - } - if err = Unmarshal(toBytes, to); err != nil { - return &PluginDeliverResponse{Error: err} - } - if err = Unmarshal(feePoolBytes, feePool); err != nil { - return &PluginDeliverResponse{Error: err} - } - log.Printf("from.Amount=%d to.Amount=%d feePool.Amount=%d", from.Amount, to.Amount, feePool.Amount) - // if the account amount is less than the amount to subtract; return insufficient funds - if from.Amount < amountToDeduct { - log.Printf("ERROR: Insufficient funds: from.Amount=%d amountToDeduct=%d", from.Amount, amountToDeduct) - return &PluginDeliverResponse{Error: ErrInsufficientFunds()} - } - // for self-transfer, use same account data - if bytes.Equal(fromKey, toKey) { - to = from - } - // subtract from sender - from.Amount -= amountToDeduct - // add the fee to the 'fee pool' - feePool.Amount += fee - // add to recipient - to.Amount += msg.Amount - log.Printf("AFTER: from.Amount=%d to.Amount=%d feePool.Amount=%d", from.Amount, to.Amount, feePool.Amount) - // convert the accounts to bytes - fromBytes, err = Marshal(from) - if err != nil { - return &PluginDeliverResponse{Error: err} - } - toBytes, err = Marshal(to) - if err != nil { - return &PluginDeliverResponse{Error: err} +func (c *Contract) EndBlock(req *PluginEndRequest) *PluginEndResponse { + height := GetGlobalHeight() + if height > 0 && height%PRIS_EPOCH_BLOCKS == 0 { + _ = c.processEpochBoundary(height) } - feePoolBytes, err = Marshal(feePool) - if err != nil { - return &PluginDeliverResponse{Error: err} - } - // execute writes to the database - var resp *PluginStateWriteResponse - // if the from account is drained - delete the from account - if from.Amount == 0 { - resp, err = c.plugin.StateWrite(c, &PluginStateWriteRequest{ - Sets: []*PluginSetOp{ - {Key: feePoolKey, Value: feePoolBytes}, - {Key: toKey, Value: toBytes}, - }, - Deletes: []*PluginDeleteOp{{Key: fromKey}}, - }) - } else { - resp, err = c.plugin.StateWrite(c, &PluginStateWriteRequest{ - Sets: []*PluginSetOp{ - {Key: feePoolKey, Value: feePoolBytes}, - {Key: toKey, Value: toBytes}, - {Key: fromKey, Value: fromBytes}, - }, - }) - } - if err != nil { - log.Printf("StateWrite internal error: %v", err) - return &PluginDeliverResponse{Error: err} - } - if resp.Error != nil { - log.Printf("StateWrite FSM error: %v", resp.Error) - return &PluginDeliverResponse{Error: resp.Error} - } - log.Printf("StateWrite SUCCESS!") - return &PluginDeliverResponse{} + return &PluginEndResponse{} } -var ( - accountPrefix = []byte{1} // store key prefix for accounts - poolPrefix = []byte{2} // store key prefix for pools - paramsPrefix = []byte{7} // store key prefix for governance parameters -) - -// KeyForAccount() returns the state database key for an account -func KeyForAccount(addr []byte) []byte { - return JoinLenPrefix(accountPrefix, addr) +func (c *Contract) CheckTx(req *PluginCheckRequest) *PluginCheckResponse { +msg, err := FromAny(req.Tx.Msg) +if err != nil { +return &PluginCheckResponse{Error: err} } - -// KeyForFeeParams() returns the state database key for governance controlled 'fee parameters' -func KeyForFeeParams() []byte { - return JoinLenPrefix(paramsPrefix, []byte("/f/")) +switch m := msg.(type) { +case *MessageSend: +return c.CheckMessageSend(m) +case *MessageCreateMarket: +return c.CheckMessageCreateMarket(m) +case *MessageSubmitPrediction: +return c.CheckMessageSubmitPrediction(m) +case *MessageClaimWinnings: +return c.CheckMessageClaimWinnings(m) +case *MessageRegisterResolver: +return c.CheckMessageRegisterResolver(m) +case *MessageProposeOutcome: +return c.CheckMessageProposeOutcome(m) +case *MessageFileDispute: +return c.CheckMessageFileDispute(m) +case *MessageCommitVote: +return c.CheckMessageCommitVote(m) +case *MessageRevealVote: +return c.CheckMessageRevealVote(m) +case *MessageTallyVotes: +return c.CheckMessageTallyVotes(m) +case *MessageFinalizeMarket: +return c.CheckMessageFinalizeMarket(m) +case *MessageClaimSlash: +return c.CheckMessageClaimSlash(m) +case *MessageReclaimStake: +return c.CheckMessageReclaimStake(m) +case *MessageForfeitPosition: +return c.CheckMessageForfeitPosition(m) +case *MessageClaimBuilderReward: +return c.CheckMessageClaimBuilderReward(m) +case *MessageClaimCreatorFee: +return c.CheckMessageClaimCreatorFee(m) +case *MessageClaimResolverReward: +return c.CheckMessageClaimResolverReward(m) +case *MessageClaimCommunityReward: +return c.CheckMessageClaimCommunityReward(m) +case *MessageClaimInvestorReward: +return c.CheckMessageClaimInvestorReward(m) +case *MessageClaimProtocolReward: +return c.CheckMessageClaimProtocolReward(m) +case *MessageUnstakeResolver: +return c.CheckMessageUnstakeResolver(m) +case *MessageCancelMarket: +return c.CheckMessageCancelMarket(m) +case *MessageClaimUnbondedStake: +return c.CheckMessageClaimUnbondedStake(m) +default: +return &PluginCheckResponse{Error: ErrInvalidMessageCast()} } - -// KeyForFeeParams() returns the state database key for governance controlled 'fee parameters' -func KeyForFeePool(chainId uint64) []byte { - return JoinLenPrefix(poolPrefix, formatUint64(chainId)) } -func formatUint64(u uint64) []byte { - b := make([]byte, 8) - binary.BigEndian.PutUint64(b, u) - return b +func (c *Contract) DeliverTx(req *PluginDeliverRequest) *PluginDeliverResponse { +msg, err := FromAny(req.Tx.Msg) +if err != nil { +return &PluginDeliverResponse{Error: err} } +fee := req.Tx.Fee +switch m := msg.(type) { +case *MessageSend: +return c.DeliverMessageSend(m, fee) +case *MessageCreateMarket: +return c.DeliverMessageCreateMarket(m, fee) +case *MessageSubmitPrediction: +return c.DeliverMessageSubmitPrediction(m, fee) +case *MessageClaimWinnings: +return c.DeliverMessageClaimWinnings(m, fee) +case *MessageRegisterResolver: +return c.DeliverMessageRegisterResolver(m, fee) +case *MessageProposeOutcome: +return c.DeliverMessageProposeOutcome(m, fee) +case *MessageFileDispute: +return c.DeliverMessageFileDispute(m, fee) +case *MessageCommitVote: +return c.DeliverMessageCommitVote(m, fee) +case *MessageRevealVote: +return c.DeliverMessageRevealVote(m, fee) +case *MessageTallyVotes: +return c.DeliverMessageTallyVotes(m, fee) +case *MessageFinalizeMarket: +return c.DeliverMessageFinalizeMarket(m, fee) +case *MessageClaimSlash: +return c.DeliverMessageClaimSlash(m, fee) +case *MessageReclaimStake: +return c.DeliverMessageReclaimStake(m, fee) +case *MessageForfeitPosition: +return c.DeliverMessageForfeitPosition(m, fee) +case *MessageClaimBuilderReward: +return c.DeliverMessageClaimBuilderReward(m, fee) +case *MessageClaimCreatorFee: +return c.DeliverMessageClaimCreatorFee(m, fee) +case *MessageClaimResolverReward: +return c.DeliverMessageClaimResolverReward(m, fee) +case *MessageClaimCommunityReward: +return c.DeliverMessageClaimCommunityReward(m, fee) +case *MessageClaimInvestorReward: +return c.DeliverMessageClaimInvestorReward(m, fee) +case *MessageClaimProtocolReward: +return c.DeliverMessageClaimProtocolReward(m, fee) +case *MessageUnstakeResolver: +return c.DeliverMessageUnstakeResolver(m, fee) +case *MessageCancelMarket: +return c.DeliverMessageCancelMarket(m, fee) +case *MessageClaimUnbondedStake: +return c.DeliverMessageClaimUnbondedStake(m, fee) +default: +return &PluginDeliverResponse{Error: ErrInvalidMessageCast()} +} +} + +var _ = (*anypb.Any)(nil) diff --git a/plugin/go/contract/error.go b/plugin/go/contract/error.go index 30a3ab6277..ef11edef22 100644 --- a/plugin/go/contract/error.go +++ b/plugin/go/contract/error.go @@ -1,76 +1,331 @@ package contract -import ( - "fmt" - "reflect" -) +// ═══════════════════════════════════════════════════════════════════════════════ +// Praxis Prediction Market — Error Definitions +// Spec authority: +// ADLMSR v5.6.6-r2-CORRECTED +// PORS v1.0-r2-CORRECTED +// +// Built-in Canopy error codes 1–14 are reserved — never reuse them. +// Praxis error codes start at 100. +// ═══════════════════════════════════════════════════════════════════════════════ -/* This file contains contract level PluginErrors */ +const errModule = "praxis" -const DefaultModule = "plugin" +// ───────────────────────────────────────────────────────────────────────────── +// CANOPY BUILT-IN ERRORS (1–14) +// These constructors wrap the built-in codes from plugin.go. +// ───────────────────────────────────────────────────────────────────────────── -// NewError() creates a plugin error -func NewError(code uint64, module, message string) *PluginError { - return &PluginError{Code: code, Module: module, Msg: message} +func ErrPluginTimeout() *PluginError { +return &PluginError{Code: 1, Module: errModule, Msg: "plugin timeout"} +} +func ErrMarshal(err error) *PluginError { +return &PluginError{Code: 2, Module: errModule, Msg: "marshal error: " + err.Error()} +} +func ErrUnmarshal(err error) *PluginError { +return &PluginError{Code: 3, Module: errModule, Msg: "unmarshal error: " + err.Error()} +} +func ErrPluginRead(err error) *PluginError { +return &PluginError{Code: 4, Module: errModule, Msg: "state read error: " + err.Error()} +} +func ErrPluginWrite(err error) *PluginError { +return &PluginError{Code: 5, Module: errModule, Msg: "state write error: " + err.Error()} +} +func ErrInvalidResponseId() *PluginError { +return &PluginError{Code: 6, Module: errModule, Msg: "invalid response id"} +} +func ErrUnexpectedType() *PluginError { +return &PluginError{Code: 7, Module: errModule, Msg: "unexpected message type"} +} +func ErrInvalidMessage() *PluginError { +return &PluginError{Code: 8, Module: errModule, Msg: "invalid message"} +} +func ErrInsufficientFunds() *PluginError { +return &PluginError{Code: 9, Module: errModule, Msg: "insufficient funds"} +} +func ErrFromAny(err error) *PluginError { +return &PluginError{Code: 10, Module: errModule, Msg: "from any error: " + err.Error()} +} +func ErrInvalidMessageCast() *PluginError { +return &PluginError{Code: 11, Module: errModule, Msg: "invalid message cast"} +} +func ErrInvalidAddress() *PluginError { +return &PluginError{Code: 12, Module: errModule, Msg: "invalid address: must be exactly 20 bytes"} +} +func ErrInvalidAmount() *PluginError { +return &PluginError{Code: 13, Module: errModule, Msg: "invalid amount: must be non-zero"} } +func ErrTxFeeBelowStateLimit() *PluginError { +return &PluginError{Code: 14, Module: errModule, Msg: "tx fee below minimum"} +} + +// ───────────────────────────────────────────────────────────────────────────── +// PRAXIS ERRORS — GENERAL (100–119) +// ───────────────────────────────────────────────────────────────────────────── -// Error() implements the errors interface -func (p *PluginError) Error() string { - return fmt.Sprintf("\nModule: %s\nCode: %d\nMessage: %s", p.Module, p.Code, p.Msg) +func ErrStateReadFailed() *PluginError { +return &PluginError{Code: 100, Module: errModule, Msg: "state read failed"} +} +func ErrMarshalFailed() *PluginError { +return &PluginError{Code: 101, Module: errModule, Msg: "marshal failed"} +} +func ErrUnmarshalFailed() *PluginError { +return &PluginError{Code: 102, Module: errModule, Msg: "unmarshal failed"} +} +func ErrHeightNotSet() *PluginError { +return &PluginError{Code: 103, Module: errModule, Msg: "global height not set: BeginBlock has not been called"} +} +func ErrInternal() *PluginError { +return &PluginError{Code: 104, Module: errModule, Msg: "internal error"} +} +func ErrUnauthorized() *PluginError { +return &PluginError{Code: 105, Module: errModule, Msg: "unauthorized: caller is not the registered resolver"} +} +func ErrInvalidParam() *PluginError { +return &PluginError{Code: 106, Module: errModule, Msg: "invalid parameter"} } -func ErrPluginTimeout() *PluginError { - return NewError(1, DefaultModule, "a plugin timeout occurred") +// ───────────────────────────────────────────────────────────────────────────── +// PRAXIS ERRORS — MARKET (120–139) +// ───────────────────────────────────────────────────────────────────────────── + +func ErrTooManyOpenMarkets() *PluginError { return &PluginError{Code: 218, Module: errModule, Msg: "creator has too many open markets"} } +func ErrMarketNotFound() *PluginError { +return &PluginError{Code: 120, Module: errModule, Msg: "market not found"} +} +func ErrMarketNotOpen() *PluginError { +return &PluginError{Code: 121, Module: errModule, Msg: "market is not open"} +} +func ErrMarketCancelled() *PluginError { +return &PluginError{Code: 122, Module: errModule, Msg: "market has been cancelled"} +} +func ErrMarketNotResolved() *PluginError { +return &PluginError{Code: 123, Module: errModule, Msg: "market has not been resolved"} +} +func ErrMarketNotExpired() *PluginError { +return &PluginError{Code: 124, Module: errModule, Msg: "market has not expired yet"} +} +func ErrResolutionTooEarly() *PluginError { +return &PluginError{Code: 125, Module: errModule, Msg: "resolution window has not opened yet"} +} +func ErrExpiryTooLarge() *PluginError { +return &PluginError{Code: 126, Module: errModule, Msg: "expiry_time exceeds MAX_EXPIRY_TIME"} +} +func ErrInvalidNonce() *PluginError { +return &PluginError{Code: 127, Module: errModule, Msg: "nonce must be non-zero"} +} +func ErrInvalidQuestion() *PluginError { +return &PluginError{Code: 128, Module: errModule, Msg: "question must be non-empty"} +} +func ErrInvalidB0() *PluginError { +return &PluginError{Code: 129, Module: errModule, Msg: "b0 must be >= MIN_B0"} } -func ErrMarshal(err error) *PluginError { - return NewError(2, DefaultModule, fmt.Sprintf("marshal() failed with err: %s", err.Error())) +// ───────────────────────────────────────────────────────────────────────────── +// PRAXIS ERRORS — POSITION / PREDICTION (140–159) +// ───────────────────────────────────────────────────────────────────────────── + +func ErrNoPosition() *PluginError { +return &PluginError{Code: 140, Module: errModule, Msg: "no position found for this address in this market"} +} +func ErrAlreadyClaimed() *PluginError { +return &PluginError{Code: 141, Module: errModule, Msg: "winnings already claimed"} +} +func ErrCostExceedsMaxCost() *PluginError { +return &PluginError{Code: 142, Module: errModule, Msg: "computed cost exceeds max_cost slippage limit"} +} +func ErrSharesBelowMinimum() *PluginError { +return &PluginError{Code: 143, Module: errModule, Msg: "shares must be >= PRECISION_SCALE"} +} +func ErrInsufficientPoolFunds() *PluginError { +return &PluginError{Code: 144, Module: errModule, Msg: "insufficient funds in market pool"} } -func ErrUnmarshal(err error) *PluginError { - return NewError(3, DefaultModule, fmt.Sprintf("unmarshal() failed with err: %s", err.Error())) +// ───────────────────────────────────────────────────────────────────────────── +// PRAXIS ERRORS — PORS RESOLVER (160–179) +// ───────────────────────────────────────────────────────────────────────────── + +func ErrResolverNotRegistered() *PluginError { +return &PluginError{Code: 160, Module: errModule, Msg: "resolver is not registered"} +} +func ErrResolverSuspended() *PluginError { +return &PluginError{Code: 161, Module: errModule, Msg: "resolver RRS score is below minimum threshold"} +} +func ErrNoResolverRegistered() *PluginError { +return &PluginError{Code: 162, Module: errModule, Msg: "no resolver registered for this market"} +} +func ErrAlreadyProposed() *PluginError { +return &PluginError{Code: 163, Module: errModule, Msg: "outcome already proposed for this market"} +} +func ErrInsufficientBond() *PluginError { +return &PluginError{Code: 164, Module: errModule, Msg: "proposal bond is below minimum required"} } -func ErrFailedPluginRead(err error) *PluginError { - return NewError(4, DefaultModule, fmt.Sprintf("a plugin read failed with err: %s", err.Error())) +// ───────────────────────────────────────────────────────────────────────────── +// PRAXIS ERRORS — PORS DISPUTE (180–199) +// ───────────────────────────────────────────────────────────────────────────── + +func ErrDisputeWindowOpen() *PluginError { +return &PluginError{Code: 181, Module: errModule, Msg: "dispute window is still open — too early to finalize"} } -func ErrFailedPluginWrite(err error) *PluginError { - return NewError(5, DefaultModule, fmt.Sprintf("a plugin write failed with err: %s", err.Error())) +func ErrDisputeWindowClosed() *PluginError { +return &PluginError{Code: 180, Module: errModule, Msg: "dispute window has closed"} +} +func ErrAlreadyDisputed() *PluginError { +return &PluginError{Code: 181, Module: errModule, Msg: "market is already disputed"} +} +func ErrNotDisputed() *PluginError { +return &PluginError{Code: 182, Module: errModule, Msg: "market is not in disputed state"} +} +func ErrNotAPanelMember() *PluginError { +return &PluginError{Code: 183, Module: errModule, Msg: "caller is not a panel member for this dispute"} +} +func ErrCommitPhaseOver() *PluginError { +return &PluginError{Code: 184, Module: errModule, Msg: "commit phase has ended"} +} +func ErrRevealPhaseNotOpen() *PluginError { +return &PluginError{Code: 185, Module: errModule, Msg: "reveal phase has not started yet"} +} +func ErrRevealPhaseOver() *PluginError { +return &PluginError{Code: 186, Module: errModule, Msg: "reveal phase has ended"} +} +func ErrCommitHashMismatch() *PluginError { +return &PluginError{Code: 187, Module: errModule, Msg: "revealed vote does not match committed hash"} +} +func ErrAlreadyCommitted() *PluginError { +return &PluginError{Code: 188, Module: errModule, Msg: "vote already committed"} +} +func ErrAlreadyRevealed() *PluginError { +return &PluginError{Code: 189, Module: errModule, Msg: "vote already revealed"} +} +func ErrTallyNotReady() *PluginError { +return &PluginError{Code: 190, Module: errModule, Msg: "reveal phase has not ended yet"} +} +func ErrAlreadyTallied() *PluginError { +return &PluginError{Code: 191, Module: errModule, Msg: "votes already tallied"} +} +func ErrNotFinalized() *PluginError { +return &PluginError{Code: 192, Module: errModule, Msg: "market has not been finalized"} +} +func ErrNoSlashToClaim() *PluginError { +return &PluginError{Code: 193, Module: errModule, Msg: "no slash proceeds to claim"} +} +func ErrInvalidCommitHash() *PluginError { +return &PluginError{Code: 194, Module: errModule, Msg: "commit hash must be exactly 32 bytes"} +} +func ErrInsufficientPanelCandidates() *PluginError { +return &PluginError{Code: 195, Module: errModule, Msg: "insufficient eligible panel candidates after position exclusion"} +} +func ErrMarketNotReclaimable() *PluginError { +return &PluginError{Code: 196, Module: errModule, Msg: "market is not eligible for stake reclaim"} +} +func ErrReclaimWindowClosed() *PluginError { +return &PluginError{Code: 197, Module: errModule, Msg: "reclaim window has not opened yet"} +} +func ErrNoStakeToReclaim() *PluginError { +return &PluginError{Code: 198, Module: errModule, Msg: "no stake or position to reclaim"} } -func ErrInvalidPluginRespId() *PluginError { - return NewError(6, DefaultModule, "plugin response id is invalid") +// ───────────────────────────────────────────────────────────────────────────── +// HELPERS +// ───────────────────────────────────────────────────────────────────────────── + +// SafeMarshal wraps Marshal and returns a typed *PluginError on failure. +// Every marshal in a handler must use this — never discard marshal errors. +func SafeMarshal(m interface{}) ([]byte, *PluginError) { +b, err := Marshal(m) +if err != nil { +return nil, ErrMarshalFailed() +} +return b, nil } -func ErrUnexpectedFSMToPlugin(t reflect.Type) *PluginError { - return NewError(7, DefaultModule, fmt.Sprintf("unexpected FSM to plugin: %v", t)) +// errCheckWrite checks both the transport error and the embedded response error +// from a StateWrite call. Both must be checked — per the Canopy plugin spec. +func errCheckWrite(wr *PluginStateWriteResponse, err *PluginError) *PluginError { +if err != nil { +return err +} +if wr != nil && wr.Error != nil { +return wr.Error +} +return nil } -func ErrInvalidFSMToPluginMMessage(t reflect.Type) *PluginError { - return NewError(8, DefaultModule, fmt.Sprintf("invalid FSM to plugin: %v", t)) +// ErrCheckResp is a convenience wrapper for returning errors from CheckTx handlers. +func ErrResolverHasPosition() *PluginError { +return &PluginError{Code: 199, Module: errModule, Msg: "resolver holds a position in this market"} } -func ErrInsufficientFunds() *PluginError { - return NewError(9, DefaultModule, "insufficient funds") +func ErrCreatorCannotResolve() *PluginError { +return &PluginError{Code: 200, Module: errModule, Msg: "market creator cannot be the resolver for the same market"} } -func ErrFromAny(err error) *PluginError { - return NewError(10, DefaultModule, fmt.Sprintf("fromAny() failed with err: %s", err.Error())) +func ErrPositionCapExceeded() *PluginError { +return &PluginError{Code: 201, Module: errModule, Msg: "position would exceed per-address cap (20% of pool)"} +} +func ErrInsufficientResolverStake() *PluginError { +return &PluginError{Code: 202, Module: errModule, Msg: "resolver stake below minimum (500,000 PRX required)"} } -func ErrInvalidMessageCast() *PluginError { - return NewError(11, DefaultModule, "the message cast failed") +func ErrCheckResp(err *PluginError) *PluginCheckResponse { +return &PluginCheckResponse{Error: err} } -func ErrInvalidAddress() *PluginError { - return NewError(12, DefaultModule, "address is invalid") +// ───────────────────────────────────────────────────────────────────────────── +// PLUGIN INFRASTRUCTURE ERRORS — required by plugin.go (never modify plugin.go) +// ───────────────────────────────────────────────────────────────────────────── + +// Error() satisfies the error interface so plugin.go can call err.Error(). +func (e *PluginError) Error() string { +if e == nil { +return "" +} +return e.Module + ": " + e.Msg } -func ErrInvalidAmount() *PluginError { - return NewError(13, DefaultModule, "amount is invalid") +func ErrUnexpectedFSMToPlugin(t interface{}) *PluginError { +return &PluginError{Code: 7, Module: errModule, Msg: "unexpected FSM-to-plugin message type"} } -func ErrTxFeeBelowStateLimit() *PluginError { - return NewError(14, DefaultModule, "tx.fee is below state limit") +func ErrInvalidFSMToPluginMMessage(t interface{}) *PluginError { +return &PluginError{Code: 8, Module: errModule, Msg: "invalid FSM-to-plugin message"} +} + +func ErrInvalidPluginRespId() *PluginError { +return &PluginError{Code: 6, Module: errModule, Msg: "invalid plugin response id"} +} + +func ErrFailedPluginWrite(err error) *PluginError { +return &PluginError{Code: 5, Module: errModule, Msg: "plugin socket write failed: " + err.Error()} +} + +func ErrFailedPluginRead(err error) *PluginError { +return &PluginError{Code: 4, Module: errModule, Msg: "plugin socket read failed: " + err.Error()} +} + +func ErrMarketExpired() *PluginError { return &PluginError{Code: 215, Module: errModule, Msg: "market has expired"} } +func ErrMarketHasPositions() *PluginError { return &PluginError{Code: 216, Module: errModule, Msg: "market has positions — cannot cancel"} } +func ErrActiveProposalExists() *PluginError { return &PluginError{Code: 210, Module: errModule, Msg: "active proposal exists — cannot unstake"} } +func ErrResolverNotActive() *PluginError { return &PluginError{Code: 211, Module: errModule, Msg: "resolver is not active"} } +func ErrNoUnbondingStake() *PluginError { return &PluginError{Code: 212, Module: errModule, Msg: "no unbonding stake to claim"} } +func ErrUnbondingNotComplete() *PluginError { return &PluginError{Code: 213, Module: errModule, Msg: "unbonding period not complete"} } +func ErrUnbondingAlreadyPending() *PluginError { return &PluginError{Code: 217, Module: errModule, Msg: "unbonding already pending"} } +func ErrResolverNotFound() *PluginError { return &PluginError{Code: 214, Module: errModule, Msg: "resolver record not found"} } +func ErrCooldownNotElapsed() *PluginError { +return &PluginError{Code: 203, Module: errModule, Msg: "cooldown period has not elapsed"} +} +func ErrEmptyPool() *PluginError { +return &PluginError{Code: 204, Module: errModule, Msg: "pool is empty — nothing to claim"} +} +func ErrMarketNotFinalized() *PluginError { +return &PluginError{Code: 205, Module: errModule, Msg: "market is not finalized"} +} +func ErrInsufficientRRS() *PluginError { +return &PluginError{Code: 207, Module: errModule, Msg: "resolver RRS score is zero — not eligible for rewards"} +} +func ErrNoResolutions() *PluginError { +return &PluginError{Code: 208, Module: errModule, Msg: "no successful resolutions in this epoch"} } diff --git a/plugin/go/contract/event.pb.go b/plugin/go/contract/event.pb.go index 6111822fe0..7b27121d26 100644 --- a/plugin/go/contract/event.pb.go +++ b/plugin/go/contract/event.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.11 // protoc v5.29.3 // source: event.proto @@ -219,7 +219,7 @@ const file_event_proto_rawDesc = "" + "\aaddress\x18` \x01(\fR\aaddressB\x05\n" + "\x03msg\"5\n" + "\vEventCustom\x12&\n" + - "\x03msg\x18\x01 \x01(\v2\x14.google.protobuf.AnyR\x03msgB.Z,github.com/canopy-network/go-plugin/contractb\x06proto3" + "\x03msg\x18\x01 \x01(\v2\x14.google.protobuf.AnyR\x03msgB5Z3github.com/canopy-network/canopy/plugin/go/contractb\x06proto3" var ( file_event_proto_rawDescOnce sync.Once diff --git a/plugin/go/contract/handler_auto_cancel.go b/plugin/go/contract/handler_auto_cancel.go new file mode 100644 index 0000000000..1dcfed0502 --- /dev/null +++ b/plugin/go/contract/handler_auto_cancel.go @@ -0,0 +1,62 @@ +package contract + +// CheckAutoCancel checks if the market should be auto-cancelled and returns +// the updated MarketState if so (status set to STATUS_CANCELLED), or nil if not. +// NO StateWrite is performed here — the caller must include the updated market +// in its own atomic StateWrite to avoid multiple writes per deliver context. +func (c *Contract) CheckAutoCancel(marketId []byte) (*MarketState, *PluginError) { +now := GetGlobalHeight() +if now == 0 { +return nil, ErrHeightNotSet() +} + +marketQId := nextQueryId() +marketKey := KeyForMarket(marketId) + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: marketKey}, +}, +}) +if err != nil { +return nil, err +} +if resp.Error != nil { +return nil, resp.Error +} + +var market *MarketState +for _, r := range resp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +if r.QueryId == marketQId { +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return nil, pe +} +} +} + +if market == nil { +return nil, nil +} +if market.Status != STATUS_OPEN { +return nil, nil +} + +resolutionDelay := RESOLUTION_DELAY_BLOCKS +gracePeriod := GRACE_PERIOD_BLOCKS +if TEST_MODE { +resolutionDelay = TEST_RESOLUTION_DELAY +gracePeriod = TEST_GRACE_PERIOD +} +cancelThreshold := market.ExpiryTime + resolutionDelay + gracePeriod +if now <= cancelThreshold { +return nil, nil +} + +// Return the cancelled market — caller writes it atomically with other state. +market.Status = STATUS_CANCELLED +return market, nil +} diff --git a/plugin/go/contract/handler_cancel_market.go b/plugin/go/contract/handler_cancel_market.go new file mode 100644 index 0000000000..9df6ee6d58 --- /dev/null +++ b/plugin/go/contract/handler_cancel_market.go @@ -0,0 +1,201 @@ +package contract + +// handler_cancel_market.go — MessageCancelMarket +// Spec: Praxis core +// +// Allows a market creator to cancel their own market before expiry. +// Conditions: +// - Market must be STATUS_OPEN +// - now < market.ExpiryTime (not yet expired) +// - Signer must be market.Creator +// - TotalPositions must be 0 (no bets placed — Option 2) +// +// On cancel: +// - Market status → STATUS_CANCELLED +// - Creator receives CreatorBond + TreasuryReserve.LockedReserve back +// - Creator fee pool + resolver fee pool swept to KeyForTreasuryPool() +// - Market pool remains — bettors claim refunds via claim_winnings (STATUS_CANCELLED path) + +func (c *Contract) CheckMessageCancelMarket(msg *MessageCancelMarket) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.CreatorAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.CreatorAddress}, +} +} + +func (c *Contract) DeliverMessageCancelMarket(msg *MessageCancelMarket, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +// ── Batch read ──────────────────────────────────────────────────────── +marketQId := nextQueryId() +treasQId := nextQueryId() +creatorAccQId := nextQueryId() +creatorFeeQId := nextQueryId() +resolverFeeQId := nextQueryId() +gTreasuryQId := nextQueryId() + ocQId := nextQueryId() +feeQId := nextQueryId() + +marketKey := KeyForMarket(msg.MarketId) +treasKey := KeyForTreasuryReserve(msg.MarketId) +creatorAccKey := KeyForAccount(msg.CreatorAddress) +creatorFeeKey := KeyForCreatorFeePool(msg.MarketId) +resolverFeeKey := KeyForResolverFeePool(msg.MarketId) +gTreasuryKey := KeyForTreasuryPool() + ocKey := KeyForCreatorOpenCount(msg.CreatorAddress) +feePoolKey := KeyForFeePool(c.Config.ChainId) + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: marketKey}, +{QueryId: treasQId, Key: treasKey}, +{QueryId: creatorAccQId, Key: creatorAccKey}, +{QueryId: creatorFeeQId, Key: creatorFeeKey}, +{QueryId: resolverFeeQId, Key: resolverFeeKey}, +{QueryId: gTreasuryQId, Key: gTreasuryKey}, + {QueryId: ocQId, Key: ocKey}, +{QueryId: feeQId, Key: feePoolKey}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +treas := &TreasuryReserve{} +creatorAcc := &Account{} +creatorFee := &Pool{} +resolverFee := &Pool{} +gTreasury := &Pool{} + openCount := Pool{} +feePool := &Pool{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +switch r.QueryId { +case marketQId: +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case treasQId: +if pe := Unmarshal(r.Entries[0].Value, treas); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case creatorAccQId: +if pe := Unmarshal(r.Entries[0].Value, creatorAcc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case creatorFeeQId: +if pe := Unmarshal(r.Entries[0].Value, creatorFee); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case resolverFeeQId: +if pe := Unmarshal(r.Entries[0].Value, resolverFee); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case ocQId: + if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { + _ = Unmarshal(r.Entries[0].Value, &openCount) + } + case gTreasuryQId: +if pe := Unmarshal(r.Entries[0].Value, gTreasury); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case feeQId: +if pe := Unmarshal(r.Entries[0].Value, feePool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +if market.Status != STATUS_OPEN { +return &PluginDeliverResponse{Error: ErrMarketNotOpen()} +} +if now >= market.ExpiryTime { +return &PluginDeliverResponse{Error: ErrMarketExpired()} +} +if !bytesEqual(msg.CreatorAddress, market.Creator) { +return &PluginDeliverResponse{Error: ErrUnauthorized()} +} +// Option 2: block cancel if any positions exist +if market.TotalPositions > 0 { +return &PluginDeliverResponse{Error: ErrMarketHasPositions()} +} + +// ── Compute refund ──────────────────────────────────────────────────── +// Creator gets back: CreatorBond + LockedReserve (finalization bounty) +refund := treas.CreatorBond + treas.LockedReserve + +// ── Mutate ──────────────────────────────────────────────────────────── +market.Status = STATUS_CANCELLED +treas.CreatorBond = 0 +treas.LockedReserve = 0 +creatorAcc.Amount += refund + +// Sweep creator fee pool + resolver fee pool to global treasury +gTreasury.Amount += creatorFee.Amount + resolverFee.Amount +creatorFee.Amount = 0 +resolverFee.Amount = 0 + +// TX fee split 50/50 +feePool.Amount += fee / 2 +gTreasury.Amount += fee - fee/2 + +// Decrement open market counter + if openCount.Amount > 0 { + openCount.Amount-- + } + + // ── Marshal ─────────────────────────────────────────────────────────── +rawMarket, pe := SafeMarshal(market) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawTreas, pe := SafeMarshal(treas) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawCreatorAcc, pe := SafeMarshal(creatorAcc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawCreatorFee, pe := SafeMarshal(creatorFee) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawResolverFee, pe := SafeMarshal(resolverFee) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawOC, pe := SafeMarshal(&openCount) + if pe != nil { return &PluginDeliverResponse{Error: pe} } + rawGTreasury, pe := SafeMarshal(gTreasury) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawFee, pe := SafeMarshal(feePool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +// ── 7-key atomic write ──────────────────────────────────────────────── +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: marketKey, Value: rawMarket}, +{Key: treasKey, Value: rawTreas}, +{Key: creatorAccKey, Value: rawCreatorAcc}, +{Key: creatorFeeKey, Value: rawCreatorFee}, +{Key: resolverFeeKey, Value: rawResolverFee}, +{Key: ocKey, Value: rawOC}, + {Key: gTreasuryKey, Value: rawGTreasury}, +{Key: feePoolKey, Value: rawFee}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_claim_builder_reward.go b/plugin/go/contract/handler_claim_builder_reward.go new file mode 100644 index 0000000000..36318b9793 --- /dev/null +++ b/plugin/go/contract/handler_claim_builder_reward.go @@ -0,0 +1,99 @@ +package contract + +// handler_claim_builder_reward.go — MessageClaimBuilderReward +// Spec: PRIS v1.0-r3 +// +// Transfers the full KeyForBuilderPool() balance to PRAXIS_BUILDER_ADDR. +// Cooldown: PRIS_BUILDER_EPOCH_BLOCKS (120,960 blocks ~ 7 days) +// Signer: PRAXIS_BUILDER_ADDR only +// +// r3 fix R3-3: cooldown tracked in KeyForBuilderLastClaimed() singleton. +// r3 fix R3-5: claims full pool balance — rewards compound across missed epochs. + +func (c *Contract) CheckMessageClaimBuilderReward(msg *MessageClaimBuilderReward) *PluginCheckResponse { +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{PRAXIS_BUILDER_ADDR}, +} +} + +func (c *Contract) DeliverMessageClaimBuilderReward(msg *MessageClaimBuilderReward, fee uint64) *PluginDeliverResponse { +height := GetGlobalHeight() +if height == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +poolQId := nextQueryId() +lastQId := nextQueryId() +accQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: poolQId, Key: KeyForBuilderPool()}, +{QueryId: lastQId, Key: KeyForBuilderLastClaimed()}, +{QueryId: accQId, Key: KeyForAccount(PRAXIS_BUILDER_ADDR)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +pool := &Pool{} +lastClaimed := uint64(0) +acc := &Account{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 { +continue +} +switch r.QueryId { +case poolQId: +if pe := Unmarshal(r.Entries[0].Value, pool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case lastQId: +lc := &LastClaimedBlock{} +if pe := Unmarshal(r.Entries[0].Value, lc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +lastClaimed = lc.Height +case accQId: +if pe := Unmarshal(r.Entries[0].Value, acc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if lastClaimed > 0 && height < lastClaimed+PRIS_BUILDER_EPOCH_BLOCKS { +return &PluginDeliverResponse{Error: ErrCooldownNotElapsed()} +} + +if pool.Amount == 0 { +return &PluginDeliverResponse{Error: ErrEmptyPool()} +} + +payout := pool.Amount +acc.Amount += payout +pool.Amount = 0 + +rawPool, pe := SafeMarshal(pool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawAcc, pe := SafeMarshal(acc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawLast, pe := SafeMarshal(&LastClaimedBlock{Height: height}) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForBuilderPool(), Value: rawPool}, +{Key: KeyForAccount(PRAXIS_BUILDER_ADDR), Value: rawAcc}, +{Key: KeyForBuilderLastClaimed(), Value: rawLast}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_claim_community_reward.go b/plugin/go/contract/handler_claim_community_reward.go new file mode 100644 index 0000000000..addd1cab52 --- /dev/null +++ b/plugin/go/contract/handler_claim_community_reward.go @@ -0,0 +1,74 @@ +package contract + +// handler_claim_community_reward.go — MessageClaimCommunityReward +// Spec: PRIS v1.0-r3 +// Signer: PRAXIS_COMMUNITY_ADDR — no cooldown (grants/airdrops on demand) + +func (c *Contract) CheckMessageClaimCommunityReward(msg *MessageClaimCommunityReward) *PluginCheckResponse { +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{PRAXIS_COMMUNITY_ADDR}, +} +} + +func (c *Contract) DeliverMessageClaimCommunityReward(msg *MessageClaimCommunityReward, fee uint64) *PluginDeliverResponse { +height := GetGlobalHeight() +if height == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +poolQId := nextQueryId() +accQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: poolQId, Key: KeyForCommunityPool()}, +{QueryId: accQId, Key: KeyForAccount(PRAXIS_COMMUNITY_ADDR)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +pool := &Pool{} +acc := &Account{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 { continue } +switch r.QueryId { +case poolQId: +if pe := Unmarshal(r.Entries[0].Value, pool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case accQId: +if pe := Unmarshal(r.Entries[0].Value, acc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if pool.Amount == 0 { +return &PluginDeliverResponse{Error: ErrEmptyPool()} +} + +acc.Amount += pool.Amount +pool.Amount = 0 + +rawPool, pe := SafeMarshal(pool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawAcc, pe := SafeMarshal(acc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForCommunityPool(), Value: rawPool}, +{Key: KeyForAccount(PRAXIS_COMMUNITY_ADDR), Value: rawAcc}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_claim_creator_fee.go b/plugin/go/contract/handler_claim_creator_fee.go new file mode 100644 index 0000000000..6079c3e5f1 --- /dev/null +++ b/plugin/go/contract/handler_claim_creator_fee.go @@ -0,0 +1,102 @@ +package contract + +// handler_claim_creator_fee.go — MessageClaimCreatorFee +// Spec: PRIS v1.0-r3 +// +// Transfers the full KeyForCreatorFeePool(marketId) balance to market.Creator. +// Condition: market must be STATUS_FINALIZED. +// Signer: market.Creator only. + +func (c *Contract) CheckMessageClaimCreatorFee(msg *MessageClaimCreatorFee) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.CreatorAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.CreatorAddress}, +} +} + +func (c *Contract) DeliverMessageClaimCreatorFee(msg *MessageClaimCreatorFee, fee uint64) *PluginDeliverResponse { +height := GetGlobalHeight() +if height == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +marketQId := nextQueryId() +feePoolQId := nextQueryId() +accQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: KeyForMarket(msg.MarketId)}, +{QueryId: feePoolQId, Key: KeyForCreatorFeePool(msg.MarketId)}, +{QueryId: accQId, Key: KeyForAccount(msg.CreatorAddress)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +market := &MarketState{} +feePool := &Pool{} +acc := &Account{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 { +continue +} +switch r.QueryId { +case marketQId: +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case feePoolQId: +if pe := Unmarshal(r.Entries[0].Value, feePool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case accQId: +if pe := Unmarshal(r.Entries[0].Value, acc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if market.Status != STATUS_FINALIZED { +return &PluginDeliverResponse{Error: ErrMarketNotFinalized()} +} +if !bytesEqual(market.Creator, msg.CreatorAddress) { +return &PluginDeliverResponse{Error: ErrUnauthorized()} +} +if feePool.Amount == 0 { +return &PluginDeliverResponse{Error: ErrEmptyPool()} +} +if fee > 0 && acc.Amount > ^uint64(0)-feePool.Amount { +return &PluginDeliverResponse{Error: ErrInvalidAmount()} +} + +payout := feePool.Amount +acc.Amount += payout +feePool.Amount = 0 + +rawFeePool, pe := SafeMarshal(feePool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawAcc, pe := SafeMarshal(acc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForCreatorFeePool(msg.MarketId), Value: rawFeePool}, +{Key: KeyForAccount(msg.CreatorAddress), Value: rawAcc}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_claim_investor_reward.go b/plugin/go/contract/handler_claim_investor_reward.go new file mode 100644 index 0000000000..b593edf7ca --- /dev/null +++ b/plugin/go/contract/handler_claim_investor_reward.go @@ -0,0 +1,89 @@ +package contract + +// handler_claim_investor_reward.go — MessageClaimInvestorReward +// Spec: PRIS v1.0-r3 +// Signer: PRAXIS_INVESTOR_ADDR — 2-week vesting cooldown (241,920 blocks) + +func (c *Contract) CheckMessageClaimInvestorReward(msg *MessageClaimInvestorReward) *PluginCheckResponse { +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{PRAXIS_INVESTOR_ADDR}, +} +} + +func (c *Contract) DeliverMessageClaimInvestorReward(msg *MessageClaimInvestorReward, fee uint64) *PluginDeliverResponse { +height := GetGlobalHeight() +if height == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +poolQId := nextQueryId() +lastQId := nextQueryId() +accQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: poolQId, Key: KeyForInvestorPool()}, +{QueryId: lastQId, Key: KeyForInvestorLastClaimed()}, +{QueryId: accQId, Key: KeyForAccount(PRAXIS_INVESTOR_ADDR)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +pool := &Pool{} +lastClaimed := uint64(0) +acc := &Account{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 { continue } +switch r.QueryId { +case poolQId: +if pe := Unmarshal(r.Entries[0].Value, pool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case lastQId: +lc := &LastClaimedBlock{} +if pe := Unmarshal(r.Entries[0].Value, lc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +lastClaimed = lc.Height +case accQId: +if pe := Unmarshal(r.Entries[0].Value, acc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if lastClaimed > 0 && height < lastClaimed+PRIS_INVESTOR_VESTING_BLOCKS { +return &PluginDeliverResponse{Error: ErrCooldownNotElapsed()} +} +if pool.Amount == 0 { +return &PluginDeliverResponse{Error: ErrEmptyPool()} +} + +acc.Amount += pool.Amount +pool.Amount = 0 + +rawPool, pe := SafeMarshal(pool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawAcc, pe := SafeMarshal(acc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawLast, pe := SafeMarshal(&LastClaimedBlock{Height: height}) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForInvestorPool(), Value: rawPool}, +{Key: KeyForAccount(PRAXIS_INVESTOR_ADDR), Value: rawAcc}, +{Key: KeyForInvestorLastClaimed(), Value: rawLast}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_claim_protocol_reward.go b/plugin/go/contract/handler_claim_protocol_reward.go new file mode 100644 index 0000000000..270ddad89c --- /dev/null +++ b/plugin/go/contract/handler_claim_protocol_reward.go @@ -0,0 +1,74 @@ +package contract + +// handler_claim_protocol_reward.go — MessageClaimProtocolReward +// Spec: PRIS v1.0-r3 +// Signer: PRAXIS_PROTOCOL_ADDR — no cooldown (audits/bounties on demand) + +func (c *Contract) CheckMessageClaimProtocolReward(msg *MessageClaimProtocolReward) *PluginCheckResponse { +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{PRAXIS_PROTOCOL_ADDR}, +} +} + +func (c *Contract) DeliverMessageClaimProtocolReward(msg *MessageClaimProtocolReward, fee uint64) *PluginDeliverResponse { +height := GetGlobalHeight() +if height == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +poolQId := nextQueryId() +accQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: poolQId, Key: KeyForProtocolPool()}, +{QueryId: accQId, Key: KeyForAccount(PRAXIS_PROTOCOL_ADDR)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +pool := &Pool{} +acc := &Account{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 { continue } +switch r.QueryId { +case poolQId: +if pe := Unmarshal(r.Entries[0].Value, pool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case accQId: +if pe := Unmarshal(r.Entries[0].Value, acc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if pool.Amount == 0 { +return &PluginDeliverResponse{Error: ErrEmptyPool()} +} + +acc.Amount += pool.Amount +pool.Amount = 0 + +rawPool, pe := SafeMarshal(pool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawAcc, pe := SafeMarshal(acc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForProtocolPool(), Value: rawPool}, +{Key: KeyForAccount(PRAXIS_PROTOCOL_ADDR), Value: rawAcc}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_claim_resolver_reward.go b/plugin/go/contract/handler_claim_resolver_reward.go new file mode 100644 index 0000000000..acf5d2a954 --- /dev/null +++ b/plugin/go/contract/handler_claim_resolver_reward.go @@ -0,0 +1,149 @@ +package contract + +// handler_claim_resolver_reward.go — MessageClaimResolverReward +// Spec: PRIS v1.0-r3 +// +// Pays a resolver their weighted share of the epoch resolver pool. +// Share formula: +// share = resolverEpochPool * (resolver_resolutions * tier_weight) / SUM(all * tier_weight) +// +// Qualifying conditions: +// - RRS > 0 +// - SuccessfulResolutions > 0 in the epoch +// - LastClaimedEpoch < epoch +// +// r3 fix R3-4: qualification is RRS > 0 (not >= 10) — Bronze tier starts at 1. + +func rrsWeight(rrs uint64) uint32 { +if rrs >= RRS_GOLD_THRESHOLD { +return VOTE_WEIGHT_GOLD +} else if rrs >= RRS_SILVER_THRESHOLD { +return VOTE_WEIGHT_SILVER +} +return VOTE_WEIGHT_BRONZE +} + +func (c *Contract) CheckMessageClaimResolverReward(msg *MessageClaimResolverReward) *PluginCheckResponse { +if len(msg.ResolverAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.ResolverAddress}, +} +} + +func (c *Contract) DeliverMessageClaimResolverReward(msg *MessageClaimResolverReward, fee uint64) *PluginDeliverResponse { +height := GetGlobalHeight() +if height == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +currentEpoch := height / PRIS_EPOCH_BLOCKS +claimEpoch := msg.Epoch + +// Can only claim past epochs — current epoch not yet snapshotted. +if claimEpoch >= currentEpoch { +return &PluginDeliverResponse{Error: ErrInvalidParam()} +} + +recQId := nextQueryId() +poolQId := nextQueryId() +accQId := nextQueryId() +statsQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: recQId, Key: KeyForResolverRecord(msg.ResolverAddress)}, +{QueryId: poolQId, Key: KeyForResolverEpochPool(claimEpoch)}, +{QueryId: accQId, Key: KeyForAccount(msg.ResolverAddress)}, +{QueryId: statsQId, Key: KeyForGlobalStats()}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +rec := &ResolverRecord{} +pool := &Pool{} +acc := &Account{} +stats := &GlobalStats{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 { +continue +} +switch r.QueryId { +case recQId: +if pe := Unmarshal(r.Entries[0].Value, rec); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case poolQId: +if pe := Unmarshal(r.Entries[0].Value, pool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case accQId: +if pe := Unmarshal(r.Entries[0].Value, acc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case statsQId: +if pe := Unmarshal(r.Entries[0].Value, stats); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +// Qualification checks +if rec.RrsScore == 0 { +return &PluginDeliverResponse{Error: ErrInsufficientRRS()} +} +if rec.SuccessfulResolutions == 0 { +return &PluginDeliverResponse{Error: ErrNoResolutions()} +} +if rec.LastClaimedEpoch >= claimEpoch { +return &PluginDeliverResponse{Error: ErrAlreadyClaimed()} +} +if pool.Amount == 0 { +return &PluginDeliverResponse{Error: ErrEmptyPool()} +} +if stats.TotalWeightedResolutions == 0 { +return &PluginDeliverResponse{Error: ErrEmptyPool()} +} + +// Compute weighted share +weight := uint64(rrsWeight(rec.RrsScore)) +myScore := rec.SuccessfulResolutions * weight +payout := pool.Amount * myScore / stats.TotalWeightedResolutions + +if payout == 0 { +return &PluginDeliverResponse{Error: ErrEmptyPool()} +} +if payout > pool.Amount { +payout = pool.Amount +} + +acc.Amount += payout +pool.Amount -= payout +rec.LastClaimedEpoch = claimEpoch + +rawRec, pe := SafeMarshal(rec) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawPool, pe := SafeMarshal(pool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawAcc, pe := SafeMarshal(acc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForResolverRecord(msg.ResolverAddress), Value: rawRec}, +{Key: KeyForResolverEpochPool(claimEpoch), Value: rawPool}, +{Key: KeyForAccount(msg.ResolverAddress), Value: rawAcc}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_claim_slash.go b/plugin/go/contract/handler_claim_slash.go new file mode 100644 index 0000000000..fdba1ce445 --- /dev/null +++ b/plugin/go/contract/handler_claim_slash.go @@ -0,0 +1,219 @@ +package contract + +func (c *Contract) CheckMessageClaimSlash(msg *MessageClaimSlash) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.ClaimantAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.ClaimantAddress}, +} +} + +func (c *Contract) DeliverMessageClaimSlash(msg *MessageClaimSlash, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +marketQId := nextQueryId() +proposalQId := nextQueryId() +disputeQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: KeyForMarket(msg.MarketId)}, +{QueryId: proposalQId, Key: KeyForProposal(msg.MarketId)}, +{QueryId: disputeQId, Key: KeyForDispute(msg.MarketId)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +var proposal *ProposalRecord +var dispute *DisputeRecord + +for _, r := range resp.Results { +switch r.QueryId { +case marketQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case proposalQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrInternal()} +} +proposal = &ProposalRecord{} +if pe := Unmarshal(r.Entries[0].Value, proposal); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case disputeQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrInternal()} +} +dispute = &DisputeRecord{} +if pe := Unmarshal(r.Entries[0].Value, dispute); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +if market.Status != STATUS_FINALIZED { +return &PluginDeliverResponse{Error: ErrNotFinalized()} +} +if proposal == nil || dispute == nil { +return &PluginDeliverResponse{Error: ErrInternal()} +} +if !bytesEqual(msg.ClaimantAddress, proposal.ResolverAddr) { +return &PluginDeliverResponse{Error: ErrUnauthorized()} +} +if dispute.VoteStatus != VOTE_TALLIED { +return &PluginDeliverResponse{Error: ErrTallyNotReady()} +} + +slashQId := nextQueryId() +claimAccQId := nextQueryId() +treasQId := nextQueryId() +resolverRecQId := nextQueryId() +resFeeQId := nextQueryId() + +resp2, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: slashQId, Key: KeyForSlashRecord(dispute.DisputerAddress)}, +{QueryId: claimAccQId, Key: KeyForAccount(msg.ClaimantAddress)}, +{QueryId: treasQId, Key: KeyForTreasuryReserve(msg.MarketId)}, +{QueryId: resolverRecQId, Key: KeyForResolverRecord(proposal.ResolverAddr)}, +{QueryId: resFeeQId, Key: KeyForResolverFeePool(msg.MarketId)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp2.Error != nil { +return &PluginDeliverResponse{Error: resp2.Error} +} + +var slash *SlashRecord +claimAcc := &Account{} +treasury := &TreasuryReserve{} +resolverRec := &ResolverRecord{} +resFeePool := &Pool{} + +for _, r := range resp2.Results { +switch r.QueryId { +case slashQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrNoSlashToClaim()} +} +slash = &SlashRecord{} +if pe := Unmarshal(r.Entries[0].Value, slash); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case claimAccQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, claimAcc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +case treasQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, treasury); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +case resolverRecQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +_ = Unmarshal(r.Entries[0].Value, resolverRec) +} +case resFeeQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +_ = Unmarshal(r.Entries[0].Value, resFeePool) +} +} +} + +if slash == nil || slash.SlashAmount == 0 { +return &PluginDeliverResponse{Error: ErrNoSlashToClaim()} +} + +slashAmount := slash.SlashAmount +if treasury.LockedReserve < slashAmount { +slashAmount = treasury.LockedReserve +} +treasury.LockedReserve -= slashAmount +claimAcc.Amount += slashAmount +slash.SlashAmount = 0 + +// PRIS v1.0-r3: RRS -50 (floor 0) and sweep resolver fee pool to treasury +if resolverRec.RrsScore >= 50 { +resolverRec.RrsScore -= 50 +} else { +resolverRec.RrsScore = PRIS_RRS_FLOOR +} +// Sweep resolver fee pool to treasury pool +if resFeePool.Amount > 0 { +// Read global treasury pool +tPoolQId := nextQueryId() +tPoolResp, tPoolErr := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: tPoolQId, Key: KeyForTreasuryPool()}, +}, +}) +if tPoolErr == nil && tPoolResp.Error == nil { +tPool := &Pool{} +for _, r := range tPoolResp.Results { +if r.QueryId == tPoolQId && len(r.Entries) > 0 { +_ = Unmarshal(r.Entries[0].Value, tPool) +} +} +tPool.Amount += resFeePool.Amount +resFeePool.Amount = 0 +rawTPool, pe2 := SafeMarshal(tPool) +if pe2 == nil { +rawResFee2, pe3 := SafeMarshal(resFeePool) +if pe3 == nil { +_, _ = c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForTreasuryPool(), Value: rawTPool}, +{Key: KeyForResolverFeePool(msg.MarketId), Value: rawResFee2}, +}, +}) +} +} +} +} + +rawSlash, pe := SafeMarshal(slash) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawAcc, pe := SafeMarshal(claimAcc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawT, pe := SafeMarshal(treasury) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForSlashRecord(dispute.DisputerAddress), Value: rawSlash}, +{Key: KeyForAccount(msg.ClaimantAddress), Value: rawAcc}, +{Key: KeyForTreasuryReserve(msg.MarketId), Value: rawT}, +{Key: KeyForResolverRecord(proposal.ResolverAddr), Value: func() []byte { b, _ := SafeMarshal(resolverRec); return b }()}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_claim_unbonded_stake.go b/plugin/go/contract/handler_claim_unbonded_stake.go new file mode 100644 index 0000000000..5fb885bd44 --- /dev/null +++ b/plugin/go/contract/handler_claim_unbonded_stake.go @@ -0,0 +1,103 @@ +package contract + +// handler_claim_unbonded_stake.go — MessageClaimUnbondedStake +// Spec: PRIS v1.0-r3 unstake extension +// +// Releases unbonded stake back to resolver account after unbonding period. +// UnbondingReleaseHeight must be <= currentHeight. + +func (c *Contract) CheckMessageClaimUnbondedStake(msg *MessageClaimUnbondedStake) *PluginCheckResponse { +if len(msg.ResolverAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.ResolverAddress}, +} +} + +func (c *Contract) DeliverMessageClaimUnbondedStake(msg *MessageClaimUnbondedStake, fee uint64) *PluginDeliverResponse { +height := GetGlobalHeight() +if height == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +// ── Batch read ──────────────────────────────────────────────────────── +recQId := nextQueryId() +accQId := nextQueryId() + +recKey := KeyForResolverRecord(msg.ResolverAddress) +accKey := KeyForAccount(msg.ResolverAddress) + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: recQId, Key: recKey}, +{QueryId: accQId, Key: accKey}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var record *ResolverRecord +acc := &Account{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +switch r.QueryId { +case recQId: +record = &ResolverRecord{} +if pe := Unmarshal(r.Entries[0].Value, record); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case accQId: +if pe := Unmarshal(r.Entries[0].Value, acc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if record == nil { +return &PluginDeliverResponse{Error: ErrResolverNotFound()} +} +if record.UnbondingAmount == 0 { +return &PluginDeliverResponse{Error: ErrNoUnbondingStake()} +} +if height < record.UnbondingReleaseHeight { +return &PluginDeliverResponse{Error: ErrUnbondingNotComplete()} +} + +// ── Release unbonded stake ──────────────────────────────────────────── +payout := record.UnbondingAmount +record.UnbondingAmount = 0 +record.UnbondingReleaseHeight = 0 +acc.Amount += payout + +// ── Pay fee ─────────────────────────────────────────────────────────── +if acc.Amount < fee { +return &PluginDeliverResponse{Error: ErrInsufficientFunds()} +} +acc.Amount -= fee + +// ── Marshal ─────────────────────────────────────────────────────────── +rawRec, pe := SafeMarshal(record) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawAcc, pe := SafeMarshal(acc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +// ── 2-key atomic write ──────────────────────────────────────────────── +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: recKey, Value: rawRec}, +{Key: accKey, Value: rawAcc}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_claim_winnings.go b/plugin/go/contract/handler_claim_winnings.go new file mode 100644 index 0000000000..adcaa166ba --- /dev/null +++ b/plugin/go/contract/handler_claim_winnings.go @@ -0,0 +1,310 @@ +package contract + +func (c *Contract) CheckMessageClaimWinnings(msg *MessageClaimWinnings) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.ClaimantAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.ClaimantAddress}, +} +} + +func (c *Contract) DeliverMessageClaimWinnings(msg *MessageClaimWinnings, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} +if len(msg.ClaimantAddress) != 20 { +return &PluginDeliverResponse{Error: ErrInvalidAddress()} +} +cancelledMarket, cancelErr := c.CheckAutoCancel(msg.MarketId) +if cancelErr != nil { +return &PluginDeliverResponse{Error: cancelErr} +} + +marketQId := nextQueryId() +posQId := nextQueryId() +poolQId := nextQueryId() +claimAccQId := nextQueryId() + +marketKey := KeyForMarket(msg.MarketId) +posKey := KeyForPosition(msg.MarketId, msg.ClaimantAddress) +poolKey := KeyForMarketPool(msg.MarketId) +claimKey := KeyForAccount(msg.ClaimantAddress) + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: marketKey}, +{QueryId: posQId, Key: posKey}, +{QueryId: poolQId, Key: poolKey}, +{QueryId: claimAccQId, Key: claimKey}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +position := &PositionState{} +marketPool := &Pool{} +claimantAcc := &Account{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +switch r.QueryId { +case marketQId: +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case posQId: +if pe := Unmarshal(r.Entries[0].Value, position); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case poolQId: +if pe := Unmarshal(r.Entries[0].Value, marketPool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case claimAccQId: +if pe := Unmarshal(r.Entries[0].Value, claimantAcc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +// Apply auto-cancel if triggered — status will be STATUS_CANCELLED. +if cancelledMarket != nil { +market = cancelledMarket +} +if position.SharesYes == 0 && position.SharesNo == 0 && position.CostPaid == 0 { +return &PluginDeliverResponse{Error: ErrNoPosition()} +} +if position.Claimed { +return &PluginDeliverResponse{Error: ErrAlreadyClaimed()} +} + +var payout uint64 +cancelTreasury := &TreasuryReserve{} +switch market.Status { +case STATUS_FINALIZED: +outQId := nextQueryId() +outResp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: outQId, Key: KeyForOutcome(msg.MarketId)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if outResp.Error != nil { +return &PluginDeliverResponse{Error: outResp.Error} +} + +var outcome *OutcomeState +for _, r := range outResp.Results { +if r.QueryId == outQId { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrInternal()} +} +outcome = &OutcomeState{} +if pe := Unmarshal(r.Entries[0].Value, outcome); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} +if outcome == nil { +return &PluginDeliverResponse{Error: ErrInternal()} +} + +var winnerShares, totalWinShares uint64 +if outcome.WinningOutcome { +winnerShares = position.SharesYes +totalWinShares = market.QYes +} else { +winnerShares = position.SharesNo +totalWinShares = market.QNo +} +if winnerShares > 0 { +if totalWinShares == 0 { +return &PluginDeliverResponse{Error: ErrInternal()} +} +// C-1 fix: use FinalizedPoolAmount (recorded at finalization) not live pool. +// Live pool shrinks as each claimant collects, causing later claimants +// to receive less than their fair pro-rata share. +poolForPayout := marketPool.Amount +if market.FinalizedPoolAmount > 0 { +poolForPayout = market.FinalizedPoolAmount +} +payout = ComputePayout(poolForPayout, winnerShares, totalWinShares) +} + +case STATUS_CANCELLED: +payout = position.CostPaid +// Slash creator bond into treasury pool on cancel. +bondSlashQId := nextQueryId() +bsResp, bsErr := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: bondSlashQId, Key: KeyForTreasuryReserve(msg.MarketId)}, +}, +}) +if bsErr != nil { +return &PluginDeliverResponse{Error: bsErr} +} +if bsResp.Error != nil { +return &PluginDeliverResponse{Error: bsResp.Error} +} +for _, r := range bsResp.Results { +if r.QueryId == bondSlashQId && len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, cancelTreasury); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +case STATUS_VOIDED: +payout = position.CostPaid + +default: +return &PluginDeliverResponse{Error: ErrMarketNotResolved()} +} + +if payout > marketPool.Amount { +return &PluginDeliverResponse{Error: ErrInsufficientPoolFunds()} +} + +position.Claimed = true +market.ClaimedCount++ +marketPool.Amount -= payout +claimantAcc.Amount += payout + +rawPos, pe := SafeMarshal(position) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawMkt, pe := SafeMarshal(market) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawMP, pe := SafeMarshal(marketPool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawAcc, pe := SafeMarshal(claimantAcc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +// Issue-14 fix: compute sweep eligibility BEFORE the atomic write. +// marketPool.Amount is already post-payout in memory — no re-read needed. +// Folding sweep into the same StateWrite eliminates the TOCTOU window where +// two concurrent last-winner calls could both re-read a non-zero pool and +// double-credit treasury. +resolutionDelay := RESOLUTION_DELAY_BLOCKS +gracePeriod := GRACE_PERIOD_BLOCKS +claimGrace := CLAIM_GRACE_PERIOD +if TEST_MODE { +resolutionDelay = TEST_RESOLUTION_DELAY +gracePeriod = TEST_GRACE_PERIOD +claimGrace = TEST_CLAIM_GRACE_PERIOD +} +graceEnd := market.ExpiryTime + resolutionDelay + gracePeriod + claimGrace +shouldSweep := (market.Status == STATUS_FINALIZED && +(market.TotalPositions > 0 && market.ClaimedCount == market.TotalPositions || now > graceEnd)) || +(market.Status == STATUS_CANCELLED && now > graceEnd) + +sets := []*PluginSetOp{ +{Key: posKey, Value: rawPos}, +{Key: marketKey, Value: rawMkt}, +{Key: poolKey, Value: rawMP}, +{Key: claimKey, Value: rawAcc}, +} + +// Fold creator bond slash into sets if this was a cancel. +if market.Status == STATUS_CANCELLED && cancelTreasury.CreatorBond > 0 { +cancelTPool := &Pool{} +tpQId := nextQueryId() +tpResp, tpErr := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: tpQId, Key: KeyForTreasuryPool()}, +}, +}) +if tpErr != nil { +return &PluginDeliverResponse{Error: tpErr} +} +if tpResp.Error != nil { +return &PluginDeliverResponse{Error: tpResp.Error} +} +for _, r := range tpResp.Results { +if r.QueryId == tpQId && len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, cancelTPool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} +cancelTPool.Amount += cancelTreasury.CreatorBond +cancelTreasury.CreatorBond = 0 +rawCT, pe := SafeMarshal(cancelTreasury) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawCTP, pe := SafeMarshal(cancelTPool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +sets = append(sets, &PluginSetOp{Key: KeyForTreasuryReserve(msg.MarketId), Value: rawCT}) +sets = append(sets, &PluginSetOp{Key: KeyForTreasuryPool(), Value: rawCTP}) +} + +if shouldSweep && marketPool.Amount > 0 { +// marketPool.Amount here is the post-payout remainder (already subtracted above). +// Read treasury once, add remainder, zero the market pool — all in one write. +tQId := nextQueryId() +tResp, tErr := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: tQId, Key: KeyForTreasuryPool()}, +}, +}) +if tErr != nil { +return &PluginDeliverResponse{Error: tErr} +} +if tResp.Error != nil { +return &PluginDeliverResponse{Error: tResp.Error} +} + +treasuryPool := &Pool{} +for _, r := range tResp.Results { +if r.QueryId == tQId && len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, treasuryPool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +// Overflow guard. +if treasuryPool.Amount > ^uint64(0)-marketPool.Amount { +return &PluginDeliverResponse{Error: ErrInvalidAmount()} +} +treasuryPool.Amount += marketPool.Amount +marketPool.Amount = 0 + +// Re-marshal pool (now zeroed) and treasury (now incremented). +rawMP, pe = SafeMarshal(marketPool) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawTreasury, pe := SafeMarshal(treasuryPool) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} + +// Replace pool op with zeroed value; append treasury op. +sets[2] = &PluginSetOp{Key: poolKey, Value: rawMP} +sets = append(sets, &PluginSetOp{Key: KeyForTreasuryPool(), Value: rawTreasury}) +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{Sets: sets}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} + +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_commit_vote.go b/plugin/go/contract/handler_commit_vote.go new file mode 100644 index 0000000000..4551b0460d --- /dev/null +++ b/plugin/go/contract/handler_commit_vote.go @@ -0,0 +1,117 @@ +package contract + +func (c *Contract) CheckMessageCommitVote(msg *MessageCommitVote) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.VoterAddr) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +if len(msg.CommitHash) != 32 { +return ErrCheckResp(ErrInvalidCommitHash()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.VoterAddr}, +} +} + +func (c *Contract) DeliverMessageCommitVote(msg *MessageCommitVote, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +marketQId := nextQueryId() +disputeQId := nextQueryId() +commitQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: KeyForMarket(msg.MarketId)}, +{QueryId: disputeQId, Key: KeyForDispute(msg.MarketId)}, +{QueryId: commitQId, Key: KeyForVoteCommit(msg.MarketId, msg.VoterAddr)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +var dispute *DisputeRecord + +for _, r := range resp.Results { +switch r.QueryId { +case marketQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case disputeQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrNotDisputed()} +} +dispute = &DisputeRecord{} +if pe := Unmarshal(r.Entries[0].Value, dispute); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case commitQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +return &PluginDeliverResponse{Error: ErrAlreadyCommitted()} +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +if market.Status != STATUS_DISPUTED { +return &PluginDeliverResponse{Error: ErrNotDisputed()} +} +if dispute == nil { +return &PluginDeliverResponse{Error: ErrNotDisputed()} +} + +commitDeadline := dispute.DisputeBlock + COMMIT_PHASE_BLOCKS +if now > commitDeadline { +return &PluginDeliverResponse{Error: ErrCommitPhaseOver()} +} + +if !isPanelMember(msg.VoterAddr, dispute.PanelMembers) { +return &PluginDeliverResponse{Error: ErrNotAPanelMember()} +} + +vc := &VoteCommit{ +VoterAddr: msg.VoterAddr, +CommitHash: msg.CommitHash, +CommittedAt: now, +} +rawVC, pe := SafeMarshal(vc) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForVoteCommit(msg.MarketId, msg.VoterAddr), Value: rawVC}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} + +func isPanelMember(addr []byte, panelMembers [][]byte) bool { +for _, m := range panelMembers { +if bytesEqual(addr, m) { +return true +} +} +return false +} diff --git a/plugin/go/contract/handler_create_market.go b/plugin/go/contract/handler_create_market.go new file mode 100644 index 0000000000..ae0d1ec87e --- /dev/null +++ b/plugin/go/contract/handler_create_market.go @@ -0,0 +1,194 @@ +package contract + +func (c *Contract) CheckMessageCreateMarket(msg *MessageCreateMarket) *PluginCheckResponse { +if len(msg.CreatorAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +if msg.B0 < MIN_B0 { +return ErrCheckResp(ErrInvalidB0()) +} +if msg.ExpiryTime == 0 { +return ErrCheckResp(ErrInvalidParam()) +} +if msg.ExpiryTime > MAX_EXPIRY_TIME { +return ErrCheckResp(ErrExpiryTooLarge()) +} +if msg.Nonce == 0 { +return ErrCheckResp(ErrInvalidNonce()) +} +if len(msg.Question) == 0 { +return ErrCheckResp(ErrInvalidQuestion()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.CreatorAddress}, +} +} + +func (c *Contract) DeliverMessageCreateMarket(msg *MessageCreateMarket, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} +if msg.ExpiryTime > MAX_EXPIRY_TIME { +return &PluginDeliverResponse{Error: ErrExpiryTooLarge()} +} + +marketId := DeriveMarketId(msg.CreatorAddress, msg.Nonce) + +marketQId := nextQueryId() +creatorQId := nextQueryId() +feeQId := nextQueryId() + gTreasuryQId := nextQueryId() + ocQId := nextQueryId() + midxQId := nextQueryId() + +marketKey := KeyForMarket(marketId) +creatorKey := KeyForAccount(msg.CreatorAddress) +feePoolKey := KeyForFeePool(c.Config.ChainId) + gTreasuryKey := KeyForTreasuryPool() + ocKey := KeyForCreatorOpenCount(msg.CreatorAddress) + midxKey := KeyForMarketIndex() +poolKey := KeyForMarketPool(marketId) +treasKey := KeyForTreasuryReserve(marketId) + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: marketKey}, +{QueryId: creatorQId, Key: creatorKey}, +{QueryId: feeQId, Key: feePoolKey}, + {QueryId: gTreasuryQId, Key: gTreasuryKey}, + {QueryId: ocQId, Key: ocKey}, + {QueryId: midxQId, Key: midxKey}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +creator := &Account{} +feePool := &Pool{} + gTreasury := &Pool{} + openCount := &Pool{} + midx := &MarketIndex{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +switch r.QueryId { +case marketQId: +return &PluginDeliverResponse{Error: ErrInvalidParam()} // already exists +case creatorQId: +if pe := Unmarshal(r.Entries[0].Value, creator); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case feeQId: +if pe := Unmarshal(r.Entries[0].Value, feePool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} + case gTreasuryQId: + case ocQId: + if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { + _ = Unmarshal(r.Entries[0].Value, openCount) + } + case midxQId: + if pe := Unmarshal(r.Entries[0].Value, midx); pe != nil { + return &PluginDeliverResponse{Error: pe} + } + if pe := Unmarshal(r.Entries[0].Value, gTreasury); pe != nil { + return &PluginDeliverResponse{Error: pe} + } + } +} + + if fee > 0 && msg.B0 > ^uint64(0)-fee { +return &PluginDeliverResponse{Error: ErrInvalidAmount()} +} +if msg.B0 > ^uint64(0)-fee-CREATOR_BOND { +return &PluginDeliverResponse{Error: ErrInvalidAmount()} +} +totalCost := msg.B0 + fee + CREATOR_BOND +if creator.Amount < totalCost { +return &PluginDeliverResponse{Error: ErrInsufficientFunds()} +} + +creator.Amount -= totalCost +feePool.Amount += fee / 2 + gTreasury.Amount += fee - fee/2 + + // Item-14: spam cap + if openCount.Amount >= MAX_OPEN_MARKETS_PER_CREATOR { + return &PluginDeliverResponse{Error: ErrTooManyOpenMarkets()} + } + openCount.Amount++ + + // Append new market to global market index + midx.MarketIds = append(midx.MarketIds, marketId) + +// Carve FINALIZATION_BOUNTY from B0 before seeding the LMSR pool. +// MIN_B0 >= FINALIZATION_BOUNTY + seed margin, so this subtraction is safe. +lmsrSeed := msg.B0 - FINALIZATION_BOUNTY +halfB0 := lmsrSeed / 2 +market := &MarketState{ +Status: STATUS_OPEN, +ExpiryTime: msg.ExpiryTime, +QYes: halfB0, +QNo: halfB0, +BEff: lmsrSeed, +Creator: msg.CreatorAddress, +ClaimedCount: 0, +TotalPositions: 0, +OpenTime: now, +ElevatedRisk: false, +Question: msg.Question, +Rules: msg.Rules, +} + +pool := &Pool{Id: c.Config.ChainId, Amount: lmsrSeed} +treasury := &TreasuryReserve{LockedReserve: FINALIZATION_BOUNTY, CreatorBond: CREATOR_BOND} + +rawMarket, pe := SafeMarshal(market) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawCreator, pe := SafeMarshal(creator) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawFee, pe := SafeMarshal(feePool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawPool, pe := SafeMarshal(pool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawTreasury, pe := SafeMarshal(treasury) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawGTreasury, pe := SafeMarshal(gTreasury) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawMidx, pe := SafeMarshal(midx) + rawOC, pe := SafeMarshal(openCount) + if pe != nil { return &PluginDeliverResponse{Error: pe} } +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +sets := []*PluginSetOp{ +{Key: marketKey, Value: rawMarket}, +{Key: poolKey, Value: rawPool}, +{Key: treasKey, Value: rawTreasury}, +{Key: feePoolKey, Value: rawFee}, + {Key: gTreasuryKey, Value: rawGTreasury}, + {Key: midxKey, Value: rawMidx}, + {Key: ocKey, Value: rawOC}, +} +var deletes []*PluginDeleteOp +if creator.Amount > 0 { +sets = append(sets, &PluginSetOp{Key: creatorKey, Value: rawCreator}) +} else { +deletes = []*PluginDeleteOp{{Key: creatorKey}} +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: sets, +Deletes: deletes, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_endblock.go b/plugin/go/contract/handler_endblock.go new file mode 100644 index 0000000000..6fd54466d8 --- /dev/null +++ b/plugin/go/contract/handler_endblock.go @@ -0,0 +1,135 @@ +package contract + +// handler_endblock.go — PRIS v1.0-r3 EndBlock epoch boundary processor +// +// Triggered inside EndBlock every PRIS_EPOCH_BLOCKS blocks. +// r3 fix R3-2: epoch snapshot is NOT triggered by user TXs — it fires +// in EndBlock which executes every block regardless of TX count. +// +// On epoch boundary: +// 1. Read treasury pool balance +// 2. Write immutable EpochSnapshot +// 3. Carve 5 distribution pools atomically (20% each) +// 4. Zero treasury pool +// +// EndBlock must never return an error that halts the chain. + + +func (c *Contract) processEpochBoundary(height uint64) *PluginError { +epoch := height / PRIS_EPOCH_BLOCKS + +treasuryQId := nextQueryId() +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: treasuryQId, Key: KeyForTreasuryPool()}, +}, +}) +if err != nil { +return err +} +if resp.Error != nil { +return resp.Error +} + +treasury := &Pool{} +for _, r := range resp.Results { +if r.QueryId == treasuryQId && len(r.Entries) > 0 { +if pe := Unmarshal(r.Entries[0].Value, treasury); pe != nil { +return pe +} +} +} + +if treasury.Amount == 0 { +return nil +} + +resolverShare := ComputeBps(treasury.Amount, PRIS_RESOLVER_SHARE_BPS) +builderShare := ComputeBps(treasury.Amount, PRIS_BUILDER_SHARE_BPS) +communityShare := ComputeBps(treasury.Amount, PRIS_COMMUNITY_SHARE_BPS) +investorShare := ComputeBps(treasury.Amount, PRIS_INVESTOR_SHARE_BPS) +protocolShare := ComputeBps(treasury.Amount, PRIS_PROTOCOL_SHARE_BPS) + +bPoolQId := nextQueryId() +cPoolQId := nextQueryId() +iPoolQId := nextQueryId() +pPoolQId := nextQueryId() +rPoolQId := nextQueryId() + +poolResp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: bPoolQId, Key: KeyForBuilderPool()}, +{QueryId: cPoolQId, Key: KeyForCommunityPool()}, +{QueryId: iPoolQId, Key: KeyForInvestorPool()}, +{QueryId: pPoolQId, Key: KeyForProtocolPool()}, +{QueryId: rPoolQId, Key: KeyForResolverEpochPool(epoch)}, +}, +}) +if err != nil { +return err +} + +builderPool := &Pool{} +communityPool := &Pool{} +investorPool := &Pool{} +protocolPool := &Pool{} +resolverPool := &Pool{} + +if poolResp != nil && poolResp.Error == nil { +for _, r := range poolResp.Results { +if len(r.Entries) == 0 { +continue +} +switch r.QueryId { +case bPoolQId: +_ = Unmarshal(r.Entries[0].Value, builderPool) +case cPoolQId: +_ = Unmarshal(r.Entries[0].Value, communityPool) +case iPoolQId: +_ = Unmarshal(r.Entries[0].Value, investorPool) +case pPoolQId: +_ = Unmarshal(r.Entries[0].Value, protocolPool) +case rPoolQId: +_ = Unmarshal(r.Entries[0].Value, resolverPool) +} +} +} + +builderPool.Amount += builderShare +communityPool.Amount += communityShare +investorPool.Amount += investorShare +protocolPool.Amount += protocolShare +resolverPool.Amount += resolverShare + +snapshot := &Pool{Amount: treasury.Amount} +rawSnap, pe := SafeMarshal(snapshot) +if pe != nil { return pe } + +rawBuilder, pe := SafeMarshal(builderPool) +if pe != nil { return pe } +rawCommunity, pe := SafeMarshal(communityPool) +if pe != nil { return pe } +rawInvestor, pe := SafeMarshal(investorPool) +if pe != nil { return pe } +rawProtocol, pe := SafeMarshal(protocolPool) +if pe != nil { return pe } +rawResolver, pe := SafeMarshal(resolverPool) +if pe != nil { return pe } + +emptyTreasury := &Pool{Amount: 0} +rawTreasury, pe := SafeMarshal(emptyTreasury) +if pe != nil { return pe } + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForEpochSnapshot(epoch), Value: rawSnap}, +{Key: KeyForBuilderPool(), Value: rawBuilder}, +{Key: KeyForCommunityPool(), Value: rawCommunity}, +{Key: KeyForInvestorPool(), Value: rawInvestor}, +{Key: KeyForProtocolPool(), Value: rawProtocol}, +{Key: KeyForResolverEpochPool(epoch), Value: rawResolver}, +{Key: KeyForTreasuryPool(), Value: rawTreasury}, +}, +}) +return errCheckWrite(wr, werr) +} diff --git a/plugin/go/contract/handler_file_dispute.go b/plugin/go/contract/handler_file_dispute.go new file mode 100644 index 0000000000..6a80133631 --- /dev/null +++ b/plugin/go/contract/handler_file_dispute.go @@ -0,0 +1,297 @@ +package contract + +func (c *Contract) CheckMessageFileDispute(msg *MessageFileDispute) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.DisputerAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +if msg.DisputeBond == 0 { +return ErrCheckResp(ErrInvalidAmount()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.DisputerAddress}, +} +} + +func (c *Contract) DeliverMessageFileDispute(msg *MessageFileDispute, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +marketQId := nextQueryId() +proposalQId := nextQueryId() +dispAccQId := nextQueryId() +entropyQId := nextQueryId() +feeQId := nextQueryId() + +marketKey := KeyForMarket(msg.MarketId) +proposalKey := KeyForProposal(msg.MarketId) +dispAccKey := KeyForAccount(msg.DisputerAddress) +feePoolKey := KeyForFeePool(c.Config.ChainId) +treasyPoolKey := KeyForTreasuryPool() +treasyQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: marketKey}, +{QueryId: proposalQId, Key: proposalKey}, +{QueryId: dispAccQId, Key: dispAccKey}, +{QueryId: entropyQId, Key: PANEL_ENTROPY_KEY}, +{QueryId: feeQId, Key: feePoolKey}, +{QueryId: treasyQId, Key: treasyPoolKey}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +var proposal *ProposalRecord +disputer := &Account{} +feePool := &Pool{} +treasyPool := &Pool{} +var entropyVal uint64 + +for _, r := range resp.Results { +switch r.QueryId { +case marketQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case proposalQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrInternal()} +} +proposal = &ProposalRecord{} +if pe := Unmarshal(r.Entries[0].Value, proposal); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case dispAccQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, disputer); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +case entropyQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +acc := &PanelEntropyAccum{} +if pe := Unmarshal(r.Entries[0].Value, acc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +entropyVal = acc.Accumulator +} +case feeQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, feePool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +if market.Status != STATUS_PROPOSED { +return &PluginDeliverResponse{Error: ErrMarketNotOpen()} +} +if proposal == nil { +return &PluginDeliverResponse{Error: ErrInternal()} +} + +disputeWindow := ComputeDisputeBlocks(market.OpenTime, market.ExpiryTime) +disputeDeadline := proposal.ProposalBlock + disputeWindow +if now > disputeDeadline { +return &PluginDeliverResponse{Error: ErrDisputeWindowClosed()} +} + +if market.Status == STATUS_DISPUTED { +return &PluginDeliverResponse{Error: ErrAlreadyDisputed()} +} + +if fee > 0 && msg.DisputeBond > ^uint64(0)-fee { +return &PluginDeliverResponse{Error: ErrInvalidAmount()} +} +totalCost := msg.DisputeBond + fee +if disputer.Amount < totalCost { +return &PluginDeliverResponse{Error: ErrInsufficientFunds()} +} + +resolverRangeQId := nextQueryId() +rangeResp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Ranges: []*PluginRangeRead{ +{ +QueryId: resolverRangeQId, +Prefix: resolverRecordPrefix, +Limit: 0, +Reverse: false, +}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if rangeResp.Error != nil { +return &PluginDeliverResponse{Error: rangeResp.Error} +} + +var candidates [][]byte +for _, r := range rangeResp.Results { +if r.QueryId != resolverRangeQId { +continue +} +for _, entry := range r.Entries { +rec := &ResolverRecord{} +if pe := Unmarshal(entry.Value, rec); pe != nil { +continue +} +if bytesEqual(rec.ResolverAddress, proposal.ResolverAddr) { +continue +} +if bytesEqual(rec.ResolverAddress, msg.DisputerAddress) { +continue +} +if rec.RrsScore < MIN_RRS_TO_PROPOSE { +continue +} +candidates = append(candidates, rec.ResolverAddress) +} +} + +// Layer 1: position exclusion +// Any resolver with an open unclaimed position in this market is ineligible. +posQueries := make([]uint64, len(candidates)) +posKeys := make([]*PluginKeyRead, len(candidates)) +for i, addr := range candidates { +qId := nextQueryId() +posQueries[i] = qId +posKeys[i] = &PluginKeyRead{QueryId: qId, Key: KeyForPosition(msg.MarketId, addr)} +} +if len(posKeys) > 0 { +posResp, err := c.plugin.StateRead(c, &PluginStateReadRequest{Keys: posKeys}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if posResp.Error != nil { +return &PluginDeliverResponse{Error: posResp.Error} +} +disqualified := make(map[string]bool) +for _, r := range posResp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +pos := &PositionState{} +if pe := Unmarshal(r.Entries[0].Value, pos); pe != nil { +continue +} +if !pos.Claimed { +for i, qId := range posQueries { +if r.QueryId == qId { +disqualified[string(candidates[i])] = true +} +} +} +} +filtered := candidates[:0] +for _, addr := range candidates { +if !disqualified[string(addr)] { +filtered = append(filtered, addr) +} +} +candidates = filtered +} + +var panelSize uint32 +if market.ElevatedRisk { +panelSize = ELEVATED_RISK_PANEL_SIZE +} else { +panelSize = MIN_PANEL_SIZE +} + +seed := entropyVal ^ (now * FIBONACCI_HASH_CONSTANT) +panel := derivePanel(candidates, int(panelSize), seed) +if len(panel) == 0 { +return &PluginDeliverResponse{Error: ErrInsufficientPanelCandidates()} +} + +market.Status = STATUS_DISPUTED +disputer.Amount -= totalCost +feePool.Amount += fee / 2 +treasyPool.Amount += fee / 2 + +dispute := &DisputeRecord{ +DisputerAddress: msg.DisputerAddress, +DisputeBond: msg.DisputeBond, +DisputeBlock: now, +VoteStatus: VOTE_PENDING, +PanelSize: uint32(len(panel)), +PanelMembers: panel, +} + +rawM, pe := SafeMarshal(market) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawD, pe := SafeMarshal(dispute) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawFee, pe := SafeMarshal(feePool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawTreasy, pe := SafeMarshal(treasyPool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +sets := []*PluginSetOp{ +{Key: marketKey, Value: rawM}, +{Key: KeyForDispute(msg.MarketId), Value: rawD}, +{Key: feePoolKey, Value: rawFee}, +{Key: treasyPoolKey, Value: rawTreasy}, +} +var deletes []*PluginDeleteOp +if disputer.Amount == 0 { +deletes = []*PluginDeleteOp{{Key: dispAccKey}} +} else { +rawDisp, pe := SafeMarshal(disputer) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +sets = append(sets, &PluginSetOp{Key: dispAccKey, Value: rawDisp}) +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: sets, +Deletes: deletes, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} + +func derivePanel(candidates [][]byte, n int, seed uint64) [][]byte { +if len(candidates) == 0 || n == 0 { +return nil +} +pool := make([][]byte, len(candidates)) +copy(pool, candidates) + +s := seed +lcgNext := func() uint64 { +s = s*6364136223846793005 + 1442695040888963407 +return s +} + +limit := len(pool) +if n < limit { +limit = n +} +for i := 0; i < limit; i++ { +j := int(lcgNext()%uint64(len(pool)-i)) + i +pool[i], pool[j] = pool[j], pool[i] +} +return pool[:limit] +} diff --git a/plugin/go/contract/handler_finalize_market.go b/plugin/go/contract/handler_finalize_market.go new file mode 100644 index 0000000000..026d1ebd5a --- /dev/null +++ b/plugin/go/contract/handler_finalize_market.go @@ -0,0 +1,313 @@ +package contract + +func (c *Contract) CheckMessageFinalizeMarket(msg *MessageFinalizeMarket) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.CallerAddr) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.CallerAddr}, +} +} + +func (c *Contract) DeliverMessageFinalizeMarket(msg *MessageFinalizeMarket, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +marketQId := nextQueryId() +disputeQId := nextQueryId() +proposalQId := nextQueryId() +treasQId := nextQueryId() +poolQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: KeyForMarket(msg.MarketId)}, +{QueryId: disputeQId, Key: KeyForDispute(msg.MarketId)}, +{QueryId: proposalQId, Key: KeyForProposal(msg.MarketId)}, +{QueryId: treasQId, Key: KeyForTreasuryReserve(msg.MarketId)}, +{QueryId: poolQId, Key: KeyForMarketPool(msg.MarketId)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +var dispute *DisputeRecord +var proposal *ProposalRecord +treasury := &TreasuryReserve{} +marketPool := &Pool{} + +for _, r := range resp.Results { +switch r.QueryId { +case marketQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case disputeQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +dispute = &DisputeRecord{} +if pe := Unmarshal(r.Entries[0].Value, dispute); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +case proposalQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +proposal = &ProposalRecord{} +if pe := Unmarshal(r.Entries[0].Value, proposal); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +case treasQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, treasury); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +case poolQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, marketPool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} + +if market.Status == STATUS_FINALIZED { +return &PluginDeliverResponse{} +} + +pathA := market.Status == STATUS_DISPUTED && dispute != nil && dispute.VoteStatus == VOTE_TALLIED +pathB := market.Status == STATUS_PROPOSED && dispute == nil +if !pathA && !pathB { +return &PluginDeliverResponse{Error: ErrNotFinalized()} +} + +if pathB { +if proposal == nil { +return &PluginDeliverResponse{Error: ErrInternal()} +} +disputeWindow := ComputeDisputeBlocks(market.OpenTime, market.ExpiryTime) +disputeDeadline := proposal.ProposalBlock + disputeWindow +// Reject if dispute window is still open — too early to finalize. +// In TEST_MODE, skip the window check so tests don't wait 34,560 blocks. +disputeWindowOpen := now <= disputeDeadline +if !TEST_MODE && disputeWindowOpen { +return &PluginDeliverResponse{Error: ErrDisputeWindowOpen()} +} +} + +callerQId := nextQueryId() +proposerQId := nextQueryId() +creatorQId := nextQueryId() + ocQId := nextQueryId() + +readKeys2 := []*PluginKeyRead{ +{QueryId: callerQId, Key: KeyForAccount(msg.CallerAddr)}, +{QueryId: creatorQId, Key: KeyForAccount(market.Creator)}, + {QueryId: ocQId, Key: KeyForCreatorOpenCount(market.Creator)}, +} + +var proposerKey []byte +var disputerKey []byte +var disputerQId uint64 + +if proposal == nil { return &PluginDeliverResponse{Error: ErrInternal()} } +proposerKey = KeyForAccount(proposal.ResolverAddr) +readKeys2 = append(readKeys2, &PluginKeyRead{QueryId: proposerQId, Key: proposerKey}) +resolverRecQId := nextQueryId() +globalStatsQId := nextQueryId() +resolverFeeQId := nextQueryId() +readKeys2 = append(readKeys2, &PluginKeyRead{QueryId: resolverRecQId, Key: KeyForResolverRecord(proposal.ResolverAddr)}) +readKeys2 = append(readKeys2, &PluginKeyRead{QueryId: globalStatsQId, Key: KeyForGlobalStats()}) +readKeys2 = append(readKeys2, &PluginKeyRead{QueryId: resolverFeeQId, Key: KeyForResolverFeePool(msg.MarketId)}) + +if pathA && dispute != nil { +disputerQId = nextQueryId() +disputerKey = KeyForAccount(dispute.DisputerAddress) +readKeys2 = append(readKeys2, &PluginKeyRead{QueryId: disputerQId, Key: disputerKey}) +} + +resp2, err := c.plugin.StateRead(c, &PluginStateReadRequest{Keys: readKeys2}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp2.Error != nil { +return &PluginDeliverResponse{Error: resp2.Error} +} + +callerAcc := &Account{} +proposerAcc := &Account{} +disputerAcc := &Account{} +resolverRec := &ResolverRecord{} +globalStats := &GlobalStats{} +resolverFeePool := &Pool{} +creatorAcc := &Account{} + openCount := Pool{} + +for _, r := range resp2.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +switch r.QueryId { +case callerQId: +if pe := Unmarshal(r.Entries[0].Value, callerAcc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case proposerQId: +if pe := Unmarshal(r.Entries[0].Value, proposerAcc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case ocQId: + if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { + _ = Unmarshal(r.Entries[0].Value, &openCount) + } + case creatorQId: +if pe := Unmarshal(r.Entries[0].Value, creatorAcc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case disputerQId: +if pe := Unmarshal(r.Entries[0].Value, disputerAcc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case resolverRecQId: +_ = Unmarshal(r.Entries[0].Value, resolverRec) +case globalStatsQId: +_ = Unmarshal(r.Entries[0].Value, globalStats) +case resolverFeeQId: +_ = Unmarshal(r.Entries[0].Value, resolverFeePool) +} +} + +bounty := FINALIZATION_BOUNTY +if treasury.LockedReserve < bounty { +bounty = treasury.LockedReserve +} +treasury.LockedReserve -= bounty +callerAcc.Amount += bounty + +var bondReturn uint64 +if proposal != nil { +bondReturn = proposal.ProposalBond +if treasury.LockedReserve < bondReturn { +bondReturn = treasury.LockedReserve +} +treasury.LockedReserve -= bondReturn +proposerAcc.Amount += bondReturn +} +// Return creator bond on successful finalization. +if treasury.CreatorBond > 0 { +creatorAcc.Amount += treasury.CreatorBond +// Strip bond from pool before snapshotting — bettor payouts exclude it. +if marketPool.Amount >= treasury.CreatorBond { +marketPool.Amount -= treasury.CreatorBond +} +treasury.CreatorBond = 0 +} + +// C-1 fix: record pool at finalization so claim_winnings uses immutable amount. +market.FinalizedPoolAmount = marketPool.Amount +market.Status = STATUS_FINALIZED + +// PRIS v1.0-r3: RRS increment and resolver fee payout on correct finalization (pathB). +if pathB && proposal != nil { +resolverRec.RrsScore += 10 +resolverRec.SuccessfulResolutions++ +weight := uint64(1) +if resolverRec.RrsScore >= RRS_GOLD_THRESHOLD { +weight = uint64(VOTE_WEIGHT_GOLD) +} else if resolverRec.RrsScore >= RRS_SILVER_THRESHOLD { +weight = uint64(VOTE_WEIGHT_SILVER) +} +globalStats.TotalWeightedResolutions += weight +// Pay resolver fee pool to resolver +if resolverFeePool.Amount > 0 { +proposerAcc.Amount += resolverFeePool.Amount +resolverFeePool.Amount = 0 +} +} + +rawM, pe := SafeMarshal(market) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawT, pe := SafeMarshal(treasury) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawCaller, pe := SafeMarshal(callerAcc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +sets := []*PluginSetOp{ +{Key: KeyForMarket(msg.MarketId), Value: rawM}, +{Key: KeyForTreasuryReserve(msg.MarketId), Value: rawT}, +{Key: KeyForAccount(msg.CallerAddr), Value: rawCaller}, +} + +if proposal != nil && proposerKey != nil { +rawProposer, pe := SafeMarshal(proposerAcc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +sets = append(sets, &PluginSetOp{Key: proposerKey, Value: rawProposer}) +} +// PRIS: write resolver record, global stats, resolver fee pool +if pathB && proposal != nil { +rawRec, pe := SafeMarshal(resolverRec) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawStats, pe := SafeMarshal(globalStats) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawResFee, pe := SafeMarshal(resolverFeePool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +sets = append(sets, &PluginSetOp{Key: KeyForResolverRecord(proposal.ResolverAddr), Value: rawRec}) +sets = append(sets, &PluginSetOp{Key: KeyForGlobalStats(), Value: rawStats}) +sets = append(sets, &PluginSetOp{Key: KeyForResolverFeePool(msg.MarketId), Value: rawResFee}) +} + if openCount.Amount > 0 { + openCount.Amount-- + } + rawOC, pe := SafeMarshal(&openCount) + if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawCreator, pe := SafeMarshal(creatorAcc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + sets = append(sets, &PluginSetOp{Key: KeyForCreatorOpenCount(market.Creator), Value: rawOC}) + sets = append(sets, &PluginSetOp{Key: KeyForAccount(market.Creator), Value: rawCreator}) + +// Write OutcomeState so claim_winnings can find the winning outcome. +if proposal != nil { +outcome := &OutcomeState{WinningOutcome: proposal.ProposedOutcome, ResolvedAt: now} +rawO, pe := SafeMarshal(outcome) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +sets = append(sets, &PluginSetOp{Key: KeyForOutcome(msg.MarketId), Value: rawO}) +} + +if pathA && dispute != nil { +slashAmount := dispute.DisputeBond +slash := &SlashRecord{ +SlashedAddress: dispute.DisputerAddress, +SlashAmount: slashAmount, +SlashedAt: now, +} +rawSlash, pe := SafeMarshal(slash) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +sets = append(sets, &PluginSetOp{Key: KeyForSlashRecord(dispute.DisputerAddress), Value: rawSlash}) +_ = disputerAcc +_ = disputerKey +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{Sets: sets}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_forfeit_position.go b/plugin/go/contract/handler_forfeit_position.go new file mode 100644 index 0000000000..4e8d0a18dd --- /dev/null +++ b/plugin/go/contract/handler_forfeit_position.go @@ -0,0 +1,107 @@ +package contract + +// handler_forfeit_position.go — Issue-2 fix +// Allows a resolver to voluntarily exit a position before proposing/resolving. +// Refunds full CostPaid, zeroes shares atomically. Satisfies COI-1 requirement +// without permanent disqualification. + +func (c *Contract) CheckMessageForfeitPosition(msg *MessageForfeitPosition) *PluginCheckResponse { + if len(msg.MarketId) != 20 { + return ErrCheckResp(ErrInvalidParam()) + } + if len(msg.ResolverAddress) != 20 { + return ErrCheckResp(ErrInvalidAddress()) + } + return &PluginCheckResponse{ + AuthorizedSigners: [][]byte{msg.ResolverAddress}, + } +} + +func (c *Contract) DeliverMessageForfeitPosition(msg *MessageForfeitPosition, fee uint64) *PluginDeliverResponse { + now := GetGlobalHeight() + if now == 0 { + return &PluginDeliverResponse{Error: ErrHeightNotSet()} + } + posQId := nextQueryId() + accQId := nextQueryId() + poolQId := nextQueryId() + resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ + Keys: []*PluginKeyRead{ + {QueryId: posQId, Key: KeyForPosition(msg.MarketId, msg.ResolverAddress)}, + {QueryId: accQId, Key: KeyForAccount(msg.ResolverAddress)}, + {QueryId: poolQId, Key: KeyForMarketPool(msg.MarketId)}, + }, + }) + if err != nil { + return &PluginDeliverResponse{Error: ErrStateReadFailed()} + } + if resp.Error != nil { + return &PluginDeliverResponse{Error: resp.Error} + } + var position *PositionState + var account *Account + var pool *Pool + for _, r := range resp.Results { + if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { + continue + } + switch r.QueryId { + case posQId: + position = &PositionState{} + if pe := Unmarshal(r.Entries[0].Value, position); pe != nil { + return &PluginDeliverResponse{Error: ErrUnmarshalFailed()} + } + case accQId: + account = &Account{} + if pe := Unmarshal(r.Entries[0].Value, account); pe != nil { + return &PluginDeliverResponse{Error: ErrUnmarshalFailed()} + } + case poolQId: + pool = &Pool{} + if pe := Unmarshal(r.Entries[0].Value, pool); pe != nil { + return &PluginDeliverResponse{Error: ErrUnmarshalFailed()} + } + } + } + if position == nil || (position.SharesYes == 0 && position.SharesNo == 0) { + return &PluginDeliverResponse{Error: ErrNoPosition()} + } + if account == nil { + return &PluginDeliverResponse{Error: ErrInsufficientFunds()} + } + if pool == nil { + return &PluginDeliverResponse{Error: ErrMarketNotFound()} + } + // Overflow guard + if account.Amount > ^uint64(0)-position.CostPaid { + return &PluginDeliverResponse{Error: ErrInvalidAmount()} + } + // Pool must cover the refund + if pool.Amount < position.CostPaid { + return &PluginDeliverResponse{Error: ErrInsufficientFunds()} + } + refund := position.CostPaid + account.Amount += refund + pool.Amount -= refund + // Zero the position + position.SharesYes = 0 + position.SharesNo = 0 + position.CostPaid = 0 + rawPos, pe := SafeMarshal(position) + if pe != nil { return &PluginDeliverResponse{Error: pe} } + rawAcc, pe := SafeMarshal(account) + if pe != nil { return &PluginDeliverResponse{Error: pe} } + rawPool, pe := SafeMarshal(pool) + if pe != nil { return &PluginDeliverResponse{Error: pe} } + wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ + Sets: []*PluginSetOp{ + {Key: KeyForPosition(msg.MarketId, msg.ResolverAddress), Value: rawPos}, + {Key: KeyForAccount(msg.ResolverAddress), Value: rawAcc}, + {Key: KeyForMarketPool(msg.MarketId), Value: rawPool}, + }, + }) + if pe := errCheckWrite(wr, werr); pe != nil { + return &PluginDeliverResponse{Error: pe} + } + return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_propose_outcome.go b/plugin/go/contract/handler_propose_outcome.go new file mode 100644 index 0000000000..5183ea9047 --- /dev/null +++ b/plugin/go/contract/handler_propose_outcome.go @@ -0,0 +1,210 @@ +package contract + +import "bytes" + +// handler_propose_outcome.go — MessageProposeOutcome +// Spec: PORS v1.0-r2-CORRECTED +// +// NF-5 FIX: writes 0x13 ResolverState as the 4th atomic key. +// 0x13 was never written before — auth check always failed. +// NF-6 FIX: accepts STATUS_OPEN markets where now > ExpiryTime and transitions +// directly to STATUS_PROPOSED. STATUS_EXPIRED is never persisted. +// +// CheckTx: market_id 20 bytes, resolver_address 20 bytes, proposal_bond non-zero. +// No status check (stateful — DeliverTx enforces). Zero StateRead (AUDIT-8). +// DeliverTx: +// Batch read: global ResolverRecord (0x16) + MarketState (0x10) + ProposalRecord (0x17) +// Auth: RRS score check +// NF-6: accept STATUS_OPEN if now > ExpiryTime — inline transition to PROPOSED +// Idempotency: ProposalRecord nil = first call +// Bond sufficiency check +// 4-key atomic write: MarketState + ResolverRecord + ProposalRecord + ResolverState (NF-5) + +func (c *Contract) CheckMessageProposeOutcome(msg *MessageProposeOutcome) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.ResolverAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +if msg.ProposalBond == 0 { +return ErrCheckResp(ErrInvalidAmount()) +} +// NF-6: no status check here — status is stateful, DeliverTx enforces. +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.ResolverAddress}, +} +} + +func (c *Contract) DeliverMessageProposeOutcome(msg *MessageProposeOutcome, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +// ── Batch read: resolver record + market state + proposal (idempotency) ─── +// CRIT-3: QueryId per PluginKeyRead. +resolRecQId := nextQueryId() +marketQId := nextQueryId() +proposalQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: resolRecQId, Key: KeyForResolverRecord(msg.ResolverAddress)}, +{QueryId: marketQId, Key: KeyForMarket(msg.MarketId)}, +{QueryId: proposalQId, Key: KeyForProposal(msg.MarketId)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var resolverRec *ResolverRecord +var market *MarketState +var proposalRaw []byte + +for _, r := range resp.Results { +switch r.QueryId { +case resolRecQId: +if len(r.Entries) == 0 { +return &PluginDeliverResponse{Error: ErrResolverNotRegistered()} +} +resolverRec = &ResolverRecord{} +if pe := Unmarshal(r.Entries[0].Value, resolverRec); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case marketQId: +if len(r.Entries) == 0 { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case proposalQId: +if len(r.Entries) > 0 { +proposalRaw = r.Entries[0].Value +} +} +} + +if resolverRec == nil { +return &PluginDeliverResponse{Error: ErrResolverNotRegistered()} +} +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} + +// ── Global registry check (0x16) ───────────────────────────────────────── +if resolverRec.RrsScore < MIN_RRS_TO_PROPOSE { +return &PluginDeliverResponse{Error: ErrResolverSuspended()} +} + + // ── COI-1: resolver must not hold a position in this market ───────────────── + resolPosQId := nextQueryId() + posResp, posErr := c.plugin.StateRead(c, &PluginStateReadRequest{ + Keys: []*PluginKeyRead{ + {QueryId: resolPosQId, Key: KeyForPosition(msg.MarketId, msg.ResolverAddress)}, + }, + }) + if posErr != nil { + return &PluginDeliverResponse{Error: posErr} + } + for _, r := range posResp.Results { + if r.QueryId == resolPosQId && len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { + resolPos := &PositionState{} + if pe := Unmarshal(r.Entries[0].Value, resolPos); pe != nil { + return &PluginDeliverResponse{Error: pe} + } + if resolPos.SharesYes > 0 || resolPos.SharesNo > 0 { + return &PluginDeliverResponse{Error: ErrResolverHasPosition()} + } + } + } + +// ── COI-2: market creator cannot be the resolver ──────────────────────────── + if bytes.Equal(market.Creator, msg.ResolverAddress) { + return &PluginDeliverResponse{Error: ErrCreatorCannotResolve()} + } + + // ── Market status check ─────────────────────────────────────────────────── +// NF-6 FIX: accept STATUS_OPEN if now > ExpiryTime — inline transition. +// STATUS_OPEN -> STATUS_PROPOSED is atomic; STATUS_EXPIRED never persisted. +if market.Status == STATUS_OPEN { +if now <= market.ExpiryTime { +return &PluginDeliverResponse{Error: ErrMarketNotExpired()} +} +// now > ExpiryTime: we are the expiry trigger. +// market.Status will be set to STATUS_PROPOSED in the atomic write below. +} else if market.Status != STATUS_EXPIRED { +// Any other status (PROPOSED, DISPUTED, FINALIZED, CANCELLED, VOIDED) is invalid. +return &PluginDeliverResponse{Error: ErrMarketNotExpired()} +} + +// ── Idempotency guard: ProposalRecord nil = first call ──────────────────── +if proposalRaw != nil { +return &PluginDeliverResponse{Error: ErrAlreadyProposed()} +} + +// ── Bond sufficiency check ──────────────────────────────────────────────── +minBond := ComputeMinBond(market) +if msg.ProposalBond < minBond { +return &PluginDeliverResponse{Error: ErrInsufficientBond()} +} +if resolverRec.StakeAmount < msg.ProposalBond { +return &PluginDeliverResponse{Error: ErrInsufficientFunds()} +} + +// ── Mutate in memory ────────────────────────────────────────────────────── +market.Status = STATUS_PROPOSED +resolverRec.StakeAmount -= msg.ProposalBond + +proposal := &ProposalRecord{ +ResolverAddr: msg.ResolverAddress, +ProposedOutcome: msg.ProposedOutcome, +ProposalBond: msg.ProposalBond, +ProposalBlock: now, +Status: PROPOSAL_OPEN, +} + +// NF-5 FIX: ResolverState for this market — written here for the first time. +// This is what makes the auth check in ResolveMarket work. +resolverState := &ResolverState{ResolverAddress: msg.ResolverAddress} + +// ── Marshal all ─────────────────────────────────────────────────────────── +rawM, pe := SafeMarshal(market) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawRR, pe := SafeMarshal(resolverRec) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawPR, pe := SafeMarshal(proposal) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawRS, pe := SafeMarshal(resolverState) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} + +// ── NF-5 FIX: 4-key atomic write ───────────────────────────────────────── +// All four commit together or none do. +// On failure: market.Status stays OPEN/EXPIRED, 0x13 stays nil, retry is safe. +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForMarket(msg.MarketId), Value: rawM}, +{Key: KeyForResolverRecord(msg.ResolverAddress), Value: rawRR}, +{Key: KeyForProposal(msg.MarketId), Value: rawPR}, +{Key: KeyForResolverState(msg.MarketId), Value: rawRS}, // NF-5 +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_reclaim_stake.go b/plugin/go/contract/handler_reclaim_stake.go new file mode 100644 index 0000000000..ffd0ce0651 --- /dev/null +++ b/plugin/go/contract/handler_reclaim_stake.go @@ -0,0 +1,177 @@ +package contract + +func (c *Contract) CheckMessageReclaimStake(msg *MessageReclaimStake) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.ClaimantAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.ClaimantAddress}, +} +} + +func (c *Contract) DeliverMessageReclaimStake(msg *MessageReclaimStake, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +marketQId := nextQueryId() +posQId := nextQueryId() +poolQId := nextQueryId() +treasQId := nextQueryId() +proposalQId := nextQueryId() +claimAccQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: KeyForMarket(msg.MarketId)}, +{QueryId: posQId, Key: KeyForPosition(msg.MarketId, msg.ClaimantAddress)}, +{QueryId: poolQId, Key: KeyForMarketPool(msg.MarketId)}, +{QueryId: treasQId, Key: KeyForTreasuryReserve(msg.MarketId)}, +{QueryId: proposalQId, Key: KeyForProposal(msg.MarketId)}, +{QueryId: claimAccQId, Key: KeyForAccount(msg.ClaimantAddress)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +var position *PositionState +marketPool := &Pool{} +treasury := &TreasuryReserve{} +claimantAcc := &Account{} +var proposalRaw []byte + +for _, r := range resp.Results { +switch r.QueryId { +case marketQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case posQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +position = &PositionState{} +if pe := Unmarshal(r.Entries[0].Value, position); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +case poolQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, marketPool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +case treasQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, treasury); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +case proposalQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +proposalRaw = r.Entries[0].Value +} +case claimAccQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, claimantAcc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} + +// Only reclaimable if STATUS_OPEN, expiry passed, and no proposal ever filed +if market.Status != STATUS_OPEN { +return &PluginDeliverResponse{Error: ErrMarketNotReclaimable()} +} +if proposalRaw != nil { +return &PluginDeliverResponse{Error: ErrMarketNotReclaimable()} +} +reclaimOpen := market.ExpiryTime + RESOLUTION_DELAY_BLOCKS + GRACE_PERIOD_BLOCKS +if now <= reclaimOpen { +return &PluginDeliverResponse{Error: ErrReclaimWindowClosed()} +} + +// Compute refund amount +var refund uint64 + +// Position refund (any bettor including creator) +if position != nil { +if position.Claimed { +return &PluginDeliverResponse{Error: ErrAlreadyClaimed()} +} +if position.SharesYes > 0 || position.SharesNo > 0 { +refund += position.CostPaid +} +} + +// Creator gets TreasuryReserve (FINALIZATION_BOUNTY) back — no resolver showed up +if bytesEqual(msg.ClaimantAddress, market.Creator) && treasury.LockedReserve > 0 { +refund += treasury.LockedReserve +} + +if refund == 0 { +return &PluginDeliverResponse{Error: ErrNoStakeToReclaim()} +} +if refund > marketPool.Amount { +return &PluginDeliverResponse{Error: ErrInsufficientPoolFunds()} +} + +// Mutate in memory +claimantAcc.Amount += refund +marketPool.Amount -= refund +isCreator := bytesEqual(msg.ClaimantAddress, market.Creator) +if isCreator { +treasury.LockedReserve = 0 +} + +// Marshal all +rawAcc, pe := SafeMarshal(claimantAcc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawPool, pe := SafeMarshal(marketPool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +sets := []*PluginSetOp{ +{Key: KeyForAccount(msg.ClaimantAddress), Value: rawAcc}, +{Key: KeyForMarketPool(msg.MarketId), Value: rawPool}, +} +var deletes []*PluginDeleteOp + +// Mark position as claimed to prevent double reclaim +if position != nil && (position.SharesYes > 0 || position.SharesNo > 0) { +position.Claimed = true +rawPos, pe := SafeMarshal(position) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +sets = append(sets, &PluginSetOp{Key: KeyForPosition(msg.MarketId, msg.ClaimantAddress), Value: rawPos}) +} + +if isCreator && treasury.LockedReserve == 0 { +rawTreas, pe := SafeMarshal(treasury) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +sets = append(sets, &PluginSetOp{Key: KeyForTreasuryReserve(msg.MarketId), Value: rawTreas}) +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: sets, +Deletes: deletes, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_register_resolver.go b/plugin/go/contract/handler_register_resolver.go new file mode 100644 index 0000000000..46c24e0088 --- /dev/null +++ b/plugin/go/contract/handler_register_resolver.go @@ -0,0 +1,148 @@ +package contract + +func (c *Contract) CheckMessageRegisterResolver(msg *MessageRegisterResolver) *PluginCheckResponse { +if len(msg.ResolverAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.ResolverAddress}, +} +} + +func (c *Contract) DeliverMessageRegisterResolver(msg *MessageRegisterResolver, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +recQId := nextQueryId() +accQId := nextQueryId() +feeQId := nextQueryId() + gTreasuryQId := nextQueryId() + ridxQId := nextQueryId() + +recKey := KeyForResolverRecord(msg.ResolverAddress) +accKey := KeyForAccount(msg.ResolverAddress) +feePoolKey := KeyForFeePool(c.Config.ChainId) + gTreasuryKey := KeyForTreasuryPool() + ridxKey := KeyForResolverIndex() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: recQId, Key: recKey}, +{QueryId: accQId, Key: accKey}, +{QueryId: feeQId, Key: feePoolKey}, + {QueryId: gTreasuryQId, Key: gTreasuryKey}, + {QueryId: ridxQId, Key: ridxKey}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var existing *ResolverRecord +account := &Account{} +feePool := &Pool{} + gTreasury := &Pool{} + ridx := &ResolverIndex{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +switch r.QueryId { +case recQId: +existing = &ResolverRecord{} +if pe := Unmarshal(r.Entries[0].Value, existing); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case accQId: +if pe := Unmarshal(r.Entries[0].Value, account); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case feeQId: +if pe := Unmarshal(r.Entries[0].Value, feePool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case gTreasuryQId: + if pe := Unmarshal(r.Entries[0].Value, gTreasury); pe != nil { + return &PluginDeliverResponse{Error: pe} + } + case ridxQId: + if pe := Unmarshal(r.Entries[0].Value, ridx); pe != nil { + return &PluginDeliverResponse{Error: pe} + } +} +} +if existing == nil && msg.StakeAmount < MIN_RESOLVER_STAKE { + return &PluginDeliverResponse{Error: ErrInsufficientResolverStake()} +} +if fee > 0 && msg.StakeAmount > ^uint64(0)-fee { +return &PluginDeliverResponse{Error: ErrInvalidAmount()} +} +totalCost := msg.StakeAmount + fee +if account.Amount < totalCost { +return &PluginDeliverResponse{Error: ErrInsufficientFunds()} +} + +account.Amount -= totalCost +feePool.Amount += fee / 2 + gTreasury.Amount += fee - fee/2 + +var record *ResolverRecord +if existing != nil { +existing.StakeAmount += msg.StakeAmount + if existing.StakeAmount < MIN_RESOLVER_STAKE { + return &PluginDeliverResponse{Error: ErrInsufficientResolverStake()} + } +existing.IsActive = true // re-activate on re-registration after full exit + if existing.RrsScore < PRIS_RRS_INITIAL { + existing.RrsScore = PRIS_RRS_INITIAL + } +record = existing +} else { +record = &ResolverRecord{ +ResolverAddress: msg.ResolverAddress, +StakeAmount: msg.StakeAmount, +RrsScore: PRIS_RRS_INITIAL, +RegisteredAt: now, +IsActive: true, +} +} + +rawRec, pe := SafeMarshal(record) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawFee, pe := SafeMarshal(feePool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawGTreasury, pe := SafeMarshal(gTreasury) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawRidx, pe := SafeMarshal(ridx) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +sets := []*PluginSetOp{ +{Key: recKey, Value: rawRec}, +{Key: feePoolKey, Value: rawFee}, + {Key: gTreasuryKey, Value: rawGTreasury}, + {Key: ridxKey, Value: rawRidx}, +} +var deletes []*PluginDeleteOp +if account.Amount == 0 { +deletes = []*PluginDeleteOp{{Key: accKey}} +} else { +rawAcc, pe := SafeMarshal(account) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +sets = append(sets, &PluginSetOp{Key: accKey, Value: rawAcc}) +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: sets, +Deletes: deletes, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_resolve_market.go b/plugin/go/contract/handler_resolve_market.go new file mode 100644 index 0000000000..368c1d80cb --- /dev/null +++ b/plugin/go/contract/handler_resolve_market.go @@ -0,0 +1,256 @@ +package contract + +// ═══════════════════════════════════════════════════════════════════════════════ +// DEPRECATED — resolve_market handler +// Issue-15: This handler is NOT registered in ContractConfig.SupportedTransactions +// or TransactionTypeUrls. Any client sending MessageResolveMarket will receive +// a routing error (unknown tx type), NOT a deprecation notice. +// SDK/frontend clients must migrate to propose_outcome (PORS flow). +// +// This handler is intentionally NOT registered in ContractConfig.SupportedTransactions +// or TransactionTypeUrls (see contract.go). It cannot be invoked via a standard TX. +// +// The PORS flow (propose_outcome → file_dispute → commit_vote → reveal_vote → +// tally_votes → finalize_market) is the sole resolution path for Praxis markets. +// +// DO NOT re-register this handler. Doing so would allow a single resolver to +// unilaterally finalize any market, bypassing the dispute/panel mechanism entirely. +// +// This file is retained only to preserve the proto message handler interface. +// It will be removed in a future cleanup pass once the proto type is deprecated. +// ═══════════════════════════════════════════════════════════════════════════════ + +const ( +bountyAmount uint64 = 50_000_000 +bondAmount uint64 = 100_000_000 +) + +func (c *Contract) CheckMessageResolveMarket(msg *MessageResolveMarket) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.ResolverAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.ResolverAddress}, +} +} + +func (c *Contract) DeliverMessageResolveMarket(msg *MessageResolveMarket, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +marketQId := nextQueryId() +resolverQId := nextQueryId() +outcomeQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: KeyForMarket(msg.MarketId)}, +{QueryId: resolverQId, Key: KeyForResolverState(msg.MarketId)}, +{QueryId: outcomeQId, Key: KeyForOutcome(msg.MarketId)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +var resolver *ResolverState +var outcomeRaw []byte + +for _, r := range resp.Results { +switch r.QueryId { +case marketQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case resolverQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrNoResolverRegistered()} +} +resolver = &ResolverState{} +if pe := Unmarshal(r.Entries[0].Value, resolver); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case outcomeQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +outcomeRaw = r.Entries[0].Value +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +if resolver == nil { +return &PluginDeliverResponse{Error: ErrNoResolverRegistered()} +} + +// R1 FIX: AUTH BEFORE IDEMPOTENCY +if !bytesEqual(resolver.ResolverAddress, msg.ResolverAddress) { +return &PluginDeliverResponse{Error: ErrUnauthorized()} +} + +// COI-1: resolver must not hold a position in this market +coiPosQId := nextQueryId() +coiPosResp, coiPosErr := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: coiPosQId, Key: KeyForPosition(msg.MarketId, msg.ResolverAddress)}, +}, +}) +if coiPosErr != nil { +return &PluginDeliverResponse{Error: ErrStateReadFailed()} +} +for _, r := range coiPosResp.Results { +if r.QueryId == coiPosQId && len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +resolPos := &PositionState{} +if pe := Unmarshal(r.Entries[0].Value, resolPos); pe != nil { +return &PluginDeliverResponse{Error: ErrUnmarshalFailed()} +} +if resolPos.SharesYes > 0 || resolPos.SharesNo > 0 { +return &PluginDeliverResponse{Error: ErrResolverHasPosition()} +} +} +} + +withinWindow := now >= market.ExpiryTime+RESOLUTION_DELAY_BLOCKS && +now <= market.ExpiryTime+RESOLUTION_DELAY_BLOCKS+GRACE_PERIOD_BLOCKS +if !withinWindow { +if _, cancelErr := c.CheckAutoCancel(msg.MarketId); cancelErr != nil { +return &PluginDeliverResponse{Error: cancelErr} +} +refreshQId := nextQueryId() +resp2, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: refreshQId, Key: KeyForMarket(msg.MarketId)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp2.Error != nil { +return &PluginDeliverResponse{Error: resp2.Error} +} +for _, r := range resp2.Results { +if r.QueryId == refreshQId && len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} +} + +if market.Status == STATUS_CANCELLED { +return &PluginDeliverResponse{Error: ErrMarketCancelled()} +} + +if outcomeRaw != nil { +return &PluginDeliverResponse{} +} + +if market.Status != STATUS_OPEN { +return &PluginDeliverResponse{Error: ErrMarketNotOpen()} +} +if now < market.ExpiryTime+RESOLUTION_DELAY_BLOCKS { +return &PluginDeliverResponse{Error: ErrResolutionTooEarly()} +} + +poolQId := nextQueryId() +resolAccQId := nextQueryId() +creatAccQId := nextQueryId() +treasQId := nextQueryId() + +payResp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: poolQId, Key: KeyForMarketPool(msg.MarketId)}, +{QueryId: resolAccQId, Key: KeyForAccount(msg.ResolverAddress)}, +{QueryId: creatAccQId, Key: KeyForAccount(market.Creator)}, +{QueryId: treasQId, Key: KeyForTreasuryReserve(msg.MarketId)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if payResp.Error != nil { +return &PluginDeliverResponse{Error: payResp.Error} +} + +mPool := &Pool{} +resolverAcc := &Account{} +creatorAcc := &Account{} +tres := &TreasuryReserve{} + +for _, r := range payResp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +switch r.QueryId { +case poolQId: +if pe := Unmarshal(r.Entries[0].Value, mPool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case resolAccQId: +if pe := Unmarshal(r.Entries[0].Value, resolverAcc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case creatAccQId: +if pe := Unmarshal(r.Entries[0].Value, creatorAcc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case treasQId: +if pe := Unmarshal(r.Entries[0].Value, tres); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if mPool.Amount < bountyAmount+bondAmount { +return &PluginDeliverResponse{Error: ErrInsufficientPoolFunds()} +} + +market.Status = STATUS_RESOLVED +mPool.Amount -= bountyAmount + bondAmount +resolverAcc.Amount += bountyAmount +creatorAcc.Amount += bondAmount +tres.LockedReserve = 0 +outcome := &OutcomeState{WinningOutcome: msg.WinningOutcome, ResolvedAt: now} + +rawM, pe := SafeMarshal(market) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawO, pe := SafeMarshal(outcome) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawT, pe := SafeMarshal(tres) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawMP, pe := SafeMarshal(mPool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawRA, pe := SafeMarshal(resolverAcc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawCA, pe := SafeMarshal(creatorAcc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForMarket(msg.MarketId), Value: rawM}, +{Key: KeyForOutcome(msg.MarketId), Value: rawO}, +{Key: KeyForTreasuryReserve(msg.MarketId), Value: rawT}, +{Key: KeyForMarketPool(msg.MarketId), Value: rawMP}, +{Key: KeyForAccount(msg.ResolverAddress), Value: rawRA}, +{Key: KeyForAccount(market.Creator), Value: rawCA}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_reveal_vote.go b/plugin/go/contract/handler_reveal_vote.go new file mode 100644 index 0000000000..130eaba2fa --- /dev/null +++ b/plugin/go/contract/handler_reveal_vote.go @@ -0,0 +1,131 @@ +package contract + +func (c *Contract) CheckMessageRevealVote(msg *MessageRevealVote) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.VoterAddr) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +if len(msg.Nonce) == 0 { +return ErrCheckResp(ErrInvalidParam()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.VoterAddr}, +} +} + +func (c *Contract) DeliverMessageRevealVote(msg *MessageRevealVote, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +marketQId := nextQueryId() +disputeQId := nextQueryId() +commitQId := nextQueryId() +revealQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: KeyForMarket(msg.MarketId)}, +{QueryId: disputeQId, Key: KeyForDispute(msg.MarketId)}, +{QueryId: commitQId, Key: KeyForVoteCommit(msg.MarketId, msg.VoterAddr)}, +{QueryId: revealQId, Key: KeyForVoteReveal(msg.MarketId, msg.VoterAddr)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +var dispute *DisputeRecord +var vc *VoteCommit + +for _, r := range resp.Results { +switch r.QueryId { +case marketQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case disputeQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrNotDisputed()} +} +dispute = &DisputeRecord{} +if pe := Unmarshal(r.Entries[0].Value, dispute); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case commitQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrNotAPanelMember()} +} +vc = &VoteCommit{} +if pe := Unmarshal(r.Entries[0].Value, vc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case revealQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +return &PluginDeliverResponse{Error: ErrAlreadyRevealed()} +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +if market.Status != STATUS_DISPUTED { +return &PluginDeliverResponse{Error: ErrNotDisputed()} +} +if dispute == nil { +return &PluginDeliverResponse{Error: ErrNotDisputed()} +} +if vc == nil { +return &PluginDeliverResponse{Error: ErrNotAPanelMember()} +} + +revealStart := dispute.DisputeBlock + COMMIT_PHASE_BLOCKS +revealEnd := revealStart + REVEAL_PHASE_BLOCKS +if now <= revealStart { +return &PluginDeliverResponse{Error: ErrRevealPhaseNotOpen()} +} +if now > revealEnd { +return &PluginDeliverResponse{Error: ErrRevealPhaseOver()} +} + +if !isPanelMember(msg.VoterAddr, dispute.PanelMembers) { +return &PluginDeliverResponse{Error: ErrNotAPanelMember()} +} + +expectedHash := ComputeCommitHash(msg.Vote, msg.Nonce, msg.VoterAddr) +if !bytesEqual(expectedHash, vc.CommitHash) { +return &PluginDeliverResponse{Error: ErrCommitHashMismatch()} +} + +vr := &VoteReveal{ +VoterAddr: msg.VoterAddr, +Vote: msg.Vote, +RevealedAt: now, +} +rawVR, pe := SafeMarshal(vr) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForVoteReveal(msg.MarketId, msg.VoterAddr), Value: rawVR}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_send.go b/plugin/go/contract/handler_send.go new file mode 100644 index 0000000000..7f69199e59 --- /dev/null +++ b/plugin/go/contract/handler_send.go @@ -0,0 +1,181 @@ +package contract + +import "bytes" + +func (c *Contract) CheckMessageSend(msg *MessageSend) *PluginCheckResponse { +if len(msg.FromAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +if len(msg.ToAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +if msg.Amount == 0 { +return ErrCheckResp(ErrInvalidAmount()) +} +return &PluginCheckResponse{ +Recipient: msg.ToAddress, +AuthorizedSigners: [][]byte{msg.FromAddress}, +} +} + +func (c *Contract) DeliverMessageSend(msg *MessageSend, fee uint64) *PluginDeliverResponse { +isSelfTransfer := bytes.Equal(msg.FromAddress, msg.ToAddress) + +fromQId := nextQueryId() +toQId := nextQueryId() +feeQId := nextQueryId() + gTreasuryQId := nextQueryId() + +fromKey := KeyForAccount(msg.FromAddress) +toKey := KeyForAccount(msg.ToAddress) +feePoolKey := KeyForFeePool(c.Config.ChainId) + gTreasuryKey := KeyForTreasuryPool() + +var readKeys []*PluginKeyRead +if isSelfTransfer { +readKeys = []*PluginKeyRead{ +{QueryId: fromQId, Key: fromKey}, +{QueryId: feeQId, Key: feePoolKey}, + {QueryId: gTreasuryQId, Key: gTreasuryKey}, +} +} else { +readKeys = []*PluginKeyRead{ +{QueryId: fromQId, Key: fromKey}, +{QueryId: toQId, Key: toKey}, +{QueryId: feeQId, Key: feePoolKey}, + {QueryId: gTreasuryQId, Key: gTreasuryKey}, +} +} + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{Keys: readKeys}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +from := &Account{} +to := &Account{} +feePool := &Pool{} + gTreasury := &Pool{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +switch r.QueryId { +case fromQId: +if pe := Unmarshal(r.Entries[0].Value, from); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case toQId: +if pe := Unmarshal(r.Entries[0].Value, to); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case feeQId: +if pe := Unmarshal(r.Entries[0].Value, feePool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case gTreasuryQId: +if pe := Unmarshal(r.Entries[0].Value, gTreasury); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +// Self-transfer: pay fee only, amount stays +if isSelfTransfer { +if from.Amount < fee { +return &PluginDeliverResponse{Error: ErrInsufficientFunds()} +} +from.Amount -= fee +feePool.Amount += fee / 2 + gTreasury.Amount += fee - fee/2 + +rawFrom, pe := SafeMarshal(from) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawFee, pe := SafeMarshal(feePool) + if pe != nil { + return &PluginDeliverResponse{Error: pe} + } + rawGTreasury, pe := SafeMarshal(gTreasury) + if pe != nil { + return &PluginDeliverResponse{Error: pe} + } + +var wr *PluginStateWriteResponse +if from.Amount == 0 { +wr, err = c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{{Key: feePoolKey, Value: rawFee}, {Key: gTreasuryKey, Value: rawGTreasury}}, +Deletes: []*PluginDeleteOp{{Key: fromKey}}, +}) +} else { +wr, err = c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: fromKey, Value: rawFrom}, +{Key: feePoolKey, Value: rawFee}, + {Key: gTreasuryKey, Value: rawGTreasury}, +}, +}) +} +if pe := errCheckWrite(wr, err); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} + +// Normal transfer +if fee > 0 && msg.Amount > ^uint64(0)-fee { +return &PluginDeliverResponse{Error: ErrInvalidAmount()} +} +totalDeduct := msg.Amount + fee +if from.Amount < totalDeduct { +return &PluginDeliverResponse{Error: ErrInsufficientFunds()} +} + +from.Amount -= totalDeduct +to.Amount += msg.Amount +feePool.Amount += fee / 2 + gTreasury.Amount += fee - fee/2 + +rawFrom, pe := SafeMarshal(from) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawTo, pe := SafeMarshal(to) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawFee, pe := SafeMarshal(feePool) + if pe != nil { + return &PluginDeliverResponse{Error: pe} + } + rawGTreasury, pe := SafeMarshal(gTreasury) + if pe != nil { + return &PluginDeliverResponse{Error: pe} + } + +var wr *PluginStateWriteResponse +if from.Amount == 0 { +wr, err = c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{{Key: toKey, Value: rawTo}, {Key: feePoolKey, Value: rawFee}, {Key: gTreasuryKey, Value: rawGTreasury}}, +Deletes: []*PluginDeleteOp{{Key: fromKey}}, +}) +} else { +wr, err = c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: fromKey, Value: rawFrom}, +{Key: toKey, Value: rawTo}, +{Key: feePoolKey, Value: rawFee}, + {Key: gTreasuryKey, Value: rawGTreasury}, +}, +}) +} +if pe := errCheckWrite(wr, err); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_submit_prediction.go b/plugin/go/contract/handler_submit_prediction.go new file mode 100644 index 0000000000..cf3080db69 --- /dev/null +++ b/plugin/go/contract/handler_submit_prediction.go @@ -0,0 +1,230 @@ +package contract + +func (c *Contract) CheckMessageSubmitPrediction(msg *MessageSubmitPrediction) *PluginCheckResponse { +if len(msg.BettorAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if msg.Shares < PRECISION_SCALE { +return ErrCheckResp(ErrSharesBelowMinimum()) +} +if msg.MaxCost == 0 { +return ErrCheckResp(ErrInvalidAmount()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.BettorAddress}, +} +} + +func (c *Contract) DeliverMessageSubmitPrediction(msg *MessageSubmitPrediction, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} +if msg.Shares < PRECISION_SCALE { +return &PluginDeliverResponse{Error: ErrSharesBelowMinimum()} +} +if _, cancelErr := c.CheckAutoCancel(msg.MarketId); cancelErr != nil { +return &PluginDeliverResponse{Error: cancelErr} +} + +marketQId := nextQueryId() +posQId := nextQueryId() +poolQId := nextQueryId() +bettorQId := nextQueryId() +feeQId := nextQueryId() + +marketKey := KeyForMarket(msg.MarketId) +posKey := KeyForPosition(msg.MarketId, msg.BettorAddress) +poolKey := KeyForMarketPool(msg.MarketId) +bettorKey := KeyForAccount(msg.BettorAddress) +feePoolKey := KeyForFeePool(c.Config.ChainId) +creatorFeeKey := KeyForCreatorFeePool(msg.MarketId) +resolverFeeKey := KeyForResolverFeePool(msg.MarketId) + gTreasuryKey := KeyForTreasuryPool() +creatorFeeQId := nextQueryId() + resolverFeeQId := nextQueryId() + gTreasuryQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: marketKey}, +{QueryId: posQId, Key: posKey}, +{QueryId: poolQId, Key: poolKey}, +{QueryId: bettorQId, Key: bettorKey}, +{QueryId: feeQId, Key: feePoolKey}, +{QueryId: creatorFeeQId, Key: creatorFeeKey}, +{QueryId: resolverFeeQId, Key: resolverFeeKey}, + {QueryId: gTreasuryQId, Key: gTreasuryKey}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +position := &PositionState{} +mPool := &Pool{} +bettor := &Account{} +feePool := &Pool{} +creatorFee := &Pool{} +resolverFee := &Pool{} + gTreasury := &Pool{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +switch r.QueryId { +case marketQId: +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case posQId: +if pe := Unmarshal(r.Entries[0].Value, position); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case poolQId: +if pe := Unmarshal(r.Entries[0].Value, mPool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case bettorQId: +if pe := Unmarshal(r.Entries[0].Value, bettor); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case feeQId: +if pe := Unmarshal(r.Entries[0].Value, feePool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case creatorFeeQId: +if pe := Unmarshal(r.Entries[0].Value, creatorFee); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case resolverFeeQId: +if pe := Unmarshal(r.Entries[0].Value, resolverFee); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case gTreasuryQId: +if pe := Unmarshal(r.Entries[0].Value, gTreasury); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +if market.Status != STATUS_OPEN { +return &PluginDeliverResponse{Error: ErrMarketNotOpen()} +} +if now > market.ExpiryTime { +return &PluginDeliverResponse{Error: ErrMarketNotOpen()} +} +if now < market.OpenTime { +return &PluginDeliverResponse{Error: ErrMarketNotOpen()} +} + +tradeCost, pe := ComputeTradeCost(market.QYes, market.QNo, market.BEff, msg.Shares, msg.Outcome) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +creatorFeeAmt := ComputeBps(tradeCost, CREATOR_FEE_BPS) +resolverFeeAmt := ComputeBps(tradeCost, RESOLVER_FEE_BPS) +if fee > 0 && tradeCost > ^uint64(0)-fee { +return &PluginDeliverResponse{Error: ErrInvalidAmount()} +} +finalCost := tradeCost + fee + creatorFeeAmt + resolverFeeAmt +if finalCost > msg.MaxCost { +return &PluginDeliverResponse{Error: ErrCostExceedsMaxCost()} +} +// COI-3: per-address position cap — capped on shares, not CostPaid. +// totalSideShares is post-trade so the cap scales with actual exposure. +var totalSideShares uint64 +if msg.Outcome { +totalSideShares = market.QYes + msg.Shares +if exceedsPositionCap(position.SharesYes, msg.Shares, totalSideShares) { +return &PluginDeliverResponse{Error: ErrPositionCapExceeded()} +} +} else { +totalSideShares = market.QNo + msg.Shares +if exceedsPositionCap(position.SharesNo, msg.Shares, totalSideShares) { +return &PluginDeliverResponse{Error: ErrPositionCapExceeded()} +} +} + +if bettor.Amount < finalCost { +return &PluginDeliverResponse{Error: ErrInsufficientFunds()} +} + +isNewPosition := position.SharesYes == 0 && position.SharesNo == 0 && position.CostPaid == 0 + +bettor.Amount -= finalCost +mPool.Amount += tradeCost +feePool.Amount += fee / 2 + gTreasury.Amount += fee - fee/2 +creatorFee.Amount += creatorFeeAmt +resolverFee.Amount += resolverFeeAmt + +if msg.Outcome { +market.QYes += msg.Shares +position.SharesYes += msg.Shares +} else { +market.QNo += msg.Shares +position.SharesNo += msg.Shares +} +position.CostPaid += tradeCost + +if isNewPosition { +market.TotalPositions++ +} + +market.ElevatedRisk = IsElevatedRisk(mPool.Amount) + +rawMarket, pe := SafeMarshal(market) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawPos, pe := SafeMarshal(position) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawPool, pe := SafeMarshal(mPool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawFee, pe := SafeMarshal(feePool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawGTreasury, pe := SafeMarshal(gTreasury) + if pe != nil { return &PluginDeliverResponse{Error: pe} } + rawCreatorFee, pe := SafeMarshal(creatorFee) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawResolverFee, pe := SafeMarshal(resolverFee) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +sets := []*PluginSetOp{ +{Key: marketKey, Value: rawMarket}, +{Key: posKey, Value: rawPos}, +{Key: poolKey, Value: rawPool}, +{Key: feePoolKey, Value: rawFee}, +{Key: creatorFeeKey, Value: rawCreatorFee}, +{Key: resolverFeeKey, Value: rawResolverFee}, + {Key: gTreasuryKey, Value: rawGTreasury}, +} +var deletes []*PluginDeleteOp +if bettor.Amount == 0 { +deletes = []*PluginDeleteOp{{Key: bettorKey}} +} else { +rawBettor, pe := SafeMarshal(bettor) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +sets = append(sets, &PluginSetOp{Key: bettorKey, Value: rawBettor}) +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: sets, +Deletes: deletes, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_tally_votes.go b/plugin/go/contract/handler_tally_votes.go new file mode 100644 index 0000000000..d42f50bf88 --- /dev/null +++ b/plugin/go/contract/handler_tally_votes.go @@ -0,0 +1,244 @@ +package contract + +func (c *Contract) CheckMessageTallyVotes(msg *MessageTallyVotes) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.CallerAddr) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.CallerAddr}, +} +} + +func (c *Contract) DeliverMessageTallyVotes(msg *MessageTallyVotes, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +marketQId := nextQueryId() +disputeQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: KeyForMarket(msg.MarketId)}, +{QueryId: disputeQId, Key: KeyForDispute(msg.MarketId)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +var dispute *DisputeRecord + +for _, r := range resp.Results { +switch r.QueryId { +case marketQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case disputeQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrNotDisputed()} +} +dispute = &DisputeRecord{} +if pe := Unmarshal(r.Entries[0].Value, dispute); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +if market.Status != STATUS_DISPUTED { +return &PluginDeliverResponse{Error: ErrNotDisputed()} +} +if dispute == nil { +return &PluginDeliverResponse{Error: ErrNotDisputed()} +} + +if dispute.VoteStatus == VOTE_TALLIED { +return &PluginDeliverResponse{} +} + +revealEnd := dispute.DisputeBlock + COMMIT_PHASE_BLOCKS + REVEAL_PHASE_BLOCKS +if now <= revealEnd { +return &PluginDeliverResponse{Error: ErrTallyNotReady()} +} + +queries := make([]uint64, 0, len(dispute.PanelMembers)) +readKeys := make([]*PluginKeyRead, 0, len(dispute.PanelMembers)) + +// Layer 2: build RRS read keys alongside vote reveal keys +rrsQueries := make([]uint64, 0, len(dispute.PanelMembers)) +rrsKeys := make([]*PluginKeyRead, 0, len(dispute.PanelMembers)) + +for _, member := range dispute.PanelMembers { +qId := nextQueryId() +queries = append(queries, qId) +readKeys = append(readKeys, &PluginKeyRead{ +QueryId: qId, +Key: KeyForVoteReveal(msg.MarketId, member), +}) +rId := nextQueryId() +rrsQueries = append(rrsQueries, rId) +rrsKeys = append(rrsKeys, &PluginKeyRead{ +QueryId: rId, +Key: KeyForResolverRecord(member), +}) +} + +// Layer 2: build queryId -> vote weight map from RRS scores +weightByQId := make(map[uint64]uint32) +if len(rrsKeys) > 0 { +rrsResp, err := c.plugin.StateRead(c, &PluginStateReadRequest{Keys: rrsKeys}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if rrsResp.Error != nil { +return &PluginDeliverResponse{Error: rrsResp.Error} +} +for idx, r := range rrsResp.Results { +weight := VOTE_WEIGHT_BRONZE +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +rec := &ResolverRecord{} +if pe := Unmarshal(r.Entries[0].Value, rec); pe == nil { +if rec.RrsScore >= RRS_GOLD_THRESHOLD { +weight = VOTE_WEIGHT_GOLD +} else if rec.RrsScore >= RRS_SILVER_THRESHOLD { +weight = VOTE_WEIGHT_SILVER +} +} +} +// map the corresponding vote reveal queryId to this weight +if idx < len(queries) { +weightByQId[queries[idx]] = weight +} +} +} + +var yesVotes, noVotes uint32 +if len(readKeys) > 0 { +voteResp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: readKeys, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if voteResp.Error != nil { +return &PluginDeliverResponse{Error: voteResp.Error} +} + +for _, r := range voteResp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +vr := &VoteReveal{} +if pe := Unmarshal(r.Entries[0].Value, vr); pe != nil { +continue +} +// Layer 2: apply RRS tier weight — default Bronze if missing +w := weightByQId[r.QueryId] +if w == 0 { +w = VOTE_WEIGHT_BRONZE +} +if vr.Vote { +yesVotes += w +} else { +noVotes += w +} +} +} + +disputerWins := yesVotes > noVotes +dispute.VoteStatus = VOTE_TALLIED + +if disputerWins { +market.Status = STATUS_PROPOSED +} + +rawD, pe := SafeMarshal(dispute) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawM, pe := SafeMarshal(market) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +sets := []*PluginSetOp{ +{Key: KeyForDispute(msg.MarketId), Value: rawD}, +{Key: KeyForMarket(msg.MarketId), Value: rawM}, +} + +// PRIS v1.0-r3: if proposer wins dispute, RRS +20 and increment GlobalStats +if !disputerWins { +propQId := nextQueryId() +recQId := nextQueryId() +statsQId := nextQueryId() +prisResp, prisErr := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: propQId, Key: KeyForProposal(msg.MarketId)}, +{QueryId: recQId, Key: KeyForGlobalStats()}, +{QueryId: statsQId, Key: KeyForGlobalStats()}, +}, +}) +if prisErr == nil && prisResp.Error == nil { +proposal := &ProposalRecord{} +globalStats := &GlobalStats{} +for _, r := range prisResp.Results { +if len(r.Entries) == 0 { continue } +switch r.QueryId { +case propQId: +_ = Unmarshal(r.Entries[0].Value, proposal) +case statsQId: +_ = Unmarshal(r.Entries[0].Value, globalStats) +} +} +if len(proposal.ResolverAddr) == 20 { +resolverRecQId := nextQueryId() +recResp, recErr := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: resolverRecQId, Key: KeyForResolverRecord(proposal.ResolverAddr)}, +}, +}) +if recErr == nil && recResp.Error == nil { +resolverRec := &ResolverRecord{} +for _, r := range recResp.Results { +if r.QueryId == resolverRecQId && len(r.Entries) > 0 { +_ = Unmarshal(r.Entries[0].Value, resolverRec) +} +} +resolverRec.RrsScore += 20 +resolverRec.SuccessfulResolutions++ +weight := uint64(1) +if resolverRec.RrsScore >= RRS_GOLD_THRESHOLD { +weight = uint64(VOTE_WEIGHT_GOLD) +} else if resolverRec.RrsScore >= RRS_SILVER_THRESHOLD { +weight = uint64(VOTE_WEIGHT_SILVER) +} +globalStats.TotalWeightedResolutions += weight +rawRec, pe2 := SafeMarshal(resolverRec) +rawStats, pe3 := SafeMarshal(globalStats) +if pe2 == nil && pe3 == nil { +sets = append(sets, &PluginSetOp{Key: KeyForResolverRecord(proposal.ResolverAddr), Value: rawRec}) +sets = append(sets, &PluginSetOp{Key: KeyForGlobalStats(), Value: rawStats}) +} +} +} +} +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{Sets: sets}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/handler_unstake_resolver.go b/plugin/go/contract/handler_unstake_resolver.go new file mode 100644 index 0000000000..016df93905 --- /dev/null +++ b/plugin/go/contract/handler_unstake_resolver.go @@ -0,0 +1,169 @@ +package contract + +// handler_unstake_resolver.go — MessageUnstakeResolver +// Spec: PRIS v1.0-r3 unstake extension +// +// Allows a resolver to unstake partially or fully. +// Rules: +// - Cannot unstake if active open proposal exists (slash-evasion guard) +// - Cannot partial unstake below MIN_RESOLVER_STAKE +// - Partial unstake: RRS -10, 120,960 block unbonding period +// - Full exit (amount=0 or amount=stakeAmount): RRS reset to PRIS_RRS_INITIAL, unbonding period +// - Tokens locked in UnbondingRecord until release height +// - claim_unbonded_stake TX releases tokens after unbonding period + +func (c *Contract) CheckMessageUnstakeResolver(msg *MessageUnstakeResolver) *PluginCheckResponse { +if len(msg.ResolverAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.ResolverAddress}, +} +} + +func (c *Contract) DeliverMessageUnstakeResolver(msg *MessageUnstakeResolver, fee uint64) *PluginDeliverResponse { +height := GetGlobalHeight() +if height == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +// ── Batch read ──────────────────────────────────────────────────────── +recQId := nextQueryId() +accQId := nextQueryId() +feeQId := nextQueryId() +gTreasuryQId := nextQueryId() + +recKey := KeyForResolverRecord(msg.ResolverAddress) +accKey := KeyForAccount(msg.ResolverAddress) +feePoolKey := KeyForFeePool(c.Config.ChainId) +gTreasuryKey := KeyForTreasuryPool() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: recQId, Key: recKey}, +{QueryId: accQId, Key: accKey}, +{QueryId: feeQId, Key: feePoolKey}, +{QueryId: gTreasuryQId, Key: gTreasuryKey}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var record *ResolverRecord +acc := &Account{} +feePool := &Pool{} +gTreasury := &Pool{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +switch r.QueryId { +case recQId: +record = &ResolverRecord{} +if pe := Unmarshal(r.Entries[0].Value, record); pe != nil { +return &PluginDeliverResponse{Error: pe} +} + +case accQId: + if pe := Unmarshal(r.Entries[0].Value, acc); pe != nil { + return &PluginDeliverResponse{Error: pe} + } + case feeQId: + if pe := Unmarshal(r.Entries[0].Value, feePool); pe != nil { + return &PluginDeliverResponse{Error: pe} + } + case gTreasuryQId: + if pe := Unmarshal(r.Entries[0].Value, gTreasury); pe != nil { + return &PluginDeliverResponse{Error: pe} + } + } + } + +if record == nil { +return &PluginDeliverResponse{Error: ErrResolverNotFound()} +} +if !record.IsActive { + return &PluginDeliverResponse{Error: ErrResolverNotActive()} +} + +// ── Determine unstake amount and type ───────────────────────────────── +fullExit := msg.Amount == 0 || msg.Amount >= record.StakeAmount +unstakeAmt := msg.Amount +if fullExit { + unstakeAmt = record.StakeAmount +} + +// Partial unstake: remaining stake must stay >= MIN_RESOLVER_STAKE +if !fullExit { + remaining := record.StakeAmount - unstakeAmt + if remaining < MIN_RESOLVER_STAKE { + return &PluginDeliverResponse{Error: ErrInsufficientResolverStake()} + } +} + +if record.UnbondingAmount > 0 { + return &PluginDeliverResponse{Error: ErrUnbondingAlreadyPending()} +} + +// ── Apply RRS penalty ───────────────────────────────────────────────── +if fullExit { +record.RrsScore = PRIS_RRS_INITIAL // reset to Bronze baseline +record.IsActive = false +} else { +if record.RrsScore > PRIS_UNSTAKE_PARTIAL_RRS_HIT { +record.RrsScore -= PRIS_UNSTAKE_PARTIAL_RRS_HIT +} else { +record.RrsScore = PRIS_RRS_FLOOR +} +} + +// ── Apply unbonding ─────────────────────────────────────────────────── +record.StakeAmount -= unstakeAmt +record.UnbondingAmount = unstakeAmt +record.UnbondingReleaseHeight = height + PRIS_UNSTAKE_UNBONDING_BLOCKS + +// ── Pay fee from account ────────────────────────────────────────────── +if acc.Amount < fee { + return &PluginDeliverResponse{Error: ErrInsufficientFunds()} +} +acc.Amount -= fee +feePool.Amount += fee / 2 +gTreasury.Amount += fee - fee/2 + +// ── Marshal ─────────────────────────────────────────────────────────── +rawRec, pe := SafeMarshal(record) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawAcc, pe := SafeMarshal(acc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawFee, pe := SafeMarshal(feePool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawGTreasury, pe := SafeMarshal(gTreasury) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +// ── Atomic write ────────────────────────────────────────────────────── +sets := []*PluginSetOp{ + {Key: recKey, Value: rawRec}, + {Key: feePoolKey, Value: rawFee}, + {Key: gTreasuryKey, Value: rawGTreasury}, +} +var deletes []*PluginDeleteOp +if acc.Amount == 0 { + deletes = []*PluginDeleteOp{{Key: accKey}} +} else { + sets = append(sets, &PluginSetOp{Key: accKey, Value: rawAcc}) +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: sets, +Deletes: deletes, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/contract/height.go b/plugin/go/contract/height.go new file mode 100644 index 0000000000..7a94334be2 --- /dev/null +++ b/plugin/go/contract/height.go @@ -0,0 +1,41 @@ +package contract + +import "sync" + +// ═══════════════════════════════════════════════════════════════════════════════ +// Praxis Prediction Market — Global Height Management +// Spec authority: ADLMSR v5.6.6-r2-CORRECTED (AUDIT-9) +// +// AUDIT-9: globalHeight must be a package-level variable protected by +// sync.RWMutex. Never use c.currentHeight on the Contract struct between +// BeginBlock and DeliverTx — concurrent DeliverTx goroutines would race +// on the struct field, producing non-deterministic height reads. +// +// Pattern: +// BeginBlock → SetGlobalHeight(req.Height) +// DeliverTx → now := GetGlobalHeight() +// if now == 0 { return ErrHeightNotSet() } +// ═══════════════════════════════════════════════════════════════════════════════ + +var ( +globalHeight uint64 +heightMu sync.RWMutex +) + +// SetGlobalHeight stores the current block height. +// Called once per block in BeginBlock before any transactions are processed. +func SetGlobalHeight(h uint64) { +heightMu.Lock() +globalHeight = h +heightMu.Unlock() +} + +// GetGlobalHeight returns the current block height. +// Called at the top of every DeliverTx handler. +// Returns 0 if BeginBlock has not been called yet — handlers must guard +// against this with: if now == 0 { return ErrHeightNotSet() } +func GetGlobalHeight() uint64 { +heightMu.RLock() +defer heightMu.RUnlock() +return globalHeight +} diff --git a/plugin/go/contract/helpers.go b/plugin/go/contract/helpers.go new file mode 100644 index 0000000000..7ce3451225 --- /dev/null +++ b/plugin/go/contract/helpers.go @@ -0,0 +1,119 @@ +package contract + +import ( +"crypto/sha256" +"encoding/binary" +"math/big" +"sync/atomic" +) + +// ═══════════════════════════════════════════════════════════════════════════════ +// Praxis Prediction Market — Shared Helpers +// Spec authority: ADLMSR v5.6.6-r2-CORRECTED + PORS v1.0-r2-CORRECTED +// ═══════════════════════════════════════════════════════════════════════════════ + +// ───────────────────────────────────────────────────────────────────────────── +// QUERY ID COUNTER +// Uses atomic counter instead of math/rand — rand collisions within a batch +// silently misroute state reads. Atomic counter guarantees uniqueness. +// ───────────────────────────────────────────────────────────────────────────── + +var queryCounter uint64 + +func nextQueryId() uint64 { +return atomic.AddUint64(&queryCounter, 1) +} + +// ───────────────────────────────────────────────────────────────────────────── +// OVERFLOW-SAFE ARITHMETIC +// mulDiv computes (a * b) / c using big.Int for the intermediate product. +// Prevents uint64 overflow in proportional calculations. +// Returns 0 if c == 0. Caps at MaxUint64 if result overflows. +// ───────────────────────────────────────────────────────────────────────────── + +func mulDiv(a, b, c uint64) uint64 { +if c == 0 { +return 0 +} +num := new(big.Int).Mul( +new(big.Int).SetUint64(a), +new(big.Int).SetUint64(b), +) +result := new(big.Int).Div(num, new(big.Int).SetUint64(c)) +maxU64 := new(big.Int).SetUint64(^uint64(0)) +if result.Cmp(maxU64) > 0 { +return ^uint64(0) +} +return result.Uint64() +} + +// ───────────────────────────────────────────────────────────────────────────── +// MARKET ID DERIVATION +// market_id = SHA256(creator_address || nonce_bytes)[:20] +// Deterministic — no sequential counter needed. Verifiable without state read. +// ───────────────────────────────────────────────────────────────────────────── + +func DeriveMarketId(creatorAddr []byte, nonce uint64) []byte { +nonceBytes := make([]byte, 8) +binary.BigEndian.PutUint64(nonceBytes, nonce) +input := make([]byte, len(creatorAddr)+8) + copy(input, creatorAddr) + copy(input[len(creatorAddr):], nonceBytes) +hash := sha256.Sum256(input) +return hash[:20] +} + +// ───────────────────────────────────────────────────────────────────────────── +// COMMIT HASH VERIFICATION +// commit_hash = SHA256(vote_byte || nonce || voter_addr) +// vote_byte: 0x01 for true (YES), 0x00 for false (NO) +// ───────────────────────────────────────────────────────────────────────────── + +func ComputeCommitHash(vote bool, nonce []byte, voterAddr []byte) []byte { +var voteByte byte +if vote { +voteByte = 0x01 +} else { +voteByte = 0x00 +} +input := make([]byte, 0, 1+len(nonce)+len(voterAddr)) +input = append(input, voteByte) +input = append(input, nonce...) +input = append(input, voterAddr...) +hash := sha256.Sum256(input) +return hash[:] +} + +// ───────────────────────────────────────────────────────────────────────────── +// BYTES EQUAL +// Convenience wrapper — avoids importing bytes in every handler file. +// ───────────────────────────────────────────────────────────────────────────── + +func bytesEqual(a, b []byte) bool { +if len(a) != len(b) { +return false +} +for i := range a { +if a[i] != b[i] { +return false +} +} +return true +} + +// COI-3: checks whether holding currentShares + newShares would exceed +// MAX_POSITION_BPS of totalSideShares (shares outstanding on that side). +// Capping on shares — not CostPaid — is correct for LMSR because CostPaid +// is path-dependent: early buyers pay less per share than late buyers, so +// a CostPaid cap penalises late entrants while allowing early actors to +// accumulate dominant share positions cheaply. +// totalSideShares is the post-trade value (market.QYes or market.QNo after +// adding msg.Shares) so the cap scales with the market's actual exposure. +func exceedsPositionCap(currentShares, newShares, totalSideShares uint64) bool { +if totalSideShares == 0 { +return false +} +newTotal := currentShares + newShares +cap := totalSideShares * MAX_POSITION_BPS / 10000 +return newTotal > cap +} diff --git a/plugin/go/contract/keys.go b/plugin/go/contract/keys.go new file mode 100644 index 0000000000..3d29c90df6 --- /dev/null +++ b/plugin/go/contract/keys.go @@ -0,0 +1,192 @@ +package contract + +import "encoding/binary" + +// ═══════════════════════════════════════════════════════════════════════════════ +// Praxis Prediction Market — State Key Functions +// Spec authority: ADLMSR v5.6.6-r2-CORRECTED + PORS v1.0-r2-CORRECTED +// +// All keys are built with JoinLenPrefix from plugin.go which length-prefixes +// each segment, guaranteeing no two different key types can collide even if +// their raw bytes overlap. +// +// Canopy base layer prefixes (do not reuse): +// 0x01 Account +// 0x02 Pool (fee pool) +// 0x07 FeeParams +// +// Praxis state prefixes (0x10–0x1C exclusively): +// 0x10 MarketState per market_id +// 0x11 PositionState per market_id + claimant_address +// 0x12 OutcomeState per market_id +// 0x13 ResolverState per market_id (written by propose_outcome — NF-5) +// 0x14 TreasuryReserve per market_id +// 0x16 ResolverRecord global per resolver_address +// 0x17 ProposalRecord per market_id +// 0x18 DisputeRecord per market_id +// 0x19 VoteCommit per market_id + voter_addr +// 0x1A VoteReveal per market_id + voter_addr +// 0x1B SlashRecord per resolver_address +// 0x1C PanelEntropyAccum singleton +// ═══════════════════════════════════════════════════════════════════════════════ + +// ───────────────────────────────────────────────────────────────────────────── +// CANOPY BASE LAYER KEYS +// ───────────────────────────────────────────────────────────────────────────── + +var ( +accountPrefix = []byte{0x01} +poolPrefix = []byte{0x02} +paramsPrefix = []byte{0x07} +) + +// KeyForAccount returns the state key for an Account record. +// addr must be exactly 20 bytes. +func KeyForAccount(addr []byte) []byte { +return JoinLenPrefix(accountPrefix, addr) +} + +// KeyForFeePool returns the state key for the chain fee pool. +func KeyForFeePool(chainId uint64) []byte { +return JoinLenPrefix(poolPrefix, uint64ToBytes(chainId)) +} + +// KeyForFeeParams returns the state key for the FeeParams singleton. +func KeyForFeeParams() []byte { +return JoinLenPrefix(paramsPrefix, []byte("/f/")) +} + +// ───────────────────────────────────────────────────────────────────────────── +// ADLMSR STATE KEYS +// ───────────────────────────────────────────────────────────────────────────── + +var ( +marketPrefix = []byte{0x10} +positionPrefix = []byte{0x11} +outcomePrefix = []byte{0x12} +resolverStatePrefix = []byte{0x13} +treasuryPrefix = []byte{0x14} +) + +// KeyForMarket returns the state key for a MarketState record. +// market_id is a 20-byte derived identifier. +func KeyForMarket(marketId []byte) []byte { +return JoinLenPrefix(marketPrefix, marketId) +} + +// KeyForPosition returns the state key for a PositionState record. +// Composite key: market_id (20 bytes) + claimant_address (20 bytes). +func KeyForPosition(marketId []byte, addr []byte) []byte { +composite := make([]byte, len(marketId)+len(addr)) +copy(composite, marketId) +copy(composite[len(marketId):], addr) +return JoinLenPrefix(positionPrefix, composite) +} + +// KeyForOutcome returns the state key for an OutcomeState record. +// Presence of this key is the idempotency sentinel for ResolveMarket. +func KeyForOutcome(marketId []byte) []byte { +return JoinLenPrefix(outcomePrefix, marketId) +} + +// KeyForResolverState returns the state key for a per-market ResolverState. +// Written by propose_outcome as the 4th atomic key (NF-5 fix). +// Distinct from KeyForResolverRecord (global per resolver_address). +func KeyForResolverState(marketId []byte) []byte { +return JoinLenPrefix(resolverStatePrefix, marketId) +} + +// KeyForTreasuryReserve returns the state key for a per-market TreasuryReserve. +func KeyForTreasuryReserve(marketId []byte) []byte { +return JoinLenPrefix(treasuryPrefix, marketId) +} + +// ───────────────────────────────────────────────────────────────────────────── +// PORS STATE KEYS +// ───────────────────────────────────────────────────────────────────────────── + +var ( +resolverRecordPrefix = []byte{0x16} +proposalPrefix = []byte{0x17} +disputePrefix = []byte{0x18} +voteCommitPrefix = []byte{0x19} +voteRevealPrefix = []byte{0x1A} +slashRecordPrefix = []byte{0x1B} +) + +// KeyForResolverRecord returns the state key for a global ResolverRecord. +// Keyed by resolver_address — this is the global resolver profile. +// Distinct from KeyForResolverState (per-market). +func KeyForResolverRecord(resolverAddr []byte) []byte { +return JoinLenPrefix(resolverRecordPrefix, resolverAddr) +} + +// KeyForProposal returns the state key for a ProposalRecord. +// Presence of this key is the idempotency sentinel for propose_outcome. +func KeyForProposal(marketId []byte) []byte { +return JoinLenPrefix(proposalPrefix, marketId) +} + +// KeyForDispute returns the state key for a DisputeRecord. +func KeyForDispute(marketId []byte) []byte { +return JoinLenPrefix(disputePrefix, marketId) +} + +// KeyForVoteCommit returns the state key for a panel member's VoteCommit. +// Composite key: market_id (20 bytes) + voter_addr (20 bytes). +func KeyForVoteCommit(marketId []byte, voterAddr []byte) []byte { +composite := make([]byte, len(marketId)+len(voterAddr)) +copy(composite, marketId) +copy(composite[len(marketId):], voterAddr) +return JoinLenPrefix(voteCommitPrefix, composite) +} + +// KeyForVoteReveal returns the state key for a panel member's VoteReveal. +// Composite key: market_id (20 bytes) + voter_addr (20 bytes). +func KeyForVoteReveal(marketId []byte, voterAddr []byte) []byte { +composite := make([]byte, len(marketId)+len(voterAddr)) +copy(composite, marketId) +copy(composite[len(marketId):], voterAddr) +return JoinLenPrefix(voteRevealPrefix, composite) +} + +// KeyForSlashRecord returns the state key for a resolver's SlashRecord. +func KeyForSlashRecord(resolverAddr []byte) []byte { +return JoinLenPrefix(slashRecordPrefix, resolverAddr) +} + +// ───────────────────────────────────────────────────────────────────────────── +// ENTROPY KEY +// Initialised in contract.go init() after JoinLenPrefix is available. +// ───────────────────────────────────────────────────────────────────────────── + +// KeyForPanelEntropy returns the singleton state key for the rolling +// entropy accumulator. Called once in init() to set PANEL_ENTROPY_KEY. +func KeyForPanelEntropy() []byte { +return JoinLenPrefix(panelEntropyPrefix, []byte("/pe/")) +} + +// ───────────────────────────────────────────────────────────────────────────── +// HELPERS +// ───────────────────────────────────────────────────────────────────────────── + +// uint64ToBytes encodes a uint64 as 8 big-endian bytes. +// Used for chain ID in fee pool keys. +func uint64ToBytes(u uint64) []byte { +b := make([]byte, 8) +binary.BigEndian.PutUint64(b, u) +return b +} + +// KeyForMarketPool returns the state key for a per-market liquidity pool. +// Uses the same 0x02 prefix as the chain fee pool but keyed by market_id. +// The JoinLenPrefix encoding ensures no collision with KeyForFeePool(chainId). +func KeyForMarketPool(marketId []byte) []byte { +return JoinLenPrefix(poolPrefix, marketId) +} + +// KeyForTreasuryPool returns the state key for the global Praxis treasury pool. +// Prefix 0x1D — receives surplus sweeps from finalized/cancelled markets. +func KeyForTreasuryPool() []byte { +return JoinLenPrefix([]byte{0x1D}, []byte("/treasury/")) +} diff --git a/plugin/go/contract/keys_pris.go b/plugin/go/contract/keys_pris.go new file mode 100644 index 0000000000..abbbc8b2cc --- /dev/null +++ b/plugin/go/contract/keys_pris.go @@ -0,0 +1,99 @@ +package contract + +// ───────────────────────────────────────────────────────────────────────────── +// PRIS STATE KEYS (0x1D – 0x27) +// Spec authority: PRIS v1.0-r3 +// +// 0x1D CreatorFeePool per market_id +// 0x1E ResolverFeePool per market_id +// 0x1F TreasuryPool singleton global accumulator +// 0x20 EpochSnapshot per epoch number +// 0x21 ResolverEpochPool per epoch number +// 0x22 BuilderPool singleton +// 0x23 CommunityPool singleton +// 0x24 InvestorPool singleton +// 0x25 ProtocolPool singleton +// 0x26 BuilderLastClaimed singleton +// 0x27 InvestorLastClaimed singleton +// ───────────────────────────────────────────────────────────────────────────── + +var ( +creatorFeePoolPrefix = []byte{0x1D} +resolverFeePoolPrefix = []byte{0x1E} +treasuryPoolPrefix = []byte{0x1F} +epochSnapshotPrefix = []byte{0x20} +resolverEpochPoolPrefix = []byte{0x21} +builderPoolPrefix = []byte{0x22} +communityPoolPrefix = []byte{0x23} +investorPoolPrefix = []byte{0x24} +protocolPoolPrefix = []byte{0x25} +builderLastClaimedPrefix = []byte{0x26} +investorLastClaimedPrefix = []byte{0x27} +) + +func KeyForCreatorFeePool(marketId []byte) []byte { +return JoinLenPrefix(creatorFeePoolPrefix, marketId) +} +func KeyForResolverFeePool(marketId []byte) []byte { +return JoinLenPrefix(resolverFeePoolPrefix, marketId) +} +func KeyForEpochSnapshot(epoch uint64) []byte { +return JoinLenPrefix(epochSnapshotPrefix, uint64ToBytes(epoch)) +} +func KeyForResolverEpochPool(epoch uint64) []byte { +return JoinLenPrefix(resolverEpochPoolPrefix, uint64ToBytes(epoch)) +} +func KeyForBuilderPool() []byte { +return JoinLenPrefix(builderPoolPrefix, []byte("/builder/")) +} +func KeyForCommunityPool() []byte { +return JoinLenPrefix(communityPoolPrefix, []byte("/community/")) +} +func KeyForInvestorPool() []byte { +return JoinLenPrefix(investorPoolPrefix, []byte("/investor/")) +} +func KeyForProtocolPool() []byte { +return JoinLenPrefix(protocolPoolPrefix, []byte("/protocol/")) +} +func KeyForBuilderLastClaimed() []byte { +return JoinLenPrefix(builderLastClaimedPrefix, []byte("/blc/")) +} +func KeyForInvestorLastClaimed() []byte { +return JoinLenPrefix(investorLastClaimedPrefix, []byte("/ilc/")) +} + +var globalStatsPrefix = []byte{0x28} +var unbondingRecordPrefix = []byte{0x29} + +// KeyForUnbondingRecord returns the singleton unbonding record for a resolver. +func KeyForUnbondingRecord(addr []byte) []byte { + return JoinLenPrefix(unbondingRecordPrefix, addr) +} + +// KeyForGlobalStats returns the singleton state key for protocol-wide resolution stats. +func KeyForGlobalStats() []byte { +return JoinLenPrefix(globalStatsPrefix, []byte("/stats/")) +} + +var resolverIndexPrefix = []byte{0x2A} +var marketIndexPrefix = []byte{0x2B} + +// KeyForResolverIndex returns the singleton key for the global resolver address list. +// Value: ResolverIndex proto — list of all registered resolver addresses. +func KeyForResolverIndex() []byte { +return JoinLenPrefix(resolverIndexPrefix, []byte("/ridx/")) +} + +// KeyForMarketIndex returns the singleton key for the global market ID list. +// Value: MarketIndex proto — list of all created market IDs. +func KeyForMarketIndex() []byte { +return JoinLenPrefix(marketIndexPrefix, []byte("/midx/")) +} + +var creatorOpenCountPrefix = []byte{0x2C} + +// KeyForCreatorOpenCount returns the state key for a creator's open market counter. +// Value: a single uint64 encoded as Pool.Amount (reuses Pool proto). +func KeyForCreatorOpenCount(addr []byte) []byte { +return JoinLenPrefix(creatorOpenCountPrefix, addr) +} diff --git a/plugin/go/contract/lmsr.go b/plugin/go/contract/lmsr.go new file mode 100644 index 0000000000..8a492d00e8 --- /dev/null +++ b/plugin/go/contract/lmsr.go @@ -0,0 +1,162 @@ +package contract + +import "math" + +// ═══════════════════════════════════════════════════════════════════════════════ +// Praxis Prediction Market — LMSR Pricing Engine +// Spec authority: ADLMSR v5.6.6-r2-CORRECTED +// +// The LMSR (Logarithmic Market Scoring Rule) cost function guarantees that +// liquidity is always available at a price that reflects collective belief. +// The market maker takes the other side of every trade automatically. +// +// Cost function: +// C(q_yes, q_no) = b_eff * ln(exp(q_yes / b_eff) + exp(q_no / b_eff)) +// +// Cost of a trade: +// cost = C(q_yes_new, q_no_new) - C(q_yes_old, q_no_old) +// +// All q values are scaled by PRECISION_SCALE to preserve integer precision. +// b_eff is also in PRECISION_SCALE units. +// +// AUDIT-1: Payout formula is overflow-safe using quot/rem pattern. +// AUDIT-3: now >= OpenTime guard before any subtraction involving heights. +// AUDIT-7: shares >= PRECISION_SCALE validated in DeliverTx. +// AUDIT-12: finalCost <= max_cost slippage guard before deducting funds. +// ═══════════════════════════════════════════════════════════════════════════════ + +// lmsrCost computes the LMSR cost function: +// C(q_yes, q_no) = b_eff * ln(exp(q_yes / b_eff) + exp(q_no / b_eff)) +// +// All inputs are in PRECISION_SCALE fixed-point units. +// Returns cost in micro-PRX (uint64). +// +// Uses float64 internally for the logarithm computation. Precision is +// sufficient for market sizes up to ~1e12 micro-PRX (1 million PRX). +// For larger markets the fixed-point scaling provides adequate resolution. +func lmsrCost(qYes, qNo, bEff uint64) uint64 { +if bEff == 0 { +return 0 +} +b := float64(bEff) +y := float64(qYes) +n := float64(qNo) + +// Use the log-sum-exp trick to prevent overflow: +// ln(exp(a) + exp(b)) = max(a,b) + ln(1 + exp(-|a-b|)) +// This keeps the argument to exp() small regardless of q values. +ay := y / b +an := n / b +var lse float64 +if ay >= an { +lse = ay + math.Log1p(math.Exp(an-ay)) +} else { +lse = an + math.Log1p(math.Exp(ay-an)) +} +// Result is b_eff * lse, converted back from float to uint64. +result := b * lse +if result < 0 { +return 0 +} +return uint64(result) +} + +// ComputeTradeCost returns the cost in micro-PRX for purchasing `shares` of +// the given outcome in a market with current state (qYes, qNo, bEff). +// +// outcome: true = YES, false = NO +// shares: number of shares in PRECISION_SCALE units (must be >= PRECISION_SCALE) +// +// Returns (cost, error). +// Error is non-nil if: +// - shares < PRECISION_SCALE (AUDIT-7) +// - bEff == 0 (degenerate market) +// - cost overflows uint64 (should not happen with MAX_EXPIRY_TIME guard) +func ComputeTradeCost(qYes, qNo, bEff, shares uint64, outcome bool) (uint64, *PluginError) { +if bEff == 0 { +return 0, ErrInvalidB0() +} +// AUDIT-7: shares must be at least one unit of precision. +if shares < PRECISION_SCALE { +return 0, ErrSharesBelowMinimum() +} + +costBefore := lmsrCost(qYes, qNo, bEff) + +var qYesNew, qNoNew uint64 +if outcome { +qYesNew = qYes + shares +qNoNew = qNo +} else { +qYesNew = qYes +qNoNew = qNo + shares +} + +costAfter := lmsrCost(qYesNew, qNoNew, bEff) + +// costAfter should always be >= costBefore for a valid trade. +// Guard against underflow from floating point rounding. +if costAfter < costBefore { +return 0, ErrInternal() +} + +return costAfter - costBefore, nil +} + +// ComputePayout computes the pro-rata payout for a winning position. +// Uses the overflow-safe quot/rem formula (AUDIT-1). +// +// quot = poolAmount / totalWinShares +// rem = poolAmount % totalWinShares +// payout = quot * winnerShares + rem * winnerShares / totalWinShares +// +// Returns 0 if totalWinShares == 0 (no winners — should not happen in practice). +func ComputePayout(poolAmount, winnerShares, totalWinShares uint64) uint64 { +if totalWinShares == 0 { +return 0 +} +quot := poolAmount / totalWinShares +rem := poolAmount % totalWinShares +return quot*winnerShares + rem*winnerShares/totalWinShares +} + +// ComputeMinBond returns the minimum proposal bond required for a market. +// Bond scales with pool size to make dishonest proposals expensive relative +// to potential gain. Minimum is 1% of the market pool, floor of MIN_B0. +func ComputeMinBond(market *MarketState) uint64 { +if market == nil { +return MIN_B0 +} +// Read total pool from the market's q values as a proxy for pool size. +// 1% of b_eff as a simple bond floor — adjustable via governance. +bond := market.BEff / 100 +if bond < MIN_B0 { +bond = MIN_B0 +} +return bond +} + +// IsElevatedRisk returns true if the market pool exceeds ELEVATED_RISK_THRESHOLD. +// Called at market creation and updated on each prediction. +// Elevated-risk markets use a larger dispute panel (P7). +func IsElevatedRisk(poolAmount uint64) bool { +return poolAmount >= ELEVATED_RISK_THRESHOLD +} + +// ComputeDisputeBlocks returns the dispute window for a market in blocks. +// DISPUTE_BLOCKS = MAX(MIN_DISPUTE_BLOCKS, market_duration / 10) +// Minimum 48h floor — longer markets get proportionally longer challenge windows. +func ComputeDisputeBlocks(openTime, expiryTime uint64) uint64 { +if TEST_MODE { +return TEST_DISPUTE_BLOCKS +} +if expiryTime <= openTime { +return MIN_DISPUTE_BLOCKS +} +duration := expiryTime - openTime +window := duration / 10 +if window < MIN_DISPUTE_BLOCKS { +return MIN_DISPUTE_BLOCKS +} +return window +} diff --git a/plugin/go/contract/plugin.go b/plugin/go/contract/plugin.go index 72e91a3bbc..27d69bde21 100644 --- a/plugin/go/contract/plugin.go +++ b/plugin/go/contract/plugin.go @@ -19,6 +19,8 @@ import ( /* This file contains boilerplate logic to interact with the Canopy FSM via socket file */ // Plugin defines the 'VM-less' extension of the Finite State Machine +var globalReqCounter uint64 + type Plugin struct { fsmConfig *PluginFSMConfig // the FSM configuration pluginConfig *PluginConfig // the plugin configuration @@ -101,7 +103,7 @@ func (p *Plugin) StateRead(c *Contract, request *PluginStateReadRequest) (*Plugi return nil, ErrUnexpectedFSMToPlugin(reflect.TypeOf(response)) } // return the unwrapped response - return wrapper.StateRead, nil +return wrapper.StateRead, nil } func (p *Plugin) StateWrite(c *Contract, request *PluginStateWriteRequest) (*PluginStateWriteResponse, *PluginError) { @@ -127,7 +129,7 @@ func (p *Plugin) ListenForInbound() { if err := p.receiveProtoMsg(msg); err != nil { log.Fatal(err.Error()) } - go func() { + go func(msg *FSMToPlugin) { if err := func() *PluginError { // create a new instance of a contract response, c := isPluginToFSM_Payload(nil), &Contract{Config: p.config, FSMConfig: p.fsmConfig, plugin: p, fsmId: msg.Id} @@ -163,11 +165,11 @@ func (p *Plugin) ListenForInbound() { }(); err != nil { log.Fatal(err.Error()) } - }() + }(msg) } } -// HandlePluginResponse() routes the inbound response appropriately +// handleFSMResponse routes the inbound FSM response to the waiting goroutine. func (p *Plugin) handleFSMResponse(msg *FSMToPlugin) *PluginError { // thread safety p.l.Lock() @@ -180,8 +182,8 @@ func (p *Plugin) handleFSMResponse(msg *FSMToPlugin) *PluginError { // remove the message from the pending list and FSM context delete(p.pending, msg.Id) delete(p.requestContract, msg.Id) - // forward the message to the requester - go func() { ch <- msg.Payload }() + // forward the message to the requester (channel is buffered, safe to send while holding lock) + ch <- msg.Payload // exit without error return nil } @@ -204,16 +206,14 @@ func (p *Plugin) sendToPluginSync(c *Contract, request isPluginToFSM_Payload) (i // sendToPluginAsync() sends to the plugin but doesn't wait for a response, tracking FSM context func (p *Plugin) sendToPluginAsync(c *Contract, request isPluginToFSM_Payload) (ch chan isFSMToPlugin_Payload, requestId uint64, err *PluginError) { - // generate the request UUID + // Use c.fsmId as request ID - FSM stores context under this ID requestId = c.fsmId // make a channel to receive the response ch = make(chan isFSMToPlugin_Payload, 1) - // add to the pending list and FSM context map p.l.Lock() p.pending[requestId] = ch - p.requestContract[requestId] = c // Track contract for this request + p.requestContract[requestId] = c p.l.Unlock() - // send the payload with the request ID err = p.sendProtoMsg(&PluginToFSM{Id: requestId, Payload: request}) // exit return diff --git a/plugin/go/contract/plugin.pb.go b/plugin/go/contract/plugin.pb.go index 103770def8..8b476a8cb8 100644 --- a/plugin/go/contract/plugin.pb.go +++ b/plugin/go/contract/plugin.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.11 // protoc v5.29.3 // source: plugin.proto @@ -1792,7 +1792,7 @@ const file_plugin_proto_rawDesc = "" + "\x03key\x18\x01 \x01(\fR\x03key\":\n" + "\x10PluginStateEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\fR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\fR\x05valueB.Z,github.com/canopy-network/go-plugin/contractb\x06proto3" + "\x05value\x18\x02 \x01(\fR\x05valueB5Z3github.com/canopy-network/canopy/plugin/go/contractb\x06proto3" var ( file_plugin_proto_rawDescOnce sync.Once diff --git a/plugin/go/contract/tx.pb.go b/plugin/go/contract/tx.pb.go index 0c637ce802..0b5cd3fa6a 100644 --- a/plugin/go/contract/tx.pb.go +++ b/plugin/go/contract/tx.pb.go @@ -1,15 +1,15 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.11 // protoc v5.29.3 // source: tx.proto package contract import ( + any1 "github.com/golang/protobuf/ptypes/any" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" - anypb "google.golang.org/protobuf/types/known/anypb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -22,35 +22,77 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// Transaction represents a request or action submitted to the network like transfer assets or perform other operations -// within the blockchain system - THIS MUST MATCH lib.Transaction EXACTLY for signing +// Required by plugin.proto +type Signature struct { + state protoimpl.MessageState `protogen:"open.v1"` + PublicKey []byte `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Signature) Reset() { + *x = Signature{} + mi := &file_tx_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Signature) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Signature) ProtoMessage() {} + +func (x *Signature) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Signature.ProtoReflect.Descriptor instead. +func (*Signature) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{0} +} + +func (x *Signature) GetPublicKey() []byte { + if x != nil { + return x.PublicKey + } + return nil +} + +func (x *Signature) GetSignature() []byte { + if x != nil { + return x.Signature + } + return nil +} + type Transaction struct { - state protoimpl.MessageState `protogen:"open.v1"` - // message_type: The type of the transaction like 'send' or 'stake' - MessageType string `protobuf:"bytes,1,opt,name=message_type,json=messageType,proto3" json:"messageType"` // @gotags: json:"messageType" - // msg: The actual transaction message payload, which is encapsulated in a generic message format - Msg *anypb.Any `protobuf:"bytes,2,opt,name=msg,proto3" json:"msg,omitempty"` - // signature: The cryptographic signature used to verify the authenticity of the transaction - Signature *Signature `protobuf:"bytes,3,opt,name=signature,proto3" json:"signature,omitempty"` - // created_height: The height when the transaction was created - allows 'safe pruning' - CreatedHeight uint64 `protobuf:"varint,4,opt,name=created_height,json=createdHeight,proto3" json:"createdHeight"` // @gotags: json:"createdHeight" - // time: The timestamp when the transaction was created - used as temporal entropy to prevent hash collisions in txs - Time uint64 `protobuf:"varint,5,opt,name=time,proto3" json:"time,omitempty"` - // fee: The fee associated with processing the transaction - Fee uint64 `protobuf:"varint,6,opt,name=fee,proto3" json:"fee,omitempty"` - // memo: An optional message or note attached to the transaction - Memo string `protobuf:"bytes,7,opt,name=memo,proto3" json:"memo,omitempty"` - // network_id: The identity of the network the transaction is intended for - NetworkId uint64 `protobuf:"varint,8,opt,name=network_id,json=networkId,proto3" json:"networkID"` // @gotags: json:"networkID" - // chain_id: The identity of the committee the transaction is intended for - ChainId uint64 `protobuf:"varint,9,opt,name=chain_id,json=chainId,proto3" json:"chainID"` // @gotags: json:"chainID" + state protoimpl.MessageState `protogen:"open.v1"` + MessageType string `protobuf:"bytes,1,opt,name=message_type,json=messageType,proto3" json:"message_type,omitempty"` + Msg *any1.Any `protobuf:"bytes,2,opt,name=msg,proto3" json:"msg,omitempty"` + Signature *Signature `protobuf:"bytes,3,opt,name=signature,proto3" json:"signature,omitempty"` + CreatedHeight uint64 `protobuf:"varint,4,opt,name=created_height,json=createdHeight,proto3" json:"created_height,omitempty"` + Time uint64 `protobuf:"varint,5,opt,name=time,proto3" json:"time,omitempty"` + Fee uint64 `protobuf:"varint,6,opt,name=fee,proto3" json:"fee,omitempty"` + Memo string `protobuf:"bytes,7,opt,name=memo,proto3" json:"memo,omitempty"` + NetworkId uint64 `protobuf:"varint,8,opt,name=network_id,json=networkId,proto3" json:"network_id,omitempty"` + ChainId uint64 `protobuf:"varint,9,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Transaction) Reset() { *x = Transaction{} - mi := &file_tx_proto_msgTypes[0] + mi := &file_tx_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -62,7 +104,7 @@ func (x *Transaction) String() string { func (*Transaction) ProtoMessage() {} func (x *Transaction) ProtoReflect() protoreflect.Message { - mi := &file_tx_proto_msgTypes[0] + mi := &file_tx_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -75,7 +117,7 @@ func (x *Transaction) ProtoReflect() protoreflect.Message { // Deprecated: Use Transaction.ProtoReflect.Descriptor instead. func (*Transaction) Descriptor() ([]byte, []int) { - return file_tx_proto_rawDescGZIP(), []int{0} + return file_tx_proto_rawDescGZIP(), []int{1} } func (x *Transaction) GetMessageType() string { @@ -85,7 +127,7 @@ func (x *Transaction) GetMessageType() string { return "" } -func (x *Transaction) GetMsg() *anypb.Any { +func (x *Transaction) GetMsg() *any1.Any { if x != nil { return x.Msg } @@ -141,35 +183,32 @@ func (x *Transaction) GetChainId() uint64 { return 0 } -// MessageSend is a standard transfer transaction, taking tokens from the sender and transferring -// them to the recipient -type MessageSend struct { - state protoimpl.MessageState `protogen:"open.v1"` - // from_address: is the sender of the funds - FromAddress []byte `protobuf:"bytes,1,opt,name=from_address,json=fromAddress,proto3" json:"fromAddress"` // @gotags: json:"fromAddress" - // to_address: is the recipient of the funds - ToAddress []byte `protobuf:"bytes,2,opt,name=to_address,json=toAddress,proto3" json:"toAddress"` // @gotags: json:"toAddress" - // amount: is the amount of tokens in micro-denomination (uCNPY) - Amount uint64 `protobuf:"varint,3,opt,name=amount,proto3" json:"amount,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +type FeeParams struct { + state protoimpl.MessageState `protogen:"open.v1"` + SendFee uint64 `protobuf:"varint,1,opt,name=send_fee,json=sendFee,proto3" json:"send_fee,omitempty"` + CreateMarketFee uint64 `protobuf:"varint,2,opt,name=create_market_fee,json=createMarketFee,proto3" json:"create_market_fee,omitempty"` + SubmitPredictionFee uint64 `protobuf:"varint,3,opt,name=submit_prediction_fee,json=submitPredictionFee,proto3" json:"submit_prediction_fee,omitempty"` + ResolveMarketFee uint64 `protobuf:"varint,4,opt,name=resolve_market_fee,json=resolveMarketFee,proto3" json:"resolve_market_fee,omitempty"` + ClaimWinningsFee uint64 `protobuf:"varint,5,opt,name=claim_winnings_fee,json=claimWinningsFee,proto3" json:"claim_winnings_fee,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *MessageSend) Reset() { - *x = MessageSend{} - mi := &file_tx_proto_msgTypes[1] +func (x *FeeParams) Reset() { + *x = FeeParams{} + mi := &file_tx_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *MessageSend) String() string { +func (x *FeeParams) String() string { return protoimpl.X.MessageStringOf(x) } -func (*MessageSend) ProtoMessage() {} +func (*FeeParams) ProtoMessage() {} -func (x *MessageSend) ProtoReflect() protoreflect.Message { - mi := &file_tx_proto_msgTypes[1] +func (x *FeeParams) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -180,56 +219,212 @@ func (x *MessageSend) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use MessageSend.ProtoReflect.Descriptor instead. -func (*MessageSend) Descriptor() ([]byte, []int) { - return file_tx_proto_rawDescGZIP(), []int{1} +// Deprecated: Use FeeParams.ProtoReflect.Descriptor instead. +func (*FeeParams) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{2} } -func (x *MessageSend) GetFromAddress() []byte { +func (x *FeeParams) GetSendFee() uint64 { if x != nil { - return x.FromAddress + return x.SendFee } - return nil + return 0 } -func (x *MessageSend) GetToAddress() []byte { +func (x *FeeParams) GetCreateMarketFee() uint64 { if x != nil { - return x.ToAddress + return x.CreateMarketFee + } + return 0 +} + +func (x *FeeParams) GetSubmitPredictionFee() uint64 { + if x != nil { + return x.SubmitPredictionFee + } + return 0 +} + +func (x *FeeParams) GetResolveMarketFee() uint64 { + if x != nil { + return x.ResolveMarketFee + } + return 0 +} + +func (x *FeeParams) GetClaimWinningsFee() uint64 { + if x != nil { + return x.ClaimWinningsFee + } + return 0 +} + +// ADLMSR state objects (0x10-0x14) +type MarketState struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status uint32 `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"` + ExpiryTime uint64 `protobuf:"varint,2,opt,name=expiry_time,json=expiryTime,proto3" json:"expiry_time,omitempty"` + QYes uint64 `protobuf:"varint,3,opt,name=q_yes,json=qYes,proto3" json:"q_yes,omitempty"` + QNo uint64 `protobuf:"varint,4,opt,name=q_no,json=qNo,proto3" json:"q_no,omitempty"` + BEff uint64 `protobuf:"varint,5,opt,name=b_eff,json=bEff,proto3" json:"b_eff,omitempty"` + Creator []byte `protobuf:"bytes,6,opt,name=creator,proto3" json:"creator,omitempty"` + ClaimedCount uint64 `protobuf:"varint,7,opt,name=claimed_count,json=claimedCount,proto3" json:"claimed_count,omitempty"` + TotalPositions uint64 `protobuf:"varint,8,opt,name=total_positions,json=totalPositions,proto3" json:"total_positions,omitempty"` + OpenTime uint64 `protobuf:"varint,9,opt,name=open_time,json=openTime,proto3" json:"open_time,omitempty"` + ElevatedRisk bool `protobuf:"varint,10,opt,name=elevated_risk,json=elevatedRisk,proto3" json:"elevated_risk,omitempty"` + FinalizedPoolAmount uint64 `protobuf:"varint,11,opt,name=finalized_pool_amount,json=finalizedPoolAmount,proto3" json:"finalized_pool_amount,omitempty"` + Question string `protobuf:"bytes,12,opt,name=question,proto3" json:"question,omitempty"` + Rules string `protobuf:"bytes,13,opt,name=rules,proto3" json:"rules,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MarketState) Reset() { + *x = MarketState{} + mi := &file_tx_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MarketState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MarketState) ProtoMessage() {} + +func (x *MarketState) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MarketState.ProtoReflect.Descriptor instead. +func (*MarketState) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{3} +} + +func (x *MarketState) GetStatus() uint32 { + if x != nil { + return x.Status + } + return 0 +} + +func (x *MarketState) GetExpiryTime() uint64 { + if x != nil { + return x.ExpiryTime + } + return 0 +} + +func (x *MarketState) GetQYes() uint64 { + if x != nil { + return x.QYes + } + return 0 +} + +func (x *MarketState) GetQNo() uint64 { + if x != nil { + return x.QNo + } + return 0 +} + +func (x *MarketState) GetBEff() uint64 { + if x != nil { + return x.BEff + } + return 0 +} + +func (x *MarketState) GetCreator() []byte { + if x != nil { + return x.Creator } return nil } -func (x *MessageSend) GetAmount() uint64 { +func (x *MarketState) GetClaimedCount() uint64 { if x != nil { - return x.Amount + return x.ClaimedCount } return 0 } -// FeeParams is the parameter space that defines various amounts for transaction fees -type FeeParams struct { - state protoimpl.MessageState `protogen:"open.v1"` - // send_fee: is the fee amount (in uCNPY) for Message Send - SendFee uint64 `protobuf:"varint,1,opt,name=send_fee,json=sendFee,proto3" json:"sendFee"` // @gotags: json:"sendFee" +func (x *MarketState) GetTotalPositions() uint64 { + if x != nil { + return x.TotalPositions + } + return 0 +} + +func (x *MarketState) GetOpenTime() uint64 { + if x != nil { + return x.OpenTime + } + return 0 +} + +func (x *MarketState) GetElevatedRisk() bool { + if x != nil { + return x.ElevatedRisk + } + return false +} + +func (x *MarketState) GetFinalizedPoolAmount() uint64 { + if x != nil { + return x.FinalizedPoolAmount + } + return 0 +} + +func (x *MarketState) GetQuestion() string { + if x != nil { + return x.Question + } + return "" +} + +func (x *MarketState) GetRules() string { + if x != nil { + return x.Rules + } + return "" +} + +type PositionState struct { + state protoimpl.MessageState `protogen:"open.v1"` + SharesYes uint64 `protobuf:"varint,1,opt,name=shares_yes,json=sharesYes,proto3" json:"shares_yes,omitempty"` + SharesNo uint64 `protobuf:"varint,2,opt,name=shares_no,json=sharesNo,proto3" json:"shares_no,omitempty"` + CostPaid uint64 `protobuf:"varint,3,opt,name=cost_paid,json=costPaid,proto3" json:"cost_paid,omitempty"` + Claimed bool `protobuf:"varint,4,opt,name=claimed,proto3" json:"claimed,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *FeeParams) Reset() { - *x = FeeParams{} - mi := &file_tx_proto_msgTypes[2] +func (x *PositionState) Reset() { + *x = PositionState{} + mi := &file_tx_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *FeeParams) String() string { +func (x *PositionState) String() string { return protoimpl.X.MessageStringOf(x) } -func (*FeeParams) ProtoMessage() {} +func (*PositionState) ProtoMessage() {} -func (x *FeeParams) ProtoReflect() protoreflect.Message { - mi := &file_tx_proto_msgTypes[2] +func (x *PositionState) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -240,45 +435,62 @@ func (x *FeeParams) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use FeeParams.ProtoReflect.Descriptor instead. -func (*FeeParams) Descriptor() ([]byte, []int) { - return file_tx_proto_rawDescGZIP(), []int{2} +// Deprecated: Use PositionState.ProtoReflect.Descriptor instead. +func (*PositionState) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{4} } -func (x *FeeParams) GetSendFee() uint64 { +func (x *PositionState) GetSharesYes() uint64 { if x != nil { - return x.SendFee + return x.SharesYes } return 0 } -// A Signature is a digital signature is a cryptographic "fingerprint" created with a private key, -// allowing others to verify the authenticity and integrity of a message using the corresponding public key -type Signature struct { - state protoimpl.MessageState `protogen:"open.v1"` - // public_key: is a cryptographic code shared openly, used to verify digital signatures - PublicKey []byte `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"publicKey"` // @gotags: json:"publicKey" - // signature: the bytes of the signature output from a private key which may be verified with the message and public - Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *PositionState) GetSharesNo() uint64 { + if x != nil { + return x.SharesNo + } + return 0 } -func (x *Signature) Reset() { - *x = Signature{} - mi := &file_tx_proto_msgTypes[3] +func (x *PositionState) GetCostPaid() uint64 { + if x != nil { + return x.CostPaid + } + return 0 +} + +func (x *PositionState) GetClaimed() bool { + if x != nil { + return x.Claimed + } + return false +} + +type OutcomeState struct { + state protoimpl.MessageState `protogen:"open.v1"` + WinningOutcome bool `protobuf:"varint,1,opt,name=winning_outcome,json=winningOutcome,proto3" json:"winning_outcome,omitempty"` + ResolvedAt uint64 `protobuf:"varint,2,opt,name=resolved_at,json=resolvedAt,proto3" json:"resolved_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OutcomeState) Reset() { + *x = OutcomeState{} + mi := &file_tx_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *Signature) String() string { +func (x *OutcomeState) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Signature) ProtoMessage() {} +func (*OutcomeState) ProtoMessage() {} -func (x *Signature) ProtoReflect() protoreflect.Message { - mi := &file_tx_proto_msgTypes[3] +func (x *OutcomeState) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -289,52 +501,2289 @@ func (x *Signature) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Signature.ProtoReflect.Descriptor instead. -func (*Signature) Descriptor() ([]byte, []int) { - return file_tx_proto_rawDescGZIP(), []int{3} +// Deprecated: Use OutcomeState.ProtoReflect.Descriptor instead. +func (*OutcomeState) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{5} } -func (x *Signature) GetPublicKey() []byte { +func (x *OutcomeState) GetWinningOutcome() bool { if x != nil { - return x.PublicKey + return x.WinningOutcome } - return nil + return false } -func (x *Signature) GetSignature() []byte { +func (x *OutcomeState) GetResolvedAt() uint64 { if x != nil { - return x.Signature + return x.ResolvedAt } - return nil + return 0 } -var File_tx_proto protoreflect.FileDescriptor +type TreasuryReserve struct { + state protoimpl.MessageState `protogen:"open.v1"` + LockedReserve uint64 `protobuf:"varint,1,opt,name=locked_reserve,json=lockedReserve,proto3" json:"locked_reserve,omitempty"` + CreatorBond uint64 `protobuf:"varint,2,opt,name=creator_bond,json=creatorBond,proto3" json:"creator_bond,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} -const file_tx_proto_rawDesc = "" + - "\n" + - "\btx.proto\x12\x05types\x1a\x19google/protobuf/any.proto\"\xa3\x02\n" + - "\vTransaction\x12!\n" + - "\fmessage_type\x18\x01 \x01(\tR\vmessageType\x12&\n" + - "\x03msg\x18\x02 \x01(\v2\x14.google.protobuf.AnyR\x03msg\x12.\n" + - "\tsignature\x18\x03 \x01(\v2\x10.types.SignatureR\tsignature\x12%\n" + - "\x0ecreated_height\x18\x04 \x01(\x04R\rcreatedHeight\x12\x12\n" + - "\x04time\x18\x05 \x01(\x04R\x04time\x12\x10\n" + - "\x03fee\x18\x06 \x01(\x04R\x03fee\x12\x12\n" + - "\x04memo\x18\a \x01(\tR\x04memo\x12\x1d\n" + - "\n" + - "network_id\x18\b \x01(\x04R\tnetworkId\x12\x19\n" + - "\bchain_id\x18\t \x01(\x04R\achainId\"g\n" + - "\vMessageSend\x12!\n" + - "\ffrom_address\x18\x01 \x01(\fR\vfromAddress\x12\x1d\n" + - "\n" + - "to_address\x18\x02 \x01(\fR\ttoAddress\x12\x16\n" + - "\x06amount\x18\x03 \x01(\x04R\x06amount\"&\n" + - "\tFeeParams\x12\x19\n" + - "\bsend_fee\x18\x01 \x01(\x04R\asendFee\"H\n" + +func (x *TreasuryReserve) Reset() { + *x = TreasuryReserve{} + mi := &file_tx_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TreasuryReserve) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TreasuryReserve) ProtoMessage() {} + +func (x *TreasuryReserve) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TreasuryReserve.ProtoReflect.Descriptor instead. +func (*TreasuryReserve) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{6} +} + +func (x *TreasuryReserve) GetLockedReserve() uint64 { + if x != nil { + return x.LockedReserve + } + return 0 +} + +func (x *TreasuryReserve) GetCreatorBond() uint64 { + if x != nil { + return x.CreatorBond + } + return 0 +} + +// PORS state objects (0x13, 0x16-0x1C) +type ResolverState struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddress []byte `protobuf:"bytes,1,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResolverState) Reset() { + *x = ResolverState{} + mi := &file_tx_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResolverState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResolverState) ProtoMessage() {} + +func (x *ResolverState) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResolverState.ProtoReflect.Descriptor instead. +func (*ResolverState) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{7} +} + +func (x *ResolverState) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +type ResolverRecord struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddress []byte `protobuf:"bytes,1,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + StakeAmount uint64 `protobuf:"varint,2,opt,name=stake_amount,json=stakeAmount,proto3" json:"stake_amount,omitempty"` + RrsScore uint64 `protobuf:"varint,3,opt,name=rrs_score,json=rrsScore,proto3" json:"rrs_score,omitempty"` + RegisteredAt uint64 `protobuf:"varint,4,opt,name=registered_at,json=registeredAt,proto3" json:"registered_at,omitempty"` + SuccessfulResolutions uint64 `protobuf:"varint,5,opt,name=successful_resolutions,json=successfulResolutions,proto3" json:"successful_resolutions,omitempty"` + LastClaimedEpoch uint64 `protobuf:"varint,6,opt,name=last_claimed_epoch,json=lastClaimedEpoch,proto3" json:"last_claimed_epoch,omitempty"` + IsActive bool `protobuf:"varint,7,opt,name=is_active,json=isActive,proto3" json:"is_active,omitempty"` + UnbondingAmount uint64 `protobuf:"varint,8,opt,name=unbonding_amount,json=unbondingAmount,proto3" json:"unbonding_amount,omitempty"` + UnbondingReleaseHeight uint64 `protobuf:"varint,9,opt,name=unbonding_release_height,json=unbondingReleaseHeight,proto3" json:"unbonding_release_height,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResolverRecord) Reset() { + *x = ResolverRecord{} + mi := &file_tx_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResolverRecord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResolverRecord) ProtoMessage() {} + +func (x *ResolverRecord) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResolverRecord.ProtoReflect.Descriptor instead. +func (*ResolverRecord) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{8} +} + +func (x *ResolverRecord) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +func (x *ResolverRecord) GetStakeAmount() uint64 { + if x != nil { + return x.StakeAmount + } + return 0 +} + +func (x *ResolverRecord) GetRrsScore() uint64 { + if x != nil { + return x.RrsScore + } + return 0 +} + +func (x *ResolverRecord) GetRegisteredAt() uint64 { + if x != nil { + return x.RegisteredAt + } + return 0 +} + +func (x *ResolverRecord) GetSuccessfulResolutions() uint64 { + if x != nil { + return x.SuccessfulResolutions + } + return 0 +} + +func (x *ResolverRecord) GetLastClaimedEpoch() uint64 { + if x != nil { + return x.LastClaimedEpoch + } + return 0 +} + +func (x *ResolverRecord) GetIsActive() bool { + if x != nil { + return x.IsActive + } + return false +} + +func (x *ResolverRecord) GetUnbondingAmount() uint64 { + if x != nil { + return x.UnbondingAmount + } + return 0 +} + +func (x *ResolverRecord) GetUnbondingReleaseHeight() uint64 { + if x != nil { + return x.UnbondingReleaseHeight + } + return 0 +} + +type ProposalRecord struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddr []byte `protobuf:"bytes,1,opt,name=resolver_addr,json=resolverAddr,proto3" json:"resolver_addr,omitempty"` + ProposedOutcome bool `protobuf:"varint,2,opt,name=proposed_outcome,json=proposedOutcome,proto3" json:"proposed_outcome,omitempty"` + ProposalBond uint64 `protobuf:"varint,3,opt,name=proposal_bond,json=proposalBond,proto3" json:"proposal_bond,omitempty"` + ProposalBlock uint64 `protobuf:"varint,4,opt,name=proposal_block,json=proposalBlock,proto3" json:"proposal_block,omitempty"` + Status uint32 `protobuf:"varint,5,opt,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProposalRecord) Reset() { + *x = ProposalRecord{} + mi := &file_tx_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProposalRecord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProposalRecord) ProtoMessage() {} + +func (x *ProposalRecord) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProposalRecord.ProtoReflect.Descriptor instead. +func (*ProposalRecord) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{9} +} + +func (x *ProposalRecord) GetResolverAddr() []byte { + if x != nil { + return x.ResolverAddr + } + return nil +} + +func (x *ProposalRecord) GetProposedOutcome() bool { + if x != nil { + return x.ProposedOutcome + } + return false +} + +func (x *ProposalRecord) GetProposalBond() uint64 { + if x != nil { + return x.ProposalBond + } + return 0 +} + +func (x *ProposalRecord) GetProposalBlock() uint64 { + if x != nil { + return x.ProposalBlock + } + return 0 +} + +func (x *ProposalRecord) GetStatus() uint32 { + if x != nil { + return x.Status + } + return 0 +} + +type DisputeRecord struct { + state protoimpl.MessageState `protogen:"open.v1"` + DisputerAddress []byte `protobuf:"bytes,1,opt,name=disputer_address,json=disputerAddress,proto3" json:"disputer_address,omitempty"` + DisputeBond uint64 `protobuf:"varint,2,opt,name=dispute_bond,json=disputeBond,proto3" json:"dispute_bond,omitempty"` + DisputeBlock uint64 `protobuf:"varint,3,opt,name=dispute_block,json=disputeBlock,proto3" json:"dispute_block,omitempty"` + VoteStatus uint32 `protobuf:"varint,4,opt,name=vote_status,json=voteStatus,proto3" json:"vote_status,omitempty"` + PanelSize uint32 `protobuf:"varint,5,opt,name=panel_size,json=panelSize,proto3" json:"panel_size,omitempty"` + PanelMembers [][]byte `protobuf:"bytes,6,rep,name=panel_members,json=panelMembers,proto3" json:"panel_members,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DisputeRecord) Reset() { + *x = DisputeRecord{} + mi := &file_tx_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DisputeRecord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DisputeRecord) ProtoMessage() {} + +func (x *DisputeRecord) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DisputeRecord.ProtoReflect.Descriptor instead. +func (*DisputeRecord) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{10} +} + +func (x *DisputeRecord) GetDisputerAddress() []byte { + if x != nil { + return x.DisputerAddress + } + return nil +} + +func (x *DisputeRecord) GetDisputeBond() uint64 { + if x != nil { + return x.DisputeBond + } + return 0 +} + +func (x *DisputeRecord) GetDisputeBlock() uint64 { + if x != nil { + return x.DisputeBlock + } + return 0 +} + +func (x *DisputeRecord) GetVoteStatus() uint32 { + if x != nil { + return x.VoteStatus + } + return 0 +} + +func (x *DisputeRecord) GetPanelSize() uint32 { + if x != nil { + return x.PanelSize + } + return 0 +} + +func (x *DisputeRecord) GetPanelMembers() [][]byte { + if x != nil { + return x.PanelMembers + } + return nil +} + +type VoteCommit struct { + state protoimpl.MessageState `protogen:"open.v1"` + VoterAddr []byte `protobuf:"bytes,1,opt,name=voter_addr,json=voterAddr,proto3" json:"voter_addr,omitempty"` + CommitHash []byte `protobuf:"bytes,2,opt,name=commit_hash,json=commitHash,proto3" json:"commit_hash,omitempty"` + CommittedAt uint64 `protobuf:"varint,3,opt,name=committed_at,json=committedAt,proto3" json:"committed_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VoteCommit) Reset() { + *x = VoteCommit{} + mi := &file_tx_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VoteCommit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VoteCommit) ProtoMessage() {} + +func (x *VoteCommit) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VoteCommit.ProtoReflect.Descriptor instead. +func (*VoteCommit) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{11} +} + +func (x *VoteCommit) GetVoterAddr() []byte { + if x != nil { + return x.VoterAddr + } + return nil +} + +func (x *VoteCommit) GetCommitHash() []byte { + if x != nil { + return x.CommitHash + } + return nil +} + +func (x *VoteCommit) GetCommittedAt() uint64 { + if x != nil { + return x.CommittedAt + } + return 0 +} + +type VoteReveal struct { + state protoimpl.MessageState `protogen:"open.v1"` + VoterAddr []byte `protobuf:"bytes,1,opt,name=voter_addr,json=voterAddr,proto3" json:"voter_addr,omitempty"` + Vote bool `protobuf:"varint,2,opt,name=vote,proto3" json:"vote,omitempty"` + RevealedAt uint64 `protobuf:"varint,3,opt,name=revealed_at,json=revealedAt,proto3" json:"revealed_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VoteReveal) Reset() { + *x = VoteReveal{} + mi := &file_tx_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VoteReveal) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VoteReveal) ProtoMessage() {} + +func (x *VoteReveal) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VoteReveal.ProtoReflect.Descriptor instead. +func (*VoteReveal) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{12} +} + +func (x *VoteReveal) GetVoterAddr() []byte { + if x != nil { + return x.VoterAddr + } + return nil +} + +func (x *VoteReveal) GetVote() bool { + if x != nil { + return x.Vote + } + return false +} + +func (x *VoteReveal) GetRevealedAt() uint64 { + if x != nil { + return x.RevealedAt + } + return 0 +} + +type SlashRecord struct { + state protoimpl.MessageState `protogen:"open.v1"` + SlashedAddress []byte `protobuf:"bytes,1,opt,name=slashed_address,json=slashedAddress,proto3" json:"slashed_address,omitempty"` + SlashAmount uint64 `protobuf:"varint,2,opt,name=slash_amount,json=slashAmount,proto3" json:"slash_amount,omitempty"` + SlashedAt uint64 `protobuf:"varint,3,opt,name=slashed_at,json=slashedAt,proto3" json:"slashed_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SlashRecord) Reset() { + *x = SlashRecord{} + mi := &file_tx_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SlashRecord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SlashRecord) ProtoMessage() {} + +func (x *SlashRecord) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SlashRecord.ProtoReflect.Descriptor instead. +func (*SlashRecord) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{13} +} + +func (x *SlashRecord) GetSlashedAddress() []byte { + if x != nil { + return x.SlashedAddress + } + return nil +} + +func (x *SlashRecord) GetSlashAmount() uint64 { + if x != nil { + return x.SlashAmount + } + return 0 +} + +func (x *SlashRecord) GetSlashedAt() uint64 { + if x != nil { + return x.SlashedAt + } + return 0 +} + +type PanelEntropyAccum struct { + state protoimpl.MessageState `protogen:"open.v1"` + Accumulator uint64 `protobuf:"varint,1,opt,name=accumulator,proto3" json:"accumulator,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PanelEntropyAccum) Reset() { + *x = PanelEntropyAccum{} + mi := &file_tx_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PanelEntropyAccum) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PanelEntropyAccum) ProtoMessage() {} + +func (x *PanelEntropyAccum) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PanelEntropyAccum.ProtoReflect.Descriptor instead. +func (*PanelEntropyAccum) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{14} +} + +func (x *PanelEntropyAccum) GetAccumulator() uint64 { + if x != nil { + return x.Accumulator + } + return 0 +} + +// Transaction messages (12 types) +type MessageSend struct { + state protoimpl.MessageState `protogen:"open.v1"` + FromAddress []byte `protobuf:"bytes,1,opt,name=from_address,json=fromAddress,proto3" json:"from_address,omitempty"` + ToAddress []byte `protobuf:"bytes,2,opt,name=to_address,json=toAddress,proto3" json:"to_address,omitempty"` + Amount uint64 `protobuf:"varint,3,opt,name=amount,proto3" json:"amount,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageSend) Reset() { + *x = MessageSend{} + mi := &file_tx_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageSend) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageSend) ProtoMessage() {} + +func (x *MessageSend) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageSend.ProtoReflect.Descriptor instead. +func (*MessageSend) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{15} +} + +func (x *MessageSend) GetFromAddress() []byte { + if x != nil { + return x.FromAddress + } + return nil +} + +func (x *MessageSend) GetToAddress() []byte { + if x != nil { + return x.ToAddress + } + return nil +} + +func (x *MessageSend) GetAmount() uint64 { + if x != nil { + return x.Amount + } + return 0 +} + +type MessageCreateMarket struct { + state protoimpl.MessageState `protogen:"open.v1"` + CreatorAddress []byte `protobuf:"bytes,1,opt,name=creator_address,json=creatorAddress,proto3" json:"creator_address,omitempty"` + B0 uint64 `protobuf:"varint,2,opt,name=b0,proto3" json:"b0,omitempty"` + ExpiryTime uint64 `protobuf:"varint,3,opt,name=expiry_time,json=expiryTime,proto3" json:"expiry_time,omitempty"` + Nonce uint64 `protobuf:"varint,4,opt,name=nonce,proto3" json:"nonce,omitempty"` + Question string `protobuf:"bytes,5,opt,name=question,proto3" json:"question,omitempty"` + Rules string `protobuf:"bytes,6,opt,name=rules,proto3" json:"rules,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageCreateMarket) Reset() { + *x = MessageCreateMarket{} + mi := &file_tx_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageCreateMarket) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageCreateMarket) ProtoMessage() {} + +func (x *MessageCreateMarket) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageCreateMarket.ProtoReflect.Descriptor instead. +func (*MessageCreateMarket) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{16} +} + +func (x *MessageCreateMarket) GetCreatorAddress() []byte { + if x != nil { + return x.CreatorAddress + } + return nil +} + +func (x *MessageCreateMarket) GetB0() uint64 { + if x != nil { + return x.B0 + } + return 0 +} + +func (x *MessageCreateMarket) GetExpiryTime() uint64 { + if x != nil { + return x.ExpiryTime + } + return 0 +} + +func (x *MessageCreateMarket) GetNonce() uint64 { + if x != nil { + return x.Nonce + } + return 0 +} + +func (x *MessageCreateMarket) GetQuestion() string { + if x != nil { + return x.Question + } + return "" +} + +func (x *MessageCreateMarket) GetRules() string { + if x != nil { + return x.Rules + } + return "" +} + +type MessageSubmitPrediction struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + BettorAddress []byte `protobuf:"bytes,2,opt,name=bettor_address,json=bettorAddress,proto3" json:"bettor_address,omitempty"` + Outcome bool `protobuf:"varint,3,opt,name=outcome,proto3" json:"outcome,omitempty"` + Shares uint64 `protobuf:"varint,4,opt,name=shares,proto3" json:"shares,omitempty"` + MaxCost uint64 `protobuf:"varint,5,opt,name=max_cost,json=maxCost,proto3" json:"max_cost,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageSubmitPrediction) Reset() { + *x = MessageSubmitPrediction{} + mi := &file_tx_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageSubmitPrediction) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageSubmitPrediction) ProtoMessage() {} + +func (x *MessageSubmitPrediction) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageSubmitPrediction.ProtoReflect.Descriptor instead. +func (*MessageSubmitPrediction) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{17} +} + +func (x *MessageSubmitPrediction) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageSubmitPrediction) GetBettorAddress() []byte { + if x != nil { + return x.BettorAddress + } + return nil +} + +func (x *MessageSubmitPrediction) GetOutcome() bool { + if x != nil { + return x.Outcome + } + return false +} + +func (x *MessageSubmitPrediction) GetShares() uint64 { + if x != nil { + return x.Shares + } + return 0 +} + +func (x *MessageSubmitPrediction) GetMaxCost() uint64 { + if x != nil { + return x.MaxCost + } + return 0 +} + +type MessageClaimWinnings struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + ClaimantAddress []byte `protobuf:"bytes,2,opt,name=claimant_address,json=claimantAddress,proto3" json:"claimant_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimWinnings) Reset() { + *x = MessageClaimWinnings{} + mi := &file_tx_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimWinnings) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimWinnings) ProtoMessage() {} + +func (x *MessageClaimWinnings) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimWinnings.ProtoReflect.Descriptor instead. +func (*MessageClaimWinnings) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{18} +} + +func (x *MessageClaimWinnings) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageClaimWinnings) GetClaimantAddress() []byte { + if x != nil { + return x.ClaimantAddress + } + return nil +} + +type MessageResolveMarket struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + ResolverAddress []byte `protobuf:"bytes,2,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + WinningOutcome bool `protobuf:"varint,3,opt,name=winning_outcome,json=winningOutcome,proto3" json:"winning_outcome,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageResolveMarket) Reset() { + *x = MessageResolveMarket{} + mi := &file_tx_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageResolveMarket) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageResolveMarket) ProtoMessage() {} + +func (x *MessageResolveMarket) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageResolveMarket.ProtoReflect.Descriptor instead. +func (*MessageResolveMarket) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{19} +} + +func (x *MessageResolveMarket) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageResolveMarket) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +func (x *MessageResolveMarket) GetWinningOutcome() bool { + if x != nil { + return x.WinningOutcome + } + return false +} + +type MessageRegisterResolver struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddress []byte `protobuf:"bytes,1,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + StakeAmount uint64 `protobuf:"varint,2,opt,name=stake_amount,json=stakeAmount,proto3" json:"stake_amount,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageRegisterResolver) Reset() { + *x = MessageRegisterResolver{} + mi := &file_tx_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageRegisterResolver) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageRegisterResolver) ProtoMessage() {} + +func (x *MessageRegisterResolver) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageRegisterResolver.ProtoReflect.Descriptor instead. +func (*MessageRegisterResolver) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{20} +} + +func (x *MessageRegisterResolver) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +func (x *MessageRegisterResolver) GetStakeAmount() uint64 { + if x != nil { + return x.StakeAmount + } + return 0 +} + +type MessageProposeOutcome struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + ResolverAddress []byte `protobuf:"bytes,2,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + ProposedOutcome bool `protobuf:"varint,3,opt,name=proposed_outcome,json=proposedOutcome,proto3" json:"proposed_outcome,omitempty"` + ProposalBond uint64 `protobuf:"varint,4,opt,name=proposal_bond,json=proposalBond,proto3" json:"proposal_bond,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageProposeOutcome) Reset() { + *x = MessageProposeOutcome{} + mi := &file_tx_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageProposeOutcome) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageProposeOutcome) ProtoMessage() {} + +func (x *MessageProposeOutcome) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageProposeOutcome.ProtoReflect.Descriptor instead. +func (*MessageProposeOutcome) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{21} +} + +func (x *MessageProposeOutcome) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageProposeOutcome) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +func (x *MessageProposeOutcome) GetProposedOutcome() bool { + if x != nil { + return x.ProposedOutcome + } + return false +} + +func (x *MessageProposeOutcome) GetProposalBond() uint64 { + if x != nil { + return x.ProposalBond + } + return 0 +} + +type MessageFileDispute struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + DisputerAddress []byte `protobuf:"bytes,2,opt,name=disputer_address,json=disputerAddress,proto3" json:"disputer_address,omitempty"` + DisputeBond uint64 `protobuf:"varint,3,opt,name=dispute_bond,json=disputeBond,proto3" json:"dispute_bond,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageFileDispute) Reset() { + *x = MessageFileDispute{} + mi := &file_tx_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageFileDispute) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageFileDispute) ProtoMessage() {} + +func (x *MessageFileDispute) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageFileDispute.ProtoReflect.Descriptor instead. +func (*MessageFileDispute) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{22} +} + +func (x *MessageFileDispute) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageFileDispute) GetDisputerAddress() []byte { + if x != nil { + return x.DisputerAddress + } + return nil +} + +func (x *MessageFileDispute) GetDisputeBond() uint64 { + if x != nil { + return x.DisputeBond + } + return 0 +} + +type MessageCommitVote struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + VoterAddr []byte `protobuf:"bytes,2,opt,name=voter_addr,json=voterAddr,proto3" json:"voter_addr,omitempty"` + CommitHash []byte `protobuf:"bytes,3,opt,name=commit_hash,json=commitHash,proto3" json:"commit_hash,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageCommitVote) Reset() { + *x = MessageCommitVote{} + mi := &file_tx_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageCommitVote) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageCommitVote) ProtoMessage() {} + +func (x *MessageCommitVote) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageCommitVote.ProtoReflect.Descriptor instead. +func (*MessageCommitVote) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{23} +} + +func (x *MessageCommitVote) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageCommitVote) GetVoterAddr() []byte { + if x != nil { + return x.VoterAddr + } + return nil +} + +func (x *MessageCommitVote) GetCommitHash() []byte { + if x != nil { + return x.CommitHash + } + return nil +} + +type MessageRevealVote struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + VoterAddr []byte `protobuf:"bytes,2,opt,name=voter_addr,json=voterAddr,proto3" json:"voter_addr,omitempty"` + Vote bool `protobuf:"varint,3,opt,name=vote,proto3" json:"vote,omitempty"` + Nonce []byte `protobuf:"bytes,4,opt,name=nonce,proto3" json:"nonce,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageRevealVote) Reset() { + *x = MessageRevealVote{} + mi := &file_tx_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageRevealVote) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageRevealVote) ProtoMessage() {} + +func (x *MessageRevealVote) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageRevealVote.ProtoReflect.Descriptor instead. +func (*MessageRevealVote) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{24} +} + +func (x *MessageRevealVote) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageRevealVote) GetVoterAddr() []byte { + if x != nil { + return x.VoterAddr + } + return nil +} + +func (x *MessageRevealVote) GetVote() bool { + if x != nil { + return x.Vote + } + return false +} + +func (x *MessageRevealVote) GetNonce() []byte { + if x != nil { + return x.Nonce + } + return nil +} + +type MessageTallyVotes struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + CallerAddr []byte `protobuf:"bytes,2,opt,name=caller_addr,json=callerAddr,proto3" json:"caller_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageTallyVotes) Reset() { + *x = MessageTallyVotes{} + mi := &file_tx_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageTallyVotes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageTallyVotes) ProtoMessage() {} + +func (x *MessageTallyVotes) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageTallyVotes.ProtoReflect.Descriptor instead. +func (*MessageTallyVotes) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{25} +} + +func (x *MessageTallyVotes) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageTallyVotes) GetCallerAddr() []byte { + if x != nil { + return x.CallerAddr + } + return nil +} + +type MessageFinalizeMarket struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + CallerAddr []byte `protobuf:"bytes,2,opt,name=caller_addr,json=callerAddr,proto3" json:"caller_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageFinalizeMarket) Reset() { + *x = MessageFinalizeMarket{} + mi := &file_tx_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageFinalizeMarket) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageFinalizeMarket) ProtoMessage() {} + +func (x *MessageFinalizeMarket) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageFinalizeMarket.ProtoReflect.Descriptor instead. +func (*MessageFinalizeMarket) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{26} +} + +func (x *MessageFinalizeMarket) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageFinalizeMarket) GetCallerAddr() []byte { + if x != nil { + return x.CallerAddr + } + return nil +} + +type MessageClaimSlash struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + ClaimantAddress []byte `protobuf:"bytes,2,opt,name=claimant_address,json=claimantAddress,proto3" json:"claimant_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimSlash) Reset() { + *x = MessageClaimSlash{} + mi := &file_tx_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimSlash) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimSlash) ProtoMessage() {} + +func (x *MessageClaimSlash) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimSlash.ProtoReflect.Descriptor instead. +func (*MessageClaimSlash) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{27} +} + +func (x *MessageClaimSlash) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageClaimSlash) GetClaimantAddress() []byte { + if x != nil { + return x.ClaimantAddress + } + return nil +} + +type MessageReclaimStake struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + ClaimantAddress []byte `protobuf:"bytes,2,opt,name=claimant_address,json=claimantAddress,proto3" json:"claimant_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageReclaimStake) Reset() { + *x = MessageReclaimStake{} + mi := &file_tx_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageReclaimStake) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageReclaimStake) ProtoMessage() {} + +func (x *MessageReclaimStake) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageReclaimStake.ProtoReflect.Descriptor instead. +func (*MessageReclaimStake) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{28} +} + +func (x *MessageReclaimStake) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageReclaimStake) GetClaimantAddress() []byte { + if x != nil { + return x.ClaimantAddress + } + return nil +} + +// MessageForfeitPosition allows a resolver to voluntarily exit a position +// in a market they intend to resolve — satisfying the COI-1 requirement. +// Issue-2: without this tx type, a resolver with even 1 share is permanently +// disqualified from resolving, with no protocol-level escape hatch. +// The full CostPaid is refunded; shares are zeroed atomically. +type MessageForfeitPosition struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + ResolverAddress []byte `protobuf:"bytes,2,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageForfeitPosition) Reset() { + *x = MessageForfeitPosition{} + mi := &file_tx_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageForfeitPosition) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageForfeitPosition) ProtoMessage() {} + +func (x *MessageForfeitPosition) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageForfeitPosition.ProtoReflect.Descriptor instead. +func (*MessageForfeitPosition) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{29} +} + +func (x *MessageForfeitPosition) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageForfeitPosition) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +type MessageClaimBuilderReward struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimBuilderReward) Reset() { + *x = MessageClaimBuilderReward{} + mi := &file_tx_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimBuilderReward) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimBuilderReward) ProtoMessage() {} + +func (x *MessageClaimBuilderReward) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimBuilderReward.ProtoReflect.Descriptor instead. +func (*MessageClaimBuilderReward) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{30} +} + +type MessageClaimCreatorFee struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + CreatorAddress []byte `protobuf:"bytes,2,opt,name=creator_address,json=creatorAddress,proto3" json:"creator_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimCreatorFee) Reset() { + *x = MessageClaimCreatorFee{} + mi := &file_tx_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimCreatorFee) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimCreatorFee) ProtoMessage() {} + +func (x *MessageClaimCreatorFee) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimCreatorFee.ProtoReflect.Descriptor instead. +func (*MessageClaimCreatorFee) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{31} +} + +func (x *MessageClaimCreatorFee) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageClaimCreatorFee) GetCreatorAddress() []byte { + if x != nil { + return x.CreatorAddress + } + return nil +} + +type MessageClaimResolverReward struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddress []byte `protobuf:"bytes,1,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + Epoch uint64 `protobuf:"varint,2,opt,name=epoch,proto3" json:"epoch,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimResolverReward) Reset() { + *x = MessageClaimResolverReward{} + mi := &file_tx_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimResolverReward) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimResolverReward) ProtoMessage() {} + +func (x *MessageClaimResolverReward) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimResolverReward.ProtoReflect.Descriptor instead. +func (*MessageClaimResolverReward) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{32} +} + +func (x *MessageClaimResolverReward) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +func (x *MessageClaimResolverReward) GetEpoch() uint64 { + if x != nil { + return x.Epoch + } + return 0 +} + +type LastClaimedBlock struct { + state protoimpl.MessageState `protogen:"open.v1"` + Height uint64 `protobuf:"varint,1,opt,name=height,proto3" json:"height,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LastClaimedBlock) Reset() { + *x = LastClaimedBlock{} + mi := &file_tx_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LastClaimedBlock) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LastClaimedBlock) ProtoMessage() {} + +func (x *LastClaimedBlock) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LastClaimedBlock.ProtoReflect.Descriptor instead. +func (*LastClaimedBlock) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{33} +} + +func (x *LastClaimedBlock) GetHeight() uint64 { + if x != nil { + return x.Height + } + return 0 +} + +type MessageClaimCommunityReward struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimCommunityReward) Reset() { + *x = MessageClaimCommunityReward{} + mi := &file_tx_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimCommunityReward) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimCommunityReward) ProtoMessage() {} + +func (x *MessageClaimCommunityReward) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimCommunityReward.ProtoReflect.Descriptor instead. +func (*MessageClaimCommunityReward) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{34} +} + +type MessageClaimInvestorReward struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimInvestorReward) Reset() { + *x = MessageClaimInvestorReward{} + mi := &file_tx_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimInvestorReward) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimInvestorReward) ProtoMessage() {} + +func (x *MessageClaimInvestorReward) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimInvestorReward.ProtoReflect.Descriptor instead. +func (*MessageClaimInvestorReward) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{35} +} + +type MessageClaimProtocolReward struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimProtocolReward) Reset() { + *x = MessageClaimProtocolReward{} + mi := &file_tx_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimProtocolReward) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimProtocolReward) ProtoMessage() {} + +func (x *MessageClaimProtocolReward) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimProtocolReward.ProtoReflect.Descriptor instead. +func (*MessageClaimProtocolReward) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{36} +} + +type MessageCancelMarket struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + CreatorAddress []byte `protobuf:"bytes,2,opt,name=creator_address,json=creatorAddress,proto3" json:"creator_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageCancelMarket) Reset() { + *x = MessageCancelMarket{} + mi := &file_tx_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageCancelMarket) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageCancelMarket) ProtoMessage() {} + +func (x *MessageCancelMarket) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageCancelMarket.ProtoReflect.Descriptor instead. +func (*MessageCancelMarket) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{37} +} + +func (x *MessageCancelMarket) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageCancelMarket) GetCreatorAddress() []byte { + if x != nil { + return x.CreatorAddress + } + return nil +} + +type MessageUnstakeResolver struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddress []byte `protobuf:"bytes,1,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + Amount uint64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"` // 0 = full exit + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageUnstakeResolver) Reset() { + *x = MessageUnstakeResolver{} + mi := &file_tx_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageUnstakeResolver) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageUnstakeResolver) ProtoMessage() {} + +func (x *MessageUnstakeResolver) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageUnstakeResolver.ProtoReflect.Descriptor instead. +func (*MessageUnstakeResolver) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{38} +} + +func (x *MessageUnstakeResolver) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +func (x *MessageUnstakeResolver) GetAmount() uint64 { + if x != nil { + return x.Amount + } + return 0 +} + +type MessageClaimUnbondedStake struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddress []byte `protobuf:"bytes,1,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimUnbondedStake) Reset() { + *x = MessageClaimUnbondedStake{} + mi := &file_tx_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimUnbondedStake) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimUnbondedStake) ProtoMessage() {} + +func (x *MessageClaimUnbondedStake) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[39] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimUnbondedStake.ProtoReflect.Descriptor instead. +func (*MessageClaimUnbondedStake) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{39} +} + +func (x *MessageClaimUnbondedStake) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +type ResolverIndex struct { + state protoimpl.MessageState `protogen:"open.v1"` + Addresses [][]byte `protobuf:"bytes,1,rep,name=addresses,proto3" json:"addresses,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResolverIndex) Reset() { + *x = ResolverIndex{} + mi := &file_tx_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResolverIndex) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResolverIndex) ProtoMessage() {} + +func (x *ResolverIndex) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[40] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResolverIndex.ProtoReflect.Descriptor instead. +func (*ResolverIndex) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{40} +} + +func (x *ResolverIndex) GetAddresses() [][]byte { + if x != nil { + return x.Addresses + } + return nil +} + +type MarketIndex struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketIds [][]byte `protobuf:"bytes,1,rep,name=market_ids,json=marketIds,proto3" json:"market_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MarketIndex) Reset() { + *x = MarketIndex{} + mi := &file_tx_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MarketIndex) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MarketIndex) ProtoMessage() {} + +func (x *MarketIndex) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[41] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MarketIndex.ProtoReflect.Descriptor instead. +func (*MarketIndex) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{41} +} + +func (x *MarketIndex) GetMarketIds() [][]byte { + if x != nil { + return x.MarketIds + } + return nil +} + +type GlobalStats struct { + state protoimpl.MessageState `protogen:"open.v1"` + TotalWeightedResolutions uint64 `protobuf:"varint,1,opt,name=total_weighted_resolutions,json=totalWeightedResolutions,proto3" json:"total_weighted_resolutions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GlobalStats) Reset() { + *x = GlobalStats{} + mi := &file_tx_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GlobalStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GlobalStats) ProtoMessage() {} + +func (x *GlobalStats) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[42] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GlobalStats.ProtoReflect.Descriptor instead. +func (*GlobalStats) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{42} +} + +func (x *GlobalStats) GetTotalWeightedResolutions() uint64 { + if x != nil { + return x.TotalWeightedResolutions + } + return 0 +} + +var File_tx_proto protoreflect.FileDescriptor + +const file_tx_proto_rawDesc = "" + + "\n" + + "\btx.proto\x12\x05types\x1a\x19google/protobuf/any.proto\"H\n" + "\tSignature\x12\x1d\n" + "\n" + "public_key\x18\x01 \x01(\fR\tpublicKey\x12\x1c\n" + - "\tsignature\x18\x02 \x01(\fR\tsignatureB.Z,github.com/canopy-network/go-plugin/contractb\x06proto3" + "\tsignature\x18\x02 \x01(\fR\tsignature\"\xa3\x02\n" + + "\vTransaction\x12!\n" + + "\fmessage_type\x18\x01 \x01(\tR\vmessageType\x12&\n" + + "\x03msg\x18\x02 \x01(\v2\x14.google.protobuf.AnyR\x03msg\x12.\n" + + "\tsignature\x18\x03 \x01(\v2\x10.types.SignatureR\tsignature\x12%\n" + + "\x0ecreated_height\x18\x04 \x01(\x04R\rcreatedHeight\x12\x12\n" + + "\x04time\x18\x05 \x01(\x04R\x04time\x12\x10\n" + + "\x03fee\x18\x06 \x01(\x04R\x03fee\x12\x12\n" + + "\x04memo\x18\a \x01(\tR\x04memo\x12\x1d\n" + + "\n" + + "network_id\x18\b \x01(\x04R\tnetworkId\x12\x19\n" + + "\bchain_id\x18\t \x01(\x04R\achainId\"\xe2\x01\n" + + "\tFeeParams\x12\x19\n" + + "\bsend_fee\x18\x01 \x01(\x04R\asendFee\x12*\n" + + "\x11create_market_fee\x18\x02 \x01(\x04R\x0fcreateMarketFee\x122\n" + + "\x15submit_prediction_fee\x18\x03 \x01(\x04R\x13submitPredictionFee\x12,\n" + + "\x12resolve_market_fee\x18\x04 \x01(\x04R\x10resolveMarketFee\x12,\n" + + "\x12claim_winnings_fee\x18\x05 \x01(\x04R\x10claimWinningsFee\"\x93\x03\n" + + "\vMarketState\x12\x16\n" + + "\x06status\x18\x01 \x01(\rR\x06status\x12\x1f\n" + + "\vexpiry_time\x18\x02 \x01(\x04R\n" + + "expiryTime\x12\x13\n" + + "\x05q_yes\x18\x03 \x01(\x04R\x04qYes\x12\x11\n" + + "\x04q_no\x18\x04 \x01(\x04R\x03qNo\x12\x13\n" + + "\x05b_eff\x18\x05 \x01(\x04R\x04bEff\x12\x18\n" + + "\acreator\x18\x06 \x01(\fR\acreator\x12#\n" + + "\rclaimed_count\x18\a \x01(\x04R\fclaimedCount\x12'\n" + + "\x0ftotal_positions\x18\b \x01(\x04R\x0etotalPositions\x12\x1b\n" + + "\topen_time\x18\t \x01(\x04R\bopenTime\x12#\n" + + "\relevated_risk\x18\n" + + " \x01(\bR\felevatedRisk\x122\n" + + "\x15finalized_pool_amount\x18\v \x01(\x04R\x13finalizedPoolAmount\x12\x1a\n" + + "\bquestion\x18\f \x01(\tR\bquestion\x12\x14\n" + + "\x05rules\x18\r \x01(\tR\x05rules\"\x82\x01\n" + + "\rPositionState\x12\x1d\n" + + "\n" + + "shares_yes\x18\x01 \x01(\x04R\tsharesYes\x12\x1b\n" + + "\tshares_no\x18\x02 \x01(\x04R\bsharesNo\x12\x1b\n" + + "\tcost_paid\x18\x03 \x01(\x04R\bcostPaid\x12\x18\n" + + "\aclaimed\x18\x04 \x01(\bR\aclaimed\"X\n" + + "\fOutcomeState\x12'\n" + + "\x0fwinning_outcome\x18\x01 \x01(\bR\x0ewinningOutcome\x12\x1f\n" + + "\vresolved_at\x18\x02 \x01(\x04R\n" + + "resolvedAt\"[\n" + + "\x0fTreasuryReserve\x12%\n" + + "\x0elocked_reserve\x18\x01 \x01(\x04R\rlockedReserve\x12!\n" + + "\fcreator_bond\x18\x02 \x01(\x04R\vcreatorBond\":\n" + + "\rResolverState\x12)\n" + + "\x10resolver_address\x18\x01 \x01(\fR\x0fresolverAddress\"\x87\x03\n" + + "\x0eResolverRecord\x12)\n" + + "\x10resolver_address\x18\x01 \x01(\fR\x0fresolverAddress\x12!\n" + + "\fstake_amount\x18\x02 \x01(\x04R\vstakeAmount\x12\x1b\n" + + "\trrs_score\x18\x03 \x01(\x04R\brrsScore\x12#\n" + + "\rregistered_at\x18\x04 \x01(\x04R\fregisteredAt\x125\n" + + "\x16successful_resolutions\x18\x05 \x01(\x04R\x15successfulResolutions\x12,\n" + + "\x12last_claimed_epoch\x18\x06 \x01(\x04R\x10lastClaimedEpoch\x12\x1b\n" + + "\tis_active\x18\a \x01(\bR\bisActive\x12)\n" + + "\x10unbonding_amount\x18\b \x01(\x04R\x0funbondingAmount\x128\n" + + "\x18unbonding_release_height\x18\t \x01(\x04R\x16unbondingReleaseHeight\"\xc4\x01\n" + + "\x0eProposalRecord\x12#\n" + + "\rresolver_addr\x18\x01 \x01(\fR\fresolverAddr\x12)\n" + + "\x10proposed_outcome\x18\x02 \x01(\bR\x0fproposedOutcome\x12#\n" + + "\rproposal_bond\x18\x03 \x01(\x04R\fproposalBond\x12%\n" + + "\x0eproposal_block\x18\x04 \x01(\x04R\rproposalBlock\x12\x16\n" + + "\x06status\x18\x05 \x01(\rR\x06status\"\xe7\x01\n" + + "\rDisputeRecord\x12)\n" + + "\x10disputer_address\x18\x01 \x01(\fR\x0fdisputerAddress\x12!\n" + + "\fdispute_bond\x18\x02 \x01(\x04R\vdisputeBond\x12#\n" + + "\rdispute_block\x18\x03 \x01(\x04R\fdisputeBlock\x12\x1f\n" + + "\vvote_status\x18\x04 \x01(\rR\n" + + "voteStatus\x12\x1d\n" + + "\n" + + "panel_size\x18\x05 \x01(\rR\tpanelSize\x12#\n" + + "\rpanel_members\x18\x06 \x03(\fR\fpanelMembers\"o\n" + + "\n" + + "VoteCommit\x12\x1d\n" + + "\n" + + "voter_addr\x18\x01 \x01(\fR\tvoterAddr\x12\x1f\n" + + "\vcommit_hash\x18\x02 \x01(\fR\n" + + "commitHash\x12!\n" + + "\fcommitted_at\x18\x03 \x01(\x04R\vcommittedAt\"`\n" + + "\n" + + "VoteReveal\x12\x1d\n" + + "\n" + + "voter_addr\x18\x01 \x01(\fR\tvoterAddr\x12\x12\n" + + "\x04vote\x18\x02 \x01(\bR\x04vote\x12\x1f\n" + + "\vrevealed_at\x18\x03 \x01(\x04R\n" + + "revealedAt\"x\n" + + "\vSlashRecord\x12'\n" + + "\x0fslashed_address\x18\x01 \x01(\fR\x0eslashedAddress\x12!\n" + + "\fslash_amount\x18\x02 \x01(\x04R\vslashAmount\x12\x1d\n" + + "\n" + + "slashed_at\x18\x03 \x01(\x04R\tslashedAt\"5\n" + + "\x11PanelEntropyAccum\x12 \n" + + "\vaccumulator\x18\x01 \x01(\x04R\vaccumulator\"g\n" + + "\vMessageSend\x12!\n" + + "\ffrom_address\x18\x01 \x01(\fR\vfromAddress\x12\x1d\n" + + "\n" + + "to_address\x18\x02 \x01(\fR\ttoAddress\x12\x16\n" + + "\x06amount\x18\x03 \x01(\x04R\x06amount\"\xb7\x01\n" + + "\x13MessageCreateMarket\x12'\n" + + "\x0fcreator_address\x18\x01 \x01(\fR\x0ecreatorAddress\x12\x0e\n" + + "\x02b0\x18\x02 \x01(\x04R\x02b0\x12\x1f\n" + + "\vexpiry_time\x18\x03 \x01(\x04R\n" + + "expiryTime\x12\x14\n" + + "\x05nonce\x18\x04 \x01(\x04R\x05nonce\x12\x1a\n" + + "\bquestion\x18\x05 \x01(\tR\bquestion\x12\x14\n" + + "\x05rules\x18\x06 \x01(\tR\x05rules\"\xaa\x01\n" + + "\x17MessageSubmitPrediction\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12%\n" + + "\x0ebettor_address\x18\x02 \x01(\fR\rbettorAddress\x12\x18\n" + + "\aoutcome\x18\x03 \x01(\bR\aoutcome\x12\x16\n" + + "\x06shares\x18\x04 \x01(\x04R\x06shares\x12\x19\n" + + "\bmax_cost\x18\x05 \x01(\x04R\amaxCost\"^\n" + + "\x14MessageClaimWinnings\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10claimant_address\x18\x02 \x01(\fR\x0fclaimantAddress\"\x87\x01\n" + + "\x14MessageResolveMarket\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10resolver_address\x18\x02 \x01(\fR\x0fresolverAddress\x12'\n" + + "\x0fwinning_outcome\x18\x03 \x01(\bR\x0ewinningOutcome\"g\n" + + "\x17MessageRegisterResolver\x12)\n" + + "\x10resolver_address\x18\x01 \x01(\fR\x0fresolverAddress\x12!\n" + + "\fstake_amount\x18\x02 \x01(\x04R\vstakeAmount\"\xaf\x01\n" + + "\x15MessageProposeOutcome\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10resolver_address\x18\x02 \x01(\fR\x0fresolverAddress\x12)\n" + + "\x10proposed_outcome\x18\x03 \x01(\bR\x0fproposedOutcome\x12#\n" + + "\rproposal_bond\x18\x04 \x01(\x04R\fproposalBond\"\x7f\n" + + "\x12MessageFileDispute\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10disputer_address\x18\x02 \x01(\fR\x0fdisputerAddress\x12!\n" + + "\fdispute_bond\x18\x03 \x01(\x04R\vdisputeBond\"p\n" + + "\x11MessageCommitVote\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12\x1d\n" + + "\n" + + "voter_addr\x18\x02 \x01(\fR\tvoterAddr\x12\x1f\n" + + "\vcommit_hash\x18\x03 \x01(\fR\n" + + "commitHash\"y\n" + + "\x11MessageRevealVote\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12\x1d\n" + + "\n" + + "voter_addr\x18\x02 \x01(\fR\tvoterAddr\x12\x12\n" + + "\x04vote\x18\x03 \x01(\bR\x04vote\x12\x14\n" + + "\x05nonce\x18\x04 \x01(\fR\x05nonce\"Q\n" + + "\x11MessageTallyVotes\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12\x1f\n" + + "\vcaller_addr\x18\x02 \x01(\fR\n" + + "callerAddr\"U\n" + + "\x15MessageFinalizeMarket\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12\x1f\n" + + "\vcaller_addr\x18\x02 \x01(\fR\n" + + "callerAddr\"[\n" + + "\x11MessageClaimSlash\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10claimant_address\x18\x02 \x01(\fR\x0fclaimantAddress\"]\n" + + "\x13MessageReclaimStake\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10claimant_address\x18\x02 \x01(\fR\x0fclaimantAddress\"`\n" + + "\x16MessageForfeitPosition\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10resolver_address\x18\x02 \x01(\fR\x0fresolverAddress\"\x1b\n" + + "\x19MessageClaimBuilderReward\"^\n" + + "\x16MessageClaimCreatorFee\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12'\n" + + "\x0fcreator_address\x18\x02 \x01(\fR\x0ecreatorAddress\"]\n" + + "\x1aMessageClaimResolverReward\x12)\n" + + "\x10resolver_address\x18\x01 \x01(\fR\x0fresolverAddress\x12\x14\n" + + "\x05epoch\x18\x02 \x01(\x04R\x05epoch\"*\n" + + "\x10LastClaimedBlock\x12\x16\n" + + "\x06height\x18\x01 \x01(\x04R\x06height\"\x1d\n" + + "\x1bMessageClaimCommunityReward\"\x1c\n" + + "\x1aMessageClaimInvestorReward\"\x1c\n" + + "\x1aMessageClaimProtocolReward\"[\n" + + "\x13MessageCancelMarket\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12'\n" + + "\x0fcreator_address\x18\x02 \x01(\fR\x0ecreatorAddress\"[\n" + + "\x16MessageUnstakeResolver\x12)\n" + + "\x10resolver_address\x18\x01 \x01(\fR\x0fresolverAddress\x12\x16\n" + + "\x06amount\x18\x02 \x01(\x04R\x06amount\"F\n" + + "\x19MessageClaimUnbondedStake\x12)\n" + + "\x10resolver_address\x18\x01 \x01(\fR\x0fresolverAddress\"-\n" + + "\rResolverIndex\x12\x1c\n" + + "\taddresses\x18\x01 \x03(\fR\taddresses\",\n" + + "\vMarketIndex\x12\x1d\n" + + "\n" + + "market_ids\x18\x01 \x03(\fR\tmarketIds\"K\n" + + "\vGlobalStats\x12<\n" + + "\x1atotal_weighted_resolutions\x18\x01 \x01(\x04R\x18totalWeightedResolutionsB5Z3github.com/canopy-network/canopy/plugin/go/contractb\x06proto3" var ( file_tx_proto_rawDescOnce sync.Once @@ -348,22 +2797,61 @@ func file_tx_proto_rawDescGZIP() []byte { return file_tx_proto_rawDescData } -var file_tx_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_tx_proto_msgTypes = make([]protoimpl.MessageInfo, 43) var file_tx_proto_goTypes = []any{ - (*Transaction)(nil), // 0: types.Transaction - (*MessageSend)(nil), // 1: types.MessageSend - (*FeeParams)(nil), // 2: types.FeeParams - (*Signature)(nil), // 3: types.Signature - (*anypb.Any)(nil), // 4: google.protobuf.Any + (*Signature)(nil), // 0: types.Signature + (*Transaction)(nil), // 1: types.Transaction + (*FeeParams)(nil), // 2: types.FeeParams + (*MarketState)(nil), // 3: types.MarketState + (*PositionState)(nil), // 4: types.PositionState + (*OutcomeState)(nil), // 5: types.OutcomeState + (*TreasuryReserve)(nil), // 6: types.TreasuryReserve + (*ResolverState)(nil), // 7: types.ResolverState + (*ResolverRecord)(nil), // 8: types.ResolverRecord + (*ProposalRecord)(nil), // 9: types.ProposalRecord + (*DisputeRecord)(nil), // 10: types.DisputeRecord + (*VoteCommit)(nil), // 11: types.VoteCommit + (*VoteReveal)(nil), // 12: types.VoteReveal + (*SlashRecord)(nil), // 13: types.SlashRecord + (*PanelEntropyAccum)(nil), // 14: types.PanelEntropyAccum + (*MessageSend)(nil), // 15: types.MessageSend + (*MessageCreateMarket)(nil), // 16: types.MessageCreateMarket + (*MessageSubmitPrediction)(nil), // 17: types.MessageSubmitPrediction + (*MessageClaimWinnings)(nil), // 18: types.MessageClaimWinnings + (*MessageResolveMarket)(nil), // 19: types.MessageResolveMarket + (*MessageRegisterResolver)(nil), // 20: types.MessageRegisterResolver + (*MessageProposeOutcome)(nil), // 21: types.MessageProposeOutcome + (*MessageFileDispute)(nil), // 22: types.MessageFileDispute + (*MessageCommitVote)(nil), // 23: types.MessageCommitVote + (*MessageRevealVote)(nil), // 24: types.MessageRevealVote + (*MessageTallyVotes)(nil), // 25: types.MessageTallyVotes + (*MessageFinalizeMarket)(nil), // 26: types.MessageFinalizeMarket + (*MessageClaimSlash)(nil), // 27: types.MessageClaimSlash + (*MessageReclaimStake)(nil), // 28: types.MessageReclaimStake + (*MessageForfeitPosition)(nil), // 29: types.MessageForfeitPosition + (*MessageClaimBuilderReward)(nil), // 30: types.MessageClaimBuilderReward + (*MessageClaimCreatorFee)(nil), // 31: types.MessageClaimCreatorFee + (*MessageClaimResolverReward)(nil), // 32: types.MessageClaimResolverReward + (*LastClaimedBlock)(nil), // 33: types.LastClaimedBlock + (*MessageClaimCommunityReward)(nil), // 34: types.MessageClaimCommunityReward + (*MessageClaimInvestorReward)(nil), // 35: types.MessageClaimInvestorReward + (*MessageClaimProtocolReward)(nil), // 36: types.MessageClaimProtocolReward + (*MessageCancelMarket)(nil), // 37: types.MessageCancelMarket + (*MessageUnstakeResolver)(nil), // 38: types.MessageUnstakeResolver + (*MessageClaimUnbondedStake)(nil), // 39: types.MessageClaimUnbondedStake + (*ResolverIndex)(nil), // 40: types.ResolverIndex + (*MarketIndex)(nil), // 41: types.MarketIndex + (*GlobalStats)(nil), // 42: types.GlobalStats + (*any1.Any)(nil), // 43: google.protobuf.Any } var file_tx_proto_depIdxs = []int32{ - 4, // 0: types.Transaction.msg:type_name -> google.protobuf.Any - 3, // 1: types.Transaction.signature:type_name -> types.Signature - 2, // [2:2] is the sub-list for method output_type - 2, // [2:2] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 43, // 0: types.Transaction.msg:type_name -> google.protobuf.Any + 0, // 1: types.Transaction.signature:type_name -> types.Signature + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_tx_proto_init() } @@ -377,7 +2865,7 @@ func file_tx_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_tx_proto_rawDesc), len(file_tx_proto_rawDesc)), NumEnums: 0, - NumMessages: 4, + NumMessages: 43, NumExtensions: 0, NumServices: 0, }, diff --git a/plugin/go/contract/z_descriptor.go b/plugin/go/contract/z_descriptor.go new file mode 100644 index 0000000000..1c36248032 --- /dev/null +++ b/plugin/go/contract/z_descriptor.go @@ -0,0 +1,49 @@ +package contract + +import ( + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protodesc" + "google.golang.org/protobuf/reflect/protoregistry" +) + +func init() { + // File_tx_proto is already registered by tx.pb.go's own init() + // Just serialize it and all its dependencies + fd := File_tx_proto + fdp := protodesc.ToFileDescriptorProto(fd) + + seen := map[string]bool{fd.Path(): true} + result := [][]byte{} + + var addFile func(path string) + addFile = func(path string) { + f, err := protoregistry.GlobalFiles.FindFileByPath(path) + if err != nil { + return + } + fp := protodesc.ToFileDescriptorProto(f) + b, err := proto.Marshal(fp) + if err != nil { + panic(err) + } + result = append(result, b) + for _, dep := range fp.Dependency { + if !seen[dep] { + seen[dep] = true + addFile(dep) + } + } + } + + // Add deps first, then tx.proto itself + for _, dep := range fdp.Dependency { + if !seen[dep] { + seen[dep] = true + addFile(dep) + } + } + b, _ := proto.Marshal(fdp) + result = append(result, b) + + ContractConfig.FileDescriptorProtos = result +} diff --git a/plugin/go/crypto/signing.go b/plugin/go/crypto/signing.go index 9d46d0bc5f..af31da6eb8 100644 --- a/plugin/go/crypto/signing.go +++ b/plugin/go/crypto/signing.go @@ -1,7 +1,7 @@ package crypto import ( - "github.com/canopy-network/go-plugin/contract" + "github.com/canopy-network/canopy/plugin/go/contract" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" ) diff --git a/plugin/go/go.mod b/plugin/go/go.mod index 06bd10edf1..2ab0af1a2c 100644 --- a/plugin/go/go.mod +++ b/plugin/go/go.mod @@ -1,10 +1,13 @@ -module github.com/canopy-network/go-plugin +module github.com/canopy-network/canopy/plugin/go -go 1.25 +go 1.24.0 + +toolchain go1.24.2 require ( github.com/drand/kyber v1.3.2 github.com/drand/kyber-bls12381 v0.3.4 + github.com/golang/protobuf v1.5.0 google.golang.org/protobuf v1.36.6 ) diff --git a/plugin/go/go.sum b/plugin/go/go.sum index 624ad4f981..83382daa71 100644 --- a/plugin/go/go.sum +++ b/plugin/go/go.sum @@ -1,9 +1,11 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/drand/kyber v1.3.2 h1:Cf3NNcb5bV3eODopr3XVHzImjDK40GiObhFUFG93Zeo= -github.com/drand/kyber v1.3.2/go.mod h1:ciDFWoC7ajb89niGJnS4C1Xeo4lSJMmbi+km5w8juAI= +github.com/drand/kyber v1.3.2/go.mod h1:TYHFYljYSYmAm9PuGav9ykoEaZzBS0ey0dl/GNA+2Uc= github.com/drand/kyber-bls12381 v0.3.4 h1:rrmYcRcXmtOAvKWVBxRQxi22qNMVcS2Jz7MAebZQJxI= github.com/drand/kyber-bls12381 v0.3.4/go.mod h1:jh3IGIAQfdLrdNKYz1HWZ3YdfJM0DWlN1TxXkh60utk= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/kilic/bls12-381 v0.1.0 h1:encrdjqKMEvabVQ7qYOKu1OvhqpK4s47wDYtNiPtlp4= @@ -21,6 +23,7 @@ golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/plugin/go/main.go b/plugin/go/main.go index 314e054485..898e0e0181 100644 --- a/plugin/go/main.go +++ b/plugin/go/main.go @@ -2,7 +2,7 @@ package main import ( "context" - "github.com/canopy-network/go-plugin/contract" + "github.com/canopy-network/canopy/plugin/go/contract" "os" "os/signal" "syscall" diff --git a/plugin/go/pluginctl.sh b/plugin/go/pluginctl.sh index 631c145825..f9083d09af 100755 --- a/plugin/go/pluginctl.sh +++ b/plugin/go/pluginctl.sh @@ -153,7 +153,7 @@ start() { mkdir -p "$PLUGIN_DIR" # Start the binary in background with nohup echo "Starting go-plugin..." - nohup "$BINARY_PATH" > "$LOG_FILE" 2>&1 & + nohup env PRAXIS_TEST_MODE=true "$BINARY_PATH" > "$LOG_FILE" 2>&1 & local pid=$! # Save PID to file echo "$pid" > "$PID_FILE" diff --git a/plugin/go/proto/_generate.sh b/plugin/go/proto/_generate.sh index 613b7239e8..b3bb92174a 100755 --- a/plugin/go/proto/_generate.sh +++ b/plugin/go/proto/_generate.sh @@ -2,8 +2,8 @@ set -eo pipefail -export PATH="$PATH:$HOME/go/bin" +export PATH="$PATH:$HOME/go/bin:$HOME/bin" -protoc -I=./ -I=/usr/include --go_out=../. --go_opt=module=github.com/canopy-network/go-plugin ./*.proto +~/bin/protoc29 -I=./ -I=/tmp/protoc29/include --go_out=../. --go_opt=module=github.com/canopy-network/canopy/plugin/go ./*.proto find ../. -name "*.pb.go" | xargs -I {} protoc-go-inject-tag -input="{}" diff --git a/plugin/go/proto/account.proto b/plugin/go/proto/account.proto index b0ad929c70..f97e7fd8d6 100644 --- a/plugin/go/proto/account.proto +++ b/plugin/go/proto/account.proto @@ -1,6 +1,6 @@ syntax = "proto3"; package types; -option go_package = "github.com/canopy-network/go-plugin/contract"; +option go_package = "github.com/canopy-network/canopy/plugin/go/contract"; // An account is a structure that holds funds and can send or receive transactions using a crypto key pair // Each account has a unique address and a balance, think a bank account - but managed by the blockchain diff --git a/plugin/go/proto/event.proto b/plugin/go/proto/event.proto index b2c9772db8..9080b56fed 100644 --- a/plugin/go/proto/event.proto +++ b/plugin/go/proto/event.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package types; -option go_package = "github.com/canopy-network/go-plugin/contract"; +option go_package = "github.com/canopy-network/canopy/plugin/go/contract"; import "google/protobuf/any.proto"; diff --git a/plugin/go/proto/plugin.proto b/plugin/go/proto/plugin.proto index 513e34f154..356e7d3bd3 100644 --- a/plugin/go/proto/plugin.proto +++ b/plugin/go/proto/plugin.proto @@ -1,6 +1,6 @@ syntax = "proto3"; package types; -option go_package = "github.com/canopy-network/go-plugin/contract"; +option go_package = "github.com/canopy-network/canopy/plugin/go/contract"; import "event.proto"; import "tx.proto"; diff --git a/plugin/go/proto/tx.pb.go b/plugin/go/proto/tx.pb.go new file mode 100644 index 0000000000..0b5cd3fa6a --- /dev/null +++ b/plugin/go/proto/tx.pb.go @@ -0,0 +1,2879 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v5.29.3 +// source: tx.proto + +package contract + +import ( + any1 "github.com/golang/protobuf/ptypes/any" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Required by plugin.proto +type Signature struct { + state protoimpl.MessageState `protogen:"open.v1"` + PublicKey []byte `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Signature) Reset() { + *x = Signature{} + mi := &file_tx_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Signature) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Signature) ProtoMessage() {} + +func (x *Signature) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Signature.ProtoReflect.Descriptor instead. +func (*Signature) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{0} +} + +func (x *Signature) GetPublicKey() []byte { + if x != nil { + return x.PublicKey + } + return nil +} + +func (x *Signature) GetSignature() []byte { + if x != nil { + return x.Signature + } + return nil +} + +type Transaction struct { + state protoimpl.MessageState `protogen:"open.v1"` + MessageType string `protobuf:"bytes,1,opt,name=message_type,json=messageType,proto3" json:"message_type,omitempty"` + Msg *any1.Any `protobuf:"bytes,2,opt,name=msg,proto3" json:"msg,omitempty"` + Signature *Signature `protobuf:"bytes,3,opt,name=signature,proto3" json:"signature,omitempty"` + CreatedHeight uint64 `protobuf:"varint,4,opt,name=created_height,json=createdHeight,proto3" json:"created_height,omitempty"` + Time uint64 `protobuf:"varint,5,opt,name=time,proto3" json:"time,omitempty"` + Fee uint64 `protobuf:"varint,6,opt,name=fee,proto3" json:"fee,omitempty"` + Memo string `protobuf:"bytes,7,opt,name=memo,proto3" json:"memo,omitempty"` + NetworkId uint64 `protobuf:"varint,8,opt,name=network_id,json=networkId,proto3" json:"network_id,omitempty"` + ChainId uint64 `protobuf:"varint,9,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Transaction) Reset() { + *x = Transaction{} + mi := &file_tx_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Transaction) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Transaction) ProtoMessage() {} + +func (x *Transaction) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Transaction.ProtoReflect.Descriptor instead. +func (*Transaction) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{1} +} + +func (x *Transaction) GetMessageType() string { + if x != nil { + return x.MessageType + } + return "" +} + +func (x *Transaction) GetMsg() *any1.Any { + if x != nil { + return x.Msg + } + return nil +} + +func (x *Transaction) GetSignature() *Signature { + if x != nil { + return x.Signature + } + return nil +} + +func (x *Transaction) GetCreatedHeight() uint64 { + if x != nil { + return x.CreatedHeight + } + return 0 +} + +func (x *Transaction) GetTime() uint64 { + if x != nil { + return x.Time + } + return 0 +} + +func (x *Transaction) GetFee() uint64 { + if x != nil { + return x.Fee + } + return 0 +} + +func (x *Transaction) GetMemo() string { + if x != nil { + return x.Memo + } + return "" +} + +func (x *Transaction) GetNetworkId() uint64 { + if x != nil { + return x.NetworkId + } + return 0 +} + +func (x *Transaction) GetChainId() uint64 { + if x != nil { + return x.ChainId + } + return 0 +} + +type FeeParams struct { + state protoimpl.MessageState `protogen:"open.v1"` + SendFee uint64 `protobuf:"varint,1,opt,name=send_fee,json=sendFee,proto3" json:"send_fee,omitempty"` + CreateMarketFee uint64 `protobuf:"varint,2,opt,name=create_market_fee,json=createMarketFee,proto3" json:"create_market_fee,omitempty"` + SubmitPredictionFee uint64 `protobuf:"varint,3,opt,name=submit_prediction_fee,json=submitPredictionFee,proto3" json:"submit_prediction_fee,omitempty"` + ResolveMarketFee uint64 `protobuf:"varint,4,opt,name=resolve_market_fee,json=resolveMarketFee,proto3" json:"resolve_market_fee,omitempty"` + ClaimWinningsFee uint64 `protobuf:"varint,5,opt,name=claim_winnings_fee,json=claimWinningsFee,proto3" json:"claim_winnings_fee,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FeeParams) Reset() { + *x = FeeParams{} + mi := &file_tx_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FeeParams) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FeeParams) ProtoMessage() {} + +func (x *FeeParams) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FeeParams.ProtoReflect.Descriptor instead. +func (*FeeParams) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{2} +} + +func (x *FeeParams) GetSendFee() uint64 { + if x != nil { + return x.SendFee + } + return 0 +} + +func (x *FeeParams) GetCreateMarketFee() uint64 { + if x != nil { + return x.CreateMarketFee + } + return 0 +} + +func (x *FeeParams) GetSubmitPredictionFee() uint64 { + if x != nil { + return x.SubmitPredictionFee + } + return 0 +} + +func (x *FeeParams) GetResolveMarketFee() uint64 { + if x != nil { + return x.ResolveMarketFee + } + return 0 +} + +func (x *FeeParams) GetClaimWinningsFee() uint64 { + if x != nil { + return x.ClaimWinningsFee + } + return 0 +} + +// ADLMSR state objects (0x10-0x14) +type MarketState struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status uint32 `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"` + ExpiryTime uint64 `protobuf:"varint,2,opt,name=expiry_time,json=expiryTime,proto3" json:"expiry_time,omitempty"` + QYes uint64 `protobuf:"varint,3,opt,name=q_yes,json=qYes,proto3" json:"q_yes,omitempty"` + QNo uint64 `protobuf:"varint,4,opt,name=q_no,json=qNo,proto3" json:"q_no,omitempty"` + BEff uint64 `protobuf:"varint,5,opt,name=b_eff,json=bEff,proto3" json:"b_eff,omitempty"` + Creator []byte `protobuf:"bytes,6,opt,name=creator,proto3" json:"creator,omitempty"` + ClaimedCount uint64 `protobuf:"varint,7,opt,name=claimed_count,json=claimedCount,proto3" json:"claimed_count,omitempty"` + TotalPositions uint64 `protobuf:"varint,8,opt,name=total_positions,json=totalPositions,proto3" json:"total_positions,omitempty"` + OpenTime uint64 `protobuf:"varint,9,opt,name=open_time,json=openTime,proto3" json:"open_time,omitempty"` + ElevatedRisk bool `protobuf:"varint,10,opt,name=elevated_risk,json=elevatedRisk,proto3" json:"elevated_risk,omitempty"` + FinalizedPoolAmount uint64 `protobuf:"varint,11,opt,name=finalized_pool_amount,json=finalizedPoolAmount,proto3" json:"finalized_pool_amount,omitempty"` + Question string `protobuf:"bytes,12,opt,name=question,proto3" json:"question,omitempty"` + Rules string `protobuf:"bytes,13,opt,name=rules,proto3" json:"rules,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MarketState) Reset() { + *x = MarketState{} + mi := &file_tx_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MarketState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MarketState) ProtoMessage() {} + +func (x *MarketState) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MarketState.ProtoReflect.Descriptor instead. +func (*MarketState) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{3} +} + +func (x *MarketState) GetStatus() uint32 { + if x != nil { + return x.Status + } + return 0 +} + +func (x *MarketState) GetExpiryTime() uint64 { + if x != nil { + return x.ExpiryTime + } + return 0 +} + +func (x *MarketState) GetQYes() uint64 { + if x != nil { + return x.QYes + } + return 0 +} + +func (x *MarketState) GetQNo() uint64 { + if x != nil { + return x.QNo + } + return 0 +} + +func (x *MarketState) GetBEff() uint64 { + if x != nil { + return x.BEff + } + return 0 +} + +func (x *MarketState) GetCreator() []byte { + if x != nil { + return x.Creator + } + return nil +} + +func (x *MarketState) GetClaimedCount() uint64 { + if x != nil { + return x.ClaimedCount + } + return 0 +} + +func (x *MarketState) GetTotalPositions() uint64 { + if x != nil { + return x.TotalPositions + } + return 0 +} + +func (x *MarketState) GetOpenTime() uint64 { + if x != nil { + return x.OpenTime + } + return 0 +} + +func (x *MarketState) GetElevatedRisk() bool { + if x != nil { + return x.ElevatedRisk + } + return false +} + +func (x *MarketState) GetFinalizedPoolAmount() uint64 { + if x != nil { + return x.FinalizedPoolAmount + } + return 0 +} + +func (x *MarketState) GetQuestion() string { + if x != nil { + return x.Question + } + return "" +} + +func (x *MarketState) GetRules() string { + if x != nil { + return x.Rules + } + return "" +} + +type PositionState struct { + state protoimpl.MessageState `protogen:"open.v1"` + SharesYes uint64 `protobuf:"varint,1,opt,name=shares_yes,json=sharesYes,proto3" json:"shares_yes,omitempty"` + SharesNo uint64 `protobuf:"varint,2,opt,name=shares_no,json=sharesNo,proto3" json:"shares_no,omitempty"` + CostPaid uint64 `protobuf:"varint,3,opt,name=cost_paid,json=costPaid,proto3" json:"cost_paid,omitempty"` + Claimed bool `protobuf:"varint,4,opt,name=claimed,proto3" json:"claimed,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PositionState) Reset() { + *x = PositionState{} + mi := &file_tx_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PositionState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PositionState) ProtoMessage() {} + +func (x *PositionState) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PositionState.ProtoReflect.Descriptor instead. +func (*PositionState) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{4} +} + +func (x *PositionState) GetSharesYes() uint64 { + if x != nil { + return x.SharesYes + } + return 0 +} + +func (x *PositionState) GetSharesNo() uint64 { + if x != nil { + return x.SharesNo + } + return 0 +} + +func (x *PositionState) GetCostPaid() uint64 { + if x != nil { + return x.CostPaid + } + return 0 +} + +func (x *PositionState) GetClaimed() bool { + if x != nil { + return x.Claimed + } + return false +} + +type OutcomeState struct { + state protoimpl.MessageState `protogen:"open.v1"` + WinningOutcome bool `protobuf:"varint,1,opt,name=winning_outcome,json=winningOutcome,proto3" json:"winning_outcome,omitempty"` + ResolvedAt uint64 `protobuf:"varint,2,opt,name=resolved_at,json=resolvedAt,proto3" json:"resolved_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OutcomeState) Reset() { + *x = OutcomeState{} + mi := &file_tx_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OutcomeState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OutcomeState) ProtoMessage() {} + +func (x *OutcomeState) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OutcomeState.ProtoReflect.Descriptor instead. +func (*OutcomeState) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{5} +} + +func (x *OutcomeState) GetWinningOutcome() bool { + if x != nil { + return x.WinningOutcome + } + return false +} + +func (x *OutcomeState) GetResolvedAt() uint64 { + if x != nil { + return x.ResolvedAt + } + return 0 +} + +type TreasuryReserve struct { + state protoimpl.MessageState `protogen:"open.v1"` + LockedReserve uint64 `protobuf:"varint,1,opt,name=locked_reserve,json=lockedReserve,proto3" json:"locked_reserve,omitempty"` + CreatorBond uint64 `protobuf:"varint,2,opt,name=creator_bond,json=creatorBond,proto3" json:"creator_bond,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TreasuryReserve) Reset() { + *x = TreasuryReserve{} + mi := &file_tx_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TreasuryReserve) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TreasuryReserve) ProtoMessage() {} + +func (x *TreasuryReserve) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TreasuryReserve.ProtoReflect.Descriptor instead. +func (*TreasuryReserve) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{6} +} + +func (x *TreasuryReserve) GetLockedReserve() uint64 { + if x != nil { + return x.LockedReserve + } + return 0 +} + +func (x *TreasuryReserve) GetCreatorBond() uint64 { + if x != nil { + return x.CreatorBond + } + return 0 +} + +// PORS state objects (0x13, 0x16-0x1C) +type ResolverState struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddress []byte `protobuf:"bytes,1,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResolverState) Reset() { + *x = ResolverState{} + mi := &file_tx_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResolverState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResolverState) ProtoMessage() {} + +func (x *ResolverState) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResolverState.ProtoReflect.Descriptor instead. +func (*ResolverState) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{7} +} + +func (x *ResolverState) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +type ResolverRecord struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddress []byte `protobuf:"bytes,1,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + StakeAmount uint64 `protobuf:"varint,2,opt,name=stake_amount,json=stakeAmount,proto3" json:"stake_amount,omitempty"` + RrsScore uint64 `protobuf:"varint,3,opt,name=rrs_score,json=rrsScore,proto3" json:"rrs_score,omitempty"` + RegisteredAt uint64 `protobuf:"varint,4,opt,name=registered_at,json=registeredAt,proto3" json:"registered_at,omitempty"` + SuccessfulResolutions uint64 `protobuf:"varint,5,opt,name=successful_resolutions,json=successfulResolutions,proto3" json:"successful_resolutions,omitempty"` + LastClaimedEpoch uint64 `protobuf:"varint,6,opt,name=last_claimed_epoch,json=lastClaimedEpoch,proto3" json:"last_claimed_epoch,omitempty"` + IsActive bool `protobuf:"varint,7,opt,name=is_active,json=isActive,proto3" json:"is_active,omitempty"` + UnbondingAmount uint64 `protobuf:"varint,8,opt,name=unbonding_amount,json=unbondingAmount,proto3" json:"unbonding_amount,omitempty"` + UnbondingReleaseHeight uint64 `protobuf:"varint,9,opt,name=unbonding_release_height,json=unbondingReleaseHeight,proto3" json:"unbonding_release_height,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResolverRecord) Reset() { + *x = ResolverRecord{} + mi := &file_tx_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResolverRecord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResolverRecord) ProtoMessage() {} + +func (x *ResolverRecord) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResolverRecord.ProtoReflect.Descriptor instead. +func (*ResolverRecord) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{8} +} + +func (x *ResolverRecord) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +func (x *ResolverRecord) GetStakeAmount() uint64 { + if x != nil { + return x.StakeAmount + } + return 0 +} + +func (x *ResolverRecord) GetRrsScore() uint64 { + if x != nil { + return x.RrsScore + } + return 0 +} + +func (x *ResolverRecord) GetRegisteredAt() uint64 { + if x != nil { + return x.RegisteredAt + } + return 0 +} + +func (x *ResolverRecord) GetSuccessfulResolutions() uint64 { + if x != nil { + return x.SuccessfulResolutions + } + return 0 +} + +func (x *ResolverRecord) GetLastClaimedEpoch() uint64 { + if x != nil { + return x.LastClaimedEpoch + } + return 0 +} + +func (x *ResolverRecord) GetIsActive() bool { + if x != nil { + return x.IsActive + } + return false +} + +func (x *ResolverRecord) GetUnbondingAmount() uint64 { + if x != nil { + return x.UnbondingAmount + } + return 0 +} + +func (x *ResolverRecord) GetUnbondingReleaseHeight() uint64 { + if x != nil { + return x.UnbondingReleaseHeight + } + return 0 +} + +type ProposalRecord struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddr []byte `protobuf:"bytes,1,opt,name=resolver_addr,json=resolverAddr,proto3" json:"resolver_addr,omitempty"` + ProposedOutcome bool `protobuf:"varint,2,opt,name=proposed_outcome,json=proposedOutcome,proto3" json:"proposed_outcome,omitempty"` + ProposalBond uint64 `protobuf:"varint,3,opt,name=proposal_bond,json=proposalBond,proto3" json:"proposal_bond,omitempty"` + ProposalBlock uint64 `protobuf:"varint,4,opt,name=proposal_block,json=proposalBlock,proto3" json:"proposal_block,omitempty"` + Status uint32 `protobuf:"varint,5,opt,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProposalRecord) Reset() { + *x = ProposalRecord{} + mi := &file_tx_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProposalRecord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProposalRecord) ProtoMessage() {} + +func (x *ProposalRecord) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProposalRecord.ProtoReflect.Descriptor instead. +func (*ProposalRecord) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{9} +} + +func (x *ProposalRecord) GetResolverAddr() []byte { + if x != nil { + return x.ResolverAddr + } + return nil +} + +func (x *ProposalRecord) GetProposedOutcome() bool { + if x != nil { + return x.ProposedOutcome + } + return false +} + +func (x *ProposalRecord) GetProposalBond() uint64 { + if x != nil { + return x.ProposalBond + } + return 0 +} + +func (x *ProposalRecord) GetProposalBlock() uint64 { + if x != nil { + return x.ProposalBlock + } + return 0 +} + +func (x *ProposalRecord) GetStatus() uint32 { + if x != nil { + return x.Status + } + return 0 +} + +type DisputeRecord struct { + state protoimpl.MessageState `protogen:"open.v1"` + DisputerAddress []byte `protobuf:"bytes,1,opt,name=disputer_address,json=disputerAddress,proto3" json:"disputer_address,omitempty"` + DisputeBond uint64 `protobuf:"varint,2,opt,name=dispute_bond,json=disputeBond,proto3" json:"dispute_bond,omitempty"` + DisputeBlock uint64 `protobuf:"varint,3,opt,name=dispute_block,json=disputeBlock,proto3" json:"dispute_block,omitempty"` + VoteStatus uint32 `protobuf:"varint,4,opt,name=vote_status,json=voteStatus,proto3" json:"vote_status,omitempty"` + PanelSize uint32 `protobuf:"varint,5,opt,name=panel_size,json=panelSize,proto3" json:"panel_size,omitempty"` + PanelMembers [][]byte `protobuf:"bytes,6,rep,name=panel_members,json=panelMembers,proto3" json:"panel_members,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DisputeRecord) Reset() { + *x = DisputeRecord{} + mi := &file_tx_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DisputeRecord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DisputeRecord) ProtoMessage() {} + +func (x *DisputeRecord) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DisputeRecord.ProtoReflect.Descriptor instead. +func (*DisputeRecord) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{10} +} + +func (x *DisputeRecord) GetDisputerAddress() []byte { + if x != nil { + return x.DisputerAddress + } + return nil +} + +func (x *DisputeRecord) GetDisputeBond() uint64 { + if x != nil { + return x.DisputeBond + } + return 0 +} + +func (x *DisputeRecord) GetDisputeBlock() uint64 { + if x != nil { + return x.DisputeBlock + } + return 0 +} + +func (x *DisputeRecord) GetVoteStatus() uint32 { + if x != nil { + return x.VoteStatus + } + return 0 +} + +func (x *DisputeRecord) GetPanelSize() uint32 { + if x != nil { + return x.PanelSize + } + return 0 +} + +func (x *DisputeRecord) GetPanelMembers() [][]byte { + if x != nil { + return x.PanelMembers + } + return nil +} + +type VoteCommit struct { + state protoimpl.MessageState `protogen:"open.v1"` + VoterAddr []byte `protobuf:"bytes,1,opt,name=voter_addr,json=voterAddr,proto3" json:"voter_addr,omitempty"` + CommitHash []byte `protobuf:"bytes,2,opt,name=commit_hash,json=commitHash,proto3" json:"commit_hash,omitempty"` + CommittedAt uint64 `protobuf:"varint,3,opt,name=committed_at,json=committedAt,proto3" json:"committed_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VoteCommit) Reset() { + *x = VoteCommit{} + mi := &file_tx_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VoteCommit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VoteCommit) ProtoMessage() {} + +func (x *VoteCommit) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VoteCommit.ProtoReflect.Descriptor instead. +func (*VoteCommit) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{11} +} + +func (x *VoteCommit) GetVoterAddr() []byte { + if x != nil { + return x.VoterAddr + } + return nil +} + +func (x *VoteCommit) GetCommitHash() []byte { + if x != nil { + return x.CommitHash + } + return nil +} + +func (x *VoteCommit) GetCommittedAt() uint64 { + if x != nil { + return x.CommittedAt + } + return 0 +} + +type VoteReveal struct { + state protoimpl.MessageState `protogen:"open.v1"` + VoterAddr []byte `protobuf:"bytes,1,opt,name=voter_addr,json=voterAddr,proto3" json:"voter_addr,omitempty"` + Vote bool `protobuf:"varint,2,opt,name=vote,proto3" json:"vote,omitempty"` + RevealedAt uint64 `protobuf:"varint,3,opt,name=revealed_at,json=revealedAt,proto3" json:"revealed_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VoteReveal) Reset() { + *x = VoteReveal{} + mi := &file_tx_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VoteReveal) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VoteReveal) ProtoMessage() {} + +func (x *VoteReveal) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VoteReveal.ProtoReflect.Descriptor instead. +func (*VoteReveal) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{12} +} + +func (x *VoteReveal) GetVoterAddr() []byte { + if x != nil { + return x.VoterAddr + } + return nil +} + +func (x *VoteReveal) GetVote() bool { + if x != nil { + return x.Vote + } + return false +} + +func (x *VoteReveal) GetRevealedAt() uint64 { + if x != nil { + return x.RevealedAt + } + return 0 +} + +type SlashRecord struct { + state protoimpl.MessageState `protogen:"open.v1"` + SlashedAddress []byte `protobuf:"bytes,1,opt,name=slashed_address,json=slashedAddress,proto3" json:"slashed_address,omitempty"` + SlashAmount uint64 `protobuf:"varint,2,opt,name=slash_amount,json=slashAmount,proto3" json:"slash_amount,omitempty"` + SlashedAt uint64 `protobuf:"varint,3,opt,name=slashed_at,json=slashedAt,proto3" json:"slashed_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SlashRecord) Reset() { + *x = SlashRecord{} + mi := &file_tx_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SlashRecord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SlashRecord) ProtoMessage() {} + +func (x *SlashRecord) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SlashRecord.ProtoReflect.Descriptor instead. +func (*SlashRecord) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{13} +} + +func (x *SlashRecord) GetSlashedAddress() []byte { + if x != nil { + return x.SlashedAddress + } + return nil +} + +func (x *SlashRecord) GetSlashAmount() uint64 { + if x != nil { + return x.SlashAmount + } + return 0 +} + +func (x *SlashRecord) GetSlashedAt() uint64 { + if x != nil { + return x.SlashedAt + } + return 0 +} + +type PanelEntropyAccum struct { + state protoimpl.MessageState `protogen:"open.v1"` + Accumulator uint64 `protobuf:"varint,1,opt,name=accumulator,proto3" json:"accumulator,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PanelEntropyAccum) Reset() { + *x = PanelEntropyAccum{} + mi := &file_tx_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PanelEntropyAccum) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PanelEntropyAccum) ProtoMessage() {} + +func (x *PanelEntropyAccum) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PanelEntropyAccum.ProtoReflect.Descriptor instead. +func (*PanelEntropyAccum) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{14} +} + +func (x *PanelEntropyAccum) GetAccumulator() uint64 { + if x != nil { + return x.Accumulator + } + return 0 +} + +// Transaction messages (12 types) +type MessageSend struct { + state protoimpl.MessageState `protogen:"open.v1"` + FromAddress []byte `protobuf:"bytes,1,opt,name=from_address,json=fromAddress,proto3" json:"from_address,omitempty"` + ToAddress []byte `protobuf:"bytes,2,opt,name=to_address,json=toAddress,proto3" json:"to_address,omitempty"` + Amount uint64 `protobuf:"varint,3,opt,name=amount,proto3" json:"amount,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageSend) Reset() { + *x = MessageSend{} + mi := &file_tx_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageSend) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageSend) ProtoMessage() {} + +func (x *MessageSend) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageSend.ProtoReflect.Descriptor instead. +func (*MessageSend) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{15} +} + +func (x *MessageSend) GetFromAddress() []byte { + if x != nil { + return x.FromAddress + } + return nil +} + +func (x *MessageSend) GetToAddress() []byte { + if x != nil { + return x.ToAddress + } + return nil +} + +func (x *MessageSend) GetAmount() uint64 { + if x != nil { + return x.Amount + } + return 0 +} + +type MessageCreateMarket struct { + state protoimpl.MessageState `protogen:"open.v1"` + CreatorAddress []byte `protobuf:"bytes,1,opt,name=creator_address,json=creatorAddress,proto3" json:"creator_address,omitempty"` + B0 uint64 `protobuf:"varint,2,opt,name=b0,proto3" json:"b0,omitempty"` + ExpiryTime uint64 `protobuf:"varint,3,opt,name=expiry_time,json=expiryTime,proto3" json:"expiry_time,omitempty"` + Nonce uint64 `protobuf:"varint,4,opt,name=nonce,proto3" json:"nonce,omitempty"` + Question string `protobuf:"bytes,5,opt,name=question,proto3" json:"question,omitempty"` + Rules string `protobuf:"bytes,6,opt,name=rules,proto3" json:"rules,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageCreateMarket) Reset() { + *x = MessageCreateMarket{} + mi := &file_tx_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageCreateMarket) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageCreateMarket) ProtoMessage() {} + +func (x *MessageCreateMarket) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageCreateMarket.ProtoReflect.Descriptor instead. +func (*MessageCreateMarket) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{16} +} + +func (x *MessageCreateMarket) GetCreatorAddress() []byte { + if x != nil { + return x.CreatorAddress + } + return nil +} + +func (x *MessageCreateMarket) GetB0() uint64 { + if x != nil { + return x.B0 + } + return 0 +} + +func (x *MessageCreateMarket) GetExpiryTime() uint64 { + if x != nil { + return x.ExpiryTime + } + return 0 +} + +func (x *MessageCreateMarket) GetNonce() uint64 { + if x != nil { + return x.Nonce + } + return 0 +} + +func (x *MessageCreateMarket) GetQuestion() string { + if x != nil { + return x.Question + } + return "" +} + +func (x *MessageCreateMarket) GetRules() string { + if x != nil { + return x.Rules + } + return "" +} + +type MessageSubmitPrediction struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + BettorAddress []byte `protobuf:"bytes,2,opt,name=bettor_address,json=bettorAddress,proto3" json:"bettor_address,omitempty"` + Outcome bool `protobuf:"varint,3,opt,name=outcome,proto3" json:"outcome,omitempty"` + Shares uint64 `protobuf:"varint,4,opt,name=shares,proto3" json:"shares,omitempty"` + MaxCost uint64 `protobuf:"varint,5,opt,name=max_cost,json=maxCost,proto3" json:"max_cost,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageSubmitPrediction) Reset() { + *x = MessageSubmitPrediction{} + mi := &file_tx_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageSubmitPrediction) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageSubmitPrediction) ProtoMessage() {} + +func (x *MessageSubmitPrediction) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageSubmitPrediction.ProtoReflect.Descriptor instead. +func (*MessageSubmitPrediction) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{17} +} + +func (x *MessageSubmitPrediction) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageSubmitPrediction) GetBettorAddress() []byte { + if x != nil { + return x.BettorAddress + } + return nil +} + +func (x *MessageSubmitPrediction) GetOutcome() bool { + if x != nil { + return x.Outcome + } + return false +} + +func (x *MessageSubmitPrediction) GetShares() uint64 { + if x != nil { + return x.Shares + } + return 0 +} + +func (x *MessageSubmitPrediction) GetMaxCost() uint64 { + if x != nil { + return x.MaxCost + } + return 0 +} + +type MessageClaimWinnings struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + ClaimantAddress []byte `protobuf:"bytes,2,opt,name=claimant_address,json=claimantAddress,proto3" json:"claimant_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimWinnings) Reset() { + *x = MessageClaimWinnings{} + mi := &file_tx_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimWinnings) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimWinnings) ProtoMessage() {} + +func (x *MessageClaimWinnings) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimWinnings.ProtoReflect.Descriptor instead. +func (*MessageClaimWinnings) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{18} +} + +func (x *MessageClaimWinnings) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageClaimWinnings) GetClaimantAddress() []byte { + if x != nil { + return x.ClaimantAddress + } + return nil +} + +type MessageResolveMarket struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + ResolverAddress []byte `protobuf:"bytes,2,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + WinningOutcome bool `protobuf:"varint,3,opt,name=winning_outcome,json=winningOutcome,proto3" json:"winning_outcome,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageResolveMarket) Reset() { + *x = MessageResolveMarket{} + mi := &file_tx_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageResolveMarket) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageResolveMarket) ProtoMessage() {} + +func (x *MessageResolveMarket) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageResolveMarket.ProtoReflect.Descriptor instead. +func (*MessageResolveMarket) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{19} +} + +func (x *MessageResolveMarket) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageResolveMarket) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +func (x *MessageResolveMarket) GetWinningOutcome() bool { + if x != nil { + return x.WinningOutcome + } + return false +} + +type MessageRegisterResolver struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddress []byte `protobuf:"bytes,1,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + StakeAmount uint64 `protobuf:"varint,2,opt,name=stake_amount,json=stakeAmount,proto3" json:"stake_amount,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageRegisterResolver) Reset() { + *x = MessageRegisterResolver{} + mi := &file_tx_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageRegisterResolver) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageRegisterResolver) ProtoMessage() {} + +func (x *MessageRegisterResolver) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageRegisterResolver.ProtoReflect.Descriptor instead. +func (*MessageRegisterResolver) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{20} +} + +func (x *MessageRegisterResolver) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +func (x *MessageRegisterResolver) GetStakeAmount() uint64 { + if x != nil { + return x.StakeAmount + } + return 0 +} + +type MessageProposeOutcome struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + ResolverAddress []byte `protobuf:"bytes,2,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + ProposedOutcome bool `protobuf:"varint,3,opt,name=proposed_outcome,json=proposedOutcome,proto3" json:"proposed_outcome,omitempty"` + ProposalBond uint64 `protobuf:"varint,4,opt,name=proposal_bond,json=proposalBond,proto3" json:"proposal_bond,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageProposeOutcome) Reset() { + *x = MessageProposeOutcome{} + mi := &file_tx_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageProposeOutcome) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageProposeOutcome) ProtoMessage() {} + +func (x *MessageProposeOutcome) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageProposeOutcome.ProtoReflect.Descriptor instead. +func (*MessageProposeOutcome) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{21} +} + +func (x *MessageProposeOutcome) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageProposeOutcome) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +func (x *MessageProposeOutcome) GetProposedOutcome() bool { + if x != nil { + return x.ProposedOutcome + } + return false +} + +func (x *MessageProposeOutcome) GetProposalBond() uint64 { + if x != nil { + return x.ProposalBond + } + return 0 +} + +type MessageFileDispute struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + DisputerAddress []byte `protobuf:"bytes,2,opt,name=disputer_address,json=disputerAddress,proto3" json:"disputer_address,omitempty"` + DisputeBond uint64 `protobuf:"varint,3,opt,name=dispute_bond,json=disputeBond,proto3" json:"dispute_bond,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageFileDispute) Reset() { + *x = MessageFileDispute{} + mi := &file_tx_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageFileDispute) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageFileDispute) ProtoMessage() {} + +func (x *MessageFileDispute) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageFileDispute.ProtoReflect.Descriptor instead. +func (*MessageFileDispute) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{22} +} + +func (x *MessageFileDispute) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageFileDispute) GetDisputerAddress() []byte { + if x != nil { + return x.DisputerAddress + } + return nil +} + +func (x *MessageFileDispute) GetDisputeBond() uint64 { + if x != nil { + return x.DisputeBond + } + return 0 +} + +type MessageCommitVote struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + VoterAddr []byte `protobuf:"bytes,2,opt,name=voter_addr,json=voterAddr,proto3" json:"voter_addr,omitempty"` + CommitHash []byte `protobuf:"bytes,3,opt,name=commit_hash,json=commitHash,proto3" json:"commit_hash,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageCommitVote) Reset() { + *x = MessageCommitVote{} + mi := &file_tx_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageCommitVote) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageCommitVote) ProtoMessage() {} + +func (x *MessageCommitVote) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageCommitVote.ProtoReflect.Descriptor instead. +func (*MessageCommitVote) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{23} +} + +func (x *MessageCommitVote) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageCommitVote) GetVoterAddr() []byte { + if x != nil { + return x.VoterAddr + } + return nil +} + +func (x *MessageCommitVote) GetCommitHash() []byte { + if x != nil { + return x.CommitHash + } + return nil +} + +type MessageRevealVote struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + VoterAddr []byte `protobuf:"bytes,2,opt,name=voter_addr,json=voterAddr,proto3" json:"voter_addr,omitempty"` + Vote bool `protobuf:"varint,3,opt,name=vote,proto3" json:"vote,omitempty"` + Nonce []byte `protobuf:"bytes,4,opt,name=nonce,proto3" json:"nonce,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageRevealVote) Reset() { + *x = MessageRevealVote{} + mi := &file_tx_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageRevealVote) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageRevealVote) ProtoMessage() {} + +func (x *MessageRevealVote) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageRevealVote.ProtoReflect.Descriptor instead. +func (*MessageRevealVote) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{24} +} + +func (x *MessageRevealVote) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageRevealVote) GetVoterAddr() []byte { + if x != nil { + return x.VoterAddr + } + return nil +} + +func (x *MessageRevealVote) GetVote() bool { + if x != nil { + return x.Vote + } + return false +} + +func (x *MessageRevealVote) GetNonce() []byte { + if x != nil { + return x.Nonce + } + return nil +} + +type MessageTallyVotes struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + CallerAddr []byte `protobuf:"bytes,2,opt,name=caller_addr,json=callerAddr,proto3" json:"caller_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageTallyVotes) Reset() { + *x = MessageTallyVotes{} + mi := &file_tx_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageTallyVotes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageTallyVotes) ProtoMessage() {} + +func (x *MessageTallyVotes) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageTallyVotes.ProtoReflect.Descriptor instead. +func (*MessageTallyVotes) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{25} +} + +func (x *MessageTallyVotes) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageTallyVotes) GetCallerAddr() []byte { + if x != nil { + return x.CallerAddr + } + return nil +} + +type MessageFinalizeMarket struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + CallerAddr []byte `protobuf:"bytes,2,opt,name=caller_addr,json=callerAddr,proto3" json:"caller_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageFinalizeMarket) Reset() { + *x = MessageFinalizeMarket{} + mi := &file_tx_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageFinalizeMarket) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageFinalizeMarket) ProtoMessage() {} + +func (x *MessageFinalizeMarket) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageFinalizeMarket.ProtoReflect.Descriptor instead. +func (*MessageFinalizeMarket) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{26} +} + +func (x *MessageFinalizeMarket) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageFinalizeMarket) GetCallerAddr() []byte { + if x != nil { + return x.CallerAddr + } + return nil +} + +type MessageClaimSlash struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + ClaimantAddress []byte `protobuf:"bytes,2,opt,name=claimant_address,json=claimantAddress,proto3" json:"claimant_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimSlash) Reset() { + *x = MessageClaimSlash{} + mi := &file_tx_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimSlash) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimSlash) ProtoMessage() {} + +func (x *MessageClaimSlash) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimSlash.ProtoReflect.Descriptor instead. +func (*MessageClaimSlash) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{27} +} + +func (x *MessageClaimSlash) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageClaimSlash) GetClaimantAddress() []byte { + if x != nil { + return x.ClaimantAddress + } + return nil +} + +type MessageReclaimStake struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + ClaimantAddress []byte `protobuf:"bytes,2,opt,name=claimant_address,json=claimantAddress,proto3" json:"claimant_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageReclaimStake) Reset() { + *x = MessageReclaimStake{} + mi := &file_tx_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageReclaimStake) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageReclaimStake) ProtoMessage() {} + +func (x *MessageReclaimStake) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageReclaimStake.ProtoReflect.Descriptor instead. +func (*MessageReclaimStake) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{28} +} + +func (x *MessageReclaimStake) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageReclaimStake) GetClaimantAddress() []byte { + if x != nil { + return x.ClaimantAddress + } + return nil +} + +// MessageForfeitPosition allows a resolver to voluntarily exit a position +// in a market they intend to resolve — satisfying the COI-1 requirement. +// Issue-2: without this tx type, a resolver with even 1 share is permanently +// disqualified from resolving, with no protocol-level escape hatch. +// The full CostPaid is refunded; shares are zeroed atomically. +type MessageForfeitPosition struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + ResolverAddress []byte `protobuf:"bytes,2,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageForfeitPosition) Reset() { + *x = MessageForfeitPosition{} + mi := &file_tx_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageForfeitPosition) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageForfeitPosition) ProtoMessage() {} + +func (x *MessageForfeitPosition) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageForfeitPosition.ProtoReflect.Descriptor instead. +func (*MessageForfeitPosition) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{29} +} + +func (x *MessageForfeitPosition) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageForfeitPosition) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +type MessageClaimBuilderReward struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimBuilderReward) Reset() { + *x = MessageClaimBuilderReward{} + mi := &file_tx_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimBuilderReward) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimBuilderReward) ProtoMessage() {} + +func (x *MessageClaimBuilderReward) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimBuilderReward.ProtoReflect.Descriptor instead. +func (*MessageClaimBuilderReward) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{30} +} + +type MessageClaimCreatorFee struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + CreatorAddress []byte `protobuf:"bytes,2,opt,name=creator_address,json=creatorAddress,proto3" json:"creator_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimCreatorFee) Reset() { + *x = MessageClaimCreatorFee{} + mi := &file_tx_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimCreatorFee) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimCreatorFee) ProtoMessage() {} + +func (x *MessageClaimCreatorFee) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimCreatorFee.ProtoReflect.Descriptor instead. +func (*MessageClaimCreatorFee) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{31} +} + +func (x *MessageClaimCreatorFee) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageClaimCreatorFee) GetCreatorAddress() []byte { + if x != nil { + return x.CreatorAddress + } + return nil +} + +type MessageClaimResolverReward struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddress []byte `protobuf:"bytes,1,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + Epoch uint64 `protobuf:"varint,2,opt,name=epoch,proto3" json:"epoch,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimResolverReward) Reset() { + *x = MessageClaimResolverReward{} + mi := &file_tx_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimResolverReward) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimResolverReward) ProtoMessage() {} + +func (x *MessageClaimResolverReward) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimResolverReward.ProtoReflect.Descriptor instead. +func (*MessageClaimResolverReward) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{32} +} + +func (x *MessageClaimResolverReward) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +func (x *MessageClaimResolverReward) GetEpoch() uint64 { + if x != nil { + return x.Epoch + } + return 0 +} + +type LastClaimedBlock struct { + state protoimpl.MessageState `protogen:"open.v1"` + Height uint64 `protobuf:"varint,1,opt,name=height,proto3" json:"height,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LastClaimedBlock) Reset() { + *x = LastClaimedBlock{} + mi := &file_tx_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LastClaimedBlock) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LastClaimedBlock) ProtoMessage() {} + +func (x *LastClaimedBlock) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LastClaimedBlock.ProtoReflect.Descriptor instead. +func (*LastClaimedBlock) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{33} +} + +func (x *LastClaimedBlock) GetHeight() uint64 { + if x != nil { + return x.Height + } + return 0 +} + +type MessageClaimCommunityReward struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimCommunityReward) Reset() { + *x = MessageClaimCommunityReward{} + mi := &file_tx_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimCommunityReward) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimCommunityReward) ProtoMessage() {} + +func (x *MessageClaimCommunityReward) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimCommunityReward.ProtoReflect.Descriptor instead. +func (*MessageClaimCommunityReward) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{34} +} + +type MessageClaimInvestorReward struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimInvestorReward) Reset() { + *x = MessageClaimInvestorReward{} + mi := &file_tx_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimInvestorReward) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimInvestorReward) ProtoMessage() {} + +func (x *MessageClaimInvestorReward) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimInvestorReward.ProtoReflect.Descriptor instead. +func (*MessageClaimInvestorReward) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{35} +} + +type MessageClaimProtocolReward struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimProtocolReward) Reset() { + *x = MessageClaimProtocolReward{} + mi := &file_tx_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimProtocolReward) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimProtocolReward) ProtoMessage() {} + +func (x *MessageClaimProtocolReward) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimProtocolReward.ProtoReflect.Descriptor instead. +func (*MessageClaimProtocolReward) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{36} +} + +type MessageCancelMarket struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + CreatorAddress []byte `protobuf:"bytes,2,opt,name=creator_address,json=creatorAddress,proto3" json:"creator_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageCancelMarket) Reset() { + *x = MessageCancelMarket{} + mi := &file_tx_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageCancelMarket) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageCancelMarket) ProtoMessage() {} + +func (x *MessageCancelMarket) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageCancelMarket.ProtoReflect.Descriptor instead. +func (*MessageCancelMarket) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{37} +} + +func (x *MessageCancelMarket) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageCancelMarket) GetCreatorAddress() []byte { + if x != nil { + return x.CreatorAddress + } + return nil +} + +type MessageUnstakeResolver struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddress []byte `protobuf:"bytes,1,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + Amount uint64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"` // 0 = full exit + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageUnstakeResolver) Reset() { + *x = MessageUnstakeResolver{} + mi := &file_tx_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageUnstakeResolver) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageUnstakeResolver) ProtoMessage() {} + +func (x *MessageUnstakeResolver) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageUnstakeResolver.ProtoReflect.Descriptor instead. +func (*MessageUnstakeResolver) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{38} +} + +func (x *MessageUnstakeResolver) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +func (x *MessageUnstakeResolver) GetAmount() uint64 { + if x != nil { + return x.Amount + } + return 0 +} + +type MessageClaimUnbondedStake struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddress []byte `protobuf:"bytes,1,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimUnbondedStake) Reset() { + *x = MessageClaimUnbondedStake{} + mi := &file_tx_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimUnbondedStake) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimUnbondedStake) ProtoMessage() {} + +func (x *MessageClaimUnbondedStake) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[39] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimUnbondedStake.ProtoReflect.Descriptor instead. +func (*MessageClaimUnbondedStake) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{39} +} + +func (x *MessageClaimUnbondedStake) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +type ResolverIndex struct { + state protoimpl.MessageState `protogen:"open.v1"` + Addresses [][]byte `protobuf:"bytes,1,rep,name=addresses,proto3" json:"addresses,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResolverIndex) Reset() { + *x = ResolverIndex{} + mi := &file_tx_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResolverIndex) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResolverIndex) ProtoMessage() {} + +func (x *ResolverIndex) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[40] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResolverIndex.ProtoReflect.Descriptor instead. +func (*ResolverIndex) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{40} +} + +func (x *ResolverIndex) GetAddresses() [][]byte { + if x != nil { + return x.Addresses + } + return nil +} + +type MarketIndex struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketIds [][]byte `protobuf:"bytes,1,rep,name=market_ids,json=marketIds,proto3" json:"market_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MarketIndex) Reset() { + *x = MarketIndex{} + mi := &file_tx_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MarketIndex) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MarketIndex) ProtoMessage() {} + +func (x *MarketIndex) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[41] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MarketIndex.ProtoReflect.Descriptor instead. +func (*MarketIndex) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{41} +} + +func (x *MarketIndex) GetMarketIds() [][]byte { + if x != nil { + return x.MarketIds + } + return nil +} + +type GlobalStats struct { + state protoimpl.MessageState `protogen:"open.v1"` + TotalWeightedResolutions uint64 `protobuf:"varint,1,opt,name=total_weighted_resolutions,json=totalWeightedResolutions,proto3" json:"total_weighted_resolutions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GlobalStats) Reset() { + *x = GlobalStats{} + mi := &file_tx_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GlobalStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GlobalStats) ProtoMessage() {} + +func (x *GlobalStats) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[42] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GlobalStats.ProtoReflect.Descriptor instead. +func (*GlobalStats) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{42} +} + +func (x *GlobalStats) GetTotalWeightedResolutions() uint64 { + if x != nil { + return x.TotalWeightedResolutions + } + return 0 +} + +var File_tx_proto protoreflect.FileDescriptor + +const file_tx_proto_rawDesc = "" + + "\n" + + "\btx.proto\x12\x05types\x1a\x19google/protobuf/any.proto\"H\n" + + "\tSignature\x12\x1d\n" + + "\n" + + "public_key\x18\x01 \x01(\fR\tpublicKey\x12\x1c\n" + + "\tsignature\x18\x02 \x01(\fR\tsignature\"\xa3\x02\n" + + "\vTransaction\x12!\n" + + "\fmessage_type\x18\x01 \x01(\tR\vmessageType\x12&\n" + + "\x03msg\x18\x02 \x01(\v2\x14.google.protobuf.AnyR\x03msg\x12.\n" + + "\tsignature\x18\x03 \x01(\v2\x10.types.SignatureR\tsignature\x12%\n" + + "\x0ecreated_height\x18\x04 \x01(\x04R\rcreatedHeight\x12\x12\n" + + "\x04time\x18\x05 \x01(\x04R\x04time\x12\x10\n" + + "\x03fee\x18\x06 \x01(\x04R\x03fee\x12\x12\n" + + "\x04memo\x18\a \x01(\tR\x04memo\x12\x1d\n" + + "\n" + + "network_id\x18\b \x01(\x04R\tnetworkId\x12\x19\n" + + "\bchain_id\x18\t \x01(\x04R\achainId\"\xe2\x01\n" + + "\tFeeParams\x12\x19\n" + + "\bsend_fee\x18\x01 \x01(\x04R\asendFee\x12*\n" + + "\x11create_market_fee\x18\x02 \x01(\x04R\x0fcreateMarketFee\x122\n" + + "\x15submit_prediction_fee\x18\x03 \x01(\x04R\x13submitPredictionFee\x12,\n" + + "\x12resolve_market_fee\x18\x04 \x01(\x04R\x10resolveMarketFee\x12,\n" + + "\x12claim_winnings_fee\x18\x05 \x01(\x04R\x10claimWinningsFee\"\x93\x03\n" + + "\vMarketState\x12\x16\n" + + "\x06status\x18\x01 \x01(\rR\x06status\x12\x1f\n" + + "\vexpiry_time\x18\x02 \x01(\x04R\n" + + "expiryTime\x12\x13\n" + + "\x05q_yes\x18\x03 \x01(\x04R\x04qYes\x12\x11\n" + + "\x04q_no\x18\x04 \x01(\x04R\x03qNo\x12\x13\n" + + "\x05b_eff\x18\x05 \x01(\x04R\x04bEff\x12\x18\n" + + "\acreator\x18\x06 \x01(\fR\acreator\x12#\n" + + "\rclaimed_count\x18\a \x01(\x04R\fclaimedCount\x12'\n" + + "\x0ftotal_positions\x18\b \x01(\x04R\x0etotalPositions\x12\x1b\n" + + "\topen_time\x18\t \x01(\x04R\bopenTime\x12#\n" + + "\relevated_risk\x18\n" + + " \x01(\bR\felevatedRisk\x122\n" + + "\x15finalized_pool_amount\x18\v \x01(\x04R\x13finalizedPoolAmount\x12\x1a\n" + + "\bquestion\x18\f \x01(\tR\bquestion\x12\x14\n" + + "\x05rules\x18\r \x01(\tR\x05rules\"\x82\x01\n" + + "\rPositionState\x12\x1d\n" + + "\n" + + "shares_yes\x18\x01 \x01(\x04R\tsharesYes\x12\x1b\n" + + "\tshares_no\x18\x02 \x01(\x04R\bsharesNo\x12\x1b\n" + + "\tcost_paid\x18\x03 \x01(\x04R\bcostPaid\x12\x18\n" + + "\aclaimed\x18\x04 \x01(\bR\aclaimed\"X\n" + + "\fOutcomeState\x12'\n" + + "\x0fwinning_outcome\x18\x01 \x01(\bR\x0ewinningOutcome\x12\x1f\n" + + "\vresolved_at\x18\x02 \x01(\x04R\n" + + "resolvedAt\"[\n" + + "\x0fTreasuryReserve\x12%\n" + + "\x0elocked_reserve\x18\x01 \x01(\x04R\rlockedReserve\x12!\n" + + "\fcreator_bond\x18\x02 \x01(\x04R\vcreatorBond\":\n" + + "\rResolverState\x12)\n" + + "\x10resolver_address\x18\x01 \x01(\fR\x0fresolverAddress\"\x87\x03\n" + + "\x0eResolverRecord\x12)\n" + + "\x10resolver_address\x18\x01 \x01(\fR\x0fresolverAddress\x12!\n" + + "\fstake_amount\x18\x02 \x01(\x04R\vstakeAmount\x12\x1b\n" + + "\trrs_score\x18\x03 \x01(\x04R\brrsScore\x12#\n" + + "\rregistered_at\x18\x04 \x01(\x04R\fregisteredAt\x125\n" + + "\x16successful_resolutions\x18\x05 \x01(\x04R\x15successfulResolutions\x12,\n" + + "\x12last_claimed_epoch\x18\x06 \x01(\x04R\x10lastClaimedEpoch\x12\x1b\n" + + "\tis_active\x18\a \x01(\bR\bisActive\x12)\n" + + "\x10unbonding_amount\x18\b \x01(\x04R\x0funbondingAmount\x128\n" + + "\x18unbonding_release_height\x18\t \x01(\x04R\x16unbondingReleaseHeight\"\xc4\x01\n" + + "\x0eProposalRecord\x12#\n" + + "\rresolver_addr\x18\x01 \x01(\fR\fresolverAddr\x12)\n" + + "\x10proposed_outcome\x18\x02 \x01(\bR\x0fproposedOutcome\x12#\n" + + "\rproposal_bond\x18\x03 \x01(\x04R\fproposalBond\x12%\n" + + "\x0eproposal_block\x18\x04 \x01(\x04R\rproposalBlock\x12\x16\n" + + "\x06status\x18\x05 \x01(\rR\x06status\"\xe7\x01\n" + + "\rDisputeRecord\x12)\n" + + "\x10disputer_address\x18\x01 \x01(\fR\x0fdisputerAddress\x12!\n" + + "\fdispute_bond\x18\x02 \x01(\x04R\vdisputeBond\x12#\n" + + "\rdispute_block\x18\x03 \x01(\x04R\fdisputeBlock\x12\x1f\n" + + "\vvote_status\x18\x04 \x01(\rR\n" + + "voteStatus\x12\x1d\n" + + "\n" + + "panel_size\x18\x05 \x01(\rR\tpanelSize\x12#\n" + + "\rpanel_members\x18\x06 \x03(\fR\fpanelMembers\"o\n" + + "\n" + + "VoteCommit\x12\x1d\n" + + "\n" + + "voter_addr\x18\x01 \x01(\fR\tvoterAddr\x12\x1f\n" + + "\vcommit_hash\x18\x02 \x01(\fR\n" + + "commitHash\x12!\n" + + "\fcommitted_at\x18\x03 \x01(\x04R\vcommittedAt\"`\n" + + "\n" + + "VoteReveal\x12\x1d\n" + + "\n" + + "voter_addr\x18\x01 \x01(\fR\tvoterAddr\x12\x12\n" + + "\x04vote\x18\x02 \x01(\bR\x04vote\x12\x1f\n" + + "\vrevealed_at\x18\x03 \x01(\x04R\n" + + "revealedAt\"x\n" + + "\vSlashRecord\x12'\n" + + "\x0fslashed_address\x18\x01 \x01(\fR\x0eslashedAddress\x12!\n" + + "\fslash_amount\x18\x02 \x01(\x04R\vslashAmount\x12\x1d\n" + + "\n" + + "slashed_at\x18\x03 \x01(\x04R\tslashedAt\"5\n" + + "\x11PanelEntropyAccum\x12 \n" + + "\vaccumulator\x18\x01 \x01(\x04R\vaccumulator\"g\n" + + "\vMessageSend\x12!\n" + + "\ffrom_address\x18\x01 \x01(\fR\vfromAddress\x12\x1d\n" + + "\n" + + "to_address\x18\x02 \x01(\fR\ttoAddress\x12\x16\n" + + "\x06amount\x18\x03 \x01(\x04R\x06amount\"\xb7\x01\n" + + "\x13MessageCreateMarket\x12'\n" + + "\x0fcreator_address\x18\x01 \x01(\fR\x0ecreatorAddress\x12\x0e\n" + + "\x02b0\x18\x02 \x01(\x04R\x02b0\x12\x1f\n" + + "\vexpiry_time\x18\x03 \x01(\x04R\n" + + "expiryTime\x12\x14\n" + + "\x05nonce\x18\x04 \x01(\x04R\x05nonce\x12\x1a\n" + + "\bquestion\x18\x05 \x01(\tR\bquestion\x12\x14\n" + + "\x05rules\x18\x06 \x01(\tR\x05rules\"\xaa\x01\n" + + "\x17MessageSubmitPrediction\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12%\n" + + "\x0ebettor_address\x18\x02 \x01(\fR\rbettorAddress\x12\x18\n" + + "\aoutcome\x18\x03 \x01(\bR\aoutcome\x12\x16\n" + + "\x06shares\x18\x04 \x01(\x04R\x06shares\x12\x19\n" + + "\bmax_cost\x18\x05 \x01(\x04R\amaxCost\"^\n" + + "\x14MessageClaimWinnings\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10claimant_address\x18\x02 \x01(\fR\x0fclaimantAddress\"\x87\x01\n" + + "\x14MessageResolveMarket\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10resolver_address\x18\x02 \x01(\fR\x0fresolverAddress\x12'\n" + + "\x0fwinning_outcome\x18\x03 \x01(\bR\x0ewinningOutcome\"g\n" + + "\x17MessageRegisterResolver\x12)\n" + + "\x10resolver_address\x18\x01 \x01(\fR\x0fresolverAddress\x12!\n" + + "\fstake_amount\x18\x02 \x01(\x04R\vstakeAmount\"\xaf\x01\n" + + "\x15MessageProposeOutcome\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10resolver_address\x18\x02 \x01(\fR\x0fresolverAddress\x12)\n" + + "\x10proposed_outcome\x18\x03 \x01(\bR\x0fproposedOutcome\x12#\n" + + "\rproposal_bond\x18\x04 \x01(\x04R\fproposalBond\"\x7f\n" + + "\x12MessageFileDispute\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10disputer_address\x18\x02 \x01(\fR\x0fdisputerAddress\x12!\n" + + "\fdispute_bond\x18\x03 \x01(\x04R\vdisputeBond\"p\n" + + "\x11MessageCommitVote\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12\x1d\n" + + "\n" + + "voter_addr\x18\x02 \x01(\fR\tvoterAddr\x12\x1f\n" + + "\vcommit_hash\x18\x03 \x01(\fR\n" + + "commitHash\"y\n" + + "\x11MessageRevealVote\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12\x1d\n" + + "\n" + + "voter_addr\x18\x02 \x01(\fR\tvoterAddr\x12\x12\n" + + "\x04vote\x18\x03 \x01(\bR\x04vote\x12\x14\n" + + "\x05nonce\x18\x04 \x01(\fR\x05nonce\"Q\n" + + "\x11MessageTallyVotes\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12\x1f\n" + + "\vcaller_addr\x18\x02 \x01(\fR\n" + + "callerAddr\"U\n" + + "\x15MessageFinalizeMarket\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12\x1f\n" + + "\vcaller_addr\x18\x02 \x01(\fR\n" + + "callerAddr\"[\n" + + "\x11MessageClaimSlash\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10claimant_address\x18\x02 \x01(\fR\x0fclaimantAddress\"]\n" + + "\x13MessageReclaimStake\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10claimant_address\x18\x02 \x01(\fR\x0fclaimantAddress\"`\n" + + "\x16MessageForfeitPosition\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10resolver_address\x18\x02 \x01(\fR\x0fresolverAddress\"\x1b\n" + + "\x19MessageClaimBuilderReward\"^\n" + + "\x16MessageClaimCreatorFee\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12'\n" + + "\x0fcreator_address\x18\x02 \x01(\fR\x0ecreatorAddress\"]\n" + + "\x1aMessageClaimResolverReward\x12)\n" + + "\x10resolver_address\x18\x01 \x01(\fR\x0fresolverAddress\x12\x14\n" + + "\x05epoch\x18\x02 \x01(\x04R\x05epoch\"*\n" + + "\x10LastClaimedBlock\x12\x16\n" + + "\x06height\x18\x01 \x01(\x04R\x06height\"\x1d\n" + + "\x1bMessageClaimCommunityReward\"\x1c\n" + + "\x1aMessageClaimInvestorReward\"\x1c\n" + + "\x1aMessageClaimProtocolReward\"[\n" + + "\x13MessageCancelMarket\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12'\n" + + "\x0fcreator_address\x18\x02 \x01(\fR\x0ecreatorAddress\"[\n" + + "\x16MessageUnstakeResolver\x12)\n" + + "\x10resolver_address\x18\x01 \x01(\fR\x0fresolverAddress\x12\x16\n" + + "\x06amount\x18\x02 \x01(\x04R\x06amount\"F\n" + + "\x19MessageClaimUnbondedStake\x12)\n" + + "\x10resolver_address\x18\x01 \x01(\fR\x0fresolverAddress\"-\n" + + "\rResolverIndex\x12\x1c\n" + + "\taddresses\x18\x01 \x03(\fR\taddresses\",\n" + + "\vMarketIndex\x12\x1d\n" + + "\n" + + "market_ids\x18\x01 \x03(\fR\tmarketIds\"K\n" + + "\vGlobalStats\x12<\n" + + "\x1atotal_weighted_resolutions\x18\x01 \x01(\x04R\x18totalWeightedResolutionsB5Z3github.com/canopy-network/canopy/plugin/go/contractb\x06proto3" + +var ( + file_tx_proto_rawDescOnce sync.Once + file_tx_proto_rawDescData []byte +) + +func file_tx_proto_rawDescGZIP() []byte { + file_tx_proto_rawDescOnce.Do(func() { + file_tx_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_tx_proto_rawDesc), len(file_tx_proto_rawDesc))) + }) + return file_tx_proto_rawDescData +} + +var file_tx_proto_msgTypes = make([]protoimpl.MessageInfo, 43) +var file_tx_proto_goTypes = []any{ + (*Signature)(nil), // 0: types.Signature + (*Transaction)(nil), // 1: types.Transaction + (*FeeParams)(nil), // 2: types.FeeParams + (*MarketState)(nil), // 3: types.MarketState + (*PositionState)(nil), // 4: types.PositionState + (*OutcomeState)(nil), // 5: types.OutcomeState + (*TreasuryReserve)(nil), // 6: types.TreasuryReserve + (*ResolverState)(nil), // 7: types.ResolverState + (*ResolverRecord)(nil), // 8: types.ResolverRecord + (*ProposalRecord)(nil), // 9: types.ProposalRecord + (*DisputeRecord)(nil), // 10: types.DisputeRecord + (*VoteCommit)(nil), // 11: types.VoteCommit + (*VoteReveal)(nil), // 12: types.VoteReveal + (*SlashRecord)(nil), // 13: types.SlashRecord + (*PanelEntropyAccum)(nil), // 14: types.PanelEntropyAccum + (*MessageSend)(nil), // 15: types.MessageSend + (*MessageCreateMarket)(nil), // 16: types.MessageCreateMarket + (*MessageSubmitPrediction)(nil), // 17: types.MessageSubmitPrediction + (*MessageClaimWinnings)(nil), // 18: types.MessageClaimWinnings + (*MessageResolveMarket)(nil), // 19: types.MessageResolveMarket + (*MessageRegisterResolver)(nil), // 20: types.MessageRegisterResolver + (*MessageProposeOutcome)(nil), // 21: types.MessageProposeOutcome + (*MessageFileDispute)(nil), // 22: types.MessageFileDispute + (*MessageCommitVote)(nil), // 23: types.MessageCommitVote + (*MessageRevealVote)(nil), // 24: types.MessageRevealVote + (*MessageTallyVotes)(nil), // 25: types.MessageTallyVotes + (*MessageFinalizeMarket)(nil), // 26: types.MessageFinalizeMarket + (*MessageClaimSlash)(nil), // 27: types.MessageClaimSlash + (*MessageReclaimStake)(nil), // 28: types.MessageReclaimStake + (*MessageForfeitPosition)(nil), // 29: types.MessageForfeitPosition + (*MessageClaimBuilderReward)(nil), // 30: types.MessageClaimBuilderReward + (*MessageClaimCreatorFee)(nil), // 31: types.MessageClaimCreatorFee + (*MessageClaimResolverReward)(nil), // 32: types.MessageClaimResolverReward + (*LastClaimedBlock)(nil), // 33: types.LastClaimedBlock + (*MessageClaimCommunityReward)(nil), // 34: types.MessageClaimCommunityReward + (*MessageClaimInvestorReward)(nil), // 35: types.MessageClaimInvestorReward + (*MessageClaimProtocolReward)(nil), // 36: types.MessageClaimProtocolReward + (*MessageCancelMarket)(nil), // 37: types.MessageCancelMarket + (*MessageUnstakeResolver)(nil), // 38: types.MessageUnstakeResolver + (*MessageClaimUnbondedStake)(nil), // 39: types.MessageClaimUnbondedStake + (*ResolverIndex)(nil), // 40: types.ResolverIndex + (*MarketIndex)(nil), // 41: types.MarketIndex + (*GlobalStats)(nil), // 42: types.GlobalStats + (*any1.Any)(nil), // 43: google.protobuf.Any +} +var file_tx_proto_depIdxs = []int32{ + 43, // 0: types.Transaction.msg:type_name -> google.protobuf.Any + 0, // 1: types.Transaction.signature:type_name -> types.Signature + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_tx_proto_init() } +func file_tx_proto_init() { + if File_tx_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_tx_proto_rawDesc), len(file_tx_proto_rawDesc)), + NumEnums: 0, + NumMessages: 43, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_tx_proto_goTypes, + DependencyIndexes: file_tx_proto_depIdxs, + MessageInfos: file_tx_proto_msgTypes, + }.Build() + File_tx_proto = out.File + file_tx_proto_goTypes = nil + file_tx_proto_depIdxs = nil +} diff --git a/plugin/go/proto/tx.proto b/plugin/go/proto/tx.proto index 8c064be9d6..1569b4d681 100644 --- a/plugin/go/proto/tx.proto +++ b/plugin/go/proto/tx.proto @@ -1,56 +1,269 @@ syntax = "proto3"; + package types; -option go_package = "github.com/canopy-network/go-plugin/contract"; + +option go_package = "github.com/canopy-network/canopy/plugin/go/contract"; import "google/protobuf/any.proto"; -// Transaction represents a request or action submitted to the network like transfer assets or perform other operations -// within the blockchain system - THIS MUST MATCH lib.Transaction EXACTLY for signing + +// Required by plugin.proto +message Signature { + bytes public_key = 1; + bytes signature = 2; +} + message Transaction { - // message_type: The type of the transaction like 'send' or 'stake' - string message_type = 1; // @gotags: json:"messageType" - // msg: The actual transaction message payload, which is encapsulated in a generic message format - google.protobuf.Any msg = 2; - // signature: The cryptographic signature used to verify the authenticity of the transaction - Signature signature = 3; - // created_height: The height when the transaction was created - allows 'safe pruning' - uint64 created_height = 4; // @gotags: json:"createdHeight" - // time: The timestamp when the transaction was created - used as temporal entropy to prevent hash collisions in txs - uint64 time = 5; - // fee: The fee associated with processing the transaction - uint64 fee = 6; - // memo: An optional message or note attached to the transaction - string memo = 7; - // network_id: The identity of the network the transaction is intended for - uint64 network_id = 8; // @gotags: json:"networkID" - // chain_id: The identity of the committee the transaction is intended for - uint64 chain_id = 9; // @gotags: json:"chainID" - // NOTE: CAN EXTEND FUNCTIONALITY BY ADDING ADDITIONAL FIELDS - // NO CONFLICT STARTING AT FIELD = 10 -} - -// MessageSend is a standard transfer transaction, taking tokens from the sender and transferring -// them to the recipient -message MessageSend { - // from_address: is the sender of the funds - bytes from_address = 1; // @gotags: json:"fromAddress" - // to_address: is the recipient of the funds - bytes to_address = 2; // @gotags: json:"toAddress" - // amount: is the amount of tokens in micro-denomination (uCNPY) - uint64 amount = 3; + string message_type = 1; + google.protobuf.Any msg = 2; + Signature signature = 3; + uint64 created_height = 4; + uint64 time = 5; + uint64 fee = 6; + string memo = 7; + uint64 network_id = 8; + uint64 chain_id = 9; } -// FeeParams is the parameter space that defines various amounts for transaction fees message FeeParams { - // send_fee: is the fee amount (in uCNPY) for Message Send - uint64 send_fee = 1; // @gotags: json:"sendFee" - // NO CONFLICT STARTING AT FIELD = 14 + uint64 send_fee = 1; + uint64 create_market_fee = 2; + uint64 submit_prediction_fee = 3; + uint64 resolve_market_fee = 4; + uint64 claim_winnings_fee = 5; } -// A Signature is a digital signature is a cryptographic "fingerprint" created with a private key, -// allowing others to verify the authenticity and integrity of a message using the corresponding public key -message Signature { - // public_key: is a cryptographic code shared openly, used to verify digital signatures - bytes public_key = 1; // @gotags: json:"publicKey" - // signature: the bytes of the signature output from a private key which may be verified with the message and public - bytes signature = 2; +// ADLMSR state objects (0x10-0x14) +message MarketState { + uint32 status = 1; + uint64 expiry_time = 2; + uint64 q_yes = 3; + uint64 q_no = 4; + uint64 b_eff = 5; + bytes creator = 6; + uint64 claimed_count = 7; + uint64 total_positions = 8; + uint64 open_time = 9; + bool elevated_risk = 10; + uint64 finalized_pool_amount = 11; + string question = 12; + string rules = 13; +} + +message PositionState { + uint64 shares_yes = 1; + uint64 shares_no = 2; + uint64 cost_paid = 3; + bool claimed = 4; +} + +message OutcomeState { + bool winning_outcome = 1; + uint64 resolved_at = 2; +} + +message TreasuryReserve { + uint64 locked_reserve = 1; + uint64 creator_bond = 2; +} + +// PORS state objects (0x13, 0x16-0x1C) +message ResolverState { + bytes resolver_address = 1; +} + +message ResolverRecord { + bytes resolver_address = 1; + uint64 stake_amount = 2; + uint64 rrs_score = 3; + uint64 registered_at = 4; + uint64 successful_resolutions = 5; + uint64 last_claimed_epoch = 6; + bool is_active = 7; + uint64 unbonding_amount = 8; + uint64 unbonding_release_height = 9; +} + +message ProposalRecord { + bytes resolver_addr = 1; + bool proposed_outcome = 2; + uint64 proposal_bond = 3; + uint64 proposal_block = 4; + uint32 status = 5; +} + +message DisputeRecord { + bytes disputer_address = 1; + uint64 dispute_bond = 2; + uint64 dispute_block = 3; + uint32 vote_status = 4; + uint32 panel_size = 5; + repeated bytes panel_members = 6; +} + +message VoteCommit { + bytes voter_addr = 1; + bytes commit_hash = 2; + uint64 committed_at = 3; +} + +message VoteReveal { + bytes voter_addr = 1; + bool vote = 2; + uint64 revealed_at = 3; +} + +message SlashRecord { + bytes slashed_address = 1; + uint64 slash_amount = 2; + uint64 slashed_at = 3; +} + +message PanelEntropyAccum { + uint64 accumulator = 1; +} + +// Transaction messages (12 types) +message MessageSend { + bytes from_address = 1; + bytes to_address = 2; + uint64 amount = 3; +} + +message MessageCreateMarket { + bytes creator_address = 1; + uint64 b0 = 2; + uint64 expiry_time = 3; + uint64 nonce = 4; + string question = 5; + string rules = 6; +} + +message MessageSubmitPrediction { + bytes market_id = 1; + bytes bettor_address = 2; + bool outcome = 3; + uint64 shares = 4; + uint64 max_cost = 5; +} + +message MessageClaimWinnings { + bytes market_id = 1; + bytes claimant_address = 2; +} + +message MessageResolveMarket { + bytes market_id = 1; + bytes resolver_address = 2; + bool winning_outcome = 3; +} + +message MessageRegisterResolver { + bytes resolver_address = 1; + uint64 stake_amount = 2; +} + +message MessageProposeOutcome { + bytes market_id = 1; + bytes resolver_address = 2; + bool proposed_outcome = 3; + uint64 proposal_bond = 4; +} + +message MessageFileDispute { + bytes market_id = 1; + bytes disputer_address = 2; + uint64 dispute_bond = 3; +} + +message MessageCommitVote { + bytes market_id = 1; + bytes voter_addr = 2; + bytes commit_hash = 3; +} + +message MessageRevealVote { + bytes market_id = 1; + bytes voter_addr = 2; + bool vote = 3; + bytes nonce = 4; +} + +message MessageTallyVotes { + bytes market_id = 1; + bytes caller_addr = 2; +} + +message MessageFinalizeMarket { + bytes market_id = 1; + bytes caller_addr = 2; +} + +message MessageClaimSlash { + bytes market_id = 1; + bytes claimant_address = 2; +} + +message MessageReclaimStake { + bytes market_id = 1; + bytes claimant_address = 2; +} + +// MessageForfeitPosition allows a resolver to voluntarily exit a position +// in a market they intend to resolve — satisfying the COI-1 requirement. +// Issue-2: without this tx type, a resolver with even 1 share is permanently +// disqualified from resolving, with no protocol-level escape hatch. +// The full CostPaid is refunded; shares are zeroed atomically. +message MessageForfeitPosition { + bytes market_id = 1; + bytes resolver_address = 2; +} + +// PRIS v1.0-r3 message types + +message MessageClaimBuilderReward {} + +message MessageClaimCreatorFee { + bytes market_id = 1; + bytes creator_address = 2; +} + +message MessageClaimResolverReward { + bytes resolver_address = 1; + uint64 epoch = 2; +} + +message LastClaimedBlock { + uint64 height = 1; +} + +message MessageClaimCommunityReward {} + +message MessageClaimInvestorReward {} + +message MessageClaimProtocolReward {} + +message MessageCancelMarket { + bytes market_id = 1; + bytes creator_address = 2; +} + +message MessageUnstakeResolver { + bytes resolver_address = 1; + uint64 amount = 2; // 0 = full exit +} + +message MessageClaimUnbondedStake { + bytes resolver_address = 1; +} + +message ResolverIndex { + repeated bytes addresses = 1; +} + +message MarketIndex { + repeated bytes market_ids = 1; +} + +message GlobalStats { + uint64 total_weighted_resolutions = 1; } diff --git a/plugin/go/tutorial/contract/account.pb.go b/plugin/go/tutorial/contract/account.pb.go index 7045b7e564..330669fe8c 100644 --- a/plugin/go/tutorial/contract/account.pb.go +++ b/plugin/go/tutorial/contract/account.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.11 // protoc v5.29.3 // source: account.proto @@ -145,7 +145,7 @@ const file_account_proto_rawDesc = "" + "\x06amount\x18\x02 \x01(\x04R\x06amount\".\n" + "\x04Pool\x12\x0e\n" + "\x02id\x18\x01 \x01(\x04R\x02id\x12\x16\n" + - "\x06amount\x18\x02 \x01(\x04R\x06amountB3Z1github.com/canopy-network/go-plugin-test/contractb\x06proto3" + "\x06amount\x18\x02 \x01(\x04R\x06amountB5Z3github.com/canopy-network/canopy/plugin/go/contractb\x06proto3" var ( file_account_proto_rawDescOnce sync.Once diff --git a/plugin/go/tutorial/contract/constants.go b/plugin/go/tutorial/contract/constants.go new file mode 100644 index 0000000000..d5f2cd9537 --- /dev/null +++ b/plugin/go/tutorial/contract/constants.go @@ -0,0 +1,167 @@ +package contract + +import "os" + +// ═══════════════════════════════════════════════════════════════════════════════ +// Praxis Prediction Market — Named Constants +// Spec authority: +// ADLMSR v5.6.6-r2-CORRECTED +// PORS v1.0-r2-CORRECTED +// ═══════════════════════════════════════════════════════════════════════════════ + +// ───────────────────────────────────────────────────────────────────────────── +// MARKET STATUS CONSTANTS +// CRIT-2: STATUS_FINALIZED (= 6) is the ClaimWinnings gate — never STATUS_RESOLVED. +// NF-6: STATUS_EXPIRED is never persisted — propose_outcome transitions inline. +// ───────────────────────────────────────────────────────────────────────────── + +const ( +STATUS_OPEN uint32 = 0 +STATUS_CANCELLED uint32 = 1 +STATUS_RESOLVED uint32 = 2 +STATUS_EXPIRED uint32 = 3 +STATUS_PROPOSED uint32 = 4 +STATUS_DISPUTED uint32 = 5 +STATUS_FINALIZED uint32 = 6 +STATUS_VOIDED uint32 = 7 +) + +// ───────────────────────────────────────────────────────────────────────────── +// PROPOSAL STATUS CONSTANTS +// ───────────────────────────────────────────────────────────────────────────── + +const ( +PROPOSAL_OPEN uint32 = 0 +PROPOSAL_DISPUTED uint32 = 1 +PROPOSAL_FINALIZED uint32 = 2 +) + +// ───────────────────────────────────────────────────────────────────────────── +// VOTE STATUS CONSTANTS +// ───────────────────────────────────────────────────────────────────────────── + +const ( +VOTE_PENDING uint32 = 0 +VOTE_COMMITTED uint32 = 1 +VOTE_REVEALED uint32 = 2 +VOTE_TALLIED uint32 = 3 +) + +// ───────────────────────────────────────────────────────────────────────────── +// LMSR PRICING CONSTANTS +// ───────────────────────────────────────────────────────────────────────────── + +const ( +PRECISION_SCALE uint64 = 1_000_000 +MIN_B0 uint64 = 60_000_000 +ELEVATED_RISK_THRESHOLD uint64 = 25_000_000_000 +FIBONACCI_HASH_CONSTANT uint64 = 0x9e3779b97f4a7c15 +) + +// ───────────────────────────────────────────────────────────────────────────── +// TIMING CONSTANTS — ADLMSR (all values in blocks) +// MAX_EXPIRY_TIME: R7 fix — parenthesised expression. +// AUDIT-11: guards uint64 overflow in all post-expiry arithmetic. +// ───────────────────────────────────────────────────────────────────────────── + +const ( +RESOLUTION_DELAY_BLOCKS uint64 = 100 +GRACE_PERIOD_BLOCKS uint64 = 200 +CLAIM_GRACE_PERIOD uint64 = 1000 +) + +const MAX_EXPIRY_TIME uint64 = (^uint64(0) - +RESOLUTION_DELAY_BLOCKS - GRACE_PERIOD_BLOCKS - CLAIM_GRACE_PERIOD - 1) + +// ───────────────────────────────────────────────────────────────────────────── +// TIMING CONSTANTS — PORS (all values in blocks) +// Issue-18: DISPUTE_BLOCKS is block-count based, not wall-clock. +// At 5s/block the floor is ~48h. If block time deviates (e.g. 4s/block +// during validator churn), the wall-clock window shrinks proportionally. +// Future: anchor to wall-clock via a protocol time parameter. +// MIN_DISPUTE_BLOCKS = 34,560 ≈ 48h at ~5s block time (P5) +// ───────────────────────────────────────────────────────────────────────────── + +const ( +MIN_DISPUTE_BLOCKS uint64 = 34_560 +COMMIT_PHASE_BLOCKS uint64 = 17_280 +REVEAL_PHASE_BLOCKS uint64 = 17_280 +) + +// ───────────────────────────────────────────────────────────────────────────── +// PORS ECONOMIC CONSTANTS +// ───────────────────────────────────────────────────────────────────────────── + +const ( +MIN_RRS_TO_PROPOSE uint64 = 10 + MAX_OPEN_MARKETS_PER_CREATOR uint64 = 50 +FINALIZATION_BOUNTY uint64 = 50_000_000 +CREATOR_BOND uint64 = 5_000_000_000 +RRS_INITIAL uint64 = 100 +MIN_RESOLVER_STAKE uint64 = 500_000_000_000 +) + +// ───────────────────────────────────────────────────────────────────────────── +// PANEL CONSTANTS +// ───────────────────────────────────────────────────────────────────────────── + +const ( +MIN_PANEL_SIZE uint32 = 3 +ELEVATED_RISK_PANEL_SIZE uint32 = 7 +) + +// ───────────────────────────────────────────────────────────────────────────── +// RRS VOTE WEIGHT TIERS (Layer 2 — anti-whale panel protection) +// Bronze: RRS 10–49 → weight 1 +// Silver: RRS 50–199 → weight 2 +// Gold: RRS 200+ → weight 3 +// Hard cap at 3 prevents infinite influence via staking. +// ───────────────────────────────────────────────────────────────────────────── +const ( +RRS_SILVER_THRESHOLD uint64 = 50 +RRS_GOLD_THRESHOLD uint64 = 200 +VOTE_WEIGHT_BRONZE uint32 = 1 +VOTE_WEIGHT_SILVER uint32 = 2 +VOTE_WEIGHT_GOLD uint32 = 3 +) + +// ───────────────────────────────────────────────────────────────────────────── +// PROTOCOL TREASURY +// PRAXIS_TREASURY_ID: destination pool ID for surplus sweeps (R2) and slashes. +// []byte cannot be const in Go — defined as var. +// ───────────────────────────────────────────────────────────────────────────── + +var PRAXIS_TREASURY_ID = []byte{ +0xe7, 0xc7, 0xda, 0xd1, 0x31, 0xa0, 0x3f, 0x7e, +0xa0, 0xcc, 0x09, 0xa6, 0x37, 0xad, 0x09, 0x6e, +0xb3, 0x49, 0x5f, 0x77, +} + +// ───────────────────────────────────────────────────────────────────────────── +// PANEL ENTROPY KEY +// Singleton state key for the 0x1C rolling entropy accumulator. +// Initialised in contract.go init() via JoinLenPrefix. +// ───────────────────────────────────────────────────────────────────────────── + +var panelEntropyPrefix = []byte{0x1C} +var PANEL_ENTROPY_KEY []byte + +// TEST_MODE — enables compressed timing windows for local testing. +// Controlled by the PRAXIS_TEST_MODE environment variable. +// Defaults to false — safe for mainnet. +// To enable: PRAXIS_TEST_MODE=true ./go-plugin +// COI-3: maximum fractional share of the winning side any single address may hold. +// Expressed in basis points: 2000 = 20%. +// Cap is enforced on shares (not CostPaid) so early cheap buyers face the same +// limit as late entrants — prevents single-address dominant share accumulation. +// Limitations (by design): +// - Does not prevent multi-address (Sybil) wash trading — on-chain identity +// is not enforced; two addresses can each hold up to 20%. +// - Cap is share-based so pool growth does not progressively loosen it. +const MAX_POSITION_BPS uint64 = 2000 + +var TEST_MODE = os.Getenv("PRAXIS_TEST_MODE") == "true" +const TEST_DISPUTE_BLOCKS uint64 = 20 +const TEST_RESOLUTION_DELAY uint64 = 2 +const TEST_GRACE_PERIOD uint64 = 2 +const TEST_CLAIM_GRACE_PERIOD uint64 = 50 diff --git a/plugin/go/tutorial/contract/constants_pris.go b/plugin/go/tutorial/contract/constants_pris.go new file mode 100644 index 0000000000..d7ffa19127 --- /dev/null +++ b/plugin/go/tutorial/contract/constants_pris.go @@ -0,0 +1,71 @@ +package contract + +// ───────────────────────────────────────────────────────────────────────────── +// PRIS v1.0-r3 CONSTANTS +// Spec authority: PRIS v1.0-r3 +// ───────────────────────────────────────────────────────────────────────────── + +const ( +// Epoch timing +PRIS_EPOCH_BLOCKS uint64 = 1_000 // ~83 minutes at 5s/block +PRIS_BUILDER_EPOCH_BLOCKS uint64 = 120_960 // ~7 days at 5s/block +PRIS_INVESTOR_VESTING_BLOCKS uint64 = 241_920 // ~14 days at 5s/block + +// Treasury distribution BPS (basis points, 10000 = 100%) +PRIS_RESOLVER_SHARE_BPS uint64 = 2_000 // 20% +PRIS_BUILDER_SHARE_BPS uint64 = 2_000 // 20% +PRIS_COMMUNITY_SHARE_BPS uint64 = 2_000 // 20% +PRIS_INVESTOR_SHARE_BPS uint64 = 2_000 // 20% +PRIS_PROTOCOL_SHARE_BPS uint64 = 2_000 // 20% + +// Fee BPS +CREATOR_FEE_BPS uint64 = 100 // 1% of tradeCost — charged on top +RESOLVER_FEE_BPS uint64 = 100 // 1% of tradeCost — charged on top +TX_TREASURY_SPLIT_BPS uint64 = 5_000 // 50% of TX fees to treasury + +// RRS +// r3 fix R3-1: RRS_INITIAL reduced from 100 to 10. +// New resolvers start at Bronze — no Sybil recycling advantage. +PRIS_RRS_INITIAL uint64 = 10 +PRIS_RRS_FLOOR uint64 = 0 // Slashed resolvers lose all tier weight +) + +// ComputeBps applies basis points to an amount. +// ComputeBps(amount, 2000) = amount * 20 / 100 +func ComputeBps(amount, bps uint64) uint64 { +return amount * bps / 10_000 +} + +// ───────────────────────────────────────────────────────────────────────────── +// PRIS AUTHORIZED WALLET ADDRESSES +// Hardcoded protocol constants — all claim TXs validate signer against these. +// ───────────────────────────────────────────────────────────────────────────── + +var ( +PRAXIS_BUILDER_ADDR = []byte{ +0x95, 0x43, 0x78, 0xba, 0x10, 0x9c, 0x5c, 0xa4, +0x5b, 0x23, 0xbf, 0xa2, 0x84, 0xf3, 0xac, 0x70, +0xe2, 0x67, 0x1b, 0x87, +} +PRAXIS_COMMUNITY_ADDR = []byte{ +0x15, 0xe6, 0x58, 0x69, 0x8d, 0x25, 0x10, 0x79, +0x93, 0x39, 0x27, 0x3f, 0x6f, 0xcc, 0xb0, 0x48, +0x4c, 0x4f, 0x4b, 0x6f, +} +PRAXIS_INVESTOR_ADDR = []byte{ +0x12, 0x5c, 0x1b, 0xb8, 0x03, 0xa2, 0xdd, 0x91, +0x94, 0xdc, 0xa4, 0x0d, 0x77, 0x44, 0x5c, 0xf7, +0x56, 0x47, 0xcb, 0x12, +} +PRAXIS_PROTOCOL_ADDR = []byte{ +0xc1, 0x76, 0x4f, 0x10, 0xad, 0x67, 0x25, 0x58, +0xaf, 0xe1, 0xa3, 0xb6, 0x66, 0x18, 0x5f, 0xd1, +0x41, 0xae, 0x1e, 0xa8, +} +) + +// Unstake constants +const ( +PRIS_UNSTAKE_UNBONDING_BLOCKS uint64 = 120_960 // 7 days at 5s/block +PRIS_UNSTAKE_PARTIAL_RRS_HIT uint64 = 10 // RRS penalty for partial unstake +) diff --git a/plugin/go/tutorial/contract/contract.go b/plugin/go/tutorial/contract/contract.go new file mode 100644 index 0000000000..bac0fe6327 --- /dev/null +++ b/plugin/go/tutorial/contract/contract.go @@ -0,0 +1,284 @@ +package contract + +import ( +"crypto/sha256" + +"google.golang.org/protobuf/types/known/anypb" +) + +var ( +PREFIX_MARKET_STATE = []byte{0x10} +PREFIX_POSITION_STATE = []byte{0x11} +PREFIX_OUTCOME_STATE = []byte{0x12} +PREFIX_RESOLVER_STATE = []byte{0x13} +PREFIX_TREASURY = []byte{0x14} +PREFIX_RESOLVER_RECORD = []byte{0x16} +PREFIX_PROPOSAL_RECORD = []byte{0x17} +PREFIX_DISPUTE_RECORD = []byte{0x18} +PREFIX_VOTE_COMMIT = []byte{0x19} +PREFIX_VOTE_REVEAL = []byte{0x1A} +PREFIX_SLASH_RECORD = []byte{0x1B} +) + +var ( +PREFIX_ACCOUNT = []byte{0x01} +PREFIX_FEE_POOL = []byte{0x02} +) + +// Issue-12: assert MIN_B0 > FINALIZATION_BOUNTY at startup. +// If this ever fails, create_market would seed a TreasuryReserve that cannot +// cover the finalization bounty, silently breaking permissionless finalization. +func init() { +if MIN_B0 <= FINALIZATION_BOUNTY { +panic("invariant violated: MIN_B0 must be greater than FINALIZATION_BOUNTY") +} +} + +var ContractConfig = &PluginConfig{ +Name: "praxis_prediction_market", +Id: 1, +Version: 1, +SupportedTransactions: []string{ +"create_market", +"submit_prediction", +"claim_winnings", +"register_resolver", +"propose_outcome", +"file_dispute", +"commit_vote", +"reveal_vote", +"tally_votes", +"finalize_market", +"claim_slash", +"reclaim_stake", +"forfeit_position", + "claim_builder_reward", + "claim_creator_fee", + "claim_resolver_reward", + "claim_community_reward", + "claim_investor_reward", + "claim_protocol_reward", + "unstake_resolver", + "cancel_market", + "claim_unbonded_stake", +}, +TransactionTypeUrls: []string{ + "type.googleapis.com/types.MessageCreateMarket", + "type.googleapis.com/types.MessageSubmitPrediction", + "type.googleapis.com/types.MessageClaimWinnings", + "type.googleapis.com/types.MessageRegisterResolver", + "type.googleapis.com/types.MessageProposeOutcome", + "type.googleapis.com/types.MessageFileDispute", + "type.googleapis.com/types.MessageCommitVote", + "type.googleapis.com/types.MessageRevealVote", + "type.googleapis.com/types.MessageTallyVotes", + "type.googleapis.com/types.MessageFinalizeMarket", + "type.googleapis.com/types.MessageClaimSlash", + "type.googleapis.com/types.MessageReclaimStake", + "type.googleapis.com/types.MessageForfeitPosition", + "type.googleapis.com/types.MessageClaimBuilderReward", + "type.googleapis.com/types.MessageClaimCreatorFee", + "type.googleapis.com/types.MessageClaimResolverReward", + "type.googleapis.com/types.MessageClaimCommunityReward", + "type.googleapis.com/types.MessageClaimInvestorReward", + "type.googleapis.com/types.MessageClaimProtocolReward", + "type.googleapis.com/types.MessageUnstakeResolver", + "type.googleapis.com/types.MessageCancelMarket", + "type.googleapis.com/types.MessageClaimUnbondedStake", +}, +} + + + +type Contract struct { +Config Config +FSMConfig *PluginFSMConfig +plugin *Plugin +fsmId uint64 +} + +func marketKey(prefix, marketId []byte) []byte { +return append(append([]byte{}, prefix...), marketId...) +} +func positionKey(marketId, addr []byte) []byte { +k := append(append([]byte{}, PREFIX_POSITION_STATE...), marketId...) +return append(k, addr...) +} +func addrKey(prefix, addr []byte) []byte { +return append(append([]byte{}, prefix...), addr...) +} + +func (c *Contract) Genesis(req *PluginGenesisRequest) *PluginGenesisResponse { +return &PluginGenesisResponse{} +} + +func (c *Contract) BeginBlock(req *PluginBeginRequest) *PluginBeginResponse { +SetGlobalHeight(req.Height) + +entropyQId := nextQueryId() +entropyResp, readErr := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: entropyQId, Key: PANEL_ENTROPY_KEY}, +}, +}) + +// Issue-17 fix: replace XOR accumulator with SHA256 hash chain. +// XOR with a deterministic height function is fully predictable by chain +// observers — they can compute the accumulator value at any future block and +// time a file_dispute call to influence panel selection. +// SHA256(prev || height_bytes) is a one-way function: knowing the output +// does not allow computing a height that produces a desired panel seed. +var prev [8]byte +if readErr == nil && entropyResp != nil { +for _, r := range entropyResp.Results { +if r.QueryId == entropyQId && len(r.Entries) > 0 && len(r.Entries[0].Value) >= 8 { +copy(prev[:], r.Entries[0].Value[:8]) +} +} +} + +heightBytes := make([]byte, 8) +for i := 7; i >= 0; i-- { +heightBytes[i] = byte(req.Height & 0xFF) +req.Height >>= 8 +} +input := append(prev[:], heightBytes...) +hash := sha256.Sum256(input) +buf := hash[:8] + +wr, writeErr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: PANEL_ENTROPY_KEY, Value: buf}, +}, +}) +if writeErr != nil || (wr != nil && wr.Error != nil) { +_ = wr +} + +return &PluginBeginResponse{} +} + +func (c *Contract) EndBlock(req *PluginEndRequest) *PluginEndResponse { + height := GetGlobalHeight() + if height > 0 && height%PRIS_EPOCH_BLOCKS == 0 { + _ = c.processEpochBoundary(height) + } + return &PluginEndResponse{} +} + +func (c *Contract) CheckTx(req *PluginCheckRequest) *PluginCheckResponse { +msg, err := FromAny(req.Tx.Msg) +if err != nil { +return &PluginCheckResponse{Error: err} +} +switch m := msg.(type) { +case *MessageSend: +return c.CheckMessageSend(m) +case *MessageCreateMarket: +return c.CheckMessageCreateMarket(m) +case *MessageSubmitPrediction: +return c.CheckMessageSubmitPrediction(m) +case *MessageClaimWinnings: +return c.CheckMessageClaimWinnings(m) +case *MessageRegisterResolver: +return c.CheckMessageRegisterResolver(m) +case *MessageProposeOutcome: +return c.CheckMessageProposeOutcome(m) +case *MessageFileDispute: +return c.CheckMessageFileDispute(m) +case *MessageCommitVote: +return c.CheckMessageCommitVote(m) +case *MessageRevealVote: +return c.CheckMessageRevealVote(m) +case *MessageTallyVotes: +return c.CheckMessageTallyVotes(m) +case *MessageFinalizeMarket: +return c.CheckMessageFinalizeMarket(m) +case *MessageClaimSlash: +return c.CheckMessageClaimSlash(m) +case *MessageReclaimStake: +return c.CheckMessageReclaimStake(m) +case *MessageForfeitPosition: +return c.CheckMessageForfeitPosition(m) +case *MessageClaimBuilderReward: +return c.CheckMessageClaimBuilderReward(m) +case *MessageClaimCreatorFee: +return c.CheckMessageClaimCreatorFee(m) +case *MessageClaimResolverReward: +return c.CheckMessageClaimResolverReward(m) +case *MessageClaimCommunityReward: +return c.CheckMessageClaimCommunityReward(m) +case *MessageClaimInvestorReward: +return c.CheckMessageClaimInvestorReward(m) +case *MessageClaimProtocolReward: +return c.CheckMessageClaimProtocolReward(m) +case *MessageUnstakeResolver: +return c.CheckMessageUnstakeResolver(m) +case *MessageCancelMarket: +return c.CheckMessageCancelMarket(m) +case *MessageClaimUnbondedStake: +return c.CheckMessageClaimUnbondedStake(m) +default: +return &PluginCheckResponse{Error: ErrInvalidMessageCast()} +} +} + +func (c *Contract) DeliverTx(req *PluginDeliverRequest) *PluginDeliverResponse { +msg, err := FromAny(req.Tx.Msg) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +fee := req.Tx.Fee +switch m := msg.(type) { +case *MessageSend: +return c.DeliverMessageSend(m, fee) +case *MessageCreateMarket: +return c.DeliverMessageCreateMarket(m, fee) +case *MessageSubmitPrediction: +return c.DeliverMessageSubmitPrediction(m, fee) +case *MessageClaimWinnings: +return c.DeliverMessageClaimWinnings(m, fee) +case *MessageRegisterResolver: +return c.DeliverMessageRegisterResolver(m, fee) +case *MessageProposeOutcome: +return c.DeliverMessageProposeOutcome(m, fee) +case *MessageFileDispute: +return c.DeliverMessageFileDispute(m, fee) +case *MessageCommitVote: +return c.DeliverMessageCommitVote(m, fee) +case *MessageRevealVote: +return c.DeliverMessageRevealVote(m, fee) +case *MessageTallyVotes: +return c.DeliverMessageTallyVotes(m, fee) +case *MessageFinalizeMarket: +return c.DeliverMessageFinalizeMarket(m, fee) +case *MessageClaimSlash: +return c.DeliverMessageClaimSlash(m, fee) +case *MessageReclaimStake: +return c.DeliverMessageReclaimStake(m, fee) +case *MessageForfeitPosition: +return c.DeliverMessageForfeitPosition(m, fee) +case *MessageClaimBuilderReward: +return c.DeliverMessageClaimBuilderReward(m, fee) +case *MessageClaimCreatorFee: +return c.DeliverMessageClaimCreatorFee(m, fee) +case *MessageClaimResolverReward: +return c.DeliverMessageClaimResolverReward(m, fee) +case *MessageClaimCommunityReward: +return c.DeliverMessageClaimCommunityReward(m, fee) +case *MessageClaimInvestorReward: +return c.DeliverMessageClaimInvestorReward(m, fee) +case *MessageClaimProtocolReward: +return c.DeliverMessageClaimProtocolReward(m, fee) +case *MessageUnstakeResolver: +return c.DeliverMessageUnstakeResolver(m, fee) +case *MessageCancelMarket: +return c.DeliverMessageCancelMarket(m, fee) +case *MessageClaimUnbondedStake: +return c.DeliverMessageClaimUnbondedStake(m, fee) +default: +return &PluginDeliverResponse{Error: ErrInvalidMessageCast()} +} +} + +var _ = (*anypb.Any)(nil) diff --git a/plugin/go/tutorial/contract/error.go b/plugin/go/tutorial/contract/error.go new file mode 100644 index 0000000000..ef11edef22 --- /dev/null +++ b/plugin/go/tutorial/contract/error.go @@ -0,0 +1,331 @@ +package contract + +// ═══════════════════════════════════════════════════════════════════════════════ +// Praxis Prediction Market — Error Definitions +// Spec authority: +// ADLMSR v5.6.6-r2-CORRECTED +// PORS v1.0-r2-CORRECTED +// +// Built-in Canopy error codes 1–14 are reserved — never reuse them. +// Praxis error codes start at 100. +// ═══════════════════════════════════════════════════════════════════════════════ + +const errModule = "praxis" + +// ───────────────────────────────────────────────────────────────────────────── +// CANOPY BUILT-IN ERRORS (1–14) +// These constructors wrap the built-in codes from plugin.go. +// ───────────────────────────────────────────────────────────────────────────── + +func ErrPluginTimeout() *PluginError { +return &PluginError{Code: 1, Module: errModule, Msg: "plugin timeout"} +} +func ErrMarshal(err error) *PluginError { +return &PluginError{Code: 2, Module: errModule, Msg: "marshal error: " + err.Error()} +} +func ErrUnmarshal(err error) *PluginError { +return &PluginError{Code: 3, Module: errModule, Msg: "unmarshal error: " + err.Error()} +} +func ErrPluginRead(err error) *PluginError { +return &PluginError{Code: 4, Module: errModule, Msg: "state read error: " + err.Error()} +} +func ErrPluginWrite(err error) *PluginError { +return &PluginError{Code: 5, Module: errModule, Msg: "state write error: " + err.Error()} +} +func ErrInvalidResponseId() *PluginError { +return &PluginError{Code: 6, Module: errModule, Msg: "invalid response id"} +} +func ErrUnexpectedType() *PluginError { +return &PluginError{Code: 7, Module: errModule, Msg: "unexpected message type"} +} +func ErrInvalidMessage() *PluginError { +return &PluginError{Code: 8, Module: errModule, Msg: "invalid message"} +} +func ErrInsufficientFunds() *PluginError { +return &PluginError{Code: 9, Module: errModule, Msg: "insufficient funds"} +} +func ErrFromAny(err error) *PluginError { +return &PluginError{Code: 10, Module: errModule, Msg: "from any error: " + err.Error()} +} +func ErrInvalidMessageCast() *PluginError { +return &PluginError{Code: 11, Module: errModule, Msg: "invalid message cast"} +} +func ErrInvalidAddress() *PluginError { +return &PluginError{Code: 12, Module: errModule, Msg: "invalid address: must be exactly 20 bytes"} +} +func ErrInvalidAmount() *PluginError { +return &PluginError{Code: 13, Module: errModule, Msg: "invalid amount: must be non-zero"} +} +func ErrTxFeeBelowStateLimit() *PluginError { +return &PluginError{Code: 14, Module: errModule, Msg: "tx fee below minimum"} +} + +// ───────────────────────────────────────────────────────────────────────────── +// PRAXIS ERRORS — GENERAL (100–119) +// ───────────────────────────────────────────────────────────────────────────── + +func ErrStateReadFailed() *PluginError { +return &PluginError{Code: 100, Module: errModule, Msg: "state read failed"} +} +func ErrMarshalFailed() *PluginError { +return &PluginError{Code: 101, Module: errModule, Msg: "marshal failed"} +} +func ErrUnmarshalFailed() *PluginError { +return &PluginError{Code: 102, Module: errModule, Msg: "unmarshal failed"} +} +func ErrHeightNotSet() *PluginError { +return &PluginError{Code: 103, Module: errModule, Msg: "global height not set: BeginBlock has not been called"} +} +func ErrInternal() *PluginError { +return &PluginError{Code: 104, Module: errModule, Msg: "internal error"} +} +func ErrUnauthorized() *PluginError { +return &PluginError{Code: 105, Module: errModule, Msg: "unauthorized: caller is not the registered resolver"} +} +func ErrInvalidParam() *PluginError { +return &PluginError{Code: 106, Module: errModule, Msg: "invalid parameter"} +} + +// ───────────────────────────────────────────────────────────────────────────── +// PRAXIS ERRORS — MARKET (120–139) +// ───────────────────────────────────────────────────────────────────────────── + +func ErrTooManyOpenMarkets() *PluginError { return &PluginError{Code: 218, Module: errModule, Msg: "creator has too many open markets"} } +func ErrMarketNotFound() *PluginError { +return &PluginError{Code: 120, Module: errModule, Msg: "market not found"} +} +func ErrMarketNotOpen() *PluginError { +return &PluginError{Code: 121, Module: errModule, Msg: "market is not open"} +} +func ErrMarketCancelled() *PluginError { +return &PluginError{Code: 122, Module: errModule, Msg: "market has been cancelled"} +} +func ErrMarketNotResolved() *PluginError { +return &PluginError{Code: 123, Module: errModule, Msg: "market has not been resolved"} +} +func ErrMarketNotExpired() *PluginError { +return &PluginError{Code: 124, Module: errModule, Msg: "market has not expired yet"} +} +func ErrResolutionTooEarly() *PluginError { +return &PluginError{Code: 125, Module: errModule, Msg: "resolution window has not opened yet"} +} +func ErrExpiryTooLarge() *PluginError { +return &PluginError{Code: 126, Module: errModule, Msg: "expiry_time exceeds MAX_EXPIRY_TIME"} +} +func ErrInvalidNonce() *PluginError { +return &PluginError{Code: 127, Module: errModule, Msg: "nonce must be non-zero"} +} +func ErrInvalidQuestion() *PluginError { +return &PluginError{Code: 128, Module: errModule, Msg: "question must be non-empty"} +} +func ErrInvalidB0() *PluginError { +return &PluginError{Code: 129, Module: errModule, Msg: "b0 must be >= MIN_B0"} +} + +// ───────────────────────────────────────────────────────────────────────────── +// PRAXIS ERRORS — POSITION / PREDICTION (140–159) +// ───────────────────────────────────────────────────────────────────────────── + +func ErrNoPosition() *PluginError { +return &PluginError{Code: 140, Module: errModule, Msg: "no position found for this address in this market"} +} +func ErrAlreadyClaimed() *PluginError { +return &PluginError{Code: 141, Module: errModule, Msg: "winnings already claimed"} +} +func ErrCostExceedsMaxCost() *PluginError { +return &PluginError{Code: 142, Module: errModule, Msg: "computed cost exceeds max_cost slippage limit"} +} +func ErrSharesBelowMinimum() *PluginError { +return &PluginError{Code: 143, Module: errModule, Msg: "shares must be >= PRECISION_SCALE"} +} +func ErrInsufficientPoolFunds() *PluginError { +return &PluginError{Code: 144, Module: errModule, Msg: "insufficient funds in market pool"} +} + +// ───────────────────────────────────────────────────────────────────────────── +// PRAXIS ERRORS — PORS RESOLVER (160–179) +// ───────────────────────────────────────────────────────────────────────────── + +func ErrResolverNotRegistered() *PluginError { +return &PluginError{Code: 160, Module: errModule, Msg: "resolver is not registered"} +} +func ErrResolverSuspended() *PluginError { +return &PluginError{Code: 161, Module: errModule, Msg: "resolver RRS score is below minimum threshold"} +} +func ErrNoResolverRegistered() *PluginError { +return &PluginError{Code: 162, Module: errModule, Msg: "no resolver registered for this market"} +} +func ErrAlreadyProposed() *PluginError { +return &PluginError{Code: 163, Module: errModule, Msg: "outcome already proposed for this market"} +} +func ErrInsufficientBond() *PluginError { +return &PluginError{Code: 164, Module: errModule, Msg: "proposal bond is below minimum required"} +} + +// ───────────────────────────────────────────────────────────────────────────── +// PRAXIS ERRORS — PORS DISPUTE (180–199) +// ───────────────────────────────────────────────────────────────────────────── + +func ErrDisputeWindowOpen() *PluginError { +return &PluginError{Code: 181, Module: errModule, Msg: "dispute window is still open — too early to finalize"} +} + +func ErrDisputeWindowClosed() *PluginError { +return &PluginError{Code: 180, Module: errModule, Msg: "dispute window has closed"} +} +func ErrAlreadyDisputed() *PluginError { +return &PluginError{Code: 181, Module: errModule, Msg: "market is already disputed"} +} +func ErrNotDisputed() *PluginError { +return &PluginError{Code: 182, Module: errModule, Msg: "market is not in disputed state"} +} +func ErrNotAPanelMember() *PluginError { +return &PluginError{Code: 183, Module: errModule, Msg: "caller is not a panel member for this dispute"} +} +func ErrCommitPhaseOver() *PluginError { +return &PluginError{Code: 184, Module: errModule, Msg: "commit phase has ended"} +} +func ErrRevealPhaseNotOpen() *PluginError { +return &PluginError{Code: 185, Module: errModule, Msg: "reveal phase has not started yet"} +} +func ErrRevealPhaseOver() *PluginError { +return &PluginError{Code: 186, Module: errModule, Msg: "reveal phase has ended"} +} +func ErrCommitHashMismatch() *PluginError { +return &PluginError{Code: 187, Module: errModule, Msg: "revealed vote does not match committed hash"} +} +func ErrAlreadyCommitted() *PluginError { +return &PluginError{Code: 188, Module: errModule, Msg: "vote already committed"} +} +func ErrAlreadyRevealed() *PluginError { +return &PluginError{Code: 189, Module: errModule, Msg: "vote already revealed"} +} +func ErrTallyNotReady() *PluginError { +return &PluginError{Code: 190, Module: errModule, Msg: "reveal phase has not ended yet"} +} +func ErrAlreadyTallied() *PluginError { +return &PluginError{Code: 191, Module: errModule, Msg: "votes already tallied"} +} +func ErrNotFinalized() *PluginError { +return &PluginError{Code: 192, Module: errModule, Msg: "market has not been finalized"} +} +func ErrNoSlashToClaim() *PluginError { +return &PluginError{Code: 193, Module: errModule, Msg: "no slash proceeds to claim"} +} +func ErrInvalidCommitHash() *PluginError { +return &PluginError{Code: 194, Module: errModule, Msg: "commit hash must be exactly 32 bytes"} +} +func ErrInsufficientPanelCandidates() *PluginError { +return &PluginError{Code: 195, Module: errModule, Msg: "insufficient eligible panel candidates after position exclusion"} +} +func ErrMarketNotReclaimable() *PluginError { +return &PluginError{Code: 196, Module: errModule, Msg: "market is not eligible for stake reclaim"} +} +func ErrReclaimWindowClosed() *PluginError { +return &PluginError{Code: 197, Module: errModule, Msg: "reclaim window has not opened yet"} +} +func ErrNoStakeToReclaim() *PluginError { +return &PluginError{Code: 198, Module: errModule, Msg: "no stake or position to reclaim"} +} + +// ───────────────────────────────────────────────────────────────────────────── +// HELPERS +// ───────────────────────────────────────────────────────────────────────────── + +// SafeMarshal wraps Marshal and returns a typed *PluginError on failure. +// Every marshal in a handler must use this — never discard marshal errors. +func SafeMarshal(m interface{}) ([]byte, *PluginError) { +b, err := Marshal(m) +if err != nil { +return nil, ErrMarshalFailed() +} +return b, nil +} + +// errCheckWrite checks both the transport error and the embedded response error +// from a StateWrite call. Both must be checked — per the Canopy plugin spec. +func errCheckWrite(wr *PluginStateWriteResponse, err *PluginError) *PluginError { +if err != nil { +return err +} +if wr != nil && wr.Error != nil { +return wr.Error +} +return nil +} + +// ErrCheckResp is a convenience wrapper for returning errors from CheckTx handlers. +func ErrResolverHasPosition() *PluginError { +return &PluginError{Code: 199, Module: errModule, Msg: "resolver holds a position in this market"} +} + +func ErrCreatorCannotResolve() *PluginError { +return &PluginError{Code: 200, Module: errModule, Msg: "market creator cannot be the resolver for the same market"} +} + +func ErrPositionCapExceeded() *PluginError { +return &PluginError{Code: 201, Module: errModule, Msg: "position would exceed per-address cap (20% of pool)"} +} +func ErrInsufficientResolverStake() *PluginError { +return &PluginError{Code: 202, Module: errModule, Msg: "resolver stake below minimum (500,000 PRX required)"} +} + +func ErrCheckResp(err *PluginError) *PluginCheckResponse { +return &PluginCheckResponse{Error: err} +} + +// ───────────────────────────────────────────────────────────────────────────── +// PLUGIN INFRASTRUCTURE ERRORS — required by plugin.go (never modify plugin.go) +// ───────────────────────────────────────────────────────────────────────────── + +// Error() satisfies the error interface so plugin.go can call err.Error(). +func (e *PluginError) Error() string { +if e == nil { +return "" +} +return e.Module + ": " + e.Msg +} + +func ErrUnexpectedFSMToPlugin(t interface{}) *PluginError { +return &PluginError{Code: 7, Module: errModule, Msg: "unexpected FSM-to-plugin message type"} +} + +func ErrInvalidFSMToPluginMMessage(t interface{}) *PluginError { +return &PluginError{Code: 8, Module: errModule, Msg: "invalid FSM-to-plugin message"} +} + +func ErrInvalidPluginRespId() *PluginError { +return &PluginError{Code: 6, Module: errModule, Msg: "invalid plugin response id"} +} + +func ErrFailedPluginWrite(err error) *PluginError { +return &PluginError{Code: 5, Module: errModule, Msg: "plugin socket write failed: " + err.Error()} +} + +func ErrFailedPluginRead(err error) *PluginError { +return &PluginError{Code: 4, Module: errModule, Msg: "plugin socket read failed: " + err.Error()} +} + +func ErrMarketExpired() *PluginError { return &PluginError{Code: 215, Module: errModule, Msg: "market has expired"} } +func ErrMarketHasPositions() *PluginError { return &PluginError{Code: 216, Module: errModule, Msg: "market has positions — cannot cancel"} } +func ErrActiveProposalExists() *PluginError { return &PluginError{Code: 210, Module: errModule, Msg: "active proposal exists — cannot unstake"} } +func ErrResolverNotActive() *PluginError { return &PluginError{Code: 211, Module: errModule, Msg: "resolver is not active"} } +func ErrNoUnbondingStake() *PluginError { return &PluginError{Code: 212, Module: errModule, Msg: "no unbonding stake to claim"} } +func ErrUnbondingNotComplete() *PluginError { return &PluginError{Code: 213, Module: errModule, Msg: "unbonding period not complete"} } +func ErrUnbondingAlreadyPending() *PluginError { return &PluginError{Code: 217, Module: errModule, Msg: "unbonding already pending"} } +func ErrResolverNotFound() *PluginError { return &PluginError{Code: 214, Module: errModule, Msg: "resolver record not found"} } +func ErrCooldownNotElapsed() *PluginError { +return &PluginError{Code: 203, Module: errModule, Msg: "cooldown period has not elapsed"} +} +func ErrEmptyPool() *PluginError { +return &PluginError{Code: 204, Module: errModule, Msg: "pool is empty — nothing to claim"} +} +func ErrMarketNotFinalized() *PluginError { +return &PluginError{Code: 205, Module: errModule, Msg: "market is not finalized"} +} +func ErrInsufficientRRS() *PluginError { +return &PluginError{Code: 207, Module: errModule, Msg: "resolver RRS score is zero — not eligible for rewards"} +} +func ErrNoResolutions() *PluginError { +return &PluginError{Code: 208, Module: errModule, Msg: "no successful resolutions in this epoch"} +} diff --git a/plugin/go/tutorial/contract/event.pb.go b/plugin/go/tutorial/contract/event.pb.go index e9a0989549..7b27121d26 100644 --- a/plugin/go/tutorial/contract/event.pb.go +++ b/plugin/go/tutorial/contract/event.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.11 // protoc v5.29.3 // source: event.proto @@ -219,7 +219,7 @@ const file_event_proto_rawDesc = "" + "\aaddress\x18` \x01(\fR\aaddressB\x05\n" + "\x03msg\"5\n" + "\vEventCustom\x12&\n" + - "\x03msg\x18\x01 \x01(\v2\x14.google.protobuf.AnyR\x03msgB3Z1github.com/canopy-network/go-plugin-test/contractb\x06proto3" + "\x03msg\x18\x01 \x01(\v2\x14.google.protobuf.AnyR\x03msgB5Z3github.com/canopy-network/canopy/plugin/go/contractb\x06proto3" var ( file_event_proto_rawDescOnce sync.Once diff --git a/plugin/go/tutorial/contract/handler_auto_cancel.go b/plugin/go/tutorial/contract/handler_auto_cancel.go new file mode 100644 index 0000000000..93c4935ccb --- /dev/null +++ b/plugin/go/tutorial/contract/handler_auto_cancel.go @@ -0,0 +1,108 @@ +package contract + +// handler_auto_cancel.go — CheckAutoCancel helper +// Spec: ADLMSR v5.6.6-r2-CORRECTED (AUDIT-2) +// +// CheckAutoCancel is called at the start of SubmitPrediction, ResolveMarket, +// and ClaimWinnings. If the market has passed its expiry window without a +// resolver committing, it cancels the market atomically. +// +// This is NOT a transaction type — it is a shared helper with a 4-key atomic write. +// AUDIT-2: single atomic write — market status + pool + treasury + resolver state. +// +// Returns nil if no cancellation was needed or cancellation succeeded. +// Returns *PluginError if state read/write failed. + +func (c *Contract) CheckAutoCancel(marketId []byte) *PluginError { +now := GetGlobalHeight() +if now == 0 { +return ErrHeightNotSet() +} + +marketQId := nextQueryId() +poolQId := nextQueryId() + +marketKey := KeyForMarket(marketId) +poolKey := KeyForMarketPool(marketId) + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: marketKey}, +{QueryId: poolQId, Key: poolKey}, +}, +}) +if err != nil { +return err +} +if resp.Error != nil { +return resp.Error +} + +var market *MarketState +var pool *Pool + +for _, r := range resp.Results { +if len(r.Entries) == 0 { +continue +} +switch r.QueryId { +case marketQId: +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return pe +} +case poolQId: +pool = &Pool{} +if pe := Unmarshal(r.Entries[0].Value, pool); pe != nil { +return pe +} +} +} + +// Market not found or already in a terminal/non-open state — nothing to cancel. +if market == nil { +return nil +} +if market.Status != STATUS_OPEN { +return nil +} + +// Market is still open — check if the resolution window has passed entirely. +// Auto-cancel condition: now > ExpiryTime + GRACE_PERIOD_BLOCKS +// (resolver had the full grace period and did not resolve). +// AUDIT-11: ExpiryTime is bounded by MAX_EXPIRY_TIME so addition cannot overflow. +cancelThreshold := market.ExpiryTime + RESOLUTION_DELAY_BLOCKS + GRACE_PERIOD_BLOCKS +if now <= cancelThreshold { +// Still within window — no cancellation. +return nil +} + +// Auto-cancel: transition market to STATUS_CANCELLED. +market.Status = STATUS_CANCELLED + +rawMarket, pe := SafeMarshal(market) +if pe != nil { +return pe +} + +sets := []*PluginSetOp{ +{Key: marketKey, Value: rawMarket}, +} + +// If pool is non-nil and has funds, sweep them to treasury. +// This handles the case where bettors submitted predictions but no resolver appeared. +if pool != nil && pool.Amount > 0 { +pool.Amount = 0 +rawPool, pe := SafeMarshal(pool) +if pe != nil { +return pe +} +sets = append(sets, &PluginSetOp{Key: poolKey, Value: rawPool}) +} + +// AUDIT-2: single atomic write. +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: sets, +}) +return errCheckWrite(wr, werr) +} diff --git a/plugin/go/tutorial/contract/handler_cancel_market.go b/plugin/go/tutorial/contract/handler_cancel_market.go new file mode 100644 index 0000000000..9df6ee6d58 --- /dev/null +++ b/plugin/go/tutorial/contract/handler_cancel_market.go @@ -0,0 +1,201 @@ +package contract + +// handler_cancel_market.go — MessageCancelMarket +// Spec: Praxis core +// +// Allows a market creator to cancel their own market before expiry. +// Conditions: +// - Market must be STATUS_OPEN +// - now < market.ExpiryTime (not yet expired) +// - Signer must be market.Creator +// - TotalPositions must be 0 (no bets placed — Option 2) +// +// On cancel: +// - Market status → STATUS_CANCELLED +// - Creator receives CreatorBond + TreasuryReserve.LockedReserve back +// - Creator fee pool + resolver fee pool swept to KeyForTreasuryPool() +// - Market pool remains — bettors claim refunds via claim_winnings (STATUS_CANCELLED path) + +func (c *Contract) CheckMessageCancelMarket(msg *MessageCancelMarket) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.CreatorAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.CreatorAddress}, +} +} + +func (c *Contract) DeliverMessageCancelMarket(msg *MessageCancelMarket, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +// ── Batch read ──────────────────────────────────────────────────────── +marketQId := nextQueryId() +treasQId := nextQueryId() +creatorAccQId := nextQueryId() +creatorFeeQId := nextQueryId() +resolverFeeQId := nextQueryId() +gTreasuryQId := nextQueryId() + ocQId := nextQueryId() +feeQId := nextQueryId() + +marketKey := KeyForMarket(msg.MarketId) +treasKey := KeyForTreasuryReserve(msg.MarketId) +creatorAccKey := KeyForAccount(msg.CreatorAddress) +creatorFeeKey := KeyForCreatorFeePool(msg.MarketId) +resolverFeeKey := KeyForResolverFeePool(msg.MarketId) +gTreasuryKey := KeyForTreasuryPool() + ocKey := KeyForCreatorOpenCount(msg.CreatorAddress) +feePoolKey := KeyForFeePool(c.Config.ChainId) + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: marketKey}, +{QueryId: treasQId, Key: treasKey}, +{QueryId: creatorAccQId, Key: creatorAccKey}, +{QueryId: creatorFeeQId, Key: creatorFeeKey}, +{QueryId: resolverFeeQId, Key: resolverFeeKey}, +{QueryId: gTreasuryQId, Key: gTreasuryKey}, + {QueryId: ocQId, Key: ocKey}, +{QueryId: feeQId, Key: feePoolKey}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +treas := &TreasuryReserve{} +creatorAcc := &Account{} +creatorFee := &Pool{} +resolverFee := &Pool{} +gTreasury := &Pool{} + openCount := Pool{} +feePool := &Pool{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +switch r.QueryId { +case marketQId: +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case treasQId: +if pe := Unmarshal(r.Entries[0].Value, treas); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case creatorAccQId: +if pe := Unmarshal(r.Entries[0].Value, creatorAcc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case creatorFeeQId: +if pe := Unmarshal(r.Entries[0].Value, creatorFee); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case resolverFeeQId: +if pe := Unmarshal(r.Entries[0].Value, resolverFee); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case ocQId: + if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { + _ = Unmarshal(r.Entries[0].Value, &openCount) + } + case gTreasuryQId: +if pe := Unmarshal(r.Entries[0].Value, gTreasury); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case feeQId: +if pe := Unmarshal(r.Entries[0].Value, feePool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +if market.Status != STATUS_OPEN { +return &PluginDeliverResponse{Error: ErrMarketNotOpen()} +} +if now >= market.ExpiryTime { +return &PluginDeliverResponse{Error: ErrMarketExpired()} +} +if !bytesEqual(msg.CreatorAddress, market.Creator) { +return &PluginDeliverResponse{Error: ErrUnauthorized()} +} +// Option 2: block cancel if any positions exist +if market.TotalPositions > 0 { +return &PluginDeliverResponse{Error: ErrMarketHasPositions()} +} + +// ── Compute refund ──────────────────────────────────────────────────── +// Creator gets back: CreatorBond + LockedReserve (finalization bounty) +refund := treas.CreatorBond + treas.LockedReserve + +// ── Mutate ──────────────────────────────────────────────────────────── +market.Status = STATUS_CANCELLED +treas.CreatorBond = 0 +treas.LockedReserve = 0 +creatorAcc.Amount += refund + +// Sweep creator fee pool + resolver fee pool to global treasury +gTreasury.Amount += creatorFee.Amount + resolverFee.Amount +creatorFee.Amount = 0 +resolverFee.Amount = 0 + +// TX fee split 50/50 +feePool.Amount += fee / 2 +gTreasury.Amount += fee - fee/2 + +// Decrement open market counter + if openCount.Amount > 0 { + openCount.Amount-- + } + + // ── Marshal ─────────────────────────────────────────────────────────── +rawMarket, pe := SafeMarshal(market) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawTreas, pe := SafeMarshal(treas) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawCreatorAcc, pe := SafeMarshal(creatorAcc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawCreatorFee, pe := SafeMarshal(creatorFee) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawResolverFee, pe := SafeMarshal(resolverFee) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawOC, pe := SafeMarshal(&openCount) + if pe != nil { return &PluginDeliverResponse{Error: pe} } + rawGTreasury, pe := SafeMarshal(gTreasury) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawFee, pe := SafeMarshal(feePool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +// ── 7-key atomic write ──────────────────────────────────────────────── +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: marketKey, Value: rawMarket}, +{Key: treasKey, Value: rawTreas}, +{Key: creatorAccKey, Value: rawCreatorAcc}, +{Key: creatorFeeKey, Value: rawCreatorFee}, +{Key: resolverFeeKey, Value: rawResolverFee}, +{Key: ocKey, Value: rawOC}, + {Key: gTreasuryKey, Value: rawGTreasury}, +{Key: feePoolKey, Value: rawFee}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_claim_builder_reward.go b/plugin/go/tutorial/contract/handler_claim_builder_reward.go new file mode 100644 index 0000000000..36318b9793 --- /dev/null +++ b/plugin/go/tutorial/contract/handler_claim_builder_reward.go @@ -0,0 +1,99 @@ +package contract + +// handler_claim_builder_reward.go — MessageClaimBuilderReward +// Spec: PRIS v1.0-r3 +// +// Transfers the full KeyForBuilderPool() balance to PRAXIS_BUILDER_ADDR. +// Cooldown: PRIS_BUILDER_EPOCH_BLOCKS (120,960 blocks ~ 7 days) +// Signer: PRAXIS_BUILDER_ADDR only +// +// r3 fix R3-3: cooldown tracked in KeyForBuilderLastClaimed() singleton. +// r3 fix R3-5: claims full pool balance — rewards compound across missed epochs. + +func (c *Contract) CheckMessageClaimBuilderReward(msg *MessageClaimBuilderReward) *PluginCheckResponse { +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{PRAXIS_BUILDER_ADDR}, +} +} + +func (c *Contract) DeliverMessageClaimBuilderReward(msg *MessageClaimBuilderReward, fee uint64) *PluginDeliverResponse { +height := GetGlobalHeight() +if height == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +poolQId := nextQueryId() +lastQId := nextQueryId() +accQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: poolQId, Key: KeyForBuilderPool()}, +{QueryId: lastQId, Key: KeyForBuilderLastClaimed()}, +{QueryId: accQId, Key: KeyForAccount(PRAXIS_BUILDER_ADDR)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +pool := &Pool{} +lastClaimed := uint64(0) +acc := &Account{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 { +continue +} +switch r.QueryId { +case poolQId: +if pe := Unmarshal(r.Entries[0].Value, pool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case lastQId: +lc := &LastClaimedBlock{} +if pe := Unmarshal(r.Entries[0].Value, lc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +lastClaimed = lc.Height +case accQId: +if pe := Unmarshal(r.Entries[0].Value, acc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if lastClaimed > 0 && height < lastClaimed+PRIS_BUILDER_EPOCH_BLOCKS { +return &PluginDeliverResponse{Error: ErrCooldownNotElapsed()} +} + +if pool.Amount == 0 { +return &PluginDeliverResponse{Error: ErrEmptyPool()} +} + +payout := pool.Amount +acc.Amount += payout +pool.Amount = 0 + +rawPool, pe := SafeMarshal(pool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawAcc, pe := SafeMarshal(acc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawLast, pe := SafeMarshal(&LastClaimedBlock{Height: height}) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForBuilderPool(), Value: rawPool}, +{Key: KeyForAccount(PRAXIS_BUILDER_ADDR), Value: rawAcc}, +{Key: KeyForBuilderLastClaimed(), Value: rawLast}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_claim_community_reward.go b/plugin/go/tutorial/contract/handler_claim_community_reward.go new file mode 100644 index 0000000000..addd1cab52 --- /dev/null +++ b/plugin/go/tutorial/contract/handler_claim_community_reward.go @@ -0,0 +1,74 @@ +package contract + +// handler_claim_community_reward.go — MessageClaimCommunityReward +// Spec: PRIS v1.0-r3 +// Signer: PRAXIS_COMMUNITY_ADDR — no cooldown (grants/airdrops on demand) + +func (c *Contract) CheckMessageClaimCommunityReward(msg *MessageClaimCommunityReward) *PluginCheckResponse { +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{PRAXIS_COMMUNITY_ADDR}, +} +} + +func (c *Contract) DeliverMessageClaimCommunityReward(msg *MessageClaimCommunityReward, fee uint64) *PluginDeliverResponse { +height := GetGlobalHeight() +if height == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +poolQId := nextQueryId() +accQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: poolQId, Key: KeyForCommunityPool()}, +{QueryId: accQId, Key: KeyForAccount(PRAXIS_COMMUNITY_ADDR)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +pool := &Pool{} +acc := &Account{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 { continue } +switch r.QueryId { +case poolQId: +if pe := Unmarshal(r.Entries[0].Value, pool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case accQId: +if pe := Unmarshal(r.Entries[0].Value, acc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if pool.Amount == 0 { +return &PluginDeliverResponse{Error: ErrEmptyPool()} +} + +acc.Amount += pool.Amount +pool.Amount = 0 + +rawPool, pe := SafeMarshal(pool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawAcc, pe := SafeMarshal(acc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForCommunityPool(), Value: rawPool}, +{Key: KeyForAccount(PRAXIS_COMMUNITY_ADDR), Value: rawAcc}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_claim_creator_fee.go b/plugin/go/tutorial/contract/handler_claim_creator_fee.go new file mode 100644 index 0000000000..6079c3e5f1 --- /dev/null +++ b/plugin/go/tutorial/contract/handler_claim_creator_fee.go @@ -0,0 +1,102 @@ +package contract + +// handler_claim_creator_fee.go — MessageClaimCreatorFee +// Spec: PRIS v1.0-r3 +// +// Transfers the full KeyForCreatorFeePool(marketId) balance to market.Creator. +// Condition: market must be STATUS_FINALIZED. +// Signer: market.Creator only. + +func (c *Contract) CheckMessageClaimCreatorFee(msg *MessageClaimCreatorFee) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.CreatorAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.CreatorAddress}, +} +} + +func (c *Contract) DeliverMessageClaimCreatorFee(msg *MessageClaimCreatorFee, fee uint64) *PluginDeliverResponse { +height := GetGlobalHeight() +if height == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +marketQId := nextQueryId() +feePoolQId := nextQueryId() +accQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: KeyForMarket(msg.MarketId)}, +{QueryId: feePoolQId, Key: KeyForCreatorFeePool(msg.MarketId)}, +{QueryId: accQId, Key: KeyForAccount(msg.CreatorAddress)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +market := &MarketState{} +feePool := &Pool{} +acc := &Account{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 { +continue +} +switch r.QueryId { +case marketQId: +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case feePoolQId: +if pe := Unmarshal(r.Entries[0].Value, feePool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case accQId: +if pe := Unmarshal(r.Entries[0].Value, acc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if market.Status != STATUS_FINALIZED { +return &PluginDeliverResponse{Error: ErrMarketNotFinalized()} +} +if !bytesEqual(market.Creator, msg.CreatorAddress) { +return &PluginDeliverResponse{Error: ErrUnauthorized()} +} +if feePool.Amount == 0 { +return &PluginDeliverResponse{Error: ErrEmptyPool()} +} +if fee > 0 && acc.Amount > ^uint64(0)-feePool.Amount { +return &PluginDeliverResponse{Error: ErrInvalidAmount()} +} + +payout := feePool.Amount +acc.Amount += payout +feePool.Amount = 0 + +rawFeePool, pe := SafeMarshal(feePool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawAcc, pe := SafeMarshal(acc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForCreatorFeePool(msg.MarketId), Value: rawFeePool}, +{Key: KeyForAccount(msg.CreatorAddress), Value: rawAcc}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_claim_investor_reward.go b/plugin/go/tutorial/contract/handler_claim_investor_reward.go new file mode 100644 index 0000000000..b593edf7ca --- /dev/null +++ b/plugin/go/tutorial/contract/handler_claim_investor_reward.go @@ -0,0 +1,89 @@ +package contract + +// handler_claim_investor_reward.go — MessageClaimInvestorReward +// Spec: PRIS v1.0-r3 +// Signer: PRAXIS_INVESTOR_ADDR — 2-week vesting cooldown (241,920 blocks) + +func (c *Contract) CheckMessageClaimInvestorReward(msg *MessageClaimInvestorReward) *PluginCheckResponse { +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{PRAXIS_INVESTOR_ADDR}, +} +} + +func (c *Contract) DeliverMessageClaimInvestorReward(msg *MessageClaimInvestorReward, fee uint64) *PluginDeliverResponse { +height := GetGlobalHeight() +if height == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +poolQId := nextQueryId() +lastQId := nextQueryId() +accQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: poolQId, Key: KeyForInvestorPool()}, +{QueryId: lastQId, Key: KeyForInvestorLastClaimed()}, +{QueryId: accQId, Key: KeyForAccount(PRAXIS_INVESTOR_ADDR)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +pool := &Pool{} +lastClaimed := uint64(0) +acc := &Account{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 { continue } +switch r.QueryId { +case poolQId: +if pe := Unmarshal(r.Entries[0].Value, pool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case lastQId: +lc := &LastClaimedBlock{} +if pe := Unmarshal(r.Entries[0].Value, lc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +lastClaimed = lc.Height +case accQId: +if pe := Unmarshal(r.Entries[0].Value, acc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if lastClaimed > 0 && height < lastClaimed+PRIS_INVESTOR_VESTING_BLOCKS { +return &PluginDeliverResponse{Error: ErrCooldownNotElapsed()} +} +if pool.Amount == 0 { +return &PluginDeliverResponse{Error: ErrEmptyPool()} +} + +acc.Amount += pool.Amount +pool.Amount = 0 + +rawPool, pe := SafeMarshal(pool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawAcc, pe := SafeMarshal(acc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawLast, pe := SafeMarshal(&LastClaimedBlock{Height: height}) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForInvestorPool(), Value: rawPool}, +{Key: KeyForAccount(PRAXIS_INVESTOR_ADDR), Value: rawAcc}, +{Key: KeyForInvestorLastClaimed(), Value: rawLast}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_claim_protocol_reward.go b/plugin/go/tutorial/contract/handler_claim_protocol_reward.go new file mode 100644 index 0000000000..270ddad89c --- /dev/null +++ b/plugin/go/tutorial/contract/handler_claim_protocol_reward.go @@ -0,0 +1,74 @@ +package contract + +// handler_claim_protocol_reward.go — MessageClaimProtocolReward +// Spec: PRIS v1.0-r3 +// Signer: PRAXIS_PROTOCOL_ADDR — no cooldown (audits/bounties on demand) + +func (c *Contract) CheckMessageClaimProtocolReward(msg *MessageClaimProtocolReward) *PluginCheckResponse { +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{PRAXIS_PROTOCOL_ADDR}, +} +} + +func (c *Contract) DeliverMessageClaimProtocolReward(msg *MessageClaimProtocolReward, fee uint64) *PluginDeliverResponse { +height := GetGlobalHeight() +if height == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +poolQId := nextQueryId() +accQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: poolQId, Key: KeyForProtocolPool()}, +{QueryId: accQId, Key: KeyForAccount(PRAXIS_PROTOCOL_ADDR)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +pool := &Pool{} +acc := &Account{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 { continue } +switch r.QueryId { +case poolQId: +if pe := Unmarshal(r.Entries[0].Value, pool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case accQId: +if pe := Unmarshal(r.Entries[0].Value, acc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if pool.Amount == 0 { +return &PluginDeliverResponse{Error: ErrEmptyPool()} +} + +acc.Amount += pool.Amount +pool.Amount = 0 + +rawPool, pe := SafeMarshal(pool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawAcc, pe := SafeMarshal(acc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForProtocolPool(), Value: rawPool}, +{Key: KeyForAccount(PRAXIS_PROTOCOL_ADDR), Value: rawAcc}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_claim_resolver_reward.go b/plugin/go/tutorial/contract/handler_claim_resolver_reward.go new file mode 100644 index 0000000000..acf5d2a954 --- /dev/null +++ b/plugin/go/tutorial/contract/handler_claim_resolver_reward.go @@ -0,0 +1,149 @@ +package contract + +// handler_claim_resolver_reward.go — MessageClaimResolverReward +// Spec: PRIS v1.0-r3 +// +// Pays a resolver their weighted share of the epoch resolver pool. +// Share formula: +// share = resolverEpochPool * (resolver_resolutions * tier_weight) / SUM(all * tier_weight) +// +// Qualifying conditions: +// - RRS > 0 +// - SuccessfulResolutions > 0 in the epoch +// - LastClaimedEpoch < epoch +// +// r3 fix R3-4: qualification is RRS > 0 (not >= 10) — Bronze tier starts at 1. + +func rrsWeight(rrs uint64) uint32 { +if rrs >= RRS_GOLD_THRESHOLD { +return VOTE_WEIGHT_GOLD +} else if rrs >= RRS_SILVER_THRESHOLD { +return VOTE_WEIGHT_SILVER +} +return VOTE_WEIGHT_BRONZE +} + +func (c *Contract) CheckMessageClaimResolverReward(msg *MessageClaimResolverReward) *PluginCheckResponse { +if len(msg.ResolverAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.ResolverAddress}, +} +} + +func (c *Contract) DeliverMessageClaimResolverReward(msg *MessageClaimResolverReward, fee uint64) *PluginDeliverResponse { +height := GetGlobalHeight() +if height == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +currentEpoch := height / PRIS_EPOCH_BLOCKS +claimEpoch := msg.Epoch + +// Can only claim past epochs — current epoch not yet snapshotted. +if claimEpoch >= currentEpoch { +return &PluginDeliverResponse{Error: ErrInvalidParam()} +} + +recQId := nextQueryId() +poolQId := nextQueryId() +accQId := nextQueryId() +statsQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: recQId, Key: KeyForResolverRecord(msg.ResolverAddress)}, +{QueryId: poolQId, Key: KeyForResolverEpochPool(claimEpoch)}, +{QueryId: accQId, Key: KeyForAccount(msg.ResolverAddress)}, +{QueryId: statsQId, Key: KeyForGlobalStats()}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +rec := &ResolverRecord{} +pool := &Pool{} +acc := &Account{} +stats := &GlobalStats{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 { +continue +} +switch r.QueryId { +case recQId: +if pe := Unmarshal(r.Entries[0].Value, rec); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case poolQId: +if pe := Unmarshal(r.Entries[0].Value, pool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case accQId: +if pe := Unmarshal(r.Entries[0].Value, acc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case statsQId: +if pe := Unmarshal(r.Entries[0].Value, stats); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +// Qualification checks +if rec.RrsScore == 0 { +return &PluginDeliverResponse{Error: ErrInsufficientRRS()} +} +if rec.SuccessfulResolutions == 0 { +return &PluginDeliverResponse{Error: ErrNoResolutions()} +} +if rec.LastClaimedEpoch >= claimEpoch { +return &PluginDeliverResponse{Error: ErrAlreadyClaimed()} +} +if pool.Amount == 0 { +return &PluginDeliverResponse{Error: ErrEmptyPool()} +} +if stats.TotalWeightedResolutions == 0 { +return &PluginDeliverResponse{Error: ErrEmptyPool()} +} + +// Compute weighted share +weight := uint64(rrsWeight(rec.RrsScore)) +myScore := rec.SuccessfulResolutions * weight +payout := pool.Amount * myScore / stats.TotalWeightedResolutions + +if payout == 0 { +return &PluginDeliverResponse{Error: ErrEmptyPool()} +} +if payout > pool.Amount { +payout = pool.Amount +} + +acc.Amount += payout +pool.Amount -= payout +rec.LastClaimedEpoch = claimEpoch + +rawRec, pe := SafeMarshal(rec) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawPool, pe := SafeMarshal(pool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawAcc, pe := SafeMarshal(acc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForResolverRecord(msg.ResolverAddress), Value: rawRec}, +{Key: KeyForResolverEpochPool(claimEpoch), Value: rawPool}, +{Key: KeyForAccount(msg.ResolverAddress), Value: rawAcc}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_claim_slash.go b/plugin/go/tutorial/contract/handler_claim_slash.go new file mode 100644 index 0000000000..fdba1ce445 --- /dev/null +++ b/plugin/go/tutorial/contract/handler_claim_slash.go @@ -0,0 +1,219 @@ +package contract + +func (c *Contract) CheckMessageClaimSlash(msg *MessageClaimSlash) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.ClaimantAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.ClaimantAddress}, +} +} + +func (c *Contract) DeliverMessageClaimSlash(msg *MessageClaimSlash, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +marketQId := nextQueryId() +proposalQId := nextQueryId() +disputeQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: KeyForMarket(msg.MarketId)}, +{QueryId: proposalQId, Key: KeyForProposal(msg.MarketId)}, +{QueryId: disputeQId, Key: KeyForDispute(msg.MarketId)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +var proposal *ProposalRecord +var dispute *DisputeRecord + +for _, r := range resp.Results { +switch r.QueryId { +case marketQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case proposalQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrInternal()} +} +proposal = &ProposalRecord{} +if pe := Unmarshal(r.Entries[0].Value, proposal); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case disputeQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrInternal()} +} +dispute = &DisputeRecord{} +if pe := Unmarshal(r.Entries[0].Value, dispute); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +if market.Status != STATUS_FINALIZED { +return &PluginDeliverResponse{Error: ErrNotFinalized()} +} +if proposal == nil || dispute == nil { +return &PluginDeliverResponse{Error: ErrInternal()} +} +if !bytesEqual(msg.ClaimantAddress, proposal.ResolverAddr) { +return &PluginDeliverResponse{Error: ErrUnauthorized()} +} +if dispute.VoteStatus != VOTE_TALLIED { +return &PluginDeliverResponse{Error: ErrTallyNotReady()} +} + +slashQId := nextQueryId() +claimAccQId := nextQueryId() +treasQId := nextQueryId() +resolverRecQId := nextQueryId() +resFeeQId := nextQueryId() + +resp2, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: slashQId, Key: KeyForSlashRecord(dispute.DisputerAddress)}, +{QueryId: claimAccQId, Key: KeyForAccount(msg.ClaimantAddress)}, +{QueryId: treasQId, Key: KeyForTreasuryReserve(msg.MarketId)}, +{QueryId: resolverRecQId, Key: KeyForResolverRecord(proposal.ResolverAddr)}, +{QueryId: resFeeQId, Key: KeyForResolverFeePool(msg.MarketId)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp2.Error != nil { +return &PluginDeliverResponse{Error: resp2.Error} +} + +var slash *SlashRecord +claimAcc := &Account{} +treasury := &TreasuryReserve{} +resolverRec := &ResolverRecord{} +resFeePool := &Pool{} + +for _, r := range resp2.Results { +switch r.QueryId { +case slashQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrNoSlashToClaim()} +} +slash = &SlashRecord{} +if pe := Unmarshal(r.Entries[0].Value, slash); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case claimAccQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, claimAcc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +case treasQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, treasury); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +case resolverRecQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +_ = Unmarshal(r.Entries[0].Value, resolverRec) +} +case resFeeQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +_ = Unmarshal(r.Entries[0].Value, resFeePool) +} +} +} + +if slash == nil || slash.SlashAmount == 0 { +return &PluginDeliverResponse{Error: ErrNoSlashToClaim()} +} + +slashAmount := slash.SlashAmount +if treasury.LockedReserve < slashAmount { +slashAmount = treasury.LockedReserve +} +treasury.LockedReserve -= slashAmount +claimAcc.Amount += slashAmount +slash.SlashAmount = 0 + +// PRIS v1.0-r3: RRS -50 (floor 0) and sweep resolver fee pool to treasury +if resolverRec.RrsScore >= 50 { +resolverRec.RrsScore -= 50 +} else { +resolverRec.RrsScore = PRIS_RRS_FLOOR +} +// Sweep resolver fee pool to treasury pool +if resFeePool.Amount > 0 { +// Read global treasury pool +tPoolQId := nextQueryId() +tPoolResp, tPoolErr := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: tPoolQId, Key: KeyForTreasuryPool()}, +}, +}) +if tPoolErr == nil && tPoolResp.Error == nil { +tPool := &Pool{} +for _, r := range tPoolResp.Results { +if r.QueryId == tPoolQId && len(r.Entries) > 0 { +_ = Unmarshal(r.Entries[0].Value, tPool) +} +} +tPool.Amount += resFeePool.Amount +resFeePool.Amount = 0 +rawTPool, pe2 := SafeMarshal(tPool) +if pe2 == nil { +rawResFee2, pe3 := SafeMarshal(resFeePool) +if pe3 == nil { +_, _ = c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForTreasuryPool(), Value: rawTPool}, +{Key: KeyForResolverFeePool(msg.MarketId), Value: rawResFee2}, +}, +}) +} +} +} +} + +rawSlash, pe := SafeMarshal(slash) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawAcc, pe := SafeMarshal(claimAcc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawT, pe := SafeMarshal(treasury) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForSlashRecord(dispute.DisputerAddress), Value: rawSlash}, +{Key: KeyForAccount(msg.ClaimantAddress), Value: rawAcc}, +{Key: KeyForTreasuryReserve(msg.MarketId), Value: rawT}, +{Key: KeyForResolverRecord(proposal.ResolverAddr), Value: func() []byte { b, _ := SafeMarshal(resolverRec); return b }()}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_claim_unbonded_stake.go b/plugin/go/tutorial/contract/handler_claim_unbonded_stake.go new file mode 100644 index 0000000000..5fb885bd44 --- /dev/null +++ b/plugin/go/tutorial/contract/handler_claim_unbonded_stake.go @@ -0,0 +1,103 @@ +package contract + +// handler_claim_unbonded_stake.go — MessageClaimUnbondedStake +// Spec: PRIS v1.0-r3 unstake extension +// +// Releases unbonded stake back to resolver account after unbonding period. +// UnbondingReleaseHeight must be <= currentHeight. + +func (c *Contract) CheckMessageClaimUnbondedStake(msg *MessageClaimUnbondedStake) *PluginCheckResponse { +if len(msg.ResolverAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.ResolverAddress}, +} +} + +func (c *Contract) DeliverMessageClaimUnbondedStake(msg *MessageClaimUnbondedStake, fee uint64) *PluginDeliverResponse { +height := GetGlobalHeight() +if height == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +// ── Batch read ──────────────────────────────────────────────────────── +recQId := nextQueryId() +accQId := nextQueryId() + +recKey := KeyForResolverRecord(msg.ResolverAddress) +accKey := KeyForAccount(msg.ResolverAddress) + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: recQId, Key: recKey}, +{QueryId: accQId, Key: accKey}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var record *ResolverRecord +acc := &Account{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +switch r.QueryId { +case recQId: +record = &ResolverRecord{} +if pe := Unmarshal(r.Entries[0].Value, record); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case accQId: +if pe := Unmarshal(r.Entries[0].Value, acc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if record == nil { +return &PluginDeliverResponse{Error: ErrResolverNotFound()} +} +if record.UnbondingAmount == 0 { +return &PluginDeliverResponse{Error: ErrNoUnbondingStake()} +} +if height < record.UnbondingReleaseHeight { +return &PluginDeliverResponse{Error: ErrUnbondingNotComplete()} +} + +// ── Release unbonded stake ──────────────────────────────────────────── +payout := record.UnbondingAmount +record.UnbondingAmount = 0 +record.UnbondingReleaseHeight = 0 +acc.Amount += payout + +// ── Pay fee ─────────────────────────────────────────────────────────── +if acc.Amount < fee { +return &PluginDeliverResponse{Error: ErrInsufficientFunds()} +} +acc.Amount -= fee + +// ── Marshal ─────────────────────────────────────────────────────────── +rawRec, pe := SafeMarshal(record) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawAcc, pe := SafeMarshal(acc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +// ── 2-key atomic write ──────────────────────────────────────────────── +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: recKey, Value: rawRec}, +{Key: accKey, Value: rawAcc}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_claim_winnings.go b/plugin/go/tutorial/contract/handler_claim_winnings.go new file mode 100644 index 0000000000..5d6278c44b --- /dev/null +++ b/plugin/go/tutorial/contract/handler_claim_winnings.go @@ -0,0 +1,274 @@ +package contract + +// handler_claim_winnings.go — MessageClaimWinnings +// Spec: ADLMSR v5.6.6-r2-CORRECTED +// +// Claims payout for a winning (or cancelled/voided) position. +// CRIT-2: gates on STATUS_FINALIZED (= 6), never STATUS_RESOLVED. +// CRIT-1: uses market.QYes/QNo and position.SharesYes/SharesNo (proto names). +// R2: surplus sweep re-reads pool from state after atomic write. +// R6: ClaimantAddress validated in CheckTx AND DeliverTx. +// AUDIT-1: overflow-safe payout formula (ComputePayout). +// AUDIT-6: ghost claimant guard — reject zero position. +// +// CheckTx: market_id 20 bytes, claimant_address 20 bytes. Zero StateRead (AUDIT-8). +// DeliverTx: +// CheckAutoCancel first +// Batch read: market, position, pool, claimant account +// Compute payout based on market status +// 4-key atomic write +// R2: surplus sweep via fresh pool re-read after write + +func (c *Contract) CheckMessageClaimWinnings(msg *MessageClaimWinnings) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +// R6: ClaimantAddress must be validated in CheckTx. +if len(msg.ClaimantAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.ClaimantAddress}, +} +} + +func (c *Contract) DeliverMessageClaimWinnings(msg *MessageClaimWinnings, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +// R6: re-validate in DeliverTx. +if len(msg.ClaimantAddress) != 20 { +return &PluginDeliverResponse{Error: ErrInvalidAddress()} +} + +if pe := c.CheckAutoCancel(msg.MarketId); pe != nil { +return &PluginDeliverResponse{Error: pe} +} + +// ── Batch read ──────────────────────────────────────────────────────────── +marketQId := nextQueryId() +posQId := nextQueryId() +poolQId := nextQueryId() +claimAccQId := nextQueryId() + +marketKey := KeyForMarket(msg.MarketId) +posKey := KeyForPosition(msg.MarketId, msg.ClaimantAddress) +poolKey := KeyForMarketPool(msg.MarketId) +claimKey := KeyForAccount(msg.ClaimantAddress) + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: marketKey}, +{QueryId: posQId, Key: posKey}, +{QueryId: poolQId, Key: poolKey}, +{QueryId: claimAccQId, Key: claimKey}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +position := &PositionState{} +marketPool := &Pool{} +claimantAcc := &Account{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 { +continue +} +switch r.QueryId { +case marketQId: +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case posQId: +if pe := Unmarshal(r.Entries[0].Value, position); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case poolQId: +if pe := Unmarshal(r.Entries[0].Value, marketPool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case claimAccQId: +if pe := Unmarshal(r.Entries[0].Value, claimantAcc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} + +// AUDIT-6: ghost claimant guard — reject zero position. +if position.SharesYes == 0 && position.SharesNo == 0 && position.CostPaid == 0 { +return &PluginDeliverResponse{Error: ErrNoPosition()} +} +if position.Claimed { +return &PluginDeliverResponse{Error: ErrAlreadyClaimed()} +} + +// ── Compute payout ──────────────────────────────────────────────────────── +var payout uint64 + +switch market.Status { +case STATUS_FINALIZED: +// CRIT-2: gate on STATUS_FINALIZED, never STATUS_RESOLVED. +// Read outcome to determine winning side. +outQId := nextQueryId() +outResp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: outQId, Key: KeyForOutcome(msg.MarketId)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if outResp.Error != nil { +return &PluginDeliverResponse{Error: outResp.Error} +} + +var outcome *OutcomeState +for _, r := range outResp.Results { +if r.QueryId == outQId { +if len(r.Entries) == 0 { +return &PluginDeliverResponse{Error: ErrInternal()} +} +outcome = &OutcomeState{} +if pe := Unmarshal(r.Entries[0].Value, outcome); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} +if outcome == nil { +return &PluginDeliverResponse{Error: ErrInternal()} +} + +// CRIT-1: market.QYes/QNo, position.SharesYes/SharesNo (proto names). +var winnerShares, totalWinShares uint64 +if outcome.WinningOutcome { +winnerShares = position.SharesYes +totalWinShares = market.QYes +} else { +winnerShares = position.SharesNo +totalWinShares = market.QNo +} +if winnerShares > 0 { +if totalWinShares == 0 { +return &PluginDeliverResponse{Error: ErrInternal()} +} +// AUDIT-1: overflow-safe payout formula. +payout = ComputePayout(marketPool.Amount, winnerShares, totalWinShares) +} + +case STATUS_CANCELLED: +payout = position.CostPaid + +case STATUS_VOIDED: +// Tier-4 void: full refund to all bettors. +payout = position.CostPaid + +default: +return &PluginDeliverResponse{Error: ErrMarketNotResolved()} +} + +if payout > marketPool.Amount { +return &PluginDeliverResponse{Error: ErrInsufficientPoolFunds()} +} + +// ── Mutate in memory ────────────────────────────────────────────────────── +position.Claimed = true +market.ClaimedCount++ +marketPool.Amount -= payout +claimantAcc.Amount += payout + +// ── Marshal all ─────────────────────────────────────────────────────────── +rawPos, pe := SafeMarshal(position) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawMkt, pe := SafeMarshal(market) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawMP, pe := SafeMarshal(marketPool) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawAcc, pe := SafeMarshal(claimantAcc) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} + +// ── 4-key atomic write ──────────────────────────────────────────────────── +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: posKey, Value: rawPos}, +{Key: marketKey, Value: rawMkt}, +{Key: poolKey, Value: rawMP}, +{Key: claimKey, Value: rawAcc}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} + +// ── R2: surplus sweep — re-read pool from state after atomic write ──────── +// Never use in-memory marketPool.Amount here — R2 fix. +graceEnd := market.ExpiryTime + RESOLUTION_DELAY_BLOCKS + +GRACE_PERIOD_BLOCKS + CLAIM_GRACE_PERIOD +shouldSweep := +(market.Status == STATUS_FINALIZED && +(market.ClaimedCount == market.TotalPositions || now > graceEnd)) || +(market.Status == STATUS_CANCELLED && now > graceEnd) + +if shouldSweep { +sweepQId := nextQueryId() +sweepResp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: sweepQId, Key: poolKey}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if sweepResp.Error != nil { +return &PluginDeliverResponse{Error: sweepResp.Error} +} + +sweepPool := &Pool{} +for _, r := range sweepResp.Results { +if r.QueryId == sweepQId && len(r.Entries) > 0 { +if pe := Unmarshal(r.Entries[0].Value, sweepPool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if sweepPool.Amount > 0 { +// Move remaining pool balance to treasury. +sweepPool.Amount = 0 +rawSweep, pe := SafeMarshal(sweepPool) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +sweepWr, sweepErr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: poolKey, Value: rawSweep}, +}, +}) +if pe := errCheckWrite(sweepWr, sweepErr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_commit_vote.go b/plugin/go/tutorial/contract/handler_commit_vote.go new file mode 100644 index 0000000000..494bc3bca6 --- /dev/null +++ b/plugin/go/tutorial/contract/handler_commit_vote.go @@ -0,0 +1,139 @@ +package contract + +// handler_commit_vote.go — MessageCommitVote +// Spec: PORS v1.0-r2-CORRECTED (P5) +// +// Panel members submit a blinded vote during the commit phase. +// commit_hash = SHA256(vote_byte || nonce || voter_addr) +// The hash is stored but not revealed until the reveal phase. +// +// CheckTx: market_id 20 bytes, voter_addr 20 bytes, commit_hash 32 bytes. +// Zero StateRead (AUDIT-8). +// DeliverTx: +// Read market + dispute + existing VoteCommit +// Validate STATUS_DISPUTED and within commit phase window +// Validate voter is a panel member +// Reject if already committed +// 1-key atomic write: VoteCommit + +func (c *Contract) CheckMessageCommitVote(msg *MessageCommitVote) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.VoterAddr) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +// commit_hash must be exactly 32 bytes (SHA256 output). +if len(msg.CommitHash) != 32 { +return ErrCheckResp(ErrInvalidCommitHash()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.VoterAddr}, +} +} + +func (c *Contract) DeliverMessageCommitVote(msg *MessageCommitVote, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +marketQId := nextQueryId() +disputeQId := nextQueryId() +commitQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: KeyForMarket(msg.MarketId)}, +{QueryId: disputeQId, Key: KeyForDispute(msg.MarketId)}, +{QueryId: commitQId, Key: KeyForVoteCommit(msg.MarketId, msg.VoterAddr)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +var dispute *DisputeRecord + +for _, r := range resp.Results { +switch r.QueryId { +case marketQId: +if len(r.Entries) == 0 { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case disputeQId: +if len(r.Entries) == 0 { +return &PluginDeliverResponse{Error: ErrNotDisputed()} +} +dispute = &DisputeRecord{} +if pe := Unmarshal(r.Entries[0].Value, dispute); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case commitQId: +// Already committed if this key exists. +if len(r.Entries) > 0 { +return &PluginDeliverResponse{Error: ErrAlreadyCommitted()} +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +if market.Status != STATUS_DISPUTED { +return &PluginDeliverResponse{Error: ErrNotDisputed()} +} +if dispute == nil { +return &PluginDeliverResponse{Error: ErrNotDisputed()} +} + +// Commit phase window: dispute_block to dispute_block + COMMIT_PHASE_BLOCKS. +commitDeadline := dispute.DisputeBlock + COMMIT_PHASE_BLOCKS +if now > commitDeadline { +return &PluginDeliverResponse{Error: ErrCommitPhaseOver()} +} + +// Verify voter is a panel member. +if !isPanelMember(msg.VoterAddr, dispute.PanelMembers) { +return &PluginDeliverResponse{Error: ErrNotAPanelMember()} +} + +// Write VoteCommit — single atomic key. +vc := &VoteCommit{ +VoterAddr: msg.VoterAddr, +CommitHash: msg.CommitHash, +CommittedAt: now, +} +rawVC, pe := SafeMarshal(vc) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForVoteCommit(msg.MarketId, msg.VoterAddr), Value: rawVC}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} + +// isPanelMember returns true if addr is in the panelMembers list. +func isPanelMember(addr []byte, panelMembers [][]byte) bool { +for _, m := range panelMembers { +if bytesEqual(addr, m) { +return true +} +} +return false +} diff --git a/plugin/go/tutorial/contract/handler_create_market.go b/plugin/go/tutorial/contract/handler_create_market.go new file mode 100644 index 0000000000..ae0d1ec87e --- /dev/null +++ b/plugin/go/tutorial/contract/handler_create_market.go @@ -0,0 +1,194 @@ +package contract + +func (c *Contract) CheckMessageCreateMarket(msg *MessageCreateMarket) *PluginCheckResponse { +if len(msg.CreatorAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +if msg.B0 < MIN_B0 { +return ErrCheckResp(ErrInvalidB0()) +} +if msg.ExpiryTime == 0 { +return ErrCheckResp(ErrInvalidParam()) +} +if msg.ExpiryTime > MAX_EXPIRY_TIME { +return ErrCheckResp(ErrExpiryTooLarge()) +} +if msg.Nonce == 0 { +return ErrCheckResp(ErrInvalidNonce()) +} +if len(msg.Question) == 0 { +return ErrCheckResp(ErrInvalidQuestion()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.CreatorAddress}, +} +} + +func (c *Contract) DeliverMessageCreateMarket(msg *MessageCreateMarket, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} +if msg.ExpiryTime > MAX_EXPIRY_TIME { +return &PluginDeliverResponse{Error: ErrExpiryTooLarge()} +} + +marketId := DeriveMarketId(msg.CreatorAddress, msg.Nonce) + +marketQId := nextQueryId() +creatorQId := nextQueryId() +feeQId := nextQueryId() + gTreasuryQId := nextQueryId() + ocQId := nextQueryId() + midxQId := nextQueryId() + +marketKey := KeyForMarket(marketId) +creatorKey := KeyForAccount(msg.CreatorAddress) +feePoolKey := KeyForFeePool(c.Config.ChainId) + gTreasuryKey := KeyForTreasuryPool() + ocKey := KeyForCreatorOpenCount(msg.CreatorAddress) + midxKey := KeyForMarketIndex() +poolKey := KeyForMarketPool(marketId) +treasKey := KeyForTreasuryReserve(marketId) + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: marketKey}, +{QueryId: creatorQId, Key: creatorKey}, +{QueryId: feeQId, Key: feePoolKey}, + {QueryId: gTreasuryQId, Key: gTreasuryKey}, + {QueryId: ocQId, Key: ocKey}, + {QueryId: midxQId, Key: midxKey}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +creator := &Account{} +feePool := &Pool{} + gTreasury := &Pool{} + openCount := &Pool{} + midx := &MarketIndex{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +switch r.QueryId { +case marketQId: +return &PluginDeliverResponse{Error: ErrInvalidParam()} // already exists +case creatorQId: +if pe := Unmarshal(r.Entries[0].Value, creator); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case feeQId: +if pe := Unmarshal(r.Entries[0].Value, feePool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} + case gTreasuryQId: + case ocQId: + if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { + _ = Unmarshal(r.Entries[0].Value, openCount) + } + case midxQId: + if pe := Unmarshal(r.Entries[0].Value, midx); pe != nil { + return &PluginDeliverResponse{Error: pe} + } + if pe := Unmarshal(r.Entries[0].Value, gTreasury); pe != nil { + return &PluginDeliverResponse{Error: pe} + } + } +} + + if fee > 0 && msg.B0 > ^uint64(0)-fee { +return &PluginDeliverResponse{Error: ErrInvalidAmount()} +} +if msg.B0 > ^uint64(0)-fee-CREATOR_BOND { +return &PluginDeliverResponse{Error: ErrInvalidAmount()} +} +totalCost := msg.B0 + fee + CREATOR_BOND +if creator.Amount < totalCost { +return &PluginDeliverResponse{Error: ErrInsufficientFunds()} +} + +creator.Amount -= totalCost +feePool.Amount += fee / 2 + gTreasury.Amount += fee - fee/2 + + // Item-14: spam cap + if openCount.Amount >= MAX_OPEN_MARKETS_PER_CREATOR { + return &PluginDeliverResponse{Error: ErrTooManyOpenMarkets()} + } + openCount.Amount++ + + // Append new market to global market index + midx.MarketIds = append(midx.MarketIds, marketId) + +// Carve FINALIZATION_BOUNTY from B0 before seeding the LMSR pool. +// MIN_B0 >= FINALIZATION_BOUNTY + seed margin, so this subtraction is safe. +lmsrSeed := msg.B0 - FINALIZATION_BOUNTY +halfB0 := lmsrSeed / 2 +market := &MarketState{ +Status: STATUS_OPEN, +ExpiryTime: msg.ExpiryTime, +QYes: halfB0, +QNo: halfB0, +BEff: lmsrSeed, +Creator: msg.CreatorAddress, +ClaimedCount: 0, +TotalPositions: 0, +OpenTime: now, +ElevatedRisk: false, +Question: msg.Question, +Rules: msg.Rules, +} + +pool := &Pool{Id: c.Config.ChainId, Amount: lmsrSeed} +treasury := &TreasuryReserve{LockedReserve: FINALIZATION_BOUNTY, CreatorBond: CREATOR_BOND} + +rawMarket, pe := SafeMarshal(market) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawCreator, pe := SafeMarshal(creator) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawFee, pe := SafeMarshal(feePool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawPool, pe := SafeMarshal(pool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawTreasury, pe := SafeMarshal(treasury) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawGTreasury, pe := SafeMarshal(gTreasury) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawMidx, pe := SafeMarshal(midx) + rawOC, pe := SafeMarshal(openCount) + if pe != nil { return &PluginDeliverResponse{Error: pe} } +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +sets := []*PluginSetOp{ +{Key: marketKey, Value: rawMarket}, +{Key: poolKey, Value: rawPool}, +{Key: treasKey, Value: rawTreasury}, +{Key: feePoolKey, Value: rawFee}, + {Key: gTreasuryKey, Value: rawGTreasury}, + {Key: midxKey, Value: rawMidx}, + {Key: ocKey, Value: rawOC}, +} +var deletes []*PluginDeleteOp +if creator.Amount > 0 { +sets = append(sets, &PluginSetOp{Key: creatorKey, Value: rawCreator}) +} else { +deletes = []*PluginDeleteOp{{Key: creatorKey}} +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: sets, +Deletes: deletes, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_endblock.go b/plugin/go/tutorial/contract/handler_endblock.go new file mode 100644 index 0000000000..6fd54466d8 --- /dev/null +++ b/plugin/go/tutorial/contract/handler_endblock.go @@ -0,0 +1,135 @@ +package contract + +// handler_endblock.go — PRIS v1.0-r3 EndBlock epoch boundary processor +// +// Triggered inside EndBlock every PRIS_EPOCH_BLOCKS blocks. +// r3 fix R3-2: epoch snapshot is NOT triggered by user TXs — it fires +// in EndBlock which executes every block regardless of TX count. +// +// On epoch boundary: +// 1. Read treasury pool balance +// 2. Write immutable EpochSnapshot +// 3. Carve 5 distribution pools atomically (20% each) +// 4. Zero treasury pool +// +// EndBlock must never return an error that halts the chain. + + +func (c *Contract) processEpochBoundary(height uint64) *PluginError { +epoch := height / PRIS_EPOCH_BLOCKS + +treasuryQId := nextQueryId() +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: treasuryQId, Key: KeyForTreasuryPool()}, +}, +}) +if err != nil { +return err +} +if resp.Error != nil { +return resp.Error +} + +treasury := &Pool{} +for _, r := range resp.Results { +if r.QueryId == treasuryQId && len(r.Entries) > 0 { +if pe := Unmarshal(r.Entries[0].Value, treasury); pe != nil { +return pe +} +} +} + +if treasury.Amount == 0 { +return nil +} + +resolverShare := ComputeBps(treasury.Amount, PRIS_RESOLVER_SHARE_BPS) +builderShare := ComputeBps(treasury.Amount, PRIS_BUILDER_SHARE_BPS) +communityShare := ComputeBps(treasury.Amount, PRIS_COMMUNITY_SHARE_BPS) +investorShare := ComputeBps(treasury.Amount, PRIS_INVESTOR_SHARE_BPS) +protocolShare := ComputeBps(treasury.Amount, PRIS_PROTOCOL_SHARE_BPS) + +bPoolQId := nextQueryId() +cPoolQId := nextQueryId() +iPoolQId := nextQueryId() +pPoolQId := nextQueryId() +rPoolQId := nextQueryId() + +poolResp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: bPoolQId, Key: KeyForBuilderPool()}, +{QueryId: cPoolQId, Key: KeyForCommunityPool()}, +{QueryId: iPoolQId, Key: KeyForInvestorPool()}, +{QueryId: pPoolQId, Key: KeyForProtocolPool()}, +{QueryId: rPoolQId, Key: KeyForResolverEpochPool(epoch)}, +}, +}) +if err != nil { +return err +} + +builderPool := &Pool{} +communityPool := &Pool{} +investorPool := &Pool{} +protocolPool := &Pool{} +resolverPool := &Pool{} + +if poolResp != nil && poolResp.Error == nil { +for _, r := range poolResp.Results { +if len(r.Entries) == 0 { +continue +} +switch r.QueryId { +case bPoolQId: +_ = Unmarshal(r.Entries[0].Value, builderPool) +case cPoolQId: +_ = Unmarshal(r.Entries[0].Value, communityPool) +case iPoolQId: +_ = Unmarshal(r.Entries[0].Value, investorPool) +case pPoolQId: +_ = Unmarshal(r.Entries[0].Value, protocolPool) +case rPoolQId: +_ = Unmarshal(r.Entries[0].Value, resolverPool) +} +} +} + +builderPool.Amount += builderShare +communityPool.Amount += communityShare +investorPool.Amount += investorShare +protocolPool.Amount += protocolShare +resolverPool.Amount += resolverShare + +snapshot := &Pool{Amount: treasury.Amount} +rawSnap, pe := SafeMarshal(snapshot) +if pe != nil { return pe } + +rawBuilder, pe := SafeMarshal(builderPool) +if pe != nil { return pe } +rawCommunity, pe := SafeMarshal(communityPool) +if pe != nil { return pe } +rawInvestor, pe := SafeMarshal(investorPool) +if pe != nil { return pe } +rawProtocol, pe := SafeMarshal(protocolPool) +if pe != nil { return pe } +rawResolver, pe := SafeMarshal(resolverPool) +if pe != nil { return pe } + +emptyTreasury := &Pool{Amount: 0} +rawTreasury, pe := SafeMarshal(emptyTreasury) +if pe != nil { return pe } + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForEpochSnapshot(epoch), Value: rawSnap}, +{Key: KeyForBuilderPool(), Value: rawBuilder}, +{Key: KeyForCommunityPool(), Value: rawCommunity}, +{Key: KeyForInvestorPool(), Value: rawInvestor}, +{Key: KeyForProtocolPool(), Value: rawProtocol}, +{Key: KeyForResolverEpochPool(epoch), Value: rawResolver}, +{Key: KeyForTreasuryPool(), Value: rawTreasury}, +}, +}) +return errCheckWrite(wr, werr) +} diff --git a/plugin/go/tutorial/contract/handler_file_dispute.go b/plugin/go/tutorial/contract/handler_file_dispute.go new file mode 100644 index 0000000000..6a80133631 --- /dev/null +++ b/plugin/go/tutorial/contract/handler_file_dispute.go @@ -0,0 +1,297 @@ +package contract + +func (c *Contract) CheckMessageFileDispute(msg *MessageFileDispute) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.DisputerAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +if msg.DisputeBond == 0 { +return ErrCheckResp(ErrInvalidAmount()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.DisputerAddress}, +} +} + +func (c *Contract) DeliverMessageFileDispute(msg *MessageFileDispute, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +marketQId := nextQueryId() +proposalQId := nextQueryId() +dispAccQId := nextQueryId() +entropyQId := nextQueryId() +feeQId := nextQueryId() + +marketKey := KeyForMarket(msg.MarketId) +proposalKey := KeyForProposal(msg.MarketId) +dispAccKey := KeyForAccount(msg.DisputerAddress) +feePoolKey := KeyForFeePool(c.Config.ChainId) +treasyPoolKey := KeyForTreasuryPool() +treasyQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: marketKey}, +{QueryId: proposalQId, Key: proposalKey}, +{QueryId: dispAccQId, Key: dispAccKey}, +{QueryId: entropyQId, Key: PANEL_ENTROPY_KEY}, +{QueryId: feeQId, Key: feePoolKey}, +{QueryId: treasyQId, Key: treasyPoolKey}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +var proposal *ProposalRecord +disputer := &Account{} +feePool := &Pool{} +treasyPool := &Pool{} +var entropyVal uint64 + +for _, r := range resp.Results { +switch r.QueryId { +case marketQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case proposalQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrInternal()} +} +proposal = &ProposalRecord{} +if pe := Unmarshal(r.Entries[0].Value, proposal); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case dispAccQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, disputer); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +case entropyQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +acc := &PanelEntropyAccum{} +if pe := Unmarshal(r.Entries[0].Value, acc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +entropyVal = acc.Accumulator +} +case feeQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, feePool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +if market.Status != STATUS_PROPOSED { +return &PluginDeliverResponse{Error: ErrMarketNotOpen()} +} +if proposal == nil { +return &PluginDeliverResponse{Error: ErrInternal()} +} + +disputeWindow := ComputeDisputeBlocks(market.OpenTime, market.ExpiryTime) +disputeDeadline := proposal.ProposalBlock + disputeWindow +if now > disputeDeadline { +return &PluginDeliverResponse{Error: ErrDisputeWindowClosed()} +} + +if market.Status == STATUS_DISPUTED { +return &PluginDeliverResponse{Error: ErrAlreadyDisputed()} +} + +if fee > 0 && msg.DisputeBond > ^uint64(0)-fee { +return &PluginDeliverResponse{Error: ErrInvalidAmount()} +} +totalCost := msg.DisputeBond + fee +if disputer.Amount < totalCost { +return &PluginDeliverResponse{Error: ErrInsufficientFunds()} +} + +resolverRangeQId := nextQueryId() +rangeResp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Ranges: []*PluginRangeRead{ +{ +QueryId: resolverRangeQId, +Prefix: resolverRecordPrefix, +Limit: 0, +Reverse: false, +}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if rangeResp.Error != nil { +return &PluginDeliverResponse{Error: rangeResp.Error} +} + +var candidates [][]byte +for _, r := range rangeResp.Results { +if r.QueryId != resolverRangeQId { +continue +} +for _, entry := range r.Entries { +rec := &ResolverRecord{} +if pe := Unmarshal(entry.Value, rec); pe != nil { +continue +} +if bytesEqual(rec.ResolverAddress, proposal.ResolverAddr) { +continue +} +if bytesEqual(rec.ResolverAddress, msg.DisputerAddress) { +continue +} +if rec.RrsScore < MIN_RRS_TO_PROPOSE { +continue +} +candidates = append(candidates, rec.ResolverAddress) +} +} + +// Layer 1: position exclusion +// Any resolver with an open unclaimed position in this market is ineligible. +posQueries := make([]uint64, len(candidates)) +posKeys := make([]*PluginKeyRead, len(candidates)) +for i, addr := range candidates { +qId := nextQueryId() +posQueries[i] = qId +posKeys[i] = &PluginKeyRead{QueryId: qId, Key: KeyForPosition(msg.MarketId, addr)} +} +if len(posKeys) > 0 { +posResp, err := c.plugin.StateRead(c, &PluginStateReadRequest{Keys: posKeys}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if posResp.Error != nil { +return &PluginDeliverResponse{Error: posResp.Error} +} +disqualified := make(map[string]bool) +for _, r := range posResp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +pos := &PositionState{} +if pe := Unmarshal(r.Entries[0].Value, pos); pe != nil { +continue +} +if !pos.Claimed { +for i, qId := range posQueries { +if r.QueryId == qId { +disqualified[string(candidates[i])] = true +} +} +} +} +filtered := candidates[:0] +for _, addr := range candidates { +if !disqualified[string(addr)] { +filtered = append(filtered, addr) +} +} +candidates = filtered +} + +var panelSize uint32 +if market.ElevatedRisk { +panelSize = ELEVATED_RISK_PANEL_SIZE +} else { +panelSize = MIN_PANEL_SIZE +} + +seed := entropyVal ^ (now * FIBONACCI_HASH_CONSTANT) +panel := derivePanel(candidates, int(panelSize), seed) +if len(panel) == 0 { +return &PluginDeliverResponse{Error: ErrInsufficientPanelCandidates()} +} + +market.Status = STATUS_DISPUTED +disputer.Amount -= totalCost +feePool.Amount += fee / 2 +treasyPool.Amount += fee / 2 + +dispute := &DisputeRecord{ +DisputerAddress: msg.DisputerAddress, +DisputeBond: msg.DisputeBond, +DisputeBlock: now, +VoteStatus: VOTE_PENDING, +PanelSize: uint32(len(panel)), +PanelMembers: panel, +} + +rawM, pe := SafeMarshal(market) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawD, pe := SafeMarshal(dispute) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawFee, pe := SafeMarshal(feePool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawTreasy, pe := SafeMarshal(treasyPool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +sets := []*PluginSetOp{ +{Key: marketKey, Value: rawM}, +{Key: KeyForDispute(msg.MarketId), Value: rawD}, +{Key: feePoolKey, Value: rawFee}, +{Key: treasyPoolKey, Value: rawTreasy}, +} +var deletes []*PluginDeleteOp +if disputer.Amount == 0 { +deletes = []*PluginDeleteOp{{Key: dispAccKey}} +} else { +rawDisp, pe := SafeMarshal(disputer) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +sets = append(sets, &PluginSetOp{Key: dispAccKey, Value: rawDisp}) +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: sets, +Deletes: deletes, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} + +func derivePanel(candidates [][]byte, n int, seed uint64) [][]byte { +if len(candidates) == 0 || n == 0 { +return nil +} +pool := make([][]byte, len(candidates)) +copy(pool, candidates) + +s := seed +lcgNext := func() uint64 { +s = s*6364136223846793005 + 1442695040888963407 +return s +} + +limit := len(pool) +if n < limit { +limit = n +} +for i := 0; i < limit; i++ { +j := int(lcgNext()%uint64(len(pool)-i)) + i +pool[i], pool[j] = pool[j], pool[i] +} +return pool[:limit] +} diff --git a/plugin/go/tutorial/contract/handler_finalize_market.go b/plugin/go/tutorial/contract/handler_finalize_market.go new file mode 100644 index 0000000000..026d1ebd5a --- /dev/null +++ b/plugin/go/tutorial/contract/handler_finalize_market.go @@ -0,0 +1,313 @@ +package contract + +func (c *Contract) CheckMessageFinalizeMarket(msg *MessageFinalizeMarket) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.CallerAddr) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.CallerAddr}, +} +} + +func (c *Contract) DeliverMessageFinalizeMarket(msg *MessageFinalizeMarket, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +marketQId := nextQueryId() +disputeQId := nextQueryId() +proposalQId := nextQueryId() +treasQId := nextQueryId() +poolQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: KeyForMarket(msg.MarketId)}, +{QueryId: disputeQId, Key: KeyForDispute(msg.MarketId)}, +{QueryId: proposalQId, Key: KeyForProposal(msg.MarketId)}, +{QueryId: treasQId, Key: KeyForTreasuryReserve(msg.MarketId)}, +{QueryId: poolQId, Key: KeyForMarketPool(msg.MarketId)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +var dispute *DisputeRecord +var proposal *ProposalRecord +treasury := &TreasuryReserve{} +marketPool := &Pool{} + +for _, r := range resp.Results { +switch r.QueryId { +case marketQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case disputeQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +dispute = &DisputeRecord{} +if pe := Unmarshal(r.Entries[0].Value, dispute); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +case proposalQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +proposal = &ProposalRecord{} +if pe := Unmarshal(r.Entries[0].Value, proposal); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +case treasQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, treasury); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +case poolQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, marketPool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} + +if market.Status == STATUS_FINALIZED { +return &PluginDeliverResponse{} +} + +pathA := market.Status == STATUS_DISPUTED && dispute != nil && dispute.VoteStatus == VOTE_TALLIED +pathB := market.Status == STATUS_PROPOSED && dispute == nil +if !pathA && !pathB { +return &PluginDeliverResponse{Error: ErrNotFinalized()} +} + +if pathB { +if proposal == nil { +return &PluginDeliverResponse{Error: ErrInternal()} +} +disputeWindow := ComputeDisputeBlocks(market.OpenTime, market.ExpiryTime) +disputeDeadline := proposal.ProposalBlock + disputeWindow +// Reject if dispute window is still open — too early to finalize. +// In TEST_MODE, skip the window check so tests don't wait 34,560 blocks. +disputeWindowOpen := now <= disputeDeadline +if !TEST_MODE && disputeWindowOpen { +return &PluginDeliverResponse{Error: ErrDisputeWindowOpen()} +} +} + +callerQId := nextQueryId() +proposerQId := nextQueryId() +creatorQId := nextQueryId() + ocQId := nextQueryId() + +readKeys2 := []*PluginKeyRead{ +{QueryId: callerQId, Key: KeyForAccount(msg.CallerAddr)}, +{QueryId: creatorQId, Key: KeyForAccount(market.Creator)}, + {QueryId: ocQId, Key: KeyForCreatorOpenCount(market.Creator)}, +} + +var proposerKey []byte +var disputerKey []byte +var disputerQId uint64 + +if proposal == nil { return &PluginDeliverResponse{Error: ErrInternal()} } +proposerKey = KeyForAccount(proposal.ResolverAddr) +readKeys2 = append(readKeys2, &PluginKeyRead{QueryId: proposerQId, Key: proposerKey}) +resolverRecQId := nextQueryId() +globalStatsQId := nextQueryId() +resolverFeeQId := nextQueryId() +readKeys2 = append(readKeys2, &PluginKeyRead{QueryId: resolverRecQId, Key: KeyForResolverRecord(proposal.ResolverAddr)}) +readKeys2 = append(readKeys2, &PluginKeyRead{QueryId: globalStatsQId, Key: KeyForGlobalStats()}) +readKeys2 = append(readKeys2, &PluginKeyRead{QueryId: resolverFeeQId, Key: KeyForResolverFeePool(msg.MarketId)}) + +if pathA && dispute != nil { +disputerQId = nextQueryId() +disputerKey = KeyForAccount(dispute.DisputerAddress) +readKeys2 = append(readKeys2, &PluginKeyRead{QueryId: disputerQId, Key: disputerKey}) +} + +resp2, err := c.plugin.StateRead(c, &PluginStateReadRequest{Keys: readKeys2}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp2.Error != nil { +return &PluginDeliverResponse{Error: resp2.Error} +} + +callerAcc := &Account{} +proposerAcc := &Account{} +disputerAcc := &Account{} +resolverRec := &ResolverRecord{} +globalStats := &GlobalStats{} +resolverFeePool := &Pool{} +creatorAcc := &Account{} + openCount := Pool{} + +for _, r := range resp2.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +switch r.QueryId { +case callerQId: +if pe := Unmarshal(r.Entries[0].Value, callerAcc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case proposerQId: +if pe := Unmarshal(r.Entries[0].Value, proposerAcc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case ocQId: + if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { + _ = Unmarshal(r.Entries[0].Value, &openCount) + } + case creatorQId: +if pe := Unmarshal(r.Entries[0].Value, creatorAcc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case disputerQId: +if pe := Unmarshal(r.Entries[0].Value, disputerAcc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case resolverRecQId: +_ = Unmarshal(r.Entries[0].Value, resolverRec) +case globalStatsQId: +_ = Unmarshal(r.Entries[0].Value, globalStats) +case resolverFeeQId: +_ = Unmarshal(r.Entries[0].Value, resolverFeePool) +} +} + +bounty := FINALIZATION_BOUNTY +if treasury.LockedReserve < bounty { +bounty = treasury.LockedReserve +} +treasury.LockedReserve -= bounty +callerAcc.Amount += bounty + +var bondReturn uint64 +if proposal != nil { +bondReturn = proposal.ProposalBond +if treasury.LockedReserve < bondReturn { +bondReturn = treasury.LockedReserve +} +treasury.LockedReserve -= bondReturn +proposerAcc.Amount += bondReturn +} +// Return creator bond on successful finalization. +if treasury.CreatorBond > 0 { +creatorAcc.Amount += treasury.CreatorBond +// Strip bond from pool before snapshotting — bettor payouts exclude it. +if marketPool.Amount >= treasury.CreatorBond { +marketPool.Amount -= treasury.CreatorBond +} +treasury.CreatorBond = 0 +} + +// C-1 fix: record pool at finalization so claim_winnings uses immutable amount. +market.FinalizedPoolAmount = marketPool.Amount +market.Status = STATUS_FINALIZED + +// PRIS v1.0-r3: RRS increment and resolver fee payout on correct finalization (pathB). +if pathB && proposal != nil { +resolverRec.RrsScore += 10 +resolverRec.SuccessfulResolutions++ +weight := uint64(1) +if resolverRec.RrsScore >= RRS_GOLD_THRESHOLD { +weight = uint64(VOTE_WEIGHT_GOLD) +} else if resolverRec.RrsScore >= RRS_SILVER_THRESHOLD { +weight = uint64(VOTE_WEIGHT_SILVER) +} +globalStats.TotalWeightedResolutions += weight +// Pay resolver fee pool to resolver +if resolverFeePool.Amount > 0 { +proposerAcc.Amount += resolverFeePool.Amount +resolverFeePool.Amount = 0 +} +} + +rawM, pe := SafeMarshal(market) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawT, pe := SafeMarshal(treasury) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawCaller, pe := SafeMarshal(callerAcc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +sets := []*PluginSetOp{ +{Key: KeyForMarket(msg.MarketId), Value: rawM}, +{Key: KeyForTreasuryReserve(msg.MarketId), Value: rawT}, +{Key: KeyForAccount(msg.CallerAddr), Value: rawCaller}, +} + +if proposal != nil && proposerKey != nil { +rawProposer, pe := SafeMarshal(proposerAcc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +sets = append(sets, &PluginSetOp{Key: proposerKey, Value: rawProposer}) +} +// PRIS: write resolver record, global stats, resolver fee pool +if pathB && proposal != nil { +rawRec, pe := SafeMarshal(resolverRec) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawStats, pe := SafeMarshal(globalStats) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawResFee, pe := SafeMarshal(resolverFeePool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +sets = append(sets, &PluginSetOp{Key: KeyForResolverRecord(proposal.ResolverAddr), Value: rawRec}) +sets = append(sets, &PluginSetOp{Key: KeyForGlobalStats(), Value: rawStats}) +sets = append(sets, &PluginSetOp{Key: KeyForResolverFeePool(msg.MarketId), Value: rawResFee}) +} + if openCount.Amount > 0 { + openCount.Amount-- + } + rawOC, pe := SafeMarshal(&openCount) + if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawCreator, pe := SafeMarshal(creatorAcc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + sets = append(sets, &PluginSetOp{Key: KeyForCreatorOpenCount(market.Creator), Value: rawOC}) + sets = append(sets, &PluginSetOp{Key: KeyForAccount(market.Creator), Value: rawCreator}) + +// Write OutcomeState so claim_winnings can find the winning outcome. +if proposal != nil { +outcome := &OutcomeState{WinningOutcome: proposal.ProposedOutcome, ResolvedAt: now} +rawO, pe := SafeMarshal(outcome) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +sets = append(sets, &PluginSetOp{Key: KeyForOutcome(msg.MarketId), Value: rawO}) +} + +if pathA && dispute != nil { +slashAmount := dispute.DisputeBond +slash := &SlashRecord{ +SlashedAddress: dispute.DisputerAddress, +SlashAmount: slashAmount, +SlashedAt: now, +} +rawSlash, pe := SafeMarshal(slash) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +sets = append(sets, &PluginSetOp{Key: KeyForSlashRecord(dispute.DisputerAddress), Value: rawSlash}) +_ = disputerAcc +_ = disputerKey +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{Sets: sets}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_forfeit_position.go b/plugin/go/tutorial/contract/handler_forfeit_position.go new file mode 100644 index 0000000000..4e8d0a18dd --- /dev/null +++ b/plugin/go/tutorial/contract/handler_forfeit_position.go @@ -0,0 +1,107 @@ +package contract + +// handler_forfeit_position.go — Issue-2 fix +// Allows a resolver to voluntarily exit a position before proposing/resolving. +// Refunds full CostPaid, zeroes shares atomically. Satisfies COI-1 requirement +// without permanent disqualification. + +func (c *Contract) CheckMessageForfeitPosition(msg *MessageForfeitPosition) *PluginCheckResponse { + if len(msg.MarketId) != 20 { + return ErrCheckResp(ErrInvalidParam()) + } + if len(msg.ResolverAddress) != 20 { + return ErrCheckResp(ErrInvalidAddress()) + } + return &PluginCheckResponse{ + AuthorizedSigners: [][]byte{msg.ResolverAddress}, + } +} + +func (c *Contract) DeliverMessageForfeitPosition(msg *MessageForfeitPosition, fee uint64) *PluginDeliverResponse { + now := GetGlobalHeight() + if now == 0 { + return &PluginDeliverResponse{Error: ErrHeightNotSet()} + } + posQId := nextQueryId() + accQId := nextQueryId() + poolQId := nextQueryId() + resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ + Keys: []*PluginKeyRead{ + {QueryId: posQId, Key: KeyForPosition(msg.MarketId, msg.ResolverAddress)}, + {QueryId: accQId, Key: KeyForAccount(msg.ResolverAddress)}, + {QueryId: poolQId, Key: KeyForMarketPool(msg.MarketId)}, + }, + }) + if err != nil { + return &PluginDeliverResponse{Error: ErrStateReadFailed()} + } + if resp.Error != nil { + return &PluginDeliverResponse{Error: resp.Error} + } + var position *PositionState + var account *Account + var pool *Pool + for _, r := range resp.Results { + if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { + continue + } + switch r.QueryId { + case posQId: + position = &PositionState{} + if pe := Unmarshal(r.Entries[0].Value, position); pe != nil { + return &PluginDeliverResponse{Error: ErrUnmarshalFailed()} + } + case accQId: + account = &Account{} + if pe := Unmarshal(r.Entries[0].Value, account); pe != nil { + return &PluginDeliverResponse{Error: ErrUnmarshalFailed()} + } + case poolQId: + pool = &Pool{} + if pe := Unmarshal(r.Entries[0].Value, pool); pe != nil { + return &PluginDeliverResponse{Error: ErrUnmarshalFailed()} + } + } + } + if position == nil || (position.SharesYes == 0 && position.SharesNo == 0) { + return &PluginDeliverResponse{Error: ErrNoPosition()} + } + if account == nil { + return &PluginDeliverResponse{Error: ErrInsufficientFunds()} + } + if pool == nil { + return &PluginDeliverResponse{Error: ErrMarketNotFound()} + } + // Overflow guard + if account.Amount > ^uint64(0)-position.CostPaid { + return &PluginDeliverResponse{Error: ErrInvalidAmount()} + } + // Pool must cover the refund + if pool.Amount < position.CostPaid { + return &PluginDeliverResponse{Error: ErrInsufficientFunds()} + } + refund := position.CostPaid + account.Amount += refund + pool.Amount -= refund + // Zero the position + position.SharesYes = 0 + position.SharesNo = 0 + position.CostPaid = 0 + rawPos, pe := SafeMarshal(position) + if pe != nil { return &PluginDeliverResponse{Error: pe} } + rawAcc, pe := SafeMarshal(account) + if pe != nil { return &PluginDeliverResponse{Error: pe} } + rawPool, pe := SafeMarshal(pool) + if pe != nil { return &PluginDeliverResponse{Error: pe} } + wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ + Sets: []*PluginSetOp{ + {Key: KeyForPosition(msg.MarketId, msg.ResolverAddress), Value: rawPos}, + {Key: KeyForAccount(msg.ResolverAddress), Value: rawAcc}, + {Key: KeyForMarketPool(msg.MarketId), Value: rawPool}, + }, + }) + if pe := errCheckWrite(wr, werr); pe != nil { + return &PluginDeliverResponse{Error: pe} + } + return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_propose_outcome.go b/plugin/go/tutorial/contract/handler_propose_outcome.go new file mode 100644 index 0000000000..5183ea9047 --- /dev/null +++ b/plugin/go/tutorial/contract/handler_propose_outcome.go @@ -0,0 +1,210 @@ +package contract + +import "bytes" + +// handler_propose_outcome.go — MessageProposeOutcome +// Spec: PORS v1.0-r2-CORRECTED +// +// NF-5 FIX: writes 0x13 ResolverState as the 4th atomic key. +// 0x13 was never written before — auth check always failed. +// NF-6 FIX: accepts STATUS_OPEN markets where now > ExpiryTime and transitions +// directly to STATUS_PROPOSED. STATUS_EXPIRED is never persisted. +// +// CheckTx: market_id 20 bytes, resolver_address 20 bytes, proposal_bond non-zero. +// No status check (stateful — DeliverTx enforces). Zero StateRead (AUDIT-8). +// DeliverTx: +// Batch read: global ResolverRecord (0x16) + MarketState (0x10) + ProposalRecord (0x17) +// Auth: RRS score check +// NF-6: accept STATUS_OPEN if now > ExpiryTime — inline transition to PROPOSED +// Idempotency: ProposalRecord nil = first call +// Bond sufficiency check +// 4-key atomic write: MarketState + ResolverRecord + ProposalRecord + ResolverState (NF-5) + +func (c *Contract) CheckMessageProposeOutcome(msg *MessageProposeOutcome) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.ResolverAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +if msg.ProposalBond == 0 { +return ErrCheckResp(ErrInvalidAmount()) +} +// NF-6: no status check here — status is stateful, DeliverTx enforces. +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.ResolverAddress}, +} +} + +func (c *Contract) DeliverMessageProposeOutcome(msg *MessageProposeOutcome, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +// ── Batch read: resolver record + market state + proposal (idempotency) ─── +// CRIT-3: QueryId per PluginKeyRead. +resolRecQId := nextQueryId() +marketQId := nextQueryId() +proposalQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: resolRecQId, Key: KeyForResolverRecord(msg.ResolverAddress)}, +{QueryId: marketQId, Key: KeyForMarket(msg.MarketId)}, +{QueryId: proposalQId, Key: KeyForProposal(msg.MarketId)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var resolverRec *ResolverRecord +var market *MarketState +var proposalRaw []byte + +for _, r := range resp.Results { +switch r.QueryId { +case resolRecQId: +if len(r.Entries) == 0 { +return &PluginDeliverResponse{Error: ErrResolverNotRegistered()} +} +resolverRec = &ResolverRecord{} +if pe := Unmarshal(r.Entries[0].Value, resolverRec); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case marketQId: +if len(r.Entries) == 0 { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case proposalQId: +if len(r.Entries) > 0 { +proposalRaw = r.Entries[0].Value +} +} +} + +if resolverRec == nil { +return &PluginDeliverResponse{Error: ErrResolverNotRegistered()} +} +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} + +// ── Global registry check (0x16) ───────────────────────────────────────── +if resolverRec.RrsScore < MIN_RRS_TO_PROPOSE { +return &PluginDeliverResponse{Error: ErrResolverSuspended()} +} + + // ── COI-1: resolver must not hold a position in this market ───────────────── + resolPosQId := nextQueryId() + posResp, posErr := c.plugin.StateRead(c, &PluginStateReadRequest{ + Keys: []*PluginKeyRead{ + {QueryId: resolPosQId, Key: KeyForPosition(msg.MarketId, msg.ResolverAddress)}, + }, + }) + if posErr != nil { + return &PluginDeliverResponse{Error: posErr} + } + for _, r := range posResp.Results { + if r.QueryId == resolPosQId && len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { + resolPos := &PositionState{} + if pe := Unmarshal(r.Entries[0].Value, resolPos); pe != nil { + return &PluginDeliverResponse{Error: pe} + } + if resolPos.SharesYes > 0 || resolPos.SharesNo > 0 { + return &PluginDeliverResponse{Error: ErrResolverHasPosition()} + } + } + } + +// ── COI-2: market creator cannot be the resolver ──────────────────────────── + if bytes.Equal(market.Creator, msg.ResolverAddress) { + return &PluginDeliverResponse{Error: ErrCreatorCannotResolve()} + } + + // ── Market status check ─────────────────────────────────────────────────── +// NF-6 FIX: accept STATUS_OPEN if now > ExpiryTime — inline transition. +// STATUS_OPEN -> STATUS_PROPOSED is atomic; STATUS_EXPIRED never persisted. +if market.Status == STATUS_OPEN { +if now <= market.ExpiryTime { +return &PluginDeliverResponse{Error: ErrMarketNotExpired()} +} +// now > ExpiryTime: we are the expiry trigger. +// market.Status will be set to STATUS_PROPOSED in the atomic write below. +} else if market.Status != STATUS_EXPIRED { +// Any other status (PROPOSED, DISPUTED, FINALIZED, CANCELLED, VOIDED) is invalid. +return &PluginDeliverResponse{Error: ErrMarketNotExpired()} +} + +// ── Idempotency guard: ProposalRecord nil = first call ──────────────────── +if proposalRaw != nil { +return &PluginDeliverResponse{Error: ErrAlreadyProposed()} +} + +// ── Bond sufficiency check ──────────────────────────────────────────────── +minBond := ComputeMinBond(market) +if msg.ProposalBond < minBond { +return &PluginDeliverResponse{Error: ErrInsufficientBond()} +} +if resolverRec.StakeAmount < msg.ProposalBond { +return &PluginDeliverResponse{Error: ErrInsufficientFunds()} +} + +// ── Mutate in memory ────────────────────────────────────────────────────── +market.Status = STATUS_PROPOSED +resolverRec.StakeAmount -= msg.ProposalBond + +proposal := &ProposalRecord{ +ResolverAddr: msg.ResolverAddress, +ProposedOutcome: msg.ProposedOutcome, +ProposalBond: msg.ProposalBond, +ProposalBlock: now, +Status: PROPOSAL_OPEN, +} + +// NF-5 FIX: ResolverState for this market — written here for the first time. +// This is what makes the auth check in ResolveMarket work. +resolverState := &ResolverState{ResolverAddress: msg.ResolverAddress} + +// ── Marshal all ─────────────────────────────────────────────────────────── +rawM, pe := SafeMarshal(market) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawRR, pe := SafeMarshal(resolverRec) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawPR, pe := SafeMarshal(proposal) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawRS, pe := SafeMarshal(resolverState) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} + +// ── NF-5 FIX: 4-key atomic write ───────────────────────────────────────── +// All four commit together or none do. +// On failure: market.Status stays OPEN/EXPIRED, 0x13 stays nil, retry is safe. +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForMarket(msg.MarketId), Value: rawM}, +{Key: KeyForResolverRecord(msg.ResolverAddress), Value: rawRR}, +{Key: KeyForProposal(msg.MarketId), Value: rawPR}, +{Key: KeyForResolverState(msg.MarketId), Value: rawRS}, // NF-5 +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_reclaim_stake.go b/plugin/go/tutorial/contract/handler_reclaim_stake.go new file mode 100644 index 0000000000..ffd0ce0651 --- /dev/null +++ b/plugin/go/tutorial/contract/handler_reclaim_stake.go @@ -0,0 +1,177 @@ +package contract + +func (c *Contract) CheckMessageReclaimStake(msg *MessageReclaimStake) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.ClaimantAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.ClaimantAddress}, +} +} + +func (c *Contract) DeliverMessageReclaimStake(msg *MessageReclaimStake, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +marketQId := nextQueryId() +posQId := nextQueryId() +poolQId := nextQueryId() +treasQId := nextQueryId() +proposalQId := nextQueryId() +claimAccQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: KeyForMarket(msg.MarketId)}, +{QueryId: posQId, Key: KeyForPosition(msg.MarketId, msg.ClaimantAddress)}, +{QueryId: poolQId, Key: KeyForMarketPool(msg.MarketId)}, +{QueryId: treasQId, Key: KeyForTreasuryReserve(msg.MarketId)}, +{QueryId: proposalQId, Key: KeyForProposal(msg.MarketId)}, +{QueryId: claimAccQId, Key: KeyForAccount(msg.ClaimantAddress)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +var position *PositionState +marketPool := &Pool{} +treasury := &TreasuryReserve{} +claimantAcc := &Account{} +var proposalRaw []byte + +for _, r := range resp.Results { +switch r.QueryId { +case marketQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case posQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +position = &PositionState{} +if pe := Unmarshal(r.Entries[0].Value, position); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +case poolQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, marketPool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +case treasQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, treasury); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +case proposalQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +proposalRaw = r.Entries[0].Value +} +case claimAccQId: +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +if pe := Unmarshal(r.Entries[0].Value, claimantAcc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} + +// Only reclaimable if STATUS_OPEN, expiry passed, and no proposal ever filed +if market.Status != STATUS_OPEN { +return &PluginDeliverResponse{Error: ErrMarketNotReclaimable()} +} +if proposalRaw != nil { +return &PluginDeliverResponse{Error: ErrMarketNotReclaimable()} +} +reclaimOpen := market.ExpiryTime + RESOLUTION_DELAY_BLOCKS + GRACE_PERIOD_BLOCKS +if now <= reclaimOpen { +return &PluginDeliverResponse{Error: ErrReclaimWindowClosed()} +} + +// Compute refund amount +var refund uint64 + +// Position refund (any bettor including creator) +if position != nil { +if position.Claimed { +return &PluginDeliverResponse{Error: ErrAlreadyClaimed()} +} +if position.SharesYes > 0 || position.SharesNo > 0 { +refund += position.CostPaid +} +} + +// Creator gets TreasuryReserve (FINALIZATION_BOUNTY) back — no resolver showed up +if bytesEqual(msg.ClaimantAddress, market.Creator) && treasury.LockedReserve > 0 { +refund += treasury.LockedReserve +} + +if refund == 0 { +return &PluginDeliverResponse{Error: ErrNoStakeToReclaim()} +} +if refund > marketPool.Amount { +return &PluginDeliverResponse{Error: ErrInsufficientPoolFunds()} +} + +// Mutate in memory +claimantAcc.Amount += refund +marketPool.Amount -= refund +isCreator := bytesEqual(msg.ClaimantAddress, market.Creator) +if isCreator { +treasury.LockedReserve = 0 +} + +// Marshal all +rawAcc, pe := SafeMarshal(claimantAcc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawPool, pe := SafeMarshal(marketPool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +sets := []*PluginSetOp{ +{Key: KeyForAccount(msg.ClaimantAddress), Value: rawAcc}, +{Key: KeyForMarketPool(msg.MarketId), Value: rawPool}, +} +var deletes []*PluginDeleteOp + +// Mark position as claimed to prevent double reclaim +if position != nil && (position.SharesYes > 0 || position.SharesNo > 0) { +position.Claimed = true +rawPos, pe := SafeMarshal(position) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +sets = append(sets, &PluginSetOp{Key: KeyForPosition(msg.MarketId, msg.ClaimantAddress), Value: rawPos}) +} + +if isCreator && treasury.LockedReserve == 0 { +rawTreas, pe := SafeMarshal(treasury) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +sets = append(sets, &PluginSetOp{Key: KeyForTreasuryReserve(msg.MarketId), Value: rawTreas}) +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: sets, +Deletes: deletes, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_register_resolver.go b/plugin/go/tutorial/contract/handler_register_resolver.go new file mode 100644 index 0000000000..46c24e0088 --- /dev/null +++ b/plugin/go/tutorial/contract/handler_register_resolver.go @@ -0,0 +1,148 @@ +package contract + +func (c *Contract) CheckMessageRegisterResolver(msg *MessageRegisterResolver) *PluginCheckResponse { +if len(msg.ResolverAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.ResolverAddress}, +} +} + +func (c *Contract) DeliverMessageRegisterResolver(msg *MessageRegisterResolver, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +recQId := nextQueryId() +accQId := nextQueryId() +feeQId := nextQueryId() + gTreasuryQId := nextQueryId() + ridxQId := nextQueryId() + +recKey := KeyForResolverRecord(msg.ResolverAddress) +accKey := KeyForAccount(msg.ResolverAddress) +feePoolKey := KeyForFeePool(c.Config.ChainId) + gTreasuryKey := KeyForTreasuryPool() + ridxKey := KeyForResolverIndex() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: recQId, Key: recKey}, +{QueryId: accQId, Key: accKey}, +{QueryId: feeQId, Key: feePoolKey}, + {QueryId: gTreasuryQId, Key: gTreasuryKey}, + {QueryId: ridxQId, Key: ridxKey}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var existing *ResolverRecord +account := &Account{} +feePool := &Pool{} + gTreasury := &Pool{} + ridx := &ResolverIndex{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +switch r.QueryId { +case recQId: +existing = &ResolverRecord{} +if pe := Unmarshal(r.Entries[0].Value, existing); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case accQId: +if pe := Unmarshal(r.Entries[0].Value, account); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case feeQId: +if pe := Unmarshal(r.Entries[0].Value, feePool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case gTreasuryQId: + if pe := Unmarshal(r.Entries[0].Value, gTreasury); pe != nil { + return &PluginDeliverResponse{Error: pe} + } + case ridxQId: + if pe := Unmarshal(r.Entries[0].Value, ridx); pe != nil { + return &PluginDeliverResponse{Error: pe} + } +} +} +if existing == nil && msg.StakeAmount < MIN_RESOLVER_STAKE { + return &PluginDeliverResponse{Error: ErrInsufficientResolverStake()} +} +if fee > 0 && msg.StakeAmount > ^uint64(0)-fee { +return &PluginDeliverResponse{Error: ErrInvalidAmount()} +} +totalCost := msg.StakeAmount + fee +if account.Amount < totalCost { +return &PluginDeliverResponse{Error: ErrInsufficientFunds()} +} + +account.Amount -= totalCost +feePool.Amount += fee / 2 + gTreasury.Amount += fee - fee/2 + +var record *ResolverRecord +if existing != nil { +existing.StakeAmount += msg.StakeAmount + if existing.StakeAmount < MIN_RESOLVER_STAKE { + return &PluginDeliverResponse{Error: ErrInsufficientResolverStake()} + } +existing.IsActive = true // re-activate on re-registration after full exit + if existing.RrsScore < PRIS_RRS_INITIAL { + existing.RrsScore = PRIS_RRS_INITIAL + } +record = existing +} else { +record = &ResolverRecord{ +ResolverAddress: msg.ResolverAddress, +StakeAmount: msg.StakeAmount, +RrsScore: PRIS_RRS_INITIAL, +RegisteredAt: now, +IsActive: true, +} +} + +rawRec, pe := SafeMarshal(record) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawFee, pe := SafeMarshal(feePool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawGTreasury, pe := SafeMarshal(gTreasury) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawRidx, pe := SafeMarshal(ridx) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +sets := []*PluginSetOp{ +{Key: recKey, Value: rawRec}, +{Key: feePoolKey, Value: rawFee}, + {Key: gTreasuryKey, Value: rawGTreasury}, + {Key: ridxKey, Value: rawRidx}, +} +var deletes []*PluginDeleteOp +if account.Amount == 0 { +deletes = []*PluginDeleteOp{{Key: accKey}} +} else { +rawAcc, pe := SafeMarshal(account) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +sets = append(sets, &PluginSetOp{Key: accKey, Value: rawAcc}) +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: sets, +Deletes: deletes, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_resolve_market.go b/plugin/go/tutorial/contract/handler_resolve_market.go new file mode 100644 index 0000000000..b0d1933505 --- /dev/null +++ b/plugin/go/tutorial/contract/handler_resolve_market.go @@ -0,0 +1,252 @@ +package contract + +// handler_resolve_market.go — MessageResolveMarket +// Spec: ADLMSR v5.6.6-r2-CORRECTED +// +// Called by the registered resolver after the resolution window opens. +// Sets market to STATUS_RESOLVED (ADLMSR intermediate — PORS leads to FINALIZED). +// Pays bounty to resolver and bond return to creator from the market pool. +// +// CheckTx: market_id 20 bytes, resolver_address 20 bytes. Zero StateRead (AUDIT-8). +// DeliverTx: +// Batch read 1: market, resolver state (auth), outcome (idempotency) +// R1 FIX: auth check BEFORE idempotency guard +// Auto-cancel check if outside resolution window +// Batch read 2: pool, resolver account, creator account, treasury +// 6-key atomic write (NEW-1 + R1) +// R5: two batch reads are deliberate — avoids pool/account reads for bad callers + +const bountyAmount uint64 = 50_000_000 // 50 PRX +const bondAmount uint64 = 100_000_000 // 100 PRX + +func (c *Contract) CheckMessageResolveMarket(msg *MessageResolveMarket) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.ResolverAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.ResolverAddress}, +} +} + +func (c *Contract) DeliverMessageResolveMarket(msg *MessageResolveMarket, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +// ── Batch read 1: market, resolver state, outcome (idempotency) ────────── +marketQId := nextQueryId() +resolverQId := nextQueryId() +outcomeQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: KeyForMarket(msg.MarketId)}, +{QueryId: resolverQId, Key: KeyForResolverState(msg.MarketId)}, +{QueryId: outcomeQId, Key: KeyForOutcome(msg.MarketId)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +var resolver *ResolverState +var outcomeRaw []byte + +for _, r := range resp.Results { +switch r.QueryId { +case marketQId: +if len(r.Entries) == 0 { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case resolverQId: +if len(r.Entries) == 0 { +return &PluginDeliverResponse{Error: ErrNoResolverRegistered()} +} +resolver = &ResolverState{} +if pe := Unmarshal(r.Entries[0].Value, resolver); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case outcomeQId: +if len(r.Entries) > 0 { +outcomeRaw = r.Entries[0].Value +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +if resolver == nil { +return &PluginDeliverResponse{Error: ErrNoResolverRegistered()} +} + +// ── R1 FIX: AUTH BEFORE IDEMPOTENCY ────────────────────────────────────── +// Wrong resolver must never receive success on retry. +if !bytesEqual(resolver.ResolverAddress, msg.ResolverAddress) { +return &PluginDeliverResponse{Error: ErrUnauthorized()} +} + +// ── Auto-cancel check + market state refresh ───────────────────────────── +withinWindow := now >= market.ExpiryTime+RESOLUTION_DELAY_BLOCKS && +now <= market.ExpiryTime+RESOLUTION_DELAY_BLOCKS+GRACE_PERIOD_BLOCKS +if !withinWindow { +if pe := c.CheckAutoCancel(msg.MarketId); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +// Re-read market after potential auto-cancel. +refreshQId := nextQueryId() +resp2, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: refreshQId, Key: KeyForMarket(msg.MarketId)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp2.Error != nil { +return &PluginDeliverResponse{Error: resp2.Error} +} +for _, r := range resp2.Results { +if r.QueryId == refreshQId && len(r.Entries) > 0 { +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} +} + +if market.Status == STATUS_CANCELLED { +return &PluginDeliverResponse{Error: ErrMarketCancelled()} +} + +// ── Idempotency guard — AFTER auth (R1 fix) ─────────────────────────────── +// OutcomeState exists iff all 6 keys committed in the atomic write below. +if outcomeRaw != nil { +return &PluginDeliverResponse{} +} + +// ── Timing and status guards ────────────────────────────────────────────── +if market.Status != STATUS_OPEN { +return &PluginDeliverResponse{Error: ErrMarketNotOpen()} +} +if now < market.ExpiryTime+RESOLUTION_DELAY_BLOCKS { +return &PluginDeliverResponse{Error: ErrResolutionTooEarly()} +} + +// ── Batch read 2: keys that will be mutated ─────────────────────────────── +// R5: deferred until after auth + timing pass to avoid wasted reads. +poolQId := nextQueryId() +resolAccQId := nextQueryId() +creatAccQId := nextQueryId() +treasQId := nextQueryId() + +payResp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: poolQId, Key: KeyForMarketPool(msg.MarketId)}, +{QueryId: resolAccQId, Key: KeyForAccount(msg.ResolverAddress)}, +{QueryId: creatAccQId, Key: KeyForAccount(market.Creator)}, +{QueryId: treasQId, Key: KeyForTreasuryReserve(msg.MarketId)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if payResp.Error != nil { +return &PluginDeliverResponse{Error: payResp.Error} +} + +mPool := &Pool{} +resolverAcc := &Account{} +creatorAcc := &Account{} +tres := &TreasuryReserve{} + +for _, r := range payResp.Results { +if len(r.Entries) == 0 { +continue +} +switch r.QueryId { +case poolQId: +if pe := Unmarshal(r.Entries[0].Value, mPool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case resolAccQId: +if pe := Unmarshal(r.Entries[0].Value, resolverAcc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case creatAccQId: +if pe := Unmarshal(r.Entries[0].Value, creatorAcc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case treasQId: +if pe := Unmarshal(r.Entries[0].Value, tres); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if mPool.Amount < bountyAmount+bondAmount { +return &PluginDeliverResponse{Error: ErrInsufficientPoolFunds()} +} + +// ── Mutate in memory ────────────────────────────────────────────────────── +market.Status = STATUS_RESOLVED // ADLMSR intermediate — PORS leads to FINALIZED +mPool.Amount -= bountyAmount + bondAmount +resolverAcc.Amount += bountyAmount +creatorAcc.Amount += bondAmount +tres.LockedReserve = 0 +outcome := &OutcomeState{WinningOutcome: msg.WinningOutcome, ResolvedAt: now} + +// ── Marshal all ─────────────────────────────────────────────────────────── +rawM, pe := SafeMarshal(market) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawO, pe := SafeMarshal(outcome) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawT, pe := SafeMarshal(tres) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawMP, pe := SafeMarshal(mPool) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawRA, pe := SafeMarshal(resolverAcc) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawCA, pe := SafeMarshal(creatorAcc) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} + +// ── 6-key atomic write (NEW-1 + R1) ────────────────────────────────────── +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForMarket(msg.MarketId), Value: rawM}, +{Key: KeyForOutcome(msg.MarketId), Value: rawO}, +{Key: KeyForTreasuryReserve(msg.MarketId), Value: rawT}, +{Key: KeyForMarketPool(msg.MarketId), Value: rawMP}, +{Key: KeyForAccount(msg.ResolverAddress), Value: rawRA}, +{Key: KeyForAccount(market.Creator), Value: rawCA}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_reveal_vote.go b/plugin/go/tutorial/contract/handler_reveal_vote.go new file mode 100644 index 0000000000..acc045003d --- /dev/null +++ b/plugin/go/tutorial/contract/handler_reveal_vote.go @@ -0,0 +1,154 @@ +package contract + +// handler_reveal_vote.go — MessageRevealVote +// Spec: PORS v1.0-r2-CORRECTED (P5) +// +// Panel members reveal their vote during the reveal phase. +// The revealed vote must match the previously committed hash: +// commit_hash == SHA256(vote_byte || nonce || voter_addr) +// +// CheckTx: market_id 20 bytes, voter_addr 20 bytes, nonce non-empty. +// Zero StateRead (AUDIT-8). +// DeliverTx: +// Read market + dispute + VoteCommit + VoteReveal +// Validate STATUS_DISPUTED and within reveal phase window +// Validate voter is a panel member +// Validate VoteCommit exists and VoteReveal does not +// Verify commit hash matches SHA256(vote || nonce || voter_addr) +// 1-key atomic write: VoteReveal + +func (c *Contract) CheckMessageRevealVote(msg *MessageRevealVote) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.VoterAddr) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +if len(msg.Nonce) == 0 { +return ErrCheckResp(ErrInvalidParam()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.VoterAddr}, +} +} + +func (c *Contract) DeliverMessageRevealVote(msg *MessageRevealVote, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +marketQId := nextQueryId() +disputeQId := nextQueryId() +commitQId := nextQueryId() +revealQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: KeyForMarket(msg.MarketId)}, +{QueryId: disputeQId, Key: KeyForDispute(msg.MarketId)}, +{QueryId: commitQId, Key: KeyForVoteCommit(msg.MarketId, msg.VoterAddr)}, +{QueryId: revealQId, Key: KeyForVoteReveal(msg.MarketId, msg.VoterAddr)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +var dispute *DisputeRecord +var vc *VoteCommit + +for _, r := range resp.Results { +switch r.QueryId { +case marketQId: +if len(r.Entries) == 0 { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case disputeQId: +if len(r.Entries) == 0 { +return &PluginDeliverResponse{Error: ErrNotDisputed()} +} +dispute = &DisputeRecord{} +if pe := Unmarshal(r.Entries[0].Value, dispute); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case commitQId: +if len(r.Entries) == 0 { +// Cannot reveal without first committing. +return &PluginDeliverResponse{Error: ErrNotAPanelMember()} +} +vc = &VoteCommit{} +if pe := Unmarshal(r.Entries[0].Value, vc); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case revealQId: +// Already revealed. +if len(r.Entries) > 0 { +return &PluginDeliverResponse{Error: ErrAlreadyRevealed()} +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +if market.Status != STATUS_DISPUTED { +return &PluginDeliverResponse{Error: ErrNotDisputed()} +} +if dispute == nil { +return &PluginDeliverResponse{Error: ErrNotDisputed()} +} +if vc == nil { +return &PluginDeliverResponse{Error: ErrNotAPanelMember()} +} + +// Reveal phase window: after commit phase ends, before reveal phase ends. +revealStart := dispute.DisputeBlock + COMMIT_PHASE_BLOCKS +revealEnd := revealStart + REVEAL_PHASE_BLOCKS +if now <= revealStart { +return &PluginDeliverResponse{Error: ErrRevealPhaseNotOpen()} +} +if now > revealEnd { +return &PluginDeliverResponse{Error: ErrRevealPhaseOver()} +} + +// Verify voter is a panel member. +if !isPanelMember(msg.VoterAddr, dispute.PanelMembers) { +return &PluginDeliverResponse{Error: ErrNotAPanelMember()} +} + +// Verify commit hash: SHA256(vote_byte || nonce || voter_addr). +expectedHash := ComputeCommitHash(msg.Vote, msg.Nonce, msg.VoterAddr) +if !bytesEqual(expectedHash, vc.CommitHash) { +return &PluginDeliverResponse{Error: ErrCommitHashMismatch()} +} + +// Write VoteReveal — single atomic key. +vr := &VoteReveal{ +VoterAddr: msg.VoterAddr, +Vote: msg.Vote, +RevealedAt: now, +} +rawVR, pe := SafeMarshal(vr) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: KeyForVoteReveal(msg.MarketId, msg.VoterAddr), Value: rawVR}, +}, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_send.go b/plugin/go/tutorial/contract/handler_send.go new file mode 100644 index 0000000000..7f69199e59 --- /dev/null +++ b/plugin/go/tutorial/contract/handler_send.go @@ -0,0 +1,181 @@ +package contract + +import "bytes" + +func (c *Contract) CheckMessageSend(msg *MessageSend) *PluginCheckResponse { +if len(msg.FromAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +if len(msg.ToAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +if msg.Amount == 0 { +return ErrCheckResp(ErrInvalidAmount()) +} +return &PluginCheckResponse{ +Recipient: msg.ToAddress, +AuthorizedSigners: [][]byte{msg.FromAddress}, +} +} + +func (c *Contract) DeliverMessageSend(msg *MessageSend, fee uint64) *PluginDeliverResponse { +isSelfTransfer := bytes.Equal(msg.FromAddress, msg.ToAddress) + +fromQId := nextQueryId() +toQId := nextQueryId() +feeQId := nextQueryId() + gTreasuryQId := nextQueryId() + +fromKey := KeyForAccount(msg.FromAddress) +toKey := KeyForAccount(msg.ToAddress) +feePoolKey := KeyForFeePool(c.Config.ChainId) + gTreasuryKey := KeyForTreasuryPool() + +var readKeys []*PluginKeyRead +if isSelfTransfer { +readKeys = []*PluginKeyRead{ +{QueryId: fromQId, Key: fromKey}, +{QueryId: feeQId, Key: feePoolKey}, + {QueryId: gTreasuryQId, Key: gTreasuryKey}, +} +} else { +readKeys = []*PluginKeyRead{ +{QueryId: fromQId, Key: fromKey}, +{QueryId: toQId, Key: toKey}, +{QueryId: feeQId, Key: feePoolKey}, + {QueryId: gTreasuryQId, Key: gTreasuryKey}, +} +} + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{Keys: readKeys}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +from := &Account{} +to := &Account{} +feePool := &Pool{} + gTreasury := &Pool{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +switch r.QueryId { +case fromQId: +if pe := Unmarshal(r.Entries[0].Value, from); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case toQId: +if pe := Unmarshal(r.Entries[0].Value, to); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case feeQId: +if pe := Unmarshal(r.Entries[0].Value, feePool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case gTreasuryQId: +if pe := Unmarshal(r.Entries[0].Value, gTreasury); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +// Self-transfer: pay fee only, amount stays +if isSelfTransfer { +if from.Amount < fee { +return &PluginDeliverResponse{Error: ErrInsufficientFunds()} +} +from.Amount -= fee +feePool.Amount += fee / 2 + gTreasury.Amount += fee - fee/2 + +rawFrom, pe := SafeMarshal(from) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawFee, pe := SafeMarshal(feePool) + if pe != nil { + return &PluginDeliverResponse{Error: pe} + } + rawGTreasury, pe := SafeMarshal(gTreasury) + if pe != nil { + return &PluginDeliverResponse{Error: pe} + } + +var wr *PluginStateWriteResponse +if from.Amount == 0 { +wr, err = c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{{Key: feePoolKey, Value: rawFee}, {Key: gTreasuryKey, Value: rawGTreasury}}, +Deletes: []*PluginDeleteOp{{Key: fromKey}}, +}) +} else { +wr, err = c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: fromKey, Value: rawFrom}, +{Key: feePoolKey, Value: rawFee}, + {Key: gTreasuryKey, Value: rawGTreasury}, +}, +}) +} +if pe := errCheckWrite(wr, err); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} + +// Normal transfer +if fee > 0 && msg.Amount > ^uint64(0)-fee { +return &PluginDeliverResponse{Error: ErrInvalidAmount()} +} +totalDeduct := msg.Amount + fee +if from.Amount < totalDeduct { +return &PluginDeliverResponse{Error: ErrInsufficientFunds()} +} + +from.Amount -= totalDeduct +to.Amount += msg.Amount +feePool.Amount += fee / 2 + gTreasury.Amount += fee - fee/2 + +rawFrom, pe := SafeMarshal(from) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawTo, pe := SafeMarshal(to) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +rawFee, pe := SafeMarshal(feePool) + if pe != nil { + return &PluginDeliverResponse{Error: pe} + } + rawGTreasury, pe := SafeMarshal(gTreasury) + if pe != nil { + return &PluginDeliverResponse{Error: pe} + } + +var wr *PluginStateWriteResponse +if from.Amount == 0 { +wr, err = c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{{Key: toKey, Value: rawTo}, {Key: feePoolKey, Value: rawFee}, {Key: gTreasuryKey, Value: rawGTreasury}}, +Deletes: []*PluginDeleteOp{{Key: fromKey}}, +}) +} else { +wr, err = c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: []*PluginSetOp{ +{Key: fromKey, Value: rawFrom}, +{Key: toKey, Value: rawTo}, +{Key: feePoolKey, Value: rawFee}, + {Key: gTreasuryKey, Value: rawGTreasury}, +}, +}) +} +if pe := errCheckWrite(wr, err); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_submit_prediction.go b/plugin/go/tutorial/contract/handler_submit_prediction.go new file mode 100644 index 0000000000..2e18085bf5 --- /dev/null +++ b/plugin/go/tutorial/contract/handler_submit_prediction.go @@ -0,0 +1,230 @@ +package contract + +func (c *Contract) CheckMessageSubmitPrediction(msg *MessageSubmitPrediction) *PluginCheckResponse { +if len(msg.BettorAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if msg.Shares < PRECISION_SCALE { +return ErrCheckResp(ErrSharesBelowMinimum()) +} +if msg.MaxCost == 0 { +return ErrCheckResp(ErrInvalidAmount()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.BettorAddress}, +} +} + +func (c *Contract) DeliverMessageSubmitPrediction(msg *MessageSubmitPrediction, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} +if msg.Shares < PRECISION_SCALE { +return &PluginDeliverResponse{Error: ErrSharesBelowMinimum()} +} +if cancelErr := c.CheckAutoCancel(msg.MarketId); cancelErr != nil { +return &PluginDeliverResponse{Error: cancelErr} +} + +marketQId := nextQueryId() +posQId := nextQueryId() +poolQId := nextQueryId() +bettorQId := nextQueryId() +feeQId := nextQueryId() + +marketKey := KeyForMarket(msg.MarketId) +posKey := KeyForPosition(msg.MarketId, msg.BettorAddress) +poolKey := KeyForMarketPool(msg.MarketId) +bettorKey := KeyForAccount(msg.BettorAddress) +feePoolKey := KeyForFeePool(c.Config.ChainId) +creatorFeeKey := KeyForCreatorFeePool(msg.MarketId) +resolverFeeKey := KeyForResolverFeePool(msg.MarketId) + gTreasuryKey := KeyForTreasuryPool() +creatorFeeQId := nextQueryId() + resolverFeeQId := nextQueryId() + gTreasuryQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: marketKey}, +{QueryId: posQId, Key: posKey}, +{QueryId: poolQId, Key: poolKey}, +{QueryId: bettorQId, Key: bettorKey}, +{QueryId: feeQId, Key: feePoolKey}, +{QueryId: creatorFeeQId, Key: creatorFeeKey}, +{QueryId: resolverFeeQId, Key: resolverFeeKey}, + {QueryId: gTreasuryQId, Key: gTreasuryKey}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +position := &PositionState{} +mPool := &Pool{} +bettor := &Account{} +feePool := &Pool{} +creatorFee := &Pool{} +resolverFee := &Pool{} + gTreasury := &Pool{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +switch r.QueryId { +case marketQId: +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case posQId: +if pe := Unmarshal(r.Entries[0].Value, position); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case poolQId: +if pe := Unmarshal(r.Entries[0].Value, mPool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case bettorQId: +if pe := Unmarshal(r.Entries[0].Value, bettor); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case feeQId: +if pe := Unmarshal(r.Entries[0].Value, feePool); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case creatorFeeQId: +if pe := Unmarshal(r.Entries[0].Value, creatorFee); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case resolverFeeQId: +if pe := Unmarshal(r.Entries[0].Value, resolverFee); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case gTreasuryQId: +if pe := Unmarshal(r.Entries[0].Value, gTreasury); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +if market.Status != STATUS_OPEN { +return &PluginDeliverResponse{Error: ErrMarketNotOpen()} +} +if now > market.ExpiryTime { +return &PluginDeliverResponse{Error: ErrMarketNotOpen()} +} +if now < market.OpenTime { +return &PluginDeliverResponse{Error: ErrMarketNotOpen()} +} + +tradeCost, pe := ComputeTradeCost(market.QYes, market.QNo, market.BEff, msg.Shares, msg.Outcome) +if pe != nil { +return &PluginDeliverResponse{Error: pe} +} +creatorFeeAmt := ComputeBps(tradeCost, CREATOR_FEE_BPS) +resolverFeeAmt := ComputeBps(tradeCost, RESOLVER_FEE_BPS) +if fee > 0 && tradeCost > ^uint64(0)-fee { +return &PluginDeliverResponse{Error: ErrInvalidAmount()} +} +finalCost := tradeCost + fee + creatorFeeAmt + resolverFeeAmt +if finalCost > msg.MaxCost { +return &PluginDeliverResponse{Error: ErrCostExceedsMaxCost()} +} +// COI-3: per-address position cap — capped on shares, not CostPaid. +// totalSideShares is post-trade so the cap scales with actual exposure. +var totalSideShares uint64 +if msg.Outcome { +totalSideShares = market.QYes + msg.Shares +if exceedsPositionCap(position.SharesYes, msg.Shares, totalSideShares) { +return &PluginDeliverResponse{Error: ErrPositionCapExceeded()} +} +} else { +totalSideShares = market.QNo + msg.Shares +if exceedsPositionCap(position.SharesNo, msg.Shares, totalSideShares) { +return &PluginDeliverResponse{Error: ErrPositionCapExceeded()} +} +} + +if bettor.Amount < finalCost { +return &PluginDeliverResponse{Error: ErrInsufficientFunds()} +} + +isNewPosition := position.SharesYes == 0 && position.SharesNo == 0 && position.CostPaid == 0 + +bettor.Amount -= finalCost +mPool.Amount += tradeCost +feePool.Amount += fee / 2 + gTreasury.Amount += fee - fee/2 +creatorFee.Amount += creatorFeeAmt +resolverFee.Amount += resolverFeeAmt + +if msg.Outcome { +market.QYes += msg.Shares +position.SharesYes += msg.Shares +} else { +market.QNo += msg.Shares +position.SharesNo += msg.Shares +} +position.CostPaid += tradeCost + +if isNewPosition { +market.TotalPositions++ +} + +market.ElevatedRisk = IsElevatedRisk(mPool.Amount) + +rawMarket, pe := SafeMarshal(market) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawPos, pe := SafeMarshal(position) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawPool, pe := SafeMarshal(mPool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawFee, pe := SafeMarshal(feePool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawGTreasury, pe := SafeMarshal(gTreasury) + if pe != nil { return &PluginDeliverResponse{Error: pe} } + rawCreatorFee, pe := SafeMarshal(creatorFee) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawResolverFee, pe := SafeMarshal(resolverFee) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +sets := []*PluginSetOp{ +{Key: marketKey, Value: rawMarket}, +{Key: posKey, Value: rawPos}, +{Key: poolKey, Value: rawPool}, +{Key: feePoolKey, Value: rawFee}, +{Key: creatorFeeKey, Value: rawCreatorFee}, +{Key: resolverFeeKey, Value: rawResolverFee}, + {Key: gTreasuryKey, Value: rawGTreasury}, +} +var deletes []*PluginDeleteOp +if bettor.Amount == 0 { +deletes = []*PluginDeleteOp{{Key: bettorKey}} +} else { +rawBettor, pe := SafeMarshal(bettor) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +sets = append(sets, &PluginSetOp{Key: bettorKey, Value: rawBettor}) +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: sets, +Deletes: deletes, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_tally_votes.go b/plugin/go/tutorial/contract/handler_tally_votes.go new file mode 100644 index 0000000000..d42f50bf88 --- /dev/null +++ b/plugin/go/tutorial/contract/handler_tally_votes.go @@ -0,0 +1,244 @@ +package contract + +func (c *Contract) CheckMessageTallyVotes(msg *MessageTallyVotes) *PluginCheckResponse { +if len(msg.MarketId) != 20 { +return ErrCheckResp(ErrInvalidParam()) +} +if len(msg.CallerAddr) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.CallerAddr}, +} +} + +func (c *Contract) DeliverMessageTallyVotes(msg *MessageTallyVotes, fee uint64) *PluginDeliverResponse { +now := GetGlobalHeight() +if now == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +marketQId := nextQueryId() +disputeQId := nextQueryId() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: marketQId, Key: KeyForMarket(msg.MarketId)}, +{QueryId: disputeQId, Key: KeyForDispute(msg.MarketId)}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var market *MarketState +var dispute *DisputeRecord + +for _, r := range resp.Results { +switch r.QueryId { +case marketQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +market = &MarketState{} +if pe := Unmarshal(r.Entries[0].Value, market); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +case disputeQId: +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +return &PluginDeliverResponse{Error: ErrNotDisputed()} +} +dispute = &DisputeRecord{} +if pe := Unmarshal(r.Entries[0].Value, dispute); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +} +} + +if market == nil { +return &PluginDeliverResponse{Error: ErrMarketNotFound()} +} +if market.Status != STATUS_DISPUTED { +return &PluginDeliverResponse{Error: ErrNotDisputed()} +} +if dispute == nil { +return &PluginDeliverResponse{Error: ErrNotDisputed()} +} + +if dispute.VoteStatus == VOTE_TALLIED { +return &PluginDeliverResponse{} +} + +revealEnd := dispute.DisputeBlock + COMMIT_PHASE_BLOCKS + REVEAL_PHASE_BLOCKS +if now <= revealEnd { +return &PluginDeliverResponse{Error: ErrTallyNotReady()} +} + +queries := make([]uint64, 0, len(dispute.PanelMembers)) +readKeys := make([]*PluginKeyRead, 0, len(dispute.PanelMembers)) + +// Layer 2: build RRS read keys alongside vote reveal keys +rrsQueries := make([]uint64, 0, len(dispute.PanelMembers)) +rrsKeys := make([]*PluginKeyRead, 0, len(dispute.PanelMembers)) + +for _, member := range dispute.PanelMembers { +qId := nextQueryId() +queries = append(queries, qId) +readKeys = append(readKeys, &PluginKeyRead{ +QueryId: qId, +Key: KeyForVoteReveal(msg.MarketId, member), +}) +rId := nextQueryId() +rrsQueries = append(rrsQueries, rId) +rrsKeys = append(rrsKeys, &PluginKeyRead{ +QueryId: rId, +Key: KeyForResolverRecord(member), +}) +} + +// Layer 2: build queryId -> vote weight map from RRS scores +weightByQId := make(map[uint64]uint32) +if len(rrsKeys) > 0 { +rrsResp, err := c.plugin.StateRead(c, &PluginStateReadRequest{Keys: rrsKeys}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if rrsResp.Error != nil { +return &PluginDeliverResponse{Error: rrsResp.Error} +} +for idx, r := range rrsResp.Results { +weight := VOTE_WEIGHT_BRONZE +if len(r.Entries) > 0 && len(r.Entries[0].Value) > 0 { +rec := &ResolverRecord{} +if pe := Unmarshal(r.Entries[0].Value, rec); pe == nil { +if rec.RrsScore >= RRS_GOLD_THRESHOLD { +weight = VOTE_WEIGHT_GOLD +} else if rec.RrsScore >= RRS_SILVER_THRESHOLD { +weight = VOTE_WEIGHT_SILVER +} +} +} +// map the corresponding vote reveal queryId to this weight +if idx < len(queries) { +weightByQId[queries[idx]] = weight +} +} +} + +var yesVotes, noVotes uint32 +if len(readKeys) > 0 { +voteResp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: readKeys, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if voteResp.Error != nil { +return &PluginDeliverResponse{Error: voteResp.Error} +} + +for _, r := range voteResp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +vr := &VoteReveal{} +if pe := Unmarshal(r.Entries[0].Value, vr); pe != nil { +continue +} +// Layer 2: apply RRS tier weight — default Bronze if missing +w := weightByQId[r.QueryId] +if w == 0 { +w = VOTE_WEIGHT_BRONZE +} +if vr.Vote { +yesVotes += w +} else { +noVotes += w +} +} +} + +disputerWins := yesVotes > noVotes +dispute.VoteStatus = VOTE_TALLIED + +if disputerWins { +market.Status = STATUS_PROPOSED +} + +rawD, pe := SafeMarshal(dispute) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawM, pe := SafeMarshal(market) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +sets := []*PluginSetOp{ +{Key: KeyForDispute(msg.MarketId), Value: rawD}, +{Key: KeyForMarket(msg.MarketId), Value: rawM}, +} + +// PRIS v1.0-r3: if proposer wins dispute, RRS +20 and increment GlobalStats +if !disputerWins { +propQId := nextQueryId() +recQId := nextQueryId() +statsQId := nextQueryId() +prisResp, prisErr := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: propQId, Key: KeyForProposal(msg.MarketId)}, +{QueryId: recQId, Key: KeyForGlobalStats()}, +{QueryId: statsQId, Key: KeyForGlobalStats()}, +}, +}) +if prisErr == nil && prisResp.Error == nil { +proposal := &ProposalRecord{} +globalStats := &GlobalStats{} +for _, r := range prisResp.Results { +if len(r.Entries) == 0 { continue } +switch r.QueryId { +case propQId: +_ = Unmarshal(r.Entries[0].Value, proposal) +case statsQId: +_ = Unmarshal(r.Entries[0].Value, globalStats) +} +} +if len(proposal.ResolverAddr) == 20 { +resolverRecQId := nextQueryId() +recResp, recErr := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: resolverRecQId, Key: KeyForResolverRecord(proposal.ResolverAddr)}, +}, +}) +if recErr == nil && recResp.Error == nil { +resolverRec := &ResolverRecord{} +for _, r := range recResp.Results { +if r.QueryId == resolverRecQId && len(r.Entries) > 0 { +_ = Unmarshal(r.Entries[0].Value, resolverRec) +} +} +resolverRec.RrsScore += 20 +resolverRec.SuccessfulResolutions++ +weight := uint64(1) +if resolverRec.RrsScore >= RRS_GOLD_THRESHOLD { +weight = uint64(VOTE_WEIGHT_GOLD) +} else if resolverRec.RrsScore >= RRS_SILVER_THRESHOLD { +weight = uint64(VOTE_WEIGHT_SILVER) +} +globalStats.TotalWeightedResolutions += weight +rawRec, pe2 := SafeMarshal(resolverRec) +rawStats, pe3 := SafeMarshal(globalStats) +if pe2 == nil && pe3 == nil { +sets = append(sets, &PluginSetOp{Key: KeyForResolverRecord(proposal.ResolverAddr), Value: rawRec}) +sets = append(sets, &PluginSetOp{Key: KeyForGlobalStats(), Value: rawStats}) +} +} +} +} +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{Sets: sets}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/handler_unstake_resolver.go b/plugin/go/tutorial/contract/handler_unstake_resolver.go new file mode 100644 index 0000000000..016df93905 --- /dev/null +++ b/plugin/go/tutorial/contract/handler_unstake_resolver.go @@ -0,0 +1,169 @@ +package contract + +// handler_unstake_resolver.go — MessageUnstakeResolver +// Spec: PRIS v1.0-r3 unstake extension +// +// Allows a resolver to unstake partially or fully. +// Rules: +// - Cannot unstake if active open proposal exists (slash-evasion guard) +// - Cannot partial unstake below MIN_RESOLVER_STAKE +// - Partial unstake: RRS -10, 120,960 block unbonding period +// - Full exit (amount=0 or amount=stakeAmount): RRS reset to PRIS_RRS_INITIAL, unbonding period +// - Tokens locked in UnbondingRecord until release height +// - claim_unbonded_stake TX releases tokens after unbonding period + +func (c *Contract) CheckMessageUnstakeResolver(msg *MessageUnstakeResolver) *PluginCheckResponse { +if len(msg.ResolverAddress) != 20 { +return ErrCheckResp(ErrInvalidAddress()) +} +return &PluginCheckResponse{ +AuthorizedSigners: [][]byte{msg.ResolverAddress}, +} +} + +func (c *Contract) DeliverMessageUnstakeResolver(msg *MessageUnstakeResolver, fee uint64) *PluginDeliverResponse { +height := GetGlobalHeight() +if height == 0 { +return &PluginDeliverResponse{Error: ErrHeightNotSet()} +} + +// ── Batch read ──────────────────────────────────────────────────────── +recQId := nextQueryId() +accQId := nextQueryId() +feeQId := nextQueryId() +gTreasuryQId := nextQueryId() + +recKey := KeyForResolverRecord(msg.ResolverAddress) +accKey := KeyForAccount(msg.ResolverAddress) +feePoolKey := KeyForFeePool(c.Config.ChainId) +gTreasuryKey := KeyForTreasuryPool() + +resp, err := c.plugin.StateRead(c, &PluginStateReadRequest{ +Keys: []*PluginKeyRead{ +{QueryId: recQId, Key: recKey}, +{QueryId: accQId, Key: accKey}, +{QueryId: feeQId, Key: feePoolKey}, +{QueryId: gTreasuryQId, Key: gTreasuryKey}, +}, +}) +if err != nil { +return &PluginDeliverResponse{Error: err} +} +if resp.Error != nil { +return &PluginDeliverResponse{Error: resp.Error} +} + +var record *ResolverRecord +acc := &Account{} +feePool := &Pool{} +gTreasury := &Pool{} + +for _, r := range resp.Results { +if len(r.Entries) == 0 || len(r.Entries[0].Value) == 0 { +continue +} +switch r.QueryId { +case recQId: +record = &ResolverRecord{} +if pe := Unmarshal(r.Entries[0].Value, record); pe != nil { +return &PluginDeliverResponse{Error: pe} +} + +case accQId: + if pe := Unmarshal(r.Entries[0].Value, acc); pe != nil { + return &PluginDeliverResponse{Error: pe} + } + case feeQId: + if pe := Unmarshal(r.Entries[0].Value, feePool); pe != nil { + return &PluginDeliverResponse{Error: pe} + } + case gTreasuryQId: + if pe := Unmarshal(r.Entries[0].Value, gTreasury); pe != nil { + return &PluginDeliverResponse{Error: pe} + } + } + } + +if record == nil { +return &PluginDeliverResponse{Error: ErrResolverNotFound()} +} +if !record.IsActive { + return &PluginDeliverResponse{Error: ErrResolverNotActive()} +} + +// ── Determine unstake amount and type ───────────────────────────────── +fullExit := msg.Amount == 0 || msg.Amount >= record.StakeAmount +unstakeAmt := msg.Amount +if fullExit { + unstakeAmt = record.StakeAmount +} + +// Partial unstake: remaining stake must stay >= MIN_RESOLVER_STAKE +if !fullExit { + remaining := record.StakeAmount - unstakeAmt + if remaining < MIN_RESOLVER_STAKE { + return &PluginDeliverResponse{Error: ErrInsufficientResolverStake()} + } +} + +if record.UnbondingAmount > 0 { + return &PluginDeliverResponse{Error: ErrUnbondingAlreadyPending()} +} + +// ── Apply RRS penalty ───────────────────────────────────────────────── +if fullExit { +record.RrsScore = PRIS_RRS_INITIAL // reset to Bronze baseline +record.IsActive = false +} else { +if record.RrsScore > PRIS_UNSTAKE_PARTIAL_RRS_HIT { +record.RrsScore -= PRIS_UNSTAKE_PARTIAL_RRS_HIT +} else { +record.RrsScore = PRIS_RRS_FLOOR +} +} + +// ── Apply unbonding ─────────────────────────────────────────────────── +record.StakeAmount -= unstakeAmt +record.UnbondingAmount = unstakeAmt +record.UnbondingReleaseHeight = height + PRIS_UNSTAKE_UNBONDING_BLOCKS + +// ── Pay fee from account ────────────────────────────────────────────── +if acc.Amount < fee { + return &PluginDeliverResponse{Error: ErrInsufficientFunds()} +} +acc.Amount -= fee +feePool.Amount += fee / 2 +gTreasury.Amount += fee - fee/2 + +// ── Marshal ─────────────────────────────────────────────────────────── +rawRec, pe := SafeMarshal(record) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawAcc, pe := SafeMarshal(acc) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawFee, pe := SafeMarshal(feePool) +if pe != nil { return &PluginDeliverResponse{Error: pe} } +rawGTreasury, pe := SafeMarshal(gTreasury) +if pe != nil { return &PluginDeliverResponse{Error: pe} } + +// ── Atomic write ────────────────────────────────────────────────────── +sets := []*PluginSetOp{ + {Key: recKey, Value: rawRec}, + {Key: feePoolKey, Value: rawFee}, + {Key: gTreasuryKey, Value: rawGTreasury}, +} +var deletes []*PluginDeleteOp +if acc.Amount == 0 { + deletes = []*PluginDeleteOp{{Key: accKey}} +} else { + sets = append(sets, &PluginSetOp{Key: accKey, Value: rawAcc}) +} + +wr, werr := c.plugin.StateWrite(c, &PluginStateWriteRequest{ +Sets: sets, +Deletes: deletes, +}) +if pe := errCheckWrite(wr, werr); pe != nil { +return &PluginDeliverResponse{Error: pe} +} +return &PluginDeliverResponse{} +} diff --git a/plugin/go/tutorial/contract/height.go b/plugin/go/tutorial/contract/height.go new file mode 100644 index 0000000000..7a94334be2 --- /dev/null +++ b/plugin/go/tutorial/contract/height.go @@ -0,0 +1,41 @@ +package contract + +import "sync" + +// ═══════════════════════════════════════════════════════════════════════════════ +// Praxis Prediction Market — Global Height Management +// Spec authority: ADLMSR v5.6.6-r2-CORRECTED (AUDIT-9) +// +// AUDIT-9: globalHeight must be a package-level variable protected by +// sync.RWMutex. Never use c.currentHeight on the Contract struct between +// BeginBlock and DeliverTx — concurrent DeliverTx goroutines would race +// on the struct field, producing non-deterministic height reads. +// +// Pattern: +// BeginBlock → SetGlobalHeight(req.Height) +// DeliverTx → now := GetGlobalHeight() +// if now == 0 { return ErrHeightNotSet() } +// ═══════════════════════════════════════════════════════════════════════════════ + +var ( +globalHeight uint64 +heightMu sync.RWMutex +) + +// SetGlobalHeight stores the current block height. +// Called once per block in BeginBlock before any transactions are processed. +func SetGlobalHeight(h uint64) { +heightMu.Lock() +globalHeight = h +heightMu.Unlock() +} + +// GetGlobalHeight returns the current block height. +// Called at the top of every DeliverTx handler. +// Returns 0 if BeginBlock has not been called yet — handlers must guard +// against this with: if now == 0 { return ErrHeightNotSet() } +func GetGlobalHeight() uint64 { +heightMu.RLock() +defer heightMu.RUnlock() +return globalHeight +} diff --git a/plugin/go/tutorial/contract/helpers.go b/plugin/go/tutorial/contract/helpers.go new file mode 100644 index 0000000000..c60d63552e --- /dev/null +++ b/plugin/go/tutorial/contract/helpers.go @@ -0,0 +1,113 @@ +package contract + +import ( +"crypto/sha256" +"encoding/binary" +"math/big" +"sync/atomic" +) + +// ═══════════════════════════════════════════════════════════════════════════════ +// Praxis Prediction Market — Shared Helpers +// Spec authority: ADLMSR v5.6.6-r2-CORRECTED + PORS v1.0-r2-CORRECTED +// ═══════════════════════════════════════════════════════════════════════════════ + +// ───────────────────────────────────────────────────────────────────────────── +// QUERY ID COUNTER +// Uses atomic counter instead of math/rand — rand collisions within a batch +// silently misroute state reads. Atomic counter guarantees uniqueness. +// ───────────────────────────────────────────────────────────────────────────── + +var queryCounter uint64 + +func nextQueryId() uint64 { +return atomic.AddUint64(&queryCounter, 1) +} + +// ───────────────────────────────────────────────────────────────────────────── +// OVERFLOW-SAFE ARITHMETIC +// mulDiv computes (a * b) / c using big.Int for the intermediate product. +// Prevents uint64 overflow in proportional calculations. +// Returns 0 if c == 0. Caps at MaxUint64 if result overflows. +// ───────────────────────────────────────────────────────────────────────────── + +func mulDiv(a, b, c uint64) uint64 { +if c == 0 { +return 0 +} +num := new(big.Int).Mul( +new(big.Int).SetUint64(a), +new(big.Int).SetUint64(b), +) +result := new(big.Int).Div(num, new(big.Int).SetUint64(c)) +maxU64 := new(big.Int).SetUint64(^uint64(0)) +if result.Cmp(maxU64) > 0 { +return ^uint64(0) +} +return result.Uint64() +} + +// ───────────────────────────────────────────────────────────────────────────── +// MARKET ID DERIVATION +// market_id = SHA256(creator_address || nonce_bytes)[:20] +// Deterministic — no sequential counter needed. Verifiable without state read. +// ───────────────────────────────────────────────────────────────────────────── + +func DeriveMarketId(creatorAddr []byte, nonce uint64) []byte { +nonceBytes := make([]byte, 8) +binary.BigEndian.PutUint64(nonceBytes, nonce) +input := make([]byte, len(creatorAddr)+8) + copy(input, creatorAddr) + copy(input[len(creatorAddr):], nonceBytes) +hash := sha256.Sum256(input) +return hash[:20] +} + +// ───────────────────────────────────────────────────────────────────────────── +// COMMIT HASH VERIFICATION +// commit_hash = SHA256(vote_byte || nonce || voter_addr) +// vote_byte: 0x01 for true (YES), 0x00 for false (NO) +// ───────────────────────────────────────────────────────────────────────────── + +func ComputeCommitHash(vote bool, nonce []byte, voterAddr []byte) []byte { +var voteByte byte +if vote { +voteByte = 0x01 +} else { +voteByte = 0x00 +} +input := make([]byte, 0, 1+len(nonce)+len(voterAddr)) +input = append(input, voteByte) +input = append(input, nonce...) +input = append(input, voterAddr...) +hash := sha256.Sum256(input) +return hash[:] +} + +// ───────────────────────────────────────────────────────────────────────────── +// BYTES EQUAL +// Convenience wrapper — avoids importing bytes in every handler file. +// ───────────────────────────────────────────────────────────────────────────── + +func bytesEqual(a, b []byte) bool { +if len(a) != len(b) { +return false +} +for i := range a { +if a[i] != b[i] { +return false +} +} +return true +} + +// exceedsPositionCap returns true if adding newShares would give the address +// more than 20% of the total side shares post-trade. +func exceedsPositionCap(currentShares, newShares, totalSideShares uint64) bool { +if totalSideShares == 0 { +return false +} +postTrade := currentShares + newShares +// cap = 20% of totalSideShares +return postTrade*5 > totalSideShares +} diff --git a/plugin/go/tutorial/contract/keys.go b/plugin/go/tutorial/contract/keys.go new file mode 100644 index 0000000000..3d29c90df6 --- /dev/null +++ b/plugin/go/tutorial/contract/keys.go @@ -0,0 +1,192 @@ +package contract + +import "encoding/binary" + +// ═══════════════════════════════════════════════════════════════════════════════ +// Praxis Prediction Market — State Key Functions +// Spec authority: ADLMSR v5.6.6-r2-CORRECTED + PORS v1.0-r2-CORRECTED +// +// All keys are built with JoinLenPrefix from plugin.go which length-prefixes +// each segment, guaranteeing no two different key types can collide even if +// their raw bytes overlap. +// +// Canopy base layer prefixes (do not reuse): +// 0x01 Account +// 0x02 Pool (fee pool) +// 0x07 FeeParams +// +// Praxis state prefixes (0x10–0x1C exclusively): +// 0x10 MarketState per market_id +// 0x11 PositionState per market_id + claimant_address +// 0x12 OutcomeState per market_id +// 0x13 ResolverState per market_id (written by propose_outcome — NF-5) +// 0x14 TreasuryReserve per market_id +// 0x16 ResolverRecord global per resolver_address +// 0x17 ProposalRecord per market_id +// 0x18 DisputeRecord per market_id +// 0x19 VoteCommit per market_id + voter_addr +// 0x1A VoteReveal per market_id + voter_addr +// 0x1B SlashRecord per resolver_address +// 0x1C PanelEntropyAccum singleton +// ═══════════════════════════════════════════════════════════════════════════════ + +// ───────────────────────────────────────────────────────────────────────────── +// CANOPY BASE LAYER KEYS +// ───────────────────────────────────────────────────────────────────────────── + +var ( +accountPrefix = []byte{0x01} +poolPrefix = []byte{0x02} +paramsPrefix = []byte{0x07} +) + +// KeyForAccount returns the state key for an Account record. +// addr must be exactly 20 bytes. +func KeyForAccount(addr []byte) []byte { +return JoinLenPrefix(accountPrefix, addr) +} + +// KeyForFeePool returns the state key for the chain fee pool. +func KeyForFeePool(chainId uint64) []byte { +return JoinLenPrefix(poolPrefix, uint64ToBytes(chainId)) +} + +// KeyForFeeParams returns the state key for the FeeParams singleton. +func KeyForFeeParams() []byte { +return JoinLenPrefix(paramsPrefix, []byte("/f/")) +} + +// ───────────────────────────────────────────────────────────────────────────── +// ADLMSR STATE KEYS +// ───────────────────────────────────────────────────────────────────────────── + +var ( +marketPrefix = []byte{0x10} +positionPrefix = []byte{0x11} +outcomePrefix = []byte{0x12} +resolverStatePrefix = []byte{0x13} +treasuryPrefix = []byte{0x14} +) + +// KeyForMarket returns the state key for a MarketState record. +// market_id is a 20-byte derived identifier. +func KeyForMarket(marketId []byte) []byte { +return JoinLenPrefix(marketPrefix, marketId) +} + +// KeyForPosition returns the state key for a PositionState record. +// Composite key: market_id (20 bytes) + claimant_address (20 bytes). +func KeyForPosition(marketId []byte, addr []byte) []byte { +composite := make([]byte, len(marketId)+len(addr)) +copy(composite, marketId) +copy(composite[len(marketId):], addr) +return JoinLenPrefix(positionPrefix, composite) +} + +// KeyForOutcome returns the state key for an OutcomeState record. +// Presence of this key is the idempotency sentinel for ResolveMarket. +func KeyForOutcome(marketId []byte) []byte { +return JoinLenPrefix(outcomePrefix, marketId) +} + +// KeyForResolverState returns the state key for a per-market ResolverState. +// Written by propose_outcome as the 4th atomic key (NF-5 fix). +// Distinct from KeyForResolverRecord (global per resolver_address). +func KeyForResolverState(marketId []byte) []byte { +return JoinLenPrefix(resolverStatePrefix, marketId) +} + +// KeyForTreasuryReserve returns the state key for a per-market TreasuryReserve. +func KeyForTreasuryReserve(marketId []byte) []byte { +return JoinLenPrefix(treasuryPrefix, marketId) +} + +// ───────────────────────────────────────────────────────────────────────────── +// PORS STATE KEYS +// ───────────────────────────────────────────────────────────────────────────── + +var ( +resolverRecordPrefix = []byte{0x16} +proposalPrefix = []byte{0x17} +disputePrefix = []byte{0x18} +voteCommitPrefix = []byte{0x19} +voteRevealPrefix = []byte{0x1A} +slashRecordPrefix = []byte{0x1B} +) + +// KeyForResolverRecord returns the state key for a global ResolverRecord. +// Keyed by resolver_address — this is the global resolver profile. +// Distinct from KeyForResolverState (per-market). +func KeyForResolverRecord(resolverAddr []byte) []byte { +return JoinLenPrefix(resolverRecordPrefix, resolverAddr) +} + +// KeyForProposal returns the state key for a ProposalRecord. +// Presence of this key is the idempotency sentinel for propose_outcome. +func KeyForProposal(marketId []byte) []byte { +return JoinLenPrefix(proposalPrefix, marketId) +} + +// KeyForDispute returns the state key for a DisputeRecord. +func KeyForDispute(marketId []byte) []byte { +return JoinLenPrefix(disputePrefix, marketId) +} + +// KeyForVoteCommit returns the state key for a panel member's VoteCommit. +// Composite key: market_id (20 bytes) + voter_addr (20 bytes). +func KeyForVoteCommit(marketId []byte, voterAddr []byte) []byte { +composite := make([]byte, len(marketId)+len(voterAddr)) +copy(composite, marketId) +copy(composite[len(marketId):], voterAddr) +return JoinLenPrefix(voteCommitPrefix, composite) +} + +// KeyForVoteReveal returns the state key for a panel member's VoteReveal. +// Composite key: market_id (20 bytes) + voter_addr (20 bytes). +func KeyForVoteReveal(marketId []byte, voterAddr []byte) []byte { +composite := make([]byte, len(marketId)+len(voterAddr)) +copy(composite, marketId) +copy(composite[len(marketId):], voterAddr) +return JoinLenPrefix(voteRevealPrefix, composite) +} + +// KeyForSlashRecord returns the state key for a resolver's SlashRecord. +func KeyForSlashRecord(resolverAddr []byte) []byte { +return JoinLenPrefix(slashRecordPrefix, resolverAddr) +} + +// ───────────────────────────────────────────────────────────────────────────── +// ENTROPY KEY +// Initialised in contract.go init() after JoinLenPrefix is available. +// ───────────────────────────────────────────────────────────────────────────── + +// KeyForPanelEntropy returns the singleton state key for the rolling +// entropy accumulator. Called once in init() to set PANEL_ENTROPY_KEY. +func KeyForPanelEntropy() []byte { +return JoinLenPrefix(panelEntropyPrefix, []byte("/pe/")) +} + +// ───────────────────────────────────────────────────────────────────────────── +// HELPERS +// ───────────────────────────────────────────────────────────────────────────── + +// uint64ToBytes encodes a uint64 as 8 big-endian bytes. +// Used for chain ID in fee pool keys. +func uint64ToBytes(u uint64) []byte { +b := make([]byte, 8) +binary.BigEndian.PutUint64(b, u) +return b +} + +// KeyForMarketPool returns the state key for a per-market liquidity pool. +// Uses the same 0x02 prefix as the chain fee pool but keyed by market_id. +// The JoinLenPrefix encoding ensures no collision with KeyForFeePool(chainId). +func KeyForMarketPool(marketId []byte) []byte { +return JoinLenPrefix(poolPrefix, marketId) +} + +// KeyForTreasuryPool returns the state key for the global Praxis treasury pool. +// Prefix 0x1D — receives surplus sweeps from finalized/cancelled markets. +func KeyForTreasuryPool() []byte { +return JoinLenPrefix([]byte{0x1D}, []byte("/treasury/")) +} diff --git a/plugin/go/tutorial/contract/keys_pris.go b/plugin/go/tutorial/contract/keys_pris.go new file mode 100644 index 0000000000..abbbc8b2cc --- /dev/null +++ b/plugin/go/tutorial/contract/keys_pris.go @@ -0,0 +1,99 @@ +package contract + +// ───────────────────────────────────────────────────────────────────────────── +// PRIS STATE KEYS (0x1D – 0x27) +// Spec authority: PRIS v1.0-r3 +// +// 0x1D CreatorFeePool per market_id +// 0x1E ResolverFeePool per market_id +// 0x1F TreasuryPool singleton global accumulator +// 0x20 EpochSnapshot per epoch number +// 0x21 ResolverEpochPool per epoch number +// 0x22 BuilderPool singleton +// 0x23 CommunityPool singleton +// 0x24 InvestorPool singleton +// 0x25 ProtocolPool singleton +// 0x26 BuilderLastClaimed singleton +// 0x27 InvestorLastClaimed singleton +// ───────────────────────────────────────────────────────────────────────────── + +var ( +creatorFeePoolPrefix = []byte{0x1D} +resolverFeePoolPrefix = []byte{0x1E} +treasuryPoolPrefix = []byte{0x1F} +epochSnapshotPrefix = []byte{0x20} +resolverEpochPoolPrefix = []byte{0x21} +builderPoolPrefix = []byte{0x22} +communityPoolPrefix = []byte{0x23} +investorPoolPrefix = []byte{0x24} +protocolPoolPrefix = []byte{0x25} +builderLastClaimedPrefix = []byte{0x26} +investorLastClaimedPrefix = []byte{0x27} +) + +func KeyForCreatorFeePool(marketId []byte) []byte { +return JoinLenPrefix(creatorFeePoolPrefix, marketId) +} +func KeyForResolverFeePool(marketId []byte) []byte { +return JoinLenPrefix(resolverFeePoolPrefix, marketId) +} +func KeyForEpochSnapshot(epoch uint64) []byte { +return JoinLenPrefix(epochSnapshotPrefix, uint64ToBytes(epoch)) +} +func KeyForResolverEpochPool(epoch uint64) []byte { +return JoinLenPrefix(resolverEpochPoolPrefix, uint64ToBytes(epoch)) +} +func KeyForBuilderPool() []byte { +return JoinLenPrefix(builderPoolPrefix, []byte("/builder/")) +} +func KeyForCommunityPool() []byte { +return JoinLenPrefix(communityPoolPrefix, []byte("/community/")) +} +func KeyForInvestorPool() []byte { +return JoinLenPrefix(investorPoolPrefix, []byte("/investor/")) +} +func KeyForProtocolPool() []byte { +return JoinLenPrefix(protocolPoolPrefix, []byte("/protocol/")) +} +func KeyForBuilderLastClaimed() []byte { +return JoinLenPrefix(builderLastClaimedPrefix, []byte("/blc/")) +} +func KeyForInvestorLastClaimed() []byte { +return JoinLenPrefix(investorLastClaimedPrefix, []byte("/ilc/")) +} + +var globalStatsPrefix = []byte{0x28} +var unbondingRecordPrefix = []byte{0x29} + +// KeyForUnbondingRecord returns the singleton unbonding record for a resolver. +func KeyForUnbondingRecord(addr []byte) []byte { + return JoinLenPrefix(unbondingRecordPrefix, addr) +} + +// KeyForGlobalStats returns the singleton state key for protocol-wide resolution stats. +func KeyForGlobalStats() []byte { +return JoinLenPrefix(globalStatsPrefix, []byte("/stats/")) +} + +var resolverIndexPrefix = []byte{0x2A} +var marketIndexPrefix = []byte{0x2B} + +// KeyForResolverIndex returns the singleton key for the global resolver address list. +// Value: ResolverIndex proto — list of all registered resolver addresses. +func KeyForResolverIndex() []byte { +return JoinLenPrefix(resolverIndexPrefix, []byte("/ridx/")) +} + +// KeyForMarketIndex returns the singleton key for the global market ID list. +// Value: MarketIndex proto — list of all created market IDs. +func KeyForMarketIndex() []byte { +return JoinLenPrefix(marketIndexPrefix, []byte("/midx/")) +} + +var creatorOpenCountPrefix = []byte{0x2C} + +// KeyForCreatorOpenCount returns the state key for a creator's open market counter. +// Value: a single uint64 encoded as Pool.Amount (reuses Pool proto). +func KeyForCreatorOpenCount(addr []byte) []byte { +return JoinLenPrefix(creatorOpenCountPrefix, addr) +} diff --git a/plugin/go/tutorial/contract/lmsr.go b/plugin/go/tutorial/contract/lmsr.go new file mode 100644 index 0000000000..8a492d00e8 --- /dev/null +++ b/plugin/go/tutorial/contract/lmsr.go @@ -0,0 +1,162 @@ +package contract + +import "math" + +// ═══════════════════════════════════════════════════════════════════════════════ +// Praxis Prediction Market — LMSR Pricing Engine +// Spec authority: ADLMSR v5.6.6-r2-CORRECTED +// +// The LMSR (Logarithmic Market Scoring Rule) cost function guarantees that +// liquidity is always available at a price that reflects collective belief. +// The market maker takes the other side of every trade automatically. +// +// Cost function: +// C(q_yes, q_no) = b_eff * ln(exp(q_yes / b_eff) + exp(q_no / b_eff)) +// +// Cost of a trade: +// cost = C(q_yes_new, q_no_new) - C(q_yes_old, q_no_old) +// +// All q values are scaled by PRECISION_SCALE to preserve integer precision. +// b_eff is also in PRECISION_SCALE units. +// +// AUDIT-1: Payout formula is overflow-safe using quot/rem pattern. +// AUDIT-3: now >= OpenTime guard before any subtraction involving heights. +// AUDIT-7: shares >= PRECISION_SCALE validated in DeliverTx. +// AUDIT-12: finalCost <= max_cost slippage guard before deducting funds. +// ═══════════════════════════════════════════════════════════════════════════════ + +// lmsrCost computes the LMSR cost function: +// C(q_yes, q_no) = b_eff * ln(exp(q_yes / b_eff) + exp(q_no / b_eff)) +// +// All inputs are in PRECISION_SCALE fixed-point units. +// Returns cost in micro-PRX (uint64). +// +// Uses float64 internally for the logarithm computation. Precision is +// sufficient for market sizes up to ~1e12 micro-PRX (1 million PRX). +// For larger markets the fixed-point scaling provides adequate resolution. +func lmsrCost(qYes, qNo, bEff uint64) uint64 { +if bEff == 0 { +return 0 +} +b := float64(bEff) +y := float64(qYes) +n := float64(qNo) + +// Use the log-sum-exp trick to prevent overflow: +// ln(exp(a) + exp(b)) = max(a,b) + ln(1 + exp(-|a-b|)) +// This keeps the argument to exp() small regardless of q values. +ay := y / b +an := n / b +var lse float64 +if ay >= an { +lse = ay + math.Log1p(math.Exp(an-ay)) +} else { +lse = an + math.Log1p(math.Exp(ay-an)) +} +// Result is b_eff * lse, converted back from float to uint64. +result := b * lse +if result < 0 { +return 0 +} +return uint64(result) +} + +// ComputeTradeCost returns the cost in micro-PRX for purchasing `shares` of +// the given outcome in a market with current state (qYes, qNo, bEff). +// +// outcome: true = YES, false = NO +// shares: number of shares in PRECISION_SCALE units (must be >= PRECISION_SCALE) +// +// Returns (cost, error). +// Error is non-nil if: +// - shares < PRECISION_SCALE (AUDIT-7) +// - bEff == 0 (degenerate market) +// - cost overflows uint64 (should not happen with MAX_EXPIRY_TIME guard) +func ComputeTradeCost(qYes, qNo, bEff, shares uint64, outcome bool) (uint64, *PluginError) { +if bEff == 0 { +return 0, ErrInvalidB0() +} +// AUDIT-7: shares must be at least one unit of precision. +if shares < PRECISION_SCALE { +return 0, ErrSharesBelowMinimum() +} + +costBefore := lmsrCost(qYes, qNo, bEff) + +var qYesNew, qNoNew uint64 +if outcome { +qYesNew = qYes + shares +qNoNew = qNo +} else { +qYesNew = qYes +qNoNew = qNo + shares +} + +costAfter := lmsrCost(qYesNew, qNoNew, bEff) + +// costAfter should always be >= costBefore for a valid trade. +// Guard against underflow from floating point rounding. +if costAfter < costBefore { +return 0, ErrInternal() +} + +return costAfter - costBefore, nil +} + +// ComputePayout computes the pro-rata payout for a winning position. +// Uses the overflow-safe quot/rem formula (AUDIT-1). +// +// quot = poolAmount / totalWinShares +// rem = poolAmount % totalWinShares +// payout = quot * winnerShares + rem * winnerShares / totalWinShares +// +// Returns 0 if totalWinShares == 0 (no winners — should not happen in practice). +func ComputePayout(poolAmount, winnerShares, totalWinShares uint64) uint64 { +if totalWinShares == 0 { +return 0 +} +quot := poolAmount / totalWinShares +rem := poolAmount % totalWinShares +return quot*winnerShares + rem*winnerShares/totalWinShares +} + +// ComputeMinBond returns the minimum proposal bond required for a market. +// Bond scales with pool size to make dishonest proposals expensive relative +// to potential gain. Minimum is 1% of the market pool, floor of MIN_B0. +func ComputeMinBond(market *MarketState) uint64 { +if market == nil { +return MIN_B0 +} +// Read total pool from the market's q values as a proxy for pool size. +// 1% of b_eff as a simple bond floor — adjustable via governance. +bond := market.BEff / 100 +if bond < MIN_B0 { +bond = MIN_B0 +} +return bond +} + +// IsElevatedRisk returns true if the market pool exceeds ELEVATED_RISK_THRESHOLD. +// Called at market creation and updated on each prediction. +// Elevated-risk markets use a larger dispute panel (P7). +func IsElevatedRisk(poolAmount uint64) bool { +return poolAmount >= ELEVATED_RISK_THRESHOLD +} + +// ComputeDisputeBlocks returns the dispute window for a market in blocks. +// DISPUTE_BLOCKS = MAX(MIN_DISPUTE_BLOCKS, market_duration / 10) +// Minimum 48h floor — longer markets get proportionally longer challenge windows. +func ComputeDisputeBlocks(openTime, expiryTime uint64) uint64 { +if TEST_MODE { +return TEST_DISPUTE_BLOCKS +} +if expiryTime <= openTime { +return MIN_DISPUTE_BLOCKS +} +duration := expiryTime - openTime +window := duration / 10 +if window < MIN_DISPUTE_BLOCKS { +return MIN_DISPUTE_BLOCKS +} +return window +} diff --git a/plugin/go/tutorial/contract/plugin.pb.go b/plugin/go/tutorial/contract/plugin.pb.go index 00e635d285..8b476a8cb8 100644 --- a/plugin/go/tutorial/contract/plugin.pb.go +++ b/plugin/go/tutorial/contract/plugin.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.11 // protoc v5.29.3 // source: plugin.proto @@ -1792,7 +1792,7 @@ const file_plugin_proto_rawDesc = "" + "\x03key\x18\x01 \x01(\fR\x03key\":\n" + "\x10PluginStateEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\fR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\fR\x05valueB3Z1github.com/canopy-network/go-plugin-test/contractb\x06proto3" + "\x05value\x18\x02 \x01(\fR\x05valueB5Z3github.com/canopy-network/canopy/plugin/go/contractb\x06proto3" var ( file_plugin_proto_rawDescOnce sync.Once diff --git a/plugin/go/tutorial/contract/shim.go b/plugin/go/tutorial/contract/shim.go new file mode 100644 index 0000000000..10a192371b --- /dev/null +++ b/plugin/go/tutorial/contract/shim.go @@ -0,0 +1,72 @@ +package contract + +import ( + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" +) + +// Shim for types and functions defined in plugin.go — test build only. + +type Config struct { + ChainId uint64 `json:"chainId"` + DataDirPath string `json:"dataDirPath"` +} + +type Plugin struct{} + +func (p *Plugin) StateRead(c *Contract, req *PluginStateReadRequest) (*PluginStateReadResponse, *PluginError) { + return nil, ErrInternal() +} + +func (p *Plugin) StateWrite(c *Contract, req *PluginStateWriteRequest) (*PluginStateWriteResponse, *PluginError) { + return nil, ErrInternal() +} + +func FromAny(any *anypb.Any) (proto.Message, *PluginError) { + msg, err := anypb.UnmarshalNew(any, proto.UnmarshalOptions{}) + if err != nil { + return nil, ErrFromAny(err) + } + return msg, nil +} + +func Marshal(message any) ([]byte, *PluginError) { + m, ok := message.(proto.Message) + if !ok { + return nil, ErrInternal() + } + b, err := proto.Marshal(m) + if err != nil { + return nil, ErrInternal() + } + return b, nil +} + +func Unmarshal(protoBytes []byte, ptr any) *PluginError { + m, ok := ptr.(proto.Message) + if !ok { + return ErrInternal() + } + if err := proto.Unmarshal(protoBytes, m); err != nil { + return ErrInternal() + } + return nil +} + +func JoinLenPrefix(toAppend ...[]byte) []byte { + totalLen := 0 + for _, item := range toAppend { + if item != nil { + totalLen += 1 + len(item) + } + } + res := make([]byte, 0, totalLen) + for _, item := range toAppend { + if item == nil { + continue + } + res = append(res, byte(len(item))) + res = append(res, item...) + } + return res +} diff --git a/plugin/go/tutorial/contract/tx.pb.go b/plugin/go/tutorial/contract/tx.pb.go index 3cd5eb7daa..0b5cd3fa6a 100644 --- a/plugin/go/tutorial/contract/tx.pb.go +++ b/plugin/go/tutorial/contract/tx.pb.go @@ -1,15 +1,15 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.11 // protoc v5.29.3 // source: tx.proto package contract import ( + any1 "github.com/golang/protobuf/ptypes/any" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" - anypb "google.golang.org/protobuf/types/known/anypb" reflect "reflect" sync "sync" unsafe "unsafe" @@ -22,35 +22,77 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// Transaction represents a request or action submitted to the network like transfer assets or perform other operations -// within the blockchain system - THIS MUST MATCH lib.Transaction EXACTLY for signing +// Required by plugin.proto +type Signature struct { + state protoimpl.MessageState `protogen:"open.v1"` + PublicKey []byte `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Signature) Reset() { + *x = Signature{} + mi := &file_tx_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Signature) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Signature) ProtoMessage() {} + +func (x *Signature) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Signature.ProtoReflect.Descriptor instead. +func (*Signature) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{0} +} + +func (x *Signature) GetPublicKey() []byte { + if x != nil { + return x.PublicKey + } + return nil +} + +func (x *Signature) GetSignature() []byte { + if x != nil { + return x.Signature + } + return nil +} + type Transaction struct { - state protoimpl.MessageState `protogen:"open.v1"` - // message_type: The type of the transaction like 'send' or 'stake' - MessageType string `protobuf:"bytes,1,opt,name=message_type,json=messageType,proto3" json:"messageType"` // @gotags: json:"messageType" - // msg: The actual transaction message payload, which is encapsulated in a generic message format - Msg *anypb.Any `protobuf:"bytes,2,opt,name=msg,proto3" json:"msg,omitempty"` - // signature: The cryptographic signature used to verify the authenticity of the transaction - Signature *Signature `protobuf:"bytes,3,opt,name=signature,proto3" json:"signature,omitempty"` - // created_height: The height when the transaction was created - allows 'safe pruning' - CreatedHeight uint64 `protobuf:"varint,4,opt,name=created_height,json=createdHeight,proto3" json:"createdHeight"` // @gotags: json:"createdHeight" - // time: The timestamp when the transaction was created - used as temporal entropy to prevent hash collisions in txs - Time uint64 `protobuf:"varint,5,opt,name=time,proto3" json:"time,omitempty"` - // fee: The fee associated with processing the transaction - Fee uint64 `protobuf:"varint,6,opt,name=fee,proto3" json:"fee,omitempty"` - // memo: An optional message or note attached to the transaction - Memo string `protobuf:"bytes,7,opt,name=memo,proto3" json:"memo,omitempty"` - // network_id: The identity of the network the transaction is intended for - NetworkId uint64 `protobuf:"varint,8,opt,name=network_id,json=networkId,proto3" json:"networkID"` // @gotags: json:"networkID" - // chain_id: The identity of the committee the transaction is intended for - ChainId uint64 `protobuf:"varint,9,opt,name=chain_id,json=chainId,proto3" json:"chainID"` // @gotags: json:"chainID" + state protoimpl.MessageState `protogen:"open.v1"` + MessageType string `protobuf:"bytes,1,opt,name=message_type,json=messageType,proto3" json:"message_type,omitempty"` + Msg *any1.Any `protobuf:"bytes,2,opt,name=msg,proto3" json:"msg,omitempty"` + Signature *Signature `protobuf:"bytes,3,opt,name=signature,proto3" json:"signature,omitempty"` + CreatedHeight uint64 `protobuf:"varint,4,opt,name=created_height,json=createdHeight,proto3" json:"created_height,omitempty"` + Time uint64 `protobuf:"varint,5,opt,name=time,proto3" json:"time,omitempty"` + Fee uint64 `protobuf:"varint,6,opt,name=fee,proto3" json:"fee,omitempty"` + Memo string `protobuf:"bytes,7,opt,name=memo,proto3" json:"memo,omitempty"` + NetworkId uint64 `protobuf:"varint,8,opt,name=network_id,json=networkId,proto3" json:"network_id,omitempty"` + ChainId uint64 `protobuf:"varint,9,opt,name=chain_id,json=chainId,proto3" json:"chain_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Transaction) Reset() { *x = Transaction{} - mi := &file_tx_proto_msgTypes[0] + mi := &file_tx_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -62,7 +104,7 @@ func (x *Transaction) String() string { func (*Transaction) ProtoMessage() {} func (x *Transaction) ProtoReflect() protoreflect.Message { - mi := &file_tx_proto_msgTypes[0] + mi := &file_tx_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -75,7 +117,7 @@ func (x *Transaction) ProtoReflect() protoreflect.Message { // Deprecated: Use Transaction.ProtoReflect.Descriptor instead. func (*Transaction) Descriptor() ([]byte, []int) { - return file_tx_proto_rawDescGZIP(), []int{0} + return file_tx_proto_rawDescGZIP(), []int{1} } func (x *Transaction) GetMessageType() string { @@ -85,7 +127,7 @@ func (x *Transaction) GetMessageType() string { return "" } -func (x *Transaction) GetMsg() *anypb.Any { +func (x *Transaction) GetMsg() *any1.Any { if x != nil { return x.Msg } @@ -141,35 +183,32 @@ func (x *Transaction) GetChainId() uint64 { return 0 } -// MessageSend is a standard transfer transaction, taking tokens from the sender and transferring -// them to the recipient -type MessageSend struct { - state protoimpl.MessageState `protogen:"open.v1"` - // from_address: is the sender of the funds - FromAddress []byte `protobuf:"bytes,1,opt,name=from_address,json=fromAddress,proto3" json:"fromAddress"` // @gotags: json:"fromAddress" - // to_address: is the recipient of the funds - ToAddress []byte `protobuf:"bytes,2,opt,name=to_address,json=toAddress,proto3" json:"toAddress"` // @gotags: json:"toAddress" - // amount: is the amount of tokens in micro-denomination (uCNPY) - Amount uint64 `protobuf:"varint,3,opt,name=amount,proto3" json:"amount,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +type FeeParams struct { + state protoimpl.MessageState `protogen:"open.v1"` + SendFee uint64 `protobuf:"varint,1,opt,name=send_fee,json=sendFee,proto3" json:"send_fee,omitempty"` + CreateMarketFee uint64 `protobuf:"varint,2,opt,name=create_market_fee,json=createMarketFee,proto3" json:"create_market_fee,omitempty"` + SubmitPredictionFee uint64 `protobuf:"varint,3,opt,name=submit_prediction_fee,json=submitPredictionFee,proto3" json:"submit_prediction_fee,omitempty"` + ResolveMarketFee uint64 `protobuf:"varint,4,opt,name=resolve_market_fee,json=resolveMarketFee,proto3" json:"resolve_market_fee,omitempty"` + ClaimWinningsFee uint64 `protobuf:"varint,5,opt,name=claim_winnings_fee,json=claimWinningsFee,proto3" json:"claim_winnings_fee,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *MessageSend) Reset() { - *x = MessageSend{} - mi := &file_tx_proto_msgTypes[1] +func (x *FeeParams) Reset() { + *x = FeeParams{} + mi := &file_tx_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *MessageSend) String() string { +func (x *FeeParams) String() string { return protoimpl.X.MessageStringOf(x) } -func (*MessageSend) ProtoMessage() {} +func (*FeeParams) ProtoMessage() {} -func (x *MessageSend) ProtoReflect() protoreflect.Message { - mi := &file_tx_proto_msgTypes[1] +func (x *FeeParams) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -180,56 +219,81 @@ func (x *MessageSend) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use MessageSend.ProtoReflect.Descriptor instead. -func (*MessageSend) Descriptor() ([]byte, []int) { - return file_tx_proto_rawDescGZIP(), []int{1} +// Deprecated: Use FeeParams.ProtoReflect.Descriptor instead. +func (*FeeParams) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{2} } -func (x *MessageSend) GetFromAddress() []byte { +func (x *FeeParams) GetSendFee() uint64 { if x != nil { - return x.FromAddress + return x.SendFee } - return nil + return 0 } -func (x *MessageSend) GetToAddress() []byte { +func (x *FeeParams) GetCreateMarketFee() uint64 { if x != nil { - return x.ToAddress + return x.CreateMarketFee } - return nil + return 0 } -func (x *MessageSend) GetAmount() uint64 { +func (x *FeeParams) GetSubmitPredictionFee() uint64 { if x != nil { - return x.Amount + return x.SubmitPredictionFee } return 0 } -// FeeParams is the parameter space that defines various amounts for transaction fees -type FeeParams struct { - state protoimpl.MessageState `protogen:"open.v1"` - // send_fee: is the fee amount (in uCNPY) for Message Send - SendFee uint64 `protobuf:"varint,1,opt,name=send_fee,json=sendFee,proto3" json:"sendFee"` // @gotags: json:"sendFee" - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *FeeParams) GetResolveMarketFee() uint64 { + if x != nil { + return x.ResolveMarketFee + } + return 0 } -func (x *FeeParams) Reset() { - *x = FeeParams{} - mi := &file_tx_proto_msgTypes[2] +func (x *FeeParams) GetClaimWinningsFee() uint64 { + if x != nil { + return x.ClaimWinningsFee + } + return 0 +} + +// ADLMSR state objects (0x10-0x14) +type MarketState struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status uint32 `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"` + ExpiryTime uint64 `protobuf:"varint,2,opt,name=expiry_time,json=expiryTime,proto3" json:"expiry_time,omitempty"` + QYes uint64 `protobuf:"varint,3,opt,name=q_yes,json=qYes,proto3" json:"q_yes,omitempty"` + QNo uint64 `protobuf:"varint,4,opt,name=q_no,json=qNo,proto3" json:"q_no,omitempty"` + BEff uint64 `protobuf:"varint,5,opt,name=b_eff,json=bEff,proto3" json:"b_eff,omitempty"` + Creator []byte `protobuf:"bytes,6,opt,name=creator,proto3" json:"creator,omitempty"` + ClaimedCount uint64 `protobuf:"varint,7,opt,name=claimed_count,json=claimedCount,proto3" json:"claimed_count,omitempty"` + TotalPositions uint64 `protobuf:"varint,8,opt,name=total_positions,json=totalPositions,proto3" json:"total_positions,omitempty"` + OpenTime uint64 `protobuf:"varint,9,opt,name=open_time,json=openTime,proto3" json:"open_time,omitempty"` + ElevatedRisk bool `protobuf:"varint,10,opt,name=elevated_risk,json=elevatedRisk,proto3" json:"elevated_risk,omitempty"` + FinalizedPoolAmount uint64 `protobuf:"varint,11,opt,name=finalized_pool_amount,json=finalizedPoolAmount,proto3" json:"finalized_pool_amount,omitempty"` + Question string `protobuf:"bytes,12,opt,name=question,proto3" json:"question,omitempty"` + Rules string `protobuf:"bytes,13,opt,name=rules,proto3" json:"rules,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MarketState) Reset() { + *x = MarketState{} + mi := &file_tx_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *FeeParams) String() string { +func (x *MarketState) String() string { return protoimpl.X.MessageStringOf(x) } -func (*FeeParams) ProtoMessage() {} +func (*MarketState) ProtoMessage() {} -func (x *FeeParams) ProtoReflect() protoreflect.Message { - mi := &file_tx_proto_msgTypes[2] +func (x *MarketState) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -240,101 +304,126 @@ func (x *FeeParams) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use FeeParams.ProtoReflect.Descriptor instead. -func (*FeeParams) Descriptor() ([]byte, []int) { - return file_tx_proto_rawDescGZIP(), []int{2} +// Deprecated: Use MarketState.ProtoReflect.Descriptor instead. +func (*MarketState) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{3} } -func (x *FeeParams) GetSendFee() uint64 { +func (x *MarketState) GetStatus() uint32 { if x != nil { - return x.SendFee + return x.Status } return 0 } -// A Signature is a digital signature is a cryptographic "fingerprint" created with a private key, -// allowing others to verify the authenticity and integrity of a message using the corresponding public key -type Signature struct { - state protoimpl.MessageState `protogen:"open.v1"` - // public_key: is a cryptographic code shared openly, used to verify digital signatures - PublicKey []byte `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"publicKey"` // @gotags: json:"publicKey" - // signature: the bytes of the signature output from a private key which may be verified with the message and public - Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *MarketState) GetExpiryTime() uint64 { + if x != nil { + return x.ExpiryTime + } + return 0 } -func (x *Signature) Reset() { - *x = Signature{} - mi := &file_tx_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) +func (x *MarketState) GetQYes() uint64 { + if x != nil { + return x.QYes + } + return 0 } -func (x *Signature) String() string { - return protoimpl.X.MessageStringOf(x) +func (x *MarketState) GetQNo() uint64 { + if x != nil { + return x.QNo + } + return 0 } -func (*Signature) ProtoMessage() {} +func (x *MarketState) GetBEff() uint64 { + if x != nil { + return x.BEff + } + return 0 +} -func (x *Signature) ProtoReflect() protoreflect.Message { - mi := &file_tx_proto_msgTypes[3] +func (x *MarketState) GetCreator() []byte { if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms + return x.Creator } - return mi.MessageOf(x) + return nil } -// Deprecated: Use Signature.ProtoReflect.Descriptor instead. -func (*Signature) Descriptor() ([]byte, []int) { - return file_tx_proto_rawDescGZIP(), []int{3} +func (x *MarketState) GetClaimedCount() uint64 { + if x != nil { + return x.ClaimedCount + } + return 0 } -func (x *Signature) GetPublicKey() []byte { +func (x *MarketState) GetTotalPositions() uint64 { if x != nil { - return x.PublicKey + return x.TotalPositions } - return nil + return 0 } -func (x *Signature) GetSignature() []byte { +func (x *MarketState) GetOpenTime() uint64 { if x != nil { - return x.Signature + return x.OpenTime } - return nil + return 0 +} + +func (x *MarketState) GetElevatedRisk() bool { + if x != nil { + return x.ElevatedRisk + } + return false +} + +func (x *MarketState) GetFinalizedPoolAmount() uint64 { + if x != nil { + return x.FinalizedPoolAmount + } + return 0 +} + +func (x *MarketState) GetQuestion() string { + if x != nil { + return x.Question + } + return "" +} + +func (x *MarketState) GetRules() string { + if x != nil { + return x.Rules + } + return "" } -// Example: MessageReward mints tokens to a recipient -type MessageReward struct { - state protoimpl.MessageState `protogen:"open.v1"` - // admin_address: the admin authorizing the reward - AdminAddress []byte `protobuf:"bytes,1,opt,name=admin_address,json=adminAddress,proto3" json:"adminAddress"` // @gotags: json:"adminAddress" - // recipient_address: who receives the reward - RecipientAddress []byte `protobuf:"bytes,2,opt,name=recipient_address,json=recipientAddress,proto3" json:"recipientAddress"` // @gotags: json:"recipientAddress" - // amount: tokens to mint - Amount uint64 `protobuf:"varint,3,opt,name=amount,proto3" json:"amount,omitempty"` +type PositionState struct { + state protoimpl.MessageState `protogen:"open.v1"` + SharesYes uint64 `protobuf:"varint,1,opt,name=shares_yes,json=sharesYes,proto3" json:"shares_yes,omitempty"` + SharesNo uint64 `protobuf:"varint,2,opt,name=shares_no,json=sharesNo,proto3" json:"shares_no,omitempty"` + CostPaid uint64 `protobuf:"varint,3,opt,name=cost_paid,json=costPaid,proto3" json:"cost_paid,omitempty"` + Claimed bool `protobuf:"varint,4,opt,name=claimed,proto3" json:"claimed,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *MessageReward) Reset() { - *x = MessageReward{} +func (x *PositionState) Reset() { + *x = PositionState{} mi := &file_tx_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *MessageReward) String() string { +func (x *PositionState) String() string { return protoimpl.X.MessageStringOf(x) } -func (*MessageReward) ProtoMessage() {} +func (*PositionState) ProtoMessage() {} -func (x *MessageReward) ProtoReflect() protoreflect.Message { +func (x *PositionState) ProtoReflect() protoreflect.Message { mi := &file_tx_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -346,60 +435,61 @@ func (x *MessageReward) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use MessageReward.ProtoReflect.Descriptor instead. -func (*MessageReward) Descriptor() ([]byte, []int) { +// Deprecated: Use PositionState.ProtoReflect.Descriptor instead. +func (*PositionState) Descriptor() ([]byte, []int) { return file_tx_proto_rawDescGZIP(), []int{4} } -func (x *MessageReward) GetAdminAddress() []byte { +func (x *PositionState) GetSharesYes() uint64 { if x != nil { - return x.AdminAddress + return x.SharesYes } - return nil + return 0 } -func (x *MessageReward) GetRecipientAddress() []byte { +func (x *PositionState) GetSharesNo() uint64 { if x != nil { - return x.RecipientAddress + return x.SharesNo } - return nil + return 0 } -func (x *MessageReward) GetAmount() uint64 { +func (x *PositionState) GetCostPaid() uint64 { if x != nil { - return x.Amount + return x.CostPaid } return 0 } -// MessageFaucet is a test-only transaction that mints tokens to any address -// No balance check required - just mints tokens for testing purposes -type MessageFaucet struct { - state protoimpl.MessageState `protogen:"open.v1"` - // signer_address: the address signing this transaction (for auth) - SignerAddress []byte `protobuf:"bytes,1,opt,name=signer_address,json=signerAddress,proto3" json:"signerAddress"` // @gotags: json:"signerAddress" - // recipient_address: who receives the tokens - RecipientAddress []byte `protobuf:"bytes,2,opt,name=recipient_address,json=recipientAddress,proto3" json:"recipientAddress"` // @gotags: json:"recipientAddress" - // amount: tokens to mint - Amount uint64 `protobuf:"varint,3,opt,name=amount,proto3" json:"amount,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache +func (x *PositionState) GetClaimed() bool { + if x != nil { + return x.Claimed + } + return false +} + +type OutcomeState struct { + state protoimpl.MessageState `protogen:"open.v1"` + WinningOutcome bool `protobuf:"varint,1,opt,name=winning_outcome,json=winningOutcome,proto3" json:"winning_outcome,omitempty"` + ResolvedAt uint64 `protobuf:"varint,2,opt,name=resolved_at,json=resolvedAt,proto3" json:"resolved_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } -func (x *MessageFaucet) Reset() { - *x = MessageFaucet{} +func (x *OutcomeState) Reset() { + *x = OutcomeState{} mi := &file_tx_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *MessageFaucet) String() string { +func (x *OutcomeState) String() string { return protoimpl.X.MessageStringOf(x) } -func (*MessageFaucet) ProtoMessage() {} +func (*OutcomeState) ProtoMessage() {} -func (x *MessageFaucet) ProtoReflect() protoreflect.Message { +func (x *OutcomeState) ProtoReflect() protoreflect.Message { mi := &file_tx_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -411,67 +501,2289 @@ func (x *MessageFaucet) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use MessageFaucet.ProtoReflect.Descriptor instead. -func (*MessageFaucet) Descriptor() ([]byte, []int) { +// Deprecated: Use OutcomeState.ProtoReflect.Descriptor instead. +func (*OutcomeState) Descriptor() ([]byte, []int) { return file_tx_proto_rawDescGZIP(), []int{5} } -func (x *MessageFaucet) GetSignerAddress() []byte { +func (x *OutcomeState) GetWinningOutcome() bool { if x != nil { - return x.SignerAddress + return x.WinningOutcome } - return nil + return false } -func (x *MessageFaucet) GetRecipientAddress() []byte { +func (x *OutcomeState) GetResolvedAt() uint64 { if x != nil { - return x.RecipientAddress + return x.ResolvedAt } - return nil + return 0 +} + +type TreasuryReserve struct { + state protoimpl.MessageState `protogen:"open.v1"` + LockedReserve uint64 `protobuf:"varint,1,opt,name=locked_reserve,json=lockedReserve,proto3" json:"locked_reserve,omitempty"` + CreatorBond uint64 `protobuf:"varint,2,opt,name=creator_bond,json=creatorBond,proto3" json:"creator_bond,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TreasuryReserve) Reset() { + *x = TreasuryReserve{} + mi := &file_tx_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TreasuryReserve) String() string { + return protoimpl.X.MessageStringOf(x) } -func (x *MessageFaucet) GetAmount() uint64 { +func (*TreasuryReserve) ProtoMessage() {} + +func (x *TreasuryReserve) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[6] if x != nil { - return x.Amount + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TreasuryReserve.ProtoReflect.Descriptor instead. +func (*TreasuryReserve) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{6} +} + +func (x *TreasuryReserve) GetLockedReserve() uint64 { + if x != nil { + return x.LockedReserve } return 0 } -var File_tx_proto protoreflect.FileDescriptor +func (x *TreasuryReserve) GetCreatorBond() uint64 { + if x != nil { + return x.CreatorBond + } + return 0 +} -const file_tx_proto_rawDesc = "" + - "\n" + - "\btx.proto\x12\x05types\x1a\x19google/protobuf/any.proto\"\xa3\x02\n" + - "\vTransaction\x12!\n" + - "\fmessage_type\x18\x01 \x01(\tR\vmessageType\x12&\n" + - "\x03msg\x18\x02 \x01(\v2\x14.google.protobuf.AnyR\x03msg\x12.\n" + - "\tsignature\x18\x03 \x01(\v2\x10.types.SignatureR\tsignature\x12%\n" + - "\x0ecreated_height\x18\x04 \x01(\x04R\rcreatedHeight\x12\x12\n" + - "\x04time\x18\x05 \x01(\x04R\x04time\x12\x10\n" + - "\x03fee\x18\x06 \x01(\x04R\x03fee\x12\x12\n" + - "\x04memo\x18\a \x01(\tR\x04memo\x12\x1d\n" + - "\n" + - "network_id\x18\b \x01(\x04R\tnetworkId\x12\x19\n" + - "\bchain_id\x18\t \x01(\x04R\achainId\"g\n" + - "\vMessageSend\x12!\n" + - "\ffrom_address\x18\x01 \x01(\fR\vfromAddress\x12\x1d\n" + - "\n" + - "to_address\x18\x02 \x01(\fR\ttoAddress\x12\x16\n" + - "\x06amount\x18\x03 \x01(\x04R\x06amount\"&\n" + - "\tFeeParams\x12\x19\n" + - "\bsend_fee\x18\x01 \x01(\x04R\asendFee\"H\n" + - "\tSignature\x12\x1d\n" + - "\n" + - "public_key\x18\x01 \x01(\fR\tpublicKey\x12\x1c\n" + - "\tsignature\x18\x02 \x01(\fR\tsignature\"y\n" + - "\rMessageReward\x12#\n" + - "\radmin_address\x18\x01 \x01(\fR\fadminAddress\x12+\n" + - "\x11recipient_address\x18\x02 \x01(\fR\x10recipientAddress\x12\x16\n" + - "\x06amount\x18\x03 \x01(\x04R\x06amount\"{\n" + - "\rMessageFaucet\x12%\n" + - "\x0esigner_address\x18\x01 \x01(\fR\rsignerAddress\x12+\n" + - "\x11recipient_address\x18\x02 \x01(\fR\x10recipientAddress\x12\x16\n" + - "\x06amount\x18\x03 \x01(\x04R\x06amountB3Z1github.com/canopy-network/go-plugin-test/contractb\x06proto3" +// PORS state objects (0x13, 0x16-0x1C) +type ResolverState struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddress []byte `protobuf:"bytes,1,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResolverState) Reset() { + *x = ResolverState{} + mi := &file_tx_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResolverState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResolverState) ProtoMessage() {} + +func (x *ResolverState) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResolverState.ProtoReflect.Descriptor instead. +func (*ResolverState) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{7} +} + +func (x *ResolverState) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +type ResolverRecord struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddress []byte `protobuf:"bytes,1,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + StakeAmount uint64 `protobuf:"varint,2,opt,name=stake_amount,json=stakeAmount,proto3" json:"stake_amount,omitempty"` + RrsScore uint64 `protobuf:"varint,3,opt,name=rrs_score,json=rrsScore,proto3" json:"rrs_score,omitempty"` + RegisteredAt uint64 `protobuf:"varint,4,opt,name=registered_at,json=registeredAt,proto3" json:"registered_at,omitempty"` + SuccessfulResolutions uint64 `protobuf:"varint,5,opt,name=successful_resolutions,json=successfulResolutions,proto3" json:"successful_resolutions,omitempty"` + LastClaimedEpoch uint64 `protobuf:"varint,6,opt,name=last_claimed_epoch,json=lastClaimedEpoch,proto3" json:"last_claimed_epoch,omitempty"` + IsActive bool `protobuf:"varint,7,opt,name=is_active,json=isActive,proto3" json:"is_active,omitempty"` + UnbondingAmount uint64 `protobuf:"varint,8,opt,name=unbonding_amount,json=unbondingAmount,proto3" json:"unbonding_amount,omitempty"` + UnbondingReleaseHeight uint64 `protobuf:"varint,9,opt,name=unbonding_release_height,json=unbondingReleaseHeight,proto3" json:"unbonding_release_height,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResolverRecord) Reset() { + *x = ResolverRecord{} + mi := &file_tx_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResolverRecord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResolverRecord) ProtoMessage() {} + +func (x *ResolverRecord) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResolverRecord.ProtoReflect.Descriptor instead. +func (*ResolverRecord) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{8} +} + +func (x *ResolverRecord) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +func (x *ResolverRecord) GetStakeAmount() uint64 { + if x != nil { + return x.StakeAmount + } + return 0 +} + +func (x *ResolverRecord) GetRrsScore() uint64 { + if x != nil { + return x.RrsScore + } + return 0 +} + +func (x *ResolverRecord) GetRegisteredAt() uint64 { + if x != nil { + return x.RegisteredAt + } + return 0 +} + +func (x *ResolverRecord) GetSuccessfulResolutions() uint64 { + if x != nil { + return x.SuccessfulResolutions + } + return 0 +} + +func (x *ResolverRecord) GetLastClaimedEpoch() uint64 { + if x != nil { + return x.LastClaimedEpoch + } + return 0 +} + +func (x *ResolverRecord) GetIsActive() bool { + if x != nil { + return x.IsActive + } + return false +} + +func (x *ResolverRecord) GetUnbondingAmount() uint64 { + if x != nil { + return x.UnbondingAmount + } + return 0 +} + +func (x *ResolverRecord) GetUnbondingReleaseHeight() uint64 { + if x != nil { + return x.UnbondingReleaseHeight + } + return 0 +} + +type ProposalRecord struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddr []byte `protobuf:"bytes,1,opt,name=resolver_addr,json=resolverAddr,proto3" json:"resolver_addr,omitempty"` + ProposedOutcome bool `protobuf:"varint,2,opt,name=proposed_outcome,json=proposedOutcome,proto3" json:"proposed_outcome,omitempty"` + ProposalBond uint64 `protobuf:"varint,3,opt,name=proposal_bond,json=proposalBond,proto3" json:"proposal_bond,omitempty"` + ProposalBlock uint64 `protobuf:"varint,4,opt,name=proposal_block,json=proposalBlock,proto3" json:"proposal_block,omitempty"` + Status uint32 `protobuf:"varint,5,opt,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProposalRecord) Reset() { + *x = ProposalRecord{} + mi := &file_tx_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProposalRecord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProposalRecord) ProtoMessage() {} + +func (x *ProposalRecord) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProposalRecord.ProtoReflect.Descriptor instead. +func (*ProposalRecord) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{9} +} + +func (x *ProposalRecord) GetResolverAddr() []byte { + if x != nil { + return x.ResolverAddr + } + return nil +} + +func (x *ProposalRecord) GetProposedOutcome() bool { + if x != nil { + return x.ProposedOutcome + } + return false +} + +func (x *ProposalRecord) GetProposalBond() uint64 { + if x != nil { + return x.ProposalBond + } + return 0 +} + +func (x *ProposalRecord) GetProposalBlock() uint64 { + if x != nil { + return x.ProposalBlock + } + return 0 +} + +func (x *ProposalRecord) GetStatus() uint32 { + if x != nil { + return x.Status + } + return 0 +} + +type DisputeRecord struct { + state protoimpl.MessageState `protogen:"open.v1"` + DisputerAddress []byte `protobuf:"bytes,1,opt,name=disputer_address,json=disputerAddress,proto3" json:"disputer_address,omitempty"` + DisputeBond uint64 `protobuf:"varint,2,opt,name=dispute_bond,json=disputeBond,proto3" json:"dispute_bond,omitempty"` + DisputeBlock uint64 `protobuf:"varint,3,opt,name=dispute_block,json=disputeBlock,proto3" json:"dispute_block,omitempty"` + VoteStatus uint32 `protobuf:"varint,4,opt,name=vote_status,json=voteStatus,proto3" json:"vote_status,omitempty"` + PanelSize uint32 `protobuf:"varint,5,opt,name=panel_size,json=panelSize,proto3" json:"panel_size,omitempty"` + PanelMembers [][]byte `protobuf:"bytes,6,rep,name=panel_members,json=panelMembers,proto3" json:"panel_members,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DisputeRecord) Reset() { + *x = DisputeRecord{} + mi := &file_tx_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DisputeRecord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DisputeRecord) ProtoMessage() {} + +func (x *DisputeRecord) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DisputeRecord.ProtoReflect.Descriptor instead. +func (*DisputeRecord) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{10} +} + +func (x *DisputeRecord) GetDisputerAddress() []byte { + if x != nil { + return x.DisputerAddress + } + return nil +} + +func (x *DisputeRecord) GetDisputeBond() uint64 { + if x != nil { + return x.DisputeBond + } + return 0 +} + +func (x *DisputeRecord) GetDisputeBlock() uint64 { + if x != nil { + return x.DisputeBlock + } + return 0 +} + +func (x *DisputeRecord) GetVoteStatus() uint32 { + if x != nil { + return x.VoteStatus + } + return 0 +} + +func (x *DisputeRecord) GetPanelSize() uint32 { + if x != nil { + return x.PanelSize + } + return 0 +} + +func (x *DisputeRecord) GetPanelMembers() [][]byte { + if x != nil { + return x.PanelMembers + } + return nil +} + +type VoteCommit struct { + state protoimpl.MessageState `protogen:"open.v1"` + VoterAddr []byte `protobuf:"bytes,1,opt,name=voter_addr,json=voterAddr,proto3" json:"voter_addr,omitempty"` + CommitHash []byte `protobuf:"bytes,2,opt,name=commit_hash,json=commitHash,proto3" json:"commit_hash,omitempty"` + CommittedAt uint64 `protobuf:"varint,3,opt,name=committed_at,json=committedAt,proto3" json:"committed_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VoteCommit) Reset() { + *x = VoteCommit{} + mi := &file_tx_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VoteCommit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VoteCommit) ProtoMessage() {} + +func (x *VoteCommit) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VoteCommit.ProtoReflect.Descriptor instead. +func (*VoteCommit) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{11} +} + +func (x *VoteCommit) GetVoterAddr() []byte { + if x != nil { + return x.VoterAddr + } + return nil +} + +func (x *VoteCommit) GetCommitHash() []byte { + if x != nil { + return x.CommitHash + } + return nil +} + +func (x *VoteCommit) GetCommittedAt() uint64 { + if x != nil { + return x.CommittedAt + } + return 0 +} + +type VoteReveal struct { + state protoimpl.MessageState `protogen:"open.v1"` + VoterAddr []byte `protobuf:"bytes,1,opt,name=voter_addr,json=voterAddr,proto3" json:"voter_addr,omitempty"` + Vote bool `protobuf:"varint,2,opt,name=vote,proto3" json:"vote,omitempty"` + RevealedAt uint64 `protobuf:"varint,3,opt,name=revealed_at,json=revealedAt,proto3" json:"revealed_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VoteReveal) Reset() { + *x = VoteReveal{} + mi := &file_tx_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VoteReveal) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VoteReveal) ProtoMessage() {} + +func (x *VoteReveal) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VoteReveal.ProtoReflect.Descriptor instead. +func (*VoteReveal) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{12} +} + +func (x *VoteReveal) GetVoterAddr() []byte { + if x != nil { + return x.VoterAddr + } + return nil +} + +func (x *VoteReveal) GetVote() bool { + if x != nil { + return x.Vote + } + return false +} + +func (x *VoteReveal) GetRevealedAt() uint64 { + if x != nil { + return x.RevealedAt + } + return 0 +} + +type SlashRecord struct { + state protoimpl.MessageState `protogen:"open.v1"` + SlashedAddress []byte `protobuf:"bytes,1,opt,name=slashed_address,json=slashedAddress,proto3" json:"slashed_address,omitempty"` + SlashAmount uint64 `protobuf:"varint,2,opt,name=slash_amount,json=slashAmount,proto3" json:"slash_amount,omitempty"` + SlashedAt uint64 `protobuf:"varint,3,opt,name=slashed_at,json=slashedAt,proto3" json:"slashed_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SlashRecord) Reset() { + *x = SlashRecord{} + mi := &file_tx_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SlashRecord) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SlashRecord) ProtoMessage() {} + +func (x *SlashRecord) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SlashRecord.ProtoReflect.Descriptor instead. +func (*SlashRecord) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{13} +} + +func (x *SlashRecord) GetSlashedAddress() []byte { + if x != nil { + return x.SlashedAddress + } + return nil +} + +func (x *SlashRecord) GetSlashAmount() uint64 { + if x != nil { + return x.SlashAmount + } + return 0 +} + +func (x *SlashRecord) GetSlashedAt() uint64 { + if x != nil { + return x.SlashedAt + } + return 0 +} + +type PanelEntropyAccum struct { + state protoimpl.MessageState `protogen:"open.v1"` + Accumulator uint64 `protobuf:"varint,1,opt,name=accumulator,proto3" json:"accumulator,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PanelEntropyAccum) Reset() { + *x = PanelEntropyAccum{} + mi := &file_tx_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PanelEntropyAccum) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PanelEntropyAccum) ProtoMessage() {} + +func (x *PanelEntropyAccum) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PanelEntropyAccum.ProtoReflect.Descriptor instead. +func (*PanelEntropyAccum) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{14} +} + +func (x *PanelEntropyAccum) GetAccumulator() uint64 { + if x != nil { + return x.Accumulator + } + return 0 +} + +// Transaction messages (12 types) +type MessageSend struct { + state protoimpl.MessageState `protogen:"open.v1"` + FromAddress []byte `protobuf:"bytes,1,opt,name=from_address,json=fromAddress,proto3" json:"from_address,omitempty"` + ToAddress []byte `protobuf:"bytes,2,opt,name=to_address,json=toAddress,proto3" json:"to_address,omitempty"` + Amount uint64 `protobuf:"varint,3,opt,name=amount,proto3" json:"amount,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageSend) Reset() { + *x = MessageSend{} + mi := &file_tx_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageSend) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageSend) ProtoMessage() {} + +func (x *MessageSend) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageSend.ProtoReflect.Descriptor instead. +func (*MessageSend) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{15} +} + +func (x *MessageSend) GetFromAddress() []byte { + if x != nil { + return x.FromAddress + } + return nil +} + +func (x *MessageSend) GetToAddress() []byte { + if x != nil { + return x.ToAddress + } + return nil +} + +func (x *MessageSend) GetAmount() uint64 { + if x != nil { + return x.Amount + } + return 0 +} + +type MessageCreateMarket struct { + state protoimpl.MessageState `protogen:"open.v1"` + CreatorAddress []byte `protobuf:"bytes,1,opt,name=creator_address,json=creatorAddress,proto3" json:"creator_address,omitempty"` + B0 uint64 `protobuf:"varint,2,opt,name=b0,proto3" json:"b0,omitempty"` + ExpiryTime uint64 `protobuf:"varint,3,opt,name=expiry_time,json=expiryTime,proto3" json:"expiry_time,omitempty"` + Nonce uint64 `protobuf:"varint,4,opt,name=nonce,proto3" json:"nonce,omitempty"` + Question string `protobuf:"bytes,5,opt,name=question,proto3" json:"question,omitempty"` + Rules string `protobuf:"bytes,6,opt,name=rules,proto3" json:"rules,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageCreateMarket) Reset() { + *x = MessageCreateMarket{} + mi := &file_tx_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageCreateMarket) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageCreateMarket) ProtoMessage() {} + +func (x *MessageCreateMarket) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageCreateMarket.ProtoReflect.Descriptor instead. +func (*MessageCreateMarket) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{16} +} + +func (x *MessageCreateMarket) GetCreatorAddress() []byte { + if x != nil { + return x.CreatorAddress + } + return nil +} + +func (x *MessageCreateMarket) GetB0() uint64 { + if x != nil { + return x.B0 + } + return 0 +} + +func (x *MessageCreateMarket) GetExpiryTime() uint64 { + if x != nil { + return x.ExpiryTime + } + return 0 +} + +func (x *MessageCreateMarket) GetNonce() uint64 { + if x != nil { + return x.Nonce + } + return 0 +} + +func (x *MessageCreateMarket) GetQuestion() string { + if x != nil { + return x.Question + } + return "" +} + +func (x *MessageCreateMarket) GetRules() string { + if x != nil { + return x.Rules + } + return "" +} + +type MessageSubmitPrediction struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + BettorAddress []byte `protobuf:"bytes,2,opt,name=bettor_address,json=bettorAddress,proto3" json:"bettor_address,omitempty"` + Outcome bool `protobuf:"varint,3,opt,name=outcome,proto3" json:"outcome,omitempty"` + Shares uint64 `protobuf:"varint,4,opt,name=shares,proto3" json:"shares,omitempty"` + MaxCost uint64 `protobuf:"varint,5,opt,name=max_cost,json=maxCost,proto3" json:"max_cost,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageSubmitPrediction) Reset() { + *x = MessageSubmitPrediction{} + mi := &file_tx_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageSubmitPrediction) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageSubmitPrediction) ProtoMessage() {} + +func (x *MessageSubmitPrediction) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageSubmitPrediction.ProtoReflect.Descriptor instead. +func (*MessageSubmitPrediction) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{17} +} + +func (x *MessageSubmitPrediction) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageSubmitPrediction) GetBettorAddress() []byte { + if x != nil { + return x.BettorAddress + } + return nil +} + +func (x *MessageSubmitPrediction) GetOutcome() bool { + if x != nil { + return x.Outcome + } + return false +} + +func (x *MessageSubmitPrediction) GetShares() uint64 { + if x != nil { + return x.Shares + } + return 0 +} + +func (x *MessageSubmitPrediction) GetMaxCost() uint64 { + if x != nil { + return x.MaxCost + } + return 0 +} + +type MessageClaimWinnings struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + ClaimantAddress []byte `protobuf:"bytes,2,opt,name=claimant_address,json=claimantAddress,proto3" json:"claimant_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimWinnings) Reset() { + *x = MessageClaimWinnings{} + mi := &file_tx_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimWinnings) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimWinnings) ProtoMessage() {} + +func (x *MessageClaimWinnings) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimWinnings.ProtoReflect.Descriptor instead. +func (*MessageClaimWinnings) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{18} +} + +func (x *MessageClaimWinnings) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageClaimWinnings) GetClaimantAddress() []byte { + if x != nil { + return x.ClaimantAddress + } + return nil +} + +type MessageResolveMarket struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + ResolverAddress []byte `protobuf:"bytes,2,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + WinningOutcome bool `protobuf:"varint,3,opt,name=winning_outcome,json=winningOutcome,proto3" json:"winning_outcome,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageResolveMarket) Reset() { + *x = MessageResolveMarket{} + mi := &file_tx_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageResolveMarket) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageResolveMarket) ProtoMessage() {} + +func (x *MessageResolveMarket) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageResolveMarket.ProtoReflect.Descriptor instead. +func (*MessageResolveMarket) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{19} +} + +func (x *MessageResolveMarket) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageResolveMarket) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +func (x *MessageResolveMarket) GetWinningOutcome() bool { + if x != nil { + return x.WinningOutcome + } + return false +} + +type MessageRegisterResolver struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddress []byte `protobuf:"bytes,1,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + StakeAmount uint64 `protobuf:"varint,2,opt,name=stake_amount,json=stakeAmount,proto3" json:"stake_amount,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageRegisterResolver) Reset() { + *x = MessageRegisterResolver{} + mi := &file_tx_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageRegisterResolver) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageRegisterResolver) ProtoMessage() {} + +func (x *MessageRegisterResolver) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageRegisterResolver.ProtoReflect.Descriptor instead. +func (*MessageRegisterResolver) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{20} +} + +func (x *MessageRegisterResolver) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +func (x *MessageRegisterResolver) GetStakeAmount() uint64 { + if x != nil { + return x.StakeAmount + } + return 0 +} + +type MessageProposeOutcome struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + ResolverAddress []byte `protobuf:"bytes,2,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + ProposedOutcome bool `protobuf:"varint,3,opt,name=proposed_outcome,json=proposedOutcome,proto3" json:"proposed_outcome,omitempty"` + ProposalBond uint64 `protobuf:"varint,4,opt,name=proposal_bond,json=proposalBond,proto3" json:"proposal_bond,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageProposeOutcome) Reset() { + *x = MessageProposeOutcome{} + mi := &file_tx_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageProposeOutcome) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageProposeOutcome) ProtoMessage() {} + +func (x *MessageProposeOutcome) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageProposeOutcome.ProtoReflect.Descriptor instead. +func (*MessageProposeOutcome) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{21} +} + +func (x *MessageProposeOutcome) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageProposeOutcome) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +func (x *MessageProposeOutcome) GetProposedOutcome() bool { + if x != nil { + return x.ProposedOutcome + } + return false +} + +func (x *MessageProposeOutcome) GetProposalBond() uint64 { + if x != nil { + return x.ProposalBond + } + return 0 +} + +type MessageFileDispute struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + DisputerAddress []byte `protobuf:"bytes,2,opt,name=disputer_address,json=disputerAddress,proto3" json:"disputer_address,omitempty"` + DisputeBond uint64 `protobuf:"varint,3,opt,name=dispute_bond,json=disputeBond,proto3" json:"dispute_bond,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageFileDispute) Reset() { + *x = MessageFileDispute{} + mi := &file_tx_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageFileDispute) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageFileDispute) ProtoMessage() {} + +func (x *MessageFileDispute) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageFileDispute.ProtoReflect.Descriptor instead. +func (*MessageFileDispute) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{22} +} + +func (x *MessageFileDispute) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageFileDispute) GetDisputerAddress() []byte { + if x != nil { + return x.DisputerAddress + } + return nil +} + +func (x *MessageFileDispute) GetDisputeBond() uint64 { + if x != nil { + return x.DisputeBond + } + return 0 +} + +type MessageCommitVote struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + VoterAddr []byte `protobuf:"bytes,2,opt,name=voter_addr,json=voterAddr,proto3" json:"voter_addr,omitempty"` + CommitHash []byte `protobuf:"bytes,3,opt,name=commit_hash,json=commitHash,proto3" json:"commit_hash,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageCommitVote) Reset() { + *x = MessageCommitVote{} + mi := &file_tx_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageCommitVote) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageCommitVote) ProtoMessage() {} + +func (x *MessageCommitVote) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageCommitVote.ProtoReflect.Descriptor instead. +func (*MessageCommitVote) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{23} +} + +func (x *MessageCommitVote) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageCommitVote) GetVoterAddr() []byte { + if x != nil { + return x.VoterAddr + } + return nil +} + +func (x *MessageCommitVote) GetCommitHash() []byte { + if x != nil { + return x.CommitHash + } + return nil +} + +type MessageRevealVote struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + VoterAddr []byte `protobuf:"bytes,2,opt,name=voter_addr,json=voterAddr,proto3" json:"voter_addr,omitempty"` + Vote bool `protobuf:"varint,3,opt,name=vote,proto3" json:"vote,omitempty"` + Nonce []byte `protobuf:"bytes,4,opt,name=nonce,proto3" json:"nonce,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageRevealVote) Reset() { + *x = MessageRevealVote{} + mi := &file_tx_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageRevealVote) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageRevealVote) ProtoMessage() {} + +func (x *MessageRevealVote) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageRevealVote.ProtoReflect.Descriptor instead. +func (*MessageRevealVote) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{24} +} + +func (x *MessageRevealVote) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageRevealVote) GetVoterAddr() []byte { + if x != nil { + return x.VoterAddr + } + return nil +} + +func (x *MessageRevealVote) GetVote() bool { + if x != nil { + return x.Vote + } + return false +} + +func (x *MessageRevealVote) GetNonce() []byte { + if x != nil { + return x.Nonce + } + return nil +} + +type MessageTallyVotes struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + CallerAddr []byte `protobuf:"bytes,2,opt,name=caller_addr,json=callerAddr,proto3" json:"caller_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageTallyVotes) Reset() { + *x = MessageTallyVotes{} + mi := &file_tx_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageTallyVotes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageTallyVotes) ProtoMessage() {} + +func (x *MessageTallyVotes) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageTallyVotes.ProtoReflect.Descriptor instead. +func (*MessageTallyVotes) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{25} +} + +func (x *MessageTallyVotes) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageTallyVotes) GetCallerAddr() []byte { + if x != nil { + return x.CallerAddr + } + return nil +} + +type MessageFinalizeMarket struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + CallerAddr []byte `protobuf:"bytes,2,opt,name=caller_addr,json=callerAddr,proto3" json:"caller_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageFinalizeMarket) Reset() { + *x = MessageFinalizeMarket{} + mi := &file_tx_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageFinalizeMarket) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageFinalizeMarket) ProtoMessage() {} + +func (x *MessageFinalizeMarket) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageFinalizeMarket.ProtoReflect.Descriptor instead. +func (*MessageFinalizeMarket) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{26} +} + +func (x *MessageFinalizeMarket) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageFinalizeMarket) GetCallerAddr() []byte { + if x != nil { + return x.CallerAddr + } + return nil +} + +type MessageClaimSlash struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + ClaimantAddress []byte `protobuf:"bytes,2,opt,name=claimant_address,json=claimantAddress,proto3" json:"claimant_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimSlash) Reset() { + *x = MessageClaimSlash{} + mi := &file_tx_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimSlash) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimSlash) ProtoMessage() {} + +func (x *MessageClaimSlash) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimSlash.ProtoReflect.Descriptor instead. +func (*MessageClaimSlash) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{27} +} + +func (x *MessageClaimSlash) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageClaimSlash) GetClaimantAddress() []byte { + if x != nil { + return x.ClaimantAddress + } + return nil +} + +type MessageReclaimStake struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + ClaimantAddress []byte `protobuf:"bytes,2,opt,name=claimant_address,json=claimantAddress,proto3" json:"claimant_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageReclaimStake) Reset() { + *x = MessageReclaimStake{} + mi := &file_tx_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageReclaimStake) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageReclaimStake) ProtoMessage() {} + +func (x *MessageReclaimStake) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageReclaimStake.ProtoReflect.Descriptor instead. +func (*MessageReclaimStake) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{28} +} + +func (x *MessageReclaimStake) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageReclaimStake) GetClaimantAddress() []byte { + if x != nil { + return x.ClaimantAddress + } + return nil +} + +// MessageForfeitPosition allows a resolver to voluntarily exit a position +// in a market they intend to resolve — satisfying the COI-1 requirement. +// Issue-2: without this tx type, a resolver with even 1 share is permanently +// disqualified from resolving, with no protocol-level escape hatch. +// The full CostPaid is refunded; shares are zeroed atomically. +type MessageForfeitPosition struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + ResolverAddress []byte `protobuf:"bytes,2,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageForfeitPosition) Reset() { + *x = MessageForfeitPosition{} + mi := &file_tx_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageForfeitPosition) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageForfeitPosition) ProtoMessage() {} + +func (x *MessageForfeitPosition) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageForfeitPosition.ProtoReflect.Descriptor instead. +func (*MessageForfeitPosition) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{29} +} + +func (x *MessageForfeitPosition) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageForfeitPosition) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +type MessageClaimBuilderReward struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimBuilderReward) Reset() { + *x = MessageClaimBuilderReward{} + mi := &file_tx_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimBuilderReward) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimBuilderReward) ProtoMessage() {} + +func (x *MessageClaimBuilderReward) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimBuilderReward.ProtoReflect.Descriptor instead. +func (*MessageClaimBuilderReward) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{30} +} + +type MessageClaimCreatorFee struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + CreatorAddress []byte `protobuf:"bytes,2,opt,name=creator_address,json=creatorAddress,proto3" json:"creator_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimCreatorFee) Reset() { + *x = MessageClaimCreatorFee{} + mi := &file_tx_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimCreatorFee) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimCreatorFee) ProtoMessage() {} + +func (x *MessageClaimCreatorFee) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimCreatorFee.ProtoReflect.Descriptor instead. +func (*MessageClaimCreatorFee) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{31} +} + +func (x *MessageClaimCreatorFee) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageClaimCreatorFee) GetCreatorAddress() []byte { + if x != nil { + return x.CreatorAddress + } + return nil +} + +type MessageClaimResolverReward struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddress []byte `protobuf:"bytes,1,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + Epoch uint64 `protobuf:"varint,2,opt,name=epoch,proto3" json:"epoch,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimResolverReward) Reset() { + *x = MessageClaimResolverReward{} + mi := &file_tx_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimResolverReward) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimResolverReward) ProtoMessage() {} + +func (x *MessageClaimResolverReward) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimResolverReward.ProtoReflect.Descriptor instead. +func (*MessageClaimResolverReward) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{32} +} + +func (x *MessageClaimResolverReward) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +func (x *MessageClaimResolverReward) GetEpoch() uint64 { + if x != nil { + return x.Epoch + } + return 0 +} + +type LastClaimedBlock struct { + state protoimpl.MessageState `protogen:"open.v1"` + Height uint64 `protobuf:"varint,1,opt,name=height,proto3" json:"height,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LastClaimedBlock) Reset() { + *x = LastClaimedBlock{} + mi := &file_tx_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LastClaimedBlock) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LastClaimedBlock) ProtoMessage() {} + +func (x *LastClaimedBlock) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LastClaimedBlock.ProtoReflect.Descriptor instead. +func (*LastClaimedBlock) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{33} +} + +func (x *LastClaimedBlock) GetHeight() uint64 { + if x != nil { + return x.Height + } + return 0 +} + +type MessageClaimCommunityReward struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimCommunityReward) Reset() { + *x = MessageClaimCommunityReward{} + mi := &file_tx_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimCommunityReward) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimCommunityReward) ProtoMessage() {} + +func (x *MessageClaimCommunityReward) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimCommunityReward.ProtoReflect.Descriptor instead. +func (*MessageClaimCommunityReward) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{34} +} + +type MessageClaimInvestorReward struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimInvestorReward) Reset() { + *x = MessageClaimInvestorReward{} + mi := &file_tx_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimInvestorReward) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimInvestorReward) ProtoMessage() {} + +func (x *MessageClaimInvestorReward) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimInvestorReward.ProtoReflect.Descriptor instead. +func (*MessageClaimInvestorReward) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{35} +} + +type MessageClaimProtocolReward struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimProtocolReward) Reset() { + *x = MessageClaimProtocolReward{} + mi := &file_tx_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimProtocolReward) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimProtocolReward) ProtoMessage() {} + +func (x *MessageClaimProtocolReward) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimProtocolReward.ProtoReflect.Descriptor instead. +func (*MessageClaimProtocolReward) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{36} +} + +type MessageCancelMarket struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketId []byte `protobuf:"bytes,1,opt,name=market_id,json=marketId,proto3" json:"market_id,omitempty"` + CreatorAddress []byte `protobuf:"bytes,2,opt,name=creator_address,json=creatorAddress,proto3" json:"creator_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageCancelMarket) Reset() { + *x = MessageCancelMarket{} + mi := &file_tx_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageCancelMarket) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageCancelMarket) ProtoMessage() {} + +func (x *MessageCancelMarket) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageCancelMarket.ProtoReflect.Descriptor instead. +func (*MessageCancelMarket) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{37} +} + +func (x *MessageCancelMarket) GetMarketId() []byte { + if x != nil { + return x.MarketId + } + return nil +} + +func (x *MessageCancelMarket) GetCreatorAddress() []byte { + if x != nil { + return x.CreatorAddress + } + return nil +} + +type MessageUnstakeResolver struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddress []byte `protobuf:"bytes,1,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + Amount uint64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"` // 0 = full exit + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageUnstakeResolver) Reset() { + *x = MessageUnstakeResolver{} + mi := &file_tx_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageUnstakeResolver) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageUnstakeResolver) ProtoMessage() {} + +func (x *MessageUnstakeResolver) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageUnstakeResolver.ProtoReflect.Descriptor instead. +func (*MessageUnstakeResolver) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{38} +} + +func (x *MessageUnstakeResolver) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +func (x *MessageUnstakeResolver) GetAmount() uint64 { + if x != nil { + return x.Amount + } + return 0 +} + +type MessageClaimUnbondedStake struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResolverAddress []byte `protobuf:"bytes,1,opt,name=resolver_address,json=resolverAddress,proto3" json:"resolver_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MessageClaimUnbondedStake) Reset() { + *x = MessageClaimUnbondedStake{} + mi := &file_tx_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MessageClaimUnbondedStake) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MessageClaimUnbondedStake) ProtoMessage() {} + +func (x *MessageClaimUnbondedStake) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[39] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MessageClaimUnbondedStake.ProtoReflect.Descriptor instead. +func (*MessageClaimUnbondedStake) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{39} +} + +func (x *MessageClaimUnbondedStake) GetResolverAddress() []byte { + if x != nil { + return x.ResolverAddress + } + return nil +} + +type ResolverIndex struct { + state protoimpl.MessageState `protogen:"open.v1"` + Addresses [][]byte `protobuf:"bytes,1,rep,name=addresses,proto3" json:"addresses,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResolverIndex) Reset() { + *x = ResolverIndex{} + mi := &file_tx_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResolverIndex) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResolverIndex) ProtoMessage() {} + +func (x *ResolverIndex) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[40] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResolverIndex.ProtoReflect.Descriptor instead. +func (*ResolverIndex) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{40} +} + +func (x *ResolverIndex) GetAddresses() [][]byte { + if x != nil { + return x.Addresses + } + return nil +} + +type MarketIndex struct { + state protoimpl.MessageState `protogen:"open.v1"` + MarketIds [][]byte `protobuf:"bytes,1,rep,name=market_ids,json=marketIds,proto3" json:"market_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MarketIndex) Reset() { + *x = MarketIndex{} + mi := &file_tx_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MarketIndex) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MarketIndex) ProtoMessage() {} + +func (x *MarketIndex) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[41] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MarketIndex.ProtoReflect.Descriptor instead. +func (*MarketIndex) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{41} +} + +func (x *MarketIndex) GetMarketIds() [][]byte { + if x != nil { + return x.MarketIds + } + return nil +} + +type GlobalStats struct { + state protoimpl.MessageState `protogen:"open.v1"` + TotalWeightedResolutions uint64 `protobuf:"varint,1,opt,name=total_weighted_resolutions,json=totalWeightedResolutions,proto3" json:"total_weighted_resolutions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GlobalStats) Reset() { + *x = GlobalStats{} + mi := &file_tx_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GlobalStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GlobalStats) ProtoMessage() {} + +func (x *GlobalStats) ProtoReflect() protoreflect.Message { + mi := &file_tx_proto_msgTypes[42] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GlobalStats.ProtoReflect.Descriptor instead. +func (*GlobalStats) Descriptor() ([]byte, []int) { + return file_tx_proto_rawDescGZIP(), []int{42} +} + +func (x *GlobalStats) GetTotalWeightedResolutions() uint64 { + if x != nil { + return x.TotalWeightedResolutions + } + return 0 +} + +var File_tx_proto protoreflect.FileDescriptor + +const file_tx_proto_rawDesc = "" + + "\n" + + "\btx.proto\x12\x05types\x1a\x19google/protobuf/any.proto\"H\n" + + "\tSignature\x12\x1d\n" + + "\n" + + "public_key\x18\x01 \x01(\fR\tpublicKey\x12\x1c\n" + + "\tsignature\x18\x02 \x01(\fR\tsignature\"\xa3\x02\n" + + "\vTransaction\x12!\n" + + "\fmessage_type\x18\x01 \x01(\tR\vmessageType\x12&\n" + + "\x03msg\x18\x02 \x01(\v2\x14.google.protobuf.AnyR\x03msg\x12.\n" + + "\tsignature\x18\x03 \x01(\v2\x10.types.SignatureR\tsignature\x12%\n" + + "\x0ecreated_height\x18\x04 \x01(\x04R\rcreatedHeight\x12\x12\n" + + "\x04time\x18\x05 \x01(\x04R\x04time\x12\x10\n" + + "\x03fee\x18\x06 \x01(\x04R\x03fee\x12\x12\n" + + "\x04memo\x18\a \x01(\tR\x04memo\x12\x1d\n" + + "\n" + + "network_id\x18\b \x01(\x04R\tnetworkId\x12\x19\n" + + "\bchain_id\x18\t \x01(\x04R\achainId\"\xe2\x01\n" + + "\tFeeParams\x12\x19\n" + + "\bsend_fee\x18\x01 \x01(\x04R\asendFee\x12*\n" + + "\x11create_market_fee\x18\x02 \x01(\x04R\x0fcreateMarketFee\x122\n" + + "\x15submit_prediction_fee\x18\x03 \x01(\x04R\x13submitPredictionFee\x12,\n" + + "\x12resolve_market_fee\x18\x04 \x01(\x04R\x10resolveMarketFee\x12,\n" + + "\x12claim_winnings_fee\x18\x05 \x01(\x04R\x10claimWinningsFee\"\x93\x03\n" + + "\vMarketState\x12\x16\n" + + "\x06status\x18\x01 \x01(\rR\x06status\x12\x1f\n" + + "\vexpiry_time\x18\x02 \x01(\x04R\n" + + "expiryTime\x12\x13\n" + + "\x05q_yes\x18\x03 \x01(\x04R\x04qYes\x12\x11\n" + + "\x04q_no\x18\x04 \x01(\x04R\x03qNo\x12\x13\n" + + "\x05b_eff\x18\x05 \x01(\x04R\x04bEff\x12\x18\n" + + "\acreator\x18\x06 \x01(\fR\acreator\x12#\n" + + "\rclaimed_count\x18\a \x01(\x04R\fclaimedCount\x12'\n" + + "\x0ftotal_positions\x18\b \x01(\x04R\x0etotalPositions\x12\x1b\n" + + "\topen_time\x18\t \x01(\x04R\bopenTime\x12#\n" + + "\relevated_risk\x18\n" + + " \x01(\bR\felevatedRisk\x122\n" + + "\x15finalized_pool_amount\x18\v \x01(\x04R\x13finalizedPoolAmount\x12\x1a\n" + + "\bquestion\x18\f \x01(\tR\bquestion\x12\x14\n" + + "\x05rules\x18\r \x01(\tR\x05rules\"\x82\x01\n" + + "\rPositionState\x12\x1d\n" + + "\n" + + "shares_yes\x18\x01 \x01(\x04R\tsharesYes\x12\x1b\n" + + "\tshares_no\x18\x02 \x01(\x04R\bsharesNo\x12\x1b\n" + + "\tcost_paid\x18\x03 \x01(\x04R\bcostPaid\x12\x18\n" + + "\aclaimed\x18\x04 \x01(\bR\aclaimed\"X\n" + + "\fOutcomeState\x12'\n" + + "\x0fwinning_outcome\x18\x01 \x01(\bR\x0ewinningOutcome\x12\x1f\n" + + "\vresolved_at\x18\x02 \x01(\x04R\n" + + "resolvedAt\"[\n" + + "\x0fTreasuryReserve\x12%\n" + + "\x0elocked_reserve\x18\x01 \x01(\x04R\rlockedReserve\x12!\n" + + "\fcreator_bond\x18\x02 \x01(\x04R\vcreatorBond\":\n" + + "\rResolverState\x12)\n" + + "\x10resolver_address\x18\x01 \x01(\fR\x0fresolverAddress\"\x87\x03\n" + + "\x0eResolverRecord\x12)\n" + + "\x10resolver_address\x18\x01 \x01(\fR\x0fresolverAddress\x12!\n" + + "\fstake_amount\x18\x02 \x01(\x04R\vstakeAmount\x12\x1b\n" + + "\trrs_score\x18\x03 \x01(\x04R\brrsScore\x12#\n" + + "\rregistered_at\x18\x04 \x01(\x04R\fregisteredAt\x125\n" + + "\x16successful_resolutions\x18\x05 \x01(\x04R\x15successfulResolutions\x12,\n" + + "\x12last_claimed_epoch\x18\x06 \x01(\x04R\x10lastClaimedEpoch\x12\x1b\n" + + "\tis_active\x18\a \x01(\bR\bisActive\x12)\n" + + "\x10unbonding_amount\x18\b \x01(\x04R\x0funbondingAmount\x128\n" + + "\x18unbonding_release_height\x18\t \x01(\x04R\x16unbondingReleaseHeight\"\xc4\x01\n" + + "\x0eProposalRecord\x12#\n" + + "\rresolver_addr\x18\x01 \x01(\fR\fresolverAddr\x12)\n" + + "\x10proposed_outcome\x18\x02 \x01(\bR\x0fproposedOutcome\x12#\n" + + "\rproposal_bond\x18\x03 \x01(\x04R\fproposalBond\x12%\n" + + "\x0eproposal_block\x18\x04 \x01(\x04R\rproposalBlock\x12\x16\n" + + "\x06status\x18\x05 \x01(\rR\x06status\"\xe7\x01\n" + + "\rDisputeRecord\x12)\n" + + "\x10disputer_address\x18\x01 \x01(\fR\x0fdisputerAddress\x12!\n" + + "\fdispute_bond\x18\x02 \x01(\x04R\vdisputeBond\x12#\n" + + "\rdispute_block\x18\x03 \x01(\x04R\fdisputeBlock\x12\x1f\n" + + "\vvote_status\x18\x04 \x01(\rR\n" + + "voteStatus\x12\x1d\n" + + "\n" + + "panel_size\x18\x05 \x01(\rR\tpanelSize\x12#\n" + + "\rpanel_members\x18\x06 \x03(\fR\fpanelMembers\"o\n" + + "\n" + + "VoteCommit\x12\x1d\n" + + "\n" + + "voter_addr\x18\x01 \x01(\fR\tvoterAddr\x12\x1f\n" + + "\vcommit_hash\x18\x02 \x01(\fR\n" + + "commitHash\x12!\n" + + "\fcommitted_at\x18\x03 \x01(\x04R\vcommittedAt\"`\n" + + "\n" + + "VoteReveal\x12\x1d\n" + + "\n" + + "voter_addr\x18\x01 \x01(\fR\tvoterAddr\x12\x12\n" + + "\x04vote\x18\x02 \x01(\bR\x04vote\x12\x1f\n" + + "\vrevealed_at\x18\x03 \x01(\x04R\n" + + "revealedAt\"x\n" + + "\vSlashRecord\x12'\n" + + "\x0fslashed_address\x18\x01 \x01(\fR\x0eslashedAddress\x12!\n" + + "\fslash_amount\x18\x02 \x01(\x04R\vslashAmount\x12\x1d\n" + + "\n" + + "slashed_at\x18\x03 \x01(\x04R\tslashedAt\"5\n" + + "\x11PanelEntropyAccum\x12 \n" + + "\vaccumulator\x18\x01 \x01(\x04R\vaccumulator\"g\n" + + "\vMessageSend\x12!\n" + + "\ffrom_address\x18\x01 \x01(\fR\vfromAddress\x12\x1d\n" + + "\n" + + "to_address\x18\x02 \x01(\fR\ttoAddress\x12\x16\n" + + "\x06amount\x18\x03 \x01(\x04R\x06amount\"\xb7\x01\n" + + "\x13MessageCreateMarket\x12'\n" + + "\x0fcreator_address\x18\x01 \x01(\fR\x0ecreatorAddress\x12\x0e\n" + + "\x02b0\x18\x02 \x01(\x04R\x02b0\x12\x1f\n" + + "\vexpiry_time\x18\x03 \x01(\x04R\n" + + "expiryTime\x12\x14\n" + + "\x05nonce\x18\x04 \x01(\x04R\x05nonce\x12\x1a\n" + + "\bquestion\x18\x05 \x01(\tR\bquestion\x12\x14\n" + + "\x05rules\x18\x06 \x01(\tR\x05rules\"\xaa\x01\n" + + "\x17MessageSubmitPrediction\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12%\n" + + "\x0ebettor_address\x18\x02 \x01(\fR\rbettorAddress\x12\x18\n" + + "\aoutcome\x18\x03 \x01(\bR\aoutcome\x12\x16\n" + + "\x06shares\x18\x04 \x01(\x04R\x06shares\x12\x19\n" + + "\bmax_cost\x18\x05 \x01(\x04R\amaxCost\"^\n" + + "\x14MessageClaimWinnings\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10claimant_address\x18\x02 \x01(\fR\x0fclaimantAddress\"\x87\x01\n" + + "\x14MessageResolveMarket\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10resolver_address\x18\x02 \x01(\fR\x0fresolverAddress\x12'\n" + + "\x0fwinning_outcome\x18\x03 \x01(\bR\x0ewinningOutcome\"g\n" + + "\x17MessageRegisterResolver\x12)\n" + + "\x10resolver_address\x18\x01 \x01(\fR\x0fresolverAddress\x12!\n" + + "\fstake_amount\x18\x02 \x01(\x04R\vstakeAmount\"\xaf\x01\n" + + "\x15MessageProposeOutcome\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10resolver_address\x18\x02 \x01(\fR\x0fresolverAddress\x12)\n" + + "\x10proposed_outcome\x18\x03 \x01(\bR\x0fproposedOutcome\x12#\n" + + "\rproposal_bond\x18\x04 \x01(\x04R\fproposalBond\"\x7f\n" + + "\x12MessageFileDispute\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10disputer_address\x18\x02 \x01(\fR\x0fdisputerAddress\x12!\n" + + "\fdispute_bond\x18\x03 \x01(\x04R\vdisputeBond\"p\n" + + "\x11MessageCommitVote\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12\x1d\n" + + "\n" + + "voter_addr\x18\x02 \x01(\fR\tvoterAddr\x12\x1f\n" + + "\vcommit_hash\x18\x03 \x01(\fR\n" + + "commitHash\"y\n" + + "\x11MessageRevealVote\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12\x1d\n" + + "\n" + + "voter_addr\x18\x02 \x01(\fR\tvoterAddr\x12\x12\n" + + "\x04vote\x18\x03 \x01(\bR\x04vote\x12\x14\n" + + "\x05nonce\x18\x04 \x01(\fR\x05nonce\"Q\n" + + "\x11MessageTallyVotes\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12\x1f\n" + + "\vcaller_addr\x18\x02 \x01(\fR\n" + + "callerAddr\"U\n" + + "\x15MessageFinalizeMarket\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12\x1f\n" + + "\vcaller_addr\x18\x02 \x01(\fR\n" + + "callerAddr\"[\n" + + "\x11MessageClaimSlash\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10claimant_address\x18\x02 \x01(\fR\x0fclaimantAddress\"]\n" + + "\x13MessageReclaimStake\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10claimant_address\x18\x02 \x01(\fR\x0fclaimantAddress\"`\n" + + "\x16MessageForfeitPosition\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12)\n" + + "\x10resolver_address\x18\x02 \x01(\fR\x0fresolverAddress\"\x1b\n" + + "\x19MessageClaimBuilderReward\"^\n" + + "\x16MessageClaimCreatorFee\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12'\n" + + "\x0fcreator_address\x18\x02 \x01(\fR\x0ecreatorAddress\"]\n" + + "\x1aMessageClaimResolverReward\x12)\n" + + "\x10resolver_address\x18\x01 \x01(\fR\x0fresolverAddress\x12\x14\n" + + "\x05epoch\x18\x02 \x01(\x04R\x05epoch\"*\n" + + "\x10LastClaimedBlock\x12\x16\n" + + "\x06height\x18\x01 \x01(\x04R\x06height\"\x1d\n" + + "\x1bMessageClaimCommunityReward\"\x1c\n" + + "\x1aMessageClaimInvestorReward\"\x1c\n" + + "\x1aMessageClaimProtocolReward\"[\n" + + "\x13MessageCancelMarket\x12\x1b\n" + + "\tmarket_id\x18\x01 \x01(\fR\bmarketId\x12'\n" + + "\x0fcreator_address\x18\x02 \x01(\fR\x0ecreatorAddress\"[\n" + + "\x16MessageUnstakeResolver\x12)\n" + + "\x10resolver_address\x18\x01 \x01(\fR\x0fresolverAddress\x12\x16\n" + + "\x06amount\x18\x02 \x01(\x04R\x06amount\"F\n" + + "\x19MessageClaimUnbondedStake\x12)\n" + + "\x10resolver_address\x18\x01 \x01(\fR\x0fresolverAddress\"-\n" + + "\rResolverIndex\x12\x1c\n" + + "\taddresses\x18\x01 \x03(\fR\taddresses\",\n" + + "\vMarketIndex\x12\x1d\n" + + "\n" + + "market_ids\x18\x01 \x03(\fR\tmarketIds\"K\n" + + "\vGlobalStats\x12<\n" + + "\x1atotal_weighted_resolutions\x18\x01 \x01(\x04R\x18totalWeightedResolutionsB5Z3github.com/canopy-network/canopy/plugin/go/contractb\x06proto3" var ( file_tx_proto_rawDescOnce sync.Once @@ -485,24 +2797,61 @@ func file_tx_proto_rawDescGZIP() []byte { return file_tx_proto_rawDescData } -var file_tx_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_tx_proto_msgTypes = make([]protoimpl.MessageInfo, 43) var file_tx_proto_goTypes = []any{ - (*Transaction)(nil), // 0: types.Transaction - (*MessageSend)(nil), // 1: types.MessageSend - (*FeeParams)(nil), // 2: types.FeeParams - (*Signature)(nil), // 3: types.Signature - (*MessageReward)(nil), // 4: types.MessageReward - (*MessageFaucet)(nil), // 5: types.MessageFaucet - (*anypb.Any)(nil), // 6: google.protobuf.Any + (*Signature)(nil), // 0: types.Signature + (*Transaction)(nil), // 1: types.Transaction + (*FeeParams)(nil), // 2: types.FeeParams + (*MarketState)(nil), // 3: types.MarketState + (*PositionState)(nil), // 4: types.PositionState + (*OutcomeState)(nil), // 5: types.OutcomeState + (*TreasuryReserve)(nil), // 6: types.TreasuryReserve + (*ResolverState)(nil), // 7: types.ResolverState + (*ResolverRecord)(nil), // 8: types.ResolverRecord + (*ProposalRecord)(nil), // 9: types.ProposalRecord + (*DisputeRecord)(nil), // 10: types.DisputeRecord + (*VoteCommit)(nil), // 11: types.VoteCommit + (*VoteReveal)(nil), // 12: types.VoteReveal + (*SlashRecord)(nil), // 13: types.SlashRecord + (*PanelEntropyAccum)(nil), // 14: types.PanelEntropyAccum + (*MessageSend)(nil), // 15: types.MessageSend + (*MessageCreateMarket)(nil), // 16: types.MessageCreateMarket + (*MessageSubmitPrediction)(nil), // 17: types.MessageSubmitPrediction + (*MessageClaimWinnings)(nil), // 18: types.MessageClaimWinnings + (*MessageResolveMarket)(nil), // 19: types.MessageResolveMarket + (*MessageRegisterResolver)(nil), // 20: types.MessageRegisterResolver + (*MessageProposeOutcome)(nil), // 21: types.MessageProposeOutcome + (*MessageFileDispute)(nil), // 22: types.MessageFileDispute + (*MessageCommitVote)(nil), // 23: types.MessageCommitVote + (*MessageRevealVote)(nil), // 24: types.MessageRevealVote + (*MessageTallyVotes)(nil), // 25: types.MessageTallyVotes + (*MessageFinalizeMarket)(nil), // 26: types.MessageFinalizeMarket + (*MessageClaimSlash)(nil), // 27: types.MessageClaimSlash + (*MessageReclaimStake)(nil), // 28: types.MessageReclaimStake + (*MessageForfeitPosition)(nil), // 29: types.MessageForfeitPosition + (*MessageClaimBuilderReward)(nil), // 30: types.MessageClaimBuilderReward + (*MessageClaimCreatorFee)(nil), // 31: types.MessageClaimCreatorFee + (*MessageClaimResolverReward)(nil), // 32: types.MessageClaimResolverReward + (*LastClaimedBlock)(nil), // 33: types.LastClaimedBlock + (*MessageClaimCommunityReward)(nil), // 34: types.MessageClaimCommunityReward + (*MessageClaimInvestorReward)(nil), // 35: types.MessageClaimInvestorReward + (*MessageClaimProtocolReward)(nil), // 36: types.MessageClaimProtocolReward + (*MessageCancelMarket)(nil), // 37: types.MessageCancelMarket + (*MessageUnstakeResolver)(nil), // 38: types.MessageUnstakeResolver + (*MessageClaimUnbondedStake)(nil), // 39: types.MessageClaimUnbondedStake + (*ResolverIndex)(nil), // 40: types.ResolverIndex + (*MarketIndex)(nil), // 41: types.MarketIndex + (*GlobalStats)(nil), // 42: types.GlobalStats + (*any1.Any)(nil), // 43: google.protobuf.Any } var file_tx_proto_depIdxs = []int32{ - 6, // 0: types.Transaction.msg:type_name -> google.protobuf.Any - 3, // 1: types.Transaction.signature:type_name -> types.Signature - 2, // [2:2] is the sub-list for method output_type - 2, // [2:2] is the sub-list for method input_type - 2, // [2:2] is the sub-list for extension type_name - 2, // [2:2] is the sub-list for extension extendee - 0, // [0:2] is the sub-list for field type_name + 43, // 0: types.Transaction.msg:type_name -> google.protobuf.Any + 0, // 1: types.Transaction.signature:type_name -> types.Signature + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_tx_proto_init() } @@ -516,7 +2865,7 @@ func file_tx_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_tx_proto_rawDesc), len(file_tx_proto_rawDesc)), NumEnums: 0, - NumMessages: 6, + NumMessages: 43, NumExtensions: 0, NumServices: 0, }, diff --git a/plugin/go/tutorial/go.mod b/plugin/go/tutorial/go.mod index afda635b81..326bd1a9b1 100644 --- a/plugin/go/tutorial/go.mod +++ b/plugin/go/tutorial/go.mod @@ -1,6 +1,8 @@ module github.com/canopy-network/go-plugin/tutorial -go 1.25 +go 1.24.0 + +toolchain go1.24.2 require ( github.com/drand/kyber v1.3.2 @@ -8,8 +10,13 @@ require ( google.golang.org/protobuf v1.36.6 ) +require github.com/golang/protobuf v1.5.0 // indirect + require ( + github.com/canopy-network/canopy/plugin/go v0.0.0 github.com/kilic/bls12-381 v0.1.0 // indirect golang.org/x/crypto v0.46.0 // indirect golang.org/x/sys v0.39.0 // indirect ) + +replace github.com/canopy-network/canopy/plugin/go => ../. diff --git a/plugin/go/tutorial/go.sum b/plugin/go/tutorial/go.sum index 624ad4f981..83382daa71 100644 --- a/plugin/go/tutorial/go.sum +++ b/plugin/go/tutorial/go.sum @@ -1,9 +1,11 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/drand/kyber v1.3.2 h1:Cf3NNcb5bV3eODopr3XVHzImjDK40GiObhFUFG93Zeo= -github.com/drand/kyber v1.3.2/go.mod h1:ciDFWoC7ajb89niGJnS4C1Xeo4lSJMmbi+km5w8juAI= +github.com/drand/kyber v1.3.2/go.mod h1:TYHFYljYSYmAm9PuGav9ykoEaZzBS0ey0dl/GNA+2Uc= github.com/drand/kyber-bls12381 v0.3.4 h1:rrmYcRcXmtOAvKWVBxRQxi22qNMVcS2Jz7MAebZQJxI= github.com/drand/kyber-bls12381 v0.3.4/go.mod h1:jh3IGIAQfdLrdNKYz1HWZ3YdfJM0DWlN1TxXkh60utk= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/kilic/bls12-381 v0.1.0 h1:encrdjqKMEvabVQ7qYOKu1OvhqpK4s47wDYtNiPtlp4= @@ -21,6 +23,7 @@ golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/plugin/go/tutorial/praxis_test.go b/plugin/go/tutorial/praxis_test.go new file mode 100644 index 0000000000..37244752e8 --- /dev/null +++ b/plugin/go/tutorial/praxis_test.go @@ -0,0 +1,1833 @@ +package main + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "os" + "fmt" + "io" + "net/http" + "testing" + "time" + + bls12381 "github.com/drand/kyber-bls12381" + bdn "github.com/drand/kyber/sign/bdn" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" + + contract "github.com/canopy-network/canopy/plugin/go/contract" +) + +const ( + queryRPCURL = "http://localhost:50002" + adminRPCURL = "http://localhost:50003" + testNetworkID = uint64(1) + testChainID = uint64(1) + testFee = uint64(10000) + testPassword = "testpassword123" +) + +type keyGroup struct { + Address string `json:"address"` + PublicKey string `json:"publicKey"` + PrivateKey string `json:"privateKey"` +} + +func postJSON(url, body string) ([]byte, error) { + resp, err := http.Post(url, "application/json", bytes.NewBufferString(body)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + data, _ := io.ReadAll(resp.Body) + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, data) + } + return data, nil +} + +func keystoreNewKey(nickname, password string) (string, error) { + body := fmt.Sprintf(`{"nickname":%q,"password":%q}`, nickname, password) + data, err := postJSON(adminRPCURL+"/v1/admin/keystore-new-key", body) + if err != nil { + return "", err + } + return string(bytes.Trim(data, "\"\n\r")), nil +} + +func keystoreGetKey(address, password string) (*keyGroup, error) { + // Hardcoded validator key — avoids password issues with existing keys + known := map[string]keyGroup{ + "e7c7dad131a03f7ea0cc09a637ad096eb3495f77": { + Address: "e7c7dad131a03f7ea0cc09a637ad096eb3495f77", + PublicKey: "ae13ea1c3a3a180b821b961561fedab3864fe037c7e159ef79c606c4399210f76f8bbb2ef7fe580c335a02cb48441b32", + PrivateKey: "14f43ca8c7f31a63d144564e8826186383844b5da679dfc2c9352d665d69f0f6", + }, + "8f8b550064ec4ee4551d1666cb0ee5d35fc5154a": { + Address: "8f8b550064ec4ee4551d1666cb0ee5d35fc5154a", + PublicKey: "88634c8e0fd9ee8911b362e5aff8c046154263e9b8e507fc5efe5b5d9cb6cb4fd14c3672bccb929c411e3050ccca44a9", + PrivateKey: "1c91a4882751adc1fa4f2574c4321bf144e36411ade55e099e9c6ffece87ee49", + }, + "205f68c279331cd17b9d41727f09eed7162b0389": { + Address: "205f68c279331cd17b9d41727f09eed7162b0389", + PublicKey: "94ed6fa9309508f451d036ebeac618e317bc10883abad9d784246c87131fc66b8cb4d3b4b1b64a08de9c5737ea035ef3", + PrivateKey: "3b0da148ffe050d58288df78a4b9ed58012a816919e4888b4a066f61d04c4e22", + }, + } + if kg, ok := known[address]; ok { + return &kg, nil + } + body := fmt.Sprintf(`{"addressOrNickname":%q,"password":%q}`, address, password) + data, err := postJSON(adminRPCURL+"/v1/admin/keystore-get", body) + if err != nil { + return nil, err + } + var kg keyGroup + json.Unmarshal(data, &kg) + return &kg, nil +} + +func getHeight() (uint64, error) { + data, err := postJSON(queryRPCURL+"/v1/query/height", "{}") + if err != nil { + return 0, err + } + var r struct { + Height uint64 `json:"height"` + } + json.Unmarshal(data, &r) + return r.Height, nil +} + +func getBalance(addr string) uint64 { + data, _ := postJSON(queryRPCURL+"/v1/query/account", + fmt.Sprintf(`{"address":%q}`, addr)) + var r struct{ Amount uint64 } + json.Unmarshal(data, &r) + return r.Amount +} + +func waitForTx(sender, txHash string, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + data, _ := postJSON(queryRPCURL+"/v1/query/txs-by-sender", + fmt.Sprintf(`{"address":%q,"perPage":50}`, sender)) + var r struct { + Results []struct { + TxHash string `json:"txHash"` + } `json:"results"` + } + json.Unmarshal(data, &r) + for _, tx := range r.Results { + if tx.TxHash == txHash { + return nil + } + } + time.Sleep(2 * time.Second) + } + return fmt.Errorf("tx not confirmed") +} + +// blsSign signs msg with privKeyHex using BLS12-381 G2 (long signatures). +// The kyber bdn scheme on G2 matches what Canopy nodes verify. +// IMPORTANT: privKeyHex must be exactly 32 bytes (64 hex chars). +func blsSign(t *testing.T, privKeyHex string, msg []byte) (sig, pubKey []byte, err error) { + t.Helper() + + privBytes, err := hex.DecodeString(privKeyHex) + if err != nil { + return nil, nil, fmt.Errorf("decode privkey: %w", err) + } + + suite := bls12381.NewBLS12381Suite() + scheme := bdn.NewSchemeOnG2(suite) + + // Reconstruct scalar from raw private key bytes + scalar := suite.G1().Scalar() + if err := scalar.UnmarshalBinary(privBytes); err != nil { + return nil, nil, fmt.Errorf("unmarshal scalar: %w", err) + } + + // Derive public key + point := suite.G1().Point().Mul(scalar, nil) + pubKey, err = point.MarshalBinary() + if err != nil { + return nil, nil, fmt.Errorf("marshal pubkey: %w", err) + } + + t.Logf("pubKey len=%d hex=%x", len(pubKey), pubKey) + + // Sign — scheme uses G2 for signatures (96 bytes) + sig, err = scheme.Sign(scalar, msg) + if err != nil { + return nil, nil, fmt.Errorf("sign: %w", err) + } + + t.Logf("sig len=%d", len(sig)) + return sig, pubKey, nil +} + +// submitSendTx handles the built-in send tx which uses msg field not msgTypeUrl/msgBytes +func submitSendTx(t *testing.T, kg *keyGroup, msg *contract.MessageSend, height uint64) string { + t.Helper() + + typeURL := "type.googleapis.com/types.MessageSend" + msgBytes, err := proto.Marshal(msg) + if err != nil { + t.Fatalf("marshal msg: %v", err) + } + + txTime := uint64(time.Now().UnixMicro()) + + tx := &contract.Transaction{ + MessageType: "send", + Msg: &anypb.Any{TypeUrl: typeURL, Value: msgBytes}, + Signature: nil, + CreatedHeight: height, + Time: txTime, + Fee: testFee, + NetworkId: testNetworkID, + ChainId: testChainID, + } + + signBytes, err := proto.MarshalOptions{Deterministic: true}.Marshal(tx) + if err != nil { + t.Fatalf("marshal sign bytes: %v", err) + } + + sig, pubKey, err := blsSign(t, kg.PrivateKey, signBytes) + if err != nil { + t.Fatalf("bls sign: %v", err) + } + + body := fmt.Sprintf(`{ + "type": "send", + "msg": { + "fromAddress": %q, + "toAddress": %q, + "amount": %d + }, + "signature": { + "publicKey": %q, + "signature": %q + }, + "time": %d, + "createdHeight": %d, + "fee": %d, + "memo": "", + "networkID": %d, + "chainID": %d +}`, + hex.EncodeToString(msg.FromAddress), + hex.EncodeToString(msg.ToAddress), + msg.Amount, + hex.EncodeToString(pubKey), + hex.EncodeToString(sig), + txTime, height, testFee, + testNetworkID, testChainID, + ) + + data, err := postJSON(queryRPCURL+"/v1/tx", body) + if err != nil { + t.Fatalf("post tx: %v", err) + } + var hash string + if err := json.Unmarshal(data, &hash); err != nil { + t.Fatalf("parse hash: %v: %s", err, data) + } + return hash +} + +// submitTx builds sign bytes by marshaling a Transaction proto with Signature=nil, +// signs it, then POSTs the JSON body to /v1/tx. +func submitTx(t *testing.T, kg *keyGroup, shortType, msgName string, msg proto.Message, height uint64) string { + t.Helper() + + typeURL := "type.googleapis.com/types." + msgName + + msgBytes, err := proto.Marshal(msg) + if err != nil { + t.Fatalf("marshal msg: %v", err) + } + t.Logf("msgBytes hex: %x", msgBytes) + + // Wrap in google.protobuf.Any + msgAny := &anypb.Any{ + TypeUrl: typeURL, + Value: msgBytes, + } + + // Compute txTime ONCE — same value must appear in sign bytes AND tx body + txTime := uint64(time.Now().UnixMicro()) + + // Build Transaction for sign bytes — Signature must be nil/omitted + // CreatedHeight is included when non-zero (proto3 omits zero values) + tx := &contract.Transaction{ + MessageType: shortType, + Msg: msgAny, + Signature: nil, // MUST be nil for sign bytes + CreatedHeight: height, + Time: txTime, + Fee: testFee, + NetworkId: testNetworkID, + ChainId: testChainID, + } + + // Deterministic marshal — this is exactly what the node recomputes + signBytes, err := proto.MarshalOptions{Deterministic: true}.Marshal(tx) + if err != nil { + t.Fatalf("marshal sign bytes: %v", err) + } + t.Logf("signBytes hex: %x", signBytes) + + sig, pubKey, err := blsSign(t, kg.PrivateKey, signBytes) + if err != nil { + t.Fatalf("bls sign: %v", err) + } + + // POST as plugin tx format (msgTypeUrl + msgBytes) + body := fmt.Sprintf(`{ + "type": %q, + "msgTypeUrl": %q, + "msgBytes": %q, + "signature": { + "publicKey": %q, + "signature": %q + }, + "time": %d, + "createdHeight": %d, + "fee": %d, + "memo": "", + "networkID": %d, + "chainID": %d +}`, + shortType, typeURL, hex.EncodeToString(msgBytes), + hex.EncodeToString(pubKey), hex.EncodeToString(sig), + txTime, height, testFee, + testNetworkID, testChainID, + ) + + t.Logf("TX body:\n%s", body) + + data, err := postJSON(queryRPCURL+"/v1/tx", body) + if err != nil { + t.Fatalf("post tx: %v", err) + } + hash := string(bytes.Trim(data, "\"\n\r")) + t.Logf("TX hash: %s", hash) + return hash +} + + +// setupResolver creates a fresh key, funds it from the validator, and registers +// it as a resolver. Returns the keyGroup and address. The creator address must +// differ from the returned resolver address (COI-2 enforcement). +func setupResolver(t *testing.T, validatorKey *keyGroup, validatorAddr string, stakeAmount uint64) (*keyGroup, string) { + t.Helper() + // Always use the hardcoded predictor key — keystore persists across node restarts. + // Fund with stakeAmount * 3 to ensure sufficient balance after fees and prior deductions. + resolverAddr := "205f68c279331cd17b9d41727f09eed7162b0389" + h, _ := getHeight() + fundMsg := &contract.MessageSend{ + FromAddress: hexDecode(validatorAddr), + ToAddress: hexDecode(resolverAddr), + Amount: stakeAmount * 3, + } + hash := submitSendTx(t, validatorKey, fundMsg, h) + if err := waitForTx(validatorAddr, hash, 60*time.Second); err != nil { + t.Fatalf("fund resolver: %v", err) + } + // Register resolver — stake adds on top of any existing stake. + h2, _ := getHeight() + regMsg := &contract.MessageRegisterResolver{ + ResolverAddress: hexDecode(resolverAddr), + StakeAmount: stakeAmount, + } + regKey := &keyGroup{ + Address: resolverAddr, + PublicKey: "94ed6fa9309508f451d036ebeac618e317bc10883abad9d784246c87131fc66b8cb4d3b4b1b64a08de9c5737ea035ef3", + PrivateKey: "3b0da148ffe050d58288df78a4b9ed58012a816919e4888b4a066f61d04c4e22", + } + regHash := submitTx(t, regKey, "register_resolver", "MessageRegisterResolver", regMsg, h2) + if err := waitForTx(resolverAddr, regHash, 60*time.Second); err != nil { + t.Fatalf("register resolver: %v", err) + } + t.Logf("Resolver %s registered with stake %d", resolverAddr, stakeAmount) + return regKey, resolverAddr +} +func hexDecode(s string) []byte { + b, _ := hex.DecodeString(s) + return b +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +func TestValidatorSend(t *testing.T) { + validatorAddr := "e7c7dad131a03f7ea0cc09a637ad096eb3495f77" + validatorKey, err := keystoreGetKey(validatorAddr, "") + if err != nil { + t.Fatalf("key: %v", err) + } + + suffix := fmt.Sprintf("%d", time.Now().UnixMicro()%1000000) + recipientAddr, err := keystoreNewKey("tst_s_"+suffix, testPassword) + if err != nil { + t.Fatalf("new key: %v", err) + } + + height, err := getHeight() + if err != nil { + t.Fatalf("height: %v", err) + } + t.Logf("height=%d recipientAddr=%s", height, recipientAddr) + + msg := &contract.MessageSend{ + FromAddress: hexDecode(validatorAddr), + ToAddress: hexDecode(recipientAddr), + Amount: 1_000_000, + } + txHash := submitSendTx(t, validatorKey, msg, height) + if err := waitForTx(validatorAddr, txHash, 60*time.Second); err != nil { + t.Fatal(err) + } + if bal := getBalance(recipientAddr); bal != 1_000_000 { + t.Errorf("expected balance 1000000, got %d", bal) + } + t.Log("Send confirmed!") +} + +func TestCreateMarket(t *testing.T) { + validatorAddr := "e7c7dad131a03f7ea0cc09a637ad096eb3495f77" + validatorKey, err := keystoreGetKey(validatorAddr, "") + if err != nil { + t.Fatalf("key: %v", err) + } + + height, err := getHeight() + if err != nil { + t.Fatalf("height: %v", err) + } + t.Logf("height=%d", height) + + msg := &contract.MessageCreateMarket{ + CreatorAddress: hexDecode(validatorAddr), + B0: 60_000_000, // MIN_B0 + ExpiryTime: height + 1000, + Nonce: uint64(time.Now().UnixMicro()), + Question: "Will Praxis launch on mainnet?", + } + + t.Logf("CreateMarket: B0=%d ExpiryTime=%d Nonce=%d", msg.B0, msg.ExpiryTime, msg.Nonce) + + txHash := submitTx(t, validatorKey, "create_market", "MessageCreateMarket", msg, height) + if err := waitForTx(validatorAddr, txHash, 60*time.Second); err != nil { + t.Fatal(err) + } + t.Log("Market created successfully!") +} + + +func TestSubmitPrediction(t *testing.T) { +addr := "e7c7dad131a03f7ea0cc09a637ad096eb3495f77" +key, err := keystoreGetKey(addr, "") +if err != nil { t.Fatalf("key: %v", err) } + +// 1. Create a market first +h, _ := getHeight() +createMsg := &contract.MessageCreateMarket{ +CreatorAddress: hexDecode(addr), +B0: 60_000_000, +ExpiryTime: h + 50000, +Nonce: uint64(time.Now().UnixMicro()), +Question: "Prediction test market", +} +marketHash := submitTx(t, key, "create_market", "MessageCreateMarket", createMsg, h) +if err := waitForTx(addr, marketHash, 60*time.Second); err != nil { +t.Fatalf("create market failed: %v", err) +} +t.Logf("Market created: %s", marketHash) + +// 2. Get the market state to know the market ID + +marketId := contract.DeriveMarketId(hexDecode(addr), createMsg.Nonce) +t.Logf("Derived market ID: %x", marketId) + +// 3. Submit a YES prediction (1 share = 1 PRX precision unit) +h2, _ := getHeight() +predMsg := &contract.MessageSubmitPrediction{ +MarketId: marketId, +BettorAddress: hexDecode(addr), +Outcome: true, +Shares: contract.PRECISION_SCALE, +MaxCost: 10_000_000, // enough for a small trade +} +txHash := submitTx(t, key, "submit_prediction", "MessageSubmitPrediction", predMsg, h2) +if err := waitForTx(addr, txHash, 60*time.Second); err != nil { +t.Fatalf("submit prediction failed: %v", err) +} +t.Log("Prediction submitted!") +} + + +func TestRegisterResolver(t *testing.T) { +addr := "e7c7dad131a03f7ea0cc09a637ad096eb3495f77" +key, _ := keystoreGetKey(addr, "") +h, _ := getHeight() + +msg := &contract.MessageRegisterResolver{ +ResolverAddress: hexDecode(addr), +StakeAmount: 500_000_000_000, // 500,000 PRX minimum +} +txHash := submitTx(t, key, "register_resolver", "MessageRegisterResolver", msg, h) +if err := waitForTx(addr, txHash, 60*time.Second); err != nil { +t.Fatalf("register resolver failed: %v", err) +} +t.Log("Resolver registered!") +} + +func TestPORSFullFlow(t *testing.T) { + addr := "e7c7dad131a03f7ea0cc09a637ad096eb3495f77" + key, err := keystoreGetKey(addr, "") + if err != nil { + t.Fatalf("key: %v", err) + } + + // Step 1: Create market with short expiry + h, _ := getHeight() + t.Logf("Starting height: %d", h) + + nonce := uint64(time.Now().UnixMicro()) + createMsg := &contract.MessageCreateMarket{ + CreatorAddress: hexDecode(addr), + B0: 60_000_000, + ExpiryTime: h + 30, + Nonce: nonce, + Question: "PORS full flow demo - Will Praxis launch on mainnet?", + } + t.Logf("Creating market: ExpiryTime=%d", createMsg.ExpiryTime) + hash := submitTx(t, key, "create_market", "MessageCreateMarket", createMsg, h) + if err := waitForTx(addr, hash, 60*time.Second); err != nil { + t.Fatalf("create_market failed: %v", err) + } + marketId := contract.DeriveMarketId(hexDecode(addr), nonce) + t.Logf("Market created. ID: %x", marketId) + + // Step 2: Submit prediction + h, _ = getHeight() + predMsg := &contract.MessageSubmitPrediction{ + MarketId: marketId, + BettorAddress: hexDecode(addr), + Outcome: true, + Shares: contract.PRECISION_SCALE, + MaxCost: 10_000_000, + } + hash = submitTx(t, key, "submit_prediction", "MessageSubmitPrediction", predMsg, h) + if err := waitForTx(addr, hash, 60*time.Second); err != nil { + t.Fatalf("submit_prediction failed: %v", err) + } + t.Log("Prediction submitted") + + // Step 3: Register resolver (separate address — COI-2: creator cannot resolve) + resolverKey, resolverAddr := setupResolver(t, key, addr, 500_000_000_000) + h, _ = getHeight() + _ = h + + // Wait for market to expire + t.Logf("Waiting for expiry (height %d)...", createMsg.ExpiryTime+2) + for { + h, _ = getHeight() + if h > createMsg.ExpiryTime+2 { + break + } + t.Logf("Height %d / need %d", h, createMsg.ExpiryTime+2) + time.Sleep(4 * time.Second) + } + t.Logf("Market expired at height %d", h) + + // Step 4: Propose outcome + h, _ = getHeight() + propMsg := &contract.MessageProposeOutcome{ + MarketId: marketId, + ResolverAddress: hexDecode(resolverAddr), + ProposedOutcome: true, + ProposalBond: 100_000_000, + } + hash = submitTx(t, resolverKey, "propose_outcome", "MessageProposeOutcome", propMsg, h) + if err := waitForTx(resolverAddr, hash, 60*time.Second); err != nil { + t.Fatalf("propose_outcome failed: %v", err) + } + t.Log("Outcome proposed") + + // Wait for dispute window (TEST_MODE = 20 blocks) + disputeEnd := h + 22 + t.Logf("Waiting for dispute window (height %d)...", disputeEnd) + for { + h, _ = getHeight() + if h > disputeEnd { + break + } + t.Logf("Height %d / need %d", h, disputeEnd) + time.Sleep(4 * time.Second) + } + t.Log("Dispute window closed") + + // Step 5: Finalize market + h, _ = getHeight() + finalMsg := &contract.MessageFinalizeMarket{ + MarketId: marketId, + CallerAddr: hexDecode(addr), + } + hash = submitTx(t, key, "finalize_market", "MessageFinalizeMarket", finalMsg, h) + if err := waitForTx(addr, hash, 60*time.Second); err != nil { + t.Fatalf("finalize_market failed: %v", err) + } + t.Log("Market finalized") + + // Step 6: Claim winnings + balBefore := getBalance(addr) + t.Logf("Balance before claim: %d", balBefore) + + h, _ = getHeight() + claimMsg := &contract.MessageClaimWinnings{ + MarketId: marketId, + ClaimantAddress: hexDecode(addr), + } + hash = submitTx(t, key, "claim_winnings", "MessageClaimWinnings", claimMsg, h) + if err := waitForTx(addr, hash, 60*time.Second); err != nil { + t.Fatalf("claim_winnings failed: %v", err) + } + + balAfter := getBalance(addr) + t.Logf("Balance after claim: %d", balAfter) + + if balAfter <= balBefore { + t.Errorf("Payout not received: before=%d after=%d", balBefore, balAfter) + } else { + t.Logf("Payout received: +%d uPRX", balAfter-balBefore) + } + + t.Log("========================================") + t.Log("PORS FULL FLOW COMPLETE") + t.Log("========================================") +} + +func TestNonValidatorPredict(t *testing.T) { +addr := "e7c7dad131a03f7ea0cc09a637ad096eb3495f77" +key, err := keystoreGetKey(addr, "") +if err != nil { +t.Fatalf("get validator key: %v", err) +} + +cfgData, cfgErr := os.ReadFile("test_config.json") +if cfgErr != nil { +t.Fatalf("read test_config.json: %v", cfgErr) +} +var cfg struct { +PredictorAddress string `json:"predictor_address"` +PredictorPrivKey string `json:"predictor_privkey"` +PredictorPubKey string `json:"predictor_pubkey"` +} +if jsonErr := json.Unmarshal(cfgData, &cfg); jsonErr != nil { +t.Fatalf("parse test_config.json: %v", jsonErr) +} +predKey := &keyGroup{Address: cfg.PredictorAddress, PublicKey: cfg.PredictorPubKey, PrivateKey: cfg.PredictorPrivKey} + +// Step 1: Fund predictor +h, _ := getHeight() +sendMsg := &contract.MessageSend{FromAddress: hexDecode(addr), ToAddress: hexDecode(predKey.Address), Amount: 10_000_000} +hash := submitSendTx(t, key, sendMsg, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("fund predictor failed: %v", err) +} +t.Logf("Predictor funded: %d uPRX", getBalance(predKey.Address)) + +// Step 2: Create market as validator +h, _ = getHeight() +nonce := uint64(time.Now().UnixMicro()) +createMsg := &contract.MessageCreateMarket{ +CreatorAddress: hexDecode(addr), +B0: 60_000_000, +ExpiryTime: h + 30, +Nonce: nonce, +Question: "Will non-validator predict successfully?", +} +hash = submitTx(t, key, "create_market", "MessageCreateMarket", createMsg, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("create_market failed: %v", err) +} +marketId := contract.DeriveMarketId(hexDecode(addr), nonce) +t.Logf("Market created. ID: %x", marketId) + +// Step 3: Submit prediction as predictor (non-validator) +h, _ = getHeight() +balBefore := getBalance(predKey.Address) +predMsg := &contract.MessageSubmitPrediction{ +MarketId: marketId, +BettorAddress: hexDecode(predKey.Address), +Outcome: true, +Shares: contract.PRECISION_SCALE, +MaxCost: 10_000_000, +} +hash = submitTx(t, predKey, "submit_prediction", "MessageSubmitPrediction", predMsg, h) +if err := waitForTx(predKey.Address, hash, 60*time.Second); err != nil { +t.Fatalf("submit_prediction failed: %v", err) +} +balAfter := getBalance(predKey.Address) +t.Logf("Balance before: %d after: %d diff: %d", balBefore, balAfter, int64(balAfter)-int64(balBefore)) +if balAfter >= balBefore { +t.Errorf("Predictor balance should have decreased") +} +t.Log("Non-validator prediction submitted successfully!") +} + +func TestClaimWinningsPayout(t *testing.T) { +addr := "e7c7dad131a03f7ea0cc09a637ad096eb3495f77" +key, err := keystoreGetKey(addr, "") +if err != nil { +t.Fatalf("get validator key: %v", err) +} + +cfgData, cfgErr := os.ReadFile("test_config.json") +if cfgErr != nil { +t.Fatalf("read test_config.json: %v", cfgErr) +} +var cfg struct { +PredictorAddress string `json:"predictor_address"` +PredictorPrivKey string `json:"predictor_privkey"` +PredictorPubKey string `json:"predictor_pubkey"` +} +if jsonErr := json.Unmarshal(cfgData, &cfg); jsonErr != nil { +t.Fatalf("parse test_config.json: %v", jsonErr) +} +bettorB := &keyGroup{Address: cfg.PredictorAddress, PublicKey: cfg.PredictorPubKey, PrivateKey: cfg.PredictorPrivKey} + +// Fund bettor B +h, _ := getHeight() +fundMsg := &contract.MessageSend{ +FromAddress: hexDecode(addr), +ToAddress: hexDecode(bettorB.Address), +Amount: 200_000_000, +} +hash := submitSendTx(t, key, fundMsg, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("fund bettorB: %v", err) +} + +// Create market — B0 = 100 PRX +h, _ = getHeight() +const b0 = uint64(100_000_000) +nonce := uint64(time.Now().UnixMicro()) +createMsg := &contract.MessageCreateMarket{ +CreatorAddress: hexDecode(addr), +B0: b0, +ExpiryTime: h + 50, +Nonce: nonce, +Question: "Payout math verification market", +} +hash = submitTx(t, key, "create_market", "MessageCreateMarket", createMsg, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("create_market: %v", err) +} +marketId := contract.DeriveMarketId(hexDecode(addr), nonce) +t.Logf("Market ID: %x", marketId) + +// Bettor A — 1 YES share +h, _ = getHeight() +sharesA := contract.PRECISION_SCALE +lmsrSeed := b0 - contract.FINALIZATION_BOUNTY +halfSeed := lmsrSeed / 2 +costA, pe := contract.ComputeTradeCost(halfSeed, halfSeed, lmsrSeed, uint64(sharesA), true) +if pe != nil { +t.Fatalf("ComputeTradeCost A: %v", pe) +} +t.Logf("Expected cost A: %d uPRX", costA) + +predMsgA := &contract.MessageSubmitPrediction{ +MarketId: marketId, +BettorAddress: hexDecode(addr), +Outcome: true, +Shares: uint64(sharesA), +MaxCost: costA * 2, +} +hash = submitTx(t, key, "submit_prediction", "MessageSubmitPrediction", predMsgA, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("prediction A: %v", err) +} + +// Bettor B — 2 YES shares +h, _ = getHeight() +sharesB := contract.PRECISION_SCALE * 2 +qYesAfterA := halfSeed + uint64(sharesA) +costB, pe := contract.ComputeTradeCost(qYesAfterA, halfSeed, lmsrSeed, uint64(sharesB), true) +if pe != nil { +t.Fatalf("ComputeTradeCost B: %v", pe) +} +t.Logf("Expected cost B: %d uPRX", costB) + +predMsgB := &contract.MessageSubmitPrediction{ +MarketId: marketId, +BettorAddress: hexDecode(bettorB.Address), +Outcome: true, +Shares: uint64(sharesB), +MaxCost: costB * 2, +} +hash = submitTx(t, bettorB, "submit_prediction", "MessageSubmitPrediction", predMsgB, h) +if err := waitForTx(bettorB.Address, hash, 60*time.Second); err != nil { +t.Fatalf("prediction B: %v", err) +} + +// Register resolver (separate address — COI-2) +resolverKey, resolverAddr := setupResolver(t, key, addr, 500_000_000_000) + +// Wait for expiry +expiryTarget := createMsg.ExpiryTime + 2 +t.Logf("Waiting for expiry (height %d)...", expiryTarget) +for { +cur, _ := getHeight() +if cur >= expiryTarget { +break +} +t.Logf("Height %d / need %d", cur, expiryTarget) +time.Sleep(2 * time.Second) +} + +// Propose YES outcome +h, _ = getHeight() +bond := contract.ComputeMinBond(&contract.MarketState{BEff: lmsrSeed}) +propMsg := &contract.MessageProposeOutcome{ +MarketId: marketId, +ResolverAddress: hexDecode(resolverAddr), +ProposedOutcome: true, +ProposalBond: bond, +} +hash = submitTx(t, resolverKey, "propose_outcome", "MessageProposeOutcome", propMsg, h) +if err := waitForTx(resolverAddr, hash, 60*time.Second); err != nil { +t.Fatalf("propose_outcome: %v", err) +} + +// Wait for dispute window +disputeTarget := h + contract.TEST_DISPUTE_BLOCKS + 2 // TEST_DISPUTE_BLOCKS=20; ComputeDisputeBlocks not used (no PRAXIS_TEST_MODE in test binary) +t.Logf("Waiting for dispute window (height %d)...", disputeTarget) +for { +cur, _ := getHeight() +if cur >= disputeTarget { +break +} +t.Logf("Height %d / need %d", cur, disputeTarget) +time.Sleep(2 * time.Second) +} + +// Finalize +h, _ = getHeight() +finMsg := &contract.MessageFinalizeMarket{ +MarketId: marketId, +CallerAddr: hexDecode(addr), +} +hash = submitTx(t, key, "finalize_market", "MessageFinalizeMarket", finMsg, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("finalize_market: %v", err) +} +t.Log("Market finalized") + +// Compute expected pool and payouts. +// Pool = lmsrSeed + costA + costB (TX fees go to feePool, not market pool). +// totalWinShares = market.QYes after both predictions = halfSeed + sharesA + sharesB. +// This mirrors what handler_claim_winnings.go uses (market.QYes). +expectedPool := lmsrSeed + costA + costB +totalWinShares := halfSeed + uint64(sharesA) + uint64(sharesB) +expectedPayoutA := contract.ComputePayout(expectedPool, uint64(sharesA), totalWinShares) +expectedPayoutB := contract.ComputePayout(expectedPool, uint64(sharesB), totalWinShares) +t.Logf("Expected pool: %d totalWinShares: %d (halfSeed=%d sharesA=%d sharesB=%d)", +expectedPool, totalWinShares, halfSeed, sharesA, sharesB) +t.Logf("Expected payout A (1 share): %d uPRX", expectedPayoutA) +t.Logf("Expected payout B (2 shares): %d uPRX", expectedPayoutB) + +// Claim A +balABefore := getBalance(addr) +h, _ = getHeight() +claimMsgA := &contract.MessageClaimWinnings{ +MarketId: marketId, +ClaimantAddress: hexDecode(addr), +} +hash = submitTx(t, key, "claim_winnings", "MessageClaimWinnings", claimMsgA, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("claim A: %v", err) +} +balAAfter := getBalance(addr) +actualPayoutA := int64(balAAfter) - int64(balABefore) +t.Logf("Bettor A — before: %d after: %d payout: %d (expected ~%d)", +balABefore, balAAfter, actualPayoutA, expectedPayoutA) + +// Claim B +balBBefore := getBalance(bettorB.Address) +h, _ = getHeight() +claimMsgB := &contract.MessageClaimWinnings{ +MarketId: marketId, +ClaimantAddress: hexDecode(bettorB.Address), +} +hash = submitTx(t, bettorB, "claim_winnings", "MessageClaimWinnings", claimMsgB, h) +if err := waitForTx(bettorB.Address, hash, 60*time.Second); err != nil { +t.Fatalf("claim B: %v", err) +} +balBAfter := getBalance(bettorB.Address) +actualPayoutB := int64(balBAfter) - int64(balBBefore) +t.Logf("Bettor B — before: %d after: %d payout: %d (expected ~%d)", +balBBefore, balBAfter, actualPayoutB, expectedPayoutB) + +// Assertions — allow 10% tolerance to account for the difference between +// our off-chain cost estimate (computed before TXs land) and the actual +// on-chain cost at the block the TX was included. The pro-rata ratio is +// the critical invariant; absolute amounts may vary with market state. +toleranceA := int64(expectedPayoutA) / 10 +toleranceB := int64(expectedPayoutB) / 10 +diffA := actualPayoutA - int64(expectedPayoutA) + int64(testFee) +diffB := actualPayoutB - int64(expectedPayoutB) + int64(testFee) +if diffA < -toleranceA || diffA > toleranceA { +t.Errorf("Payout A mismatch: got %d expected %d (diff %d, tolerance ±%d)", +actualPayoutA, expectedPayoutA, diffA, toleranceA) +} +if diffB < -toleranceB || diffB > toleranceB { +t.Errorf("Payout B mismatch: got %d expected %d (diff %d, tolerance ±%d)", +actualPayoutB, expectedPayoutB, diffB, toleranceB) +} + +// B holds 2x the shares of A so should receive ~2x the payout. +// Use ratio check rather than exact multiply to handle integer division dust. +ratioTolerance := int64(expectedPayoutA) / 10 +ratioOk := int64(expectedPayoutB) >= int64(expectedPayoutA)*2-ratioTolerance && int64(expectedPayoutB) <= int64(expectedPayoutA)*2+ratioTolerance +if !ratioOk { +t.Errorf("Payout ratio wrong: A=%d B=%d (expected B≈2×A)", expectedPayoutA, expectedPayoutB) +} + +t.Log("Payout math verified ✓") +} + +func TestAllBettorsOneSide(t *testing.T) { +addr := "e7c7dad131a03f7ea0cc09a637ad096eb3495f77" +key, err := keystoreGetKey(addr, "") +if err != nil { +t.Fatalf("get validator key: %v", err) +} + +// Create market +h, _ := getHeight() +const b0 = uint64(60_000_000) +nonce := uint64(time.Now().UnixMicro()) +createMsg := &contract.MessageCreateMarket{ +CreatorAddress: hexDecode(addr), +B0: b0, +ExpiryTime: h + 30, +Nonce: nonce, +Question: "All bettors one side test", +} +hash := submitTx(t, key, "create_market", "MessageCreateMarket", createMsg, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("create_market: %v", err) +} +marketId := contract.DeriveMarketId(hexDecode(addr), nonce) +t.Logf("Market ID: %x", marketId) + +// Single bettor — YES only, nobody bets NO +h, _ = getHeight() +lmsrSeed := b0 - contract.FINALIZATION_BOUNTY +halfSeed := lmsrSeed / 2 +shares := contract.PRECISION_SCALE +cost, pe := contract.ComputeTradeCost(halfSeed, halfSeed, lmsrSeed, shares, true) +if pe != nil { +t.Fatalf("ComputeTradeCost: %v", pe) +} +predMsg := &contract.MessageSubmitPrediction{ +MarketId: marketId, +BettorAddress: hexDecode(addr), +Outcome: true, +Shares: shares, +MaxCost: cost * 2, +} +hash = submitTx(t, key, "submit_prediction", "MessageSubmitPrediction", predMsg, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("submit_prediction: %v", err) +} +t.Logf("Single YES bettor cost: %d uPRX", cost) + +// Register resolver (separate address — COI-2) +resolverKey, resolverAddr := setupResolver(t, key, addr, 500_000_000_000) + +expiryTarget := createMsg.ExpiryTime + 2 +t.Logf("Waiting for expiry (height %d)...", expiryTarget) +for { +cur, _ := getHeight() +if cur >= expiryTarget { +break +} +time.Sleep(2 * time.Second) +} + +// Propose YES outcome +h, _ = getHeight() +bond := contract.ComputeMinBond(&contract.MarketState{BEff: lmsrSeed}) +propMsg := &contract.MessageProposeOutcome{ +MarketId: marketId, +ResolverAddress: hexDecode(resolverAddr), +ProposedOutcome: true, +ProposalBond: bond, +} +hash = submitTx(t, resolverKey, "propose_outcome", "MessageProposeOutcome", propMsg, h) +if err := waitForTx(resolverAddr, hash, 60*time.Second); err != nil { +t.Fatalf("propose_outcome: %v", err) +} + +// Wait for dispute window +disputeTarget := h + contract.TEST_DISPUTE_BLOCKS + 2 +t.Logf("Waiting for dispute window (height %d)...", disputeTarget) +for { +cur, _ := getHeight() +if cur >= disputeTarget { +break +} +time.Sleep(2 * time.Second) +} + +// Finalize +h, _ = getHeight() +finMsg := &contract.MessageFinalizeMarket{ +MarketId: marketId, +CallerAddr: hexDecode(addr), +} +hash = submitTx(t, key, "finalize_market", "MessageFinalizeMarket", finMsg, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("finalize_market: %v", err) +} +t.Log("Market finalized") + +// Claim — sole YES winner should receive entire pool +expectedPool := lmsrSeed + cost +totalWinShares := halfSeed + shares +expectedPayout := contract.ComputePayout(expectedPool, shares, totalWinShares) +t.Logf("Expected pool: %d totalWinShares: %d expectedPayout: %d", expectedPool, totalWinShares, expectedPayout) + +balBefore := getBalance(addr) +h, _ = getHeight() +claimMsg := &contract.MessageClaimWinnings{ +MarketId: marketId, +ClaimantAddress: hexDecode(addr), +} +hash = submitTx(t, key, "claim_winnings", "MessageClaimWinnings", claimMsg, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("claim_winnings: %v", err) +} +balAfter := getBalance(addr) +actualPayout := int64(balAfter) - int64(balBefore) +t.Logf("Balance before: %d after: %d payout: %d (expected ~%d)", +balBefore, balAfter, actualPayout, expectedPayout) + +// Payout must be positive +if actualPayout <= 0 { +t.Errorf("Expected positive payout, got %d", actualPayout) +} + +// Payout within 15% of expected — off-chain estimate uses pre-TX market state +// so actual on-chain cost may differ slightly from ComputeTradeCost estimate. +tolerance := int64(expectedPayout) * 15 / 100 +diff := actualPayout - int64(expectedPayout) + int64(testFee) +if diff < -tolerance || diff > tolerance { +t.Errorf("Payout mismatch: got %d expected %d (diff %d, tolerance ±%d)", +actualPayout, expectedPayout, diff, tolerance) +} + +t.Log("All-one-side payout verified ✓") +} + +func TestCancelledMarketRefund(t *testing.T) { +addr := "e7c7dad131a03f7ea0cc09a637ad096eb3495f77" +key, err := keystoreGetKey(addr, "") +if err != nil { +t.Fatalf("get validator key: %v", err) +} + +// Create market +h, _ := getHeight() +const b0 = uint64(60_000_000) +nonce := uint64(time.Now().UnixMicro()) +createMsg := &contract.MessageCreateMarket{ +CreatorAddress: hexDecode(addr), +B0: b0, +ExpiryTime: h + 30, +Nonce: nonce, +Question: "Cancelled market refund test", +} +hash := submitTx(t, key, "create_market", "MessageCreateMarket", createMsg, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("create_market: %v", err) +} +marketId := contract.DeriveMarketId(hexDecode(addr), nonce) +t.Logf("Market ID: %x", marketId) + +// Submit prediction +h, _ = getHeight() +lmsrSeed := b0 - contract.FINALIZATION_BOUNTY +halfSeed := lmsrSeed / 2 +shares := contract.PRECISION_SCALE +cost, pe := contract.ComputeTradeCost(halfSeed, halfSeed, lmsrSeed, shares, true) +if pe != nil { +t.Fatalf("ComputeTradeCost: %v", pe) +} +predMsg := &contract.MessageSubmitPrediction{ +MarketId: marketId, +BettorAddress: hexDecode(addr), +Outcome: true, +Shares: shares, +MaxCost: cost * 2, +} +hash = submitTx(t, key, "submit_prediction", "MessageSubmitPrediction", predMsg, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("submit_prediction: %v", err) +} +t.Logf("Prediction cost: %d uPRX", cost) + +// Wait past cancel threshold: ExpiryTime + TEST_RESOLUTION_DELAY + TEST_GRACE_PERIOD + margin +// TEST_RESOLUTION_DELAY=2, TEST_GRACE_PERIOD=2, so cancelThreshold = ExpiryTime + 4 +cancelTarget := createMsg.ExpiryTime + contract.TEST_RESOLUTION_DELAY + contract.TEST_GRACE_PERIOD + 2 +t.Logf("Waiting for cancel threshold (height %d)...", cancelTarget) +for { +cur, _ := getHeight() +if cur >= cancelTarget { +break +} +t.Logf("Height %d / need %d", cur, cancelTarget) +time.Sleep(2 * time.Second) +} +t.Log("Cancel threshold passed — market should auto-cancel on next claim") + +// Claim — should trigger auto-cancel and return CostPaid +balBefore := getBalance(addr) +h, _ = getHeight() +claimMsg := &contract.MessageClaimWinnings{ +MarketId: marketId, +ClaimantAddress: hexDecode(addr), +} +hash = submitTx(t, key, "claim_winnings", "MessageClaimWinnings", claimMsg, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("claim_winnings: %v", err) +} +balAfter := getBalance(addr) +actualRefund := int64(balAfter) - int64(balBefore) +t.Logf("Balance before: %d after: %d refund: %d (expected cost ~%d)", +balBefore, balAfter, actualRefund, cost) + +// Refund must be positive +if actualRefund <= 0 { +t.Errorf("Expected positive refund, got %d", actualRefund) +} + +// Refund should be close to original cost paid (within 15%) +tolerance := int64(cost) * 15 / 100 +diff := actualRefund - int64(cost) + int64(testFee) +if diff < -tolerance || diff > tolerance { +t.Errorf("Refund mismatch: got %d expected ~%d (diff %d, tolerance ±%d)", +actualRefund, cost, diff, tolerance) +} + +t.Log("Cancelled market refund verified ✓") +} + +func TestLosingBettorZeroPayout(t *testing.T) { +addr := "e7c7dad131a03f7ea0cc09a637ad096eb3495f77" +key, err := keystoreGetKey(addr, "") +if err != nil { +t.Fatalf("get validator key: %v", err) +} + +cfgData, cfgErr := os.ReadFile("test_config.json") +if cfgErr != nil { +t.Fatalf("read test_config.json: %v", cfgErr) +} +var cfg struct { +PredictorAddress string `json:"predictor_address"` +PredictorPrivKey string `json:"predictor_privkey"` +PredictorPubKey string `json:"predictor_pubkey"` +} +if jsonErr := json.Unmarshal(cfgData, &cfg); jsonErr != nil { +t.Fatalf("parse test_config.json: %v", jsonErr) +} +loser := &keyGroup{Address: cfg.PredictorAddress, PublicKey: cfg.PredictorPubKey, PrivateKey: cfg.PredictorPrivKey} + +// Fund loser +h, _ := getHeight() +fundMsg := &contract.MessageSend{ +FromAddress: hexDecode(addr), +ToAddress: hexDecode(loser.Address), +Amount: 100_000_000, +} +hash := submitSendTx(t, key, fundMsg, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("fund loser: %v", err) +} + +// Create market +h, _ = getHeight() +const b0 = uint64(60_000_000) +nonce := uint64(time.Now().UnixMicro()) +createMsg := &contract.MessageCreateMarket{ +CreatorAddress: hexDecode(addr), +B0: b0, +ExpiryTime: h + 30, +Nonce: nonce, +Question: "Losing bettor zero payout test", +} +hash = submitTx(t, key, "create_market", "MessageCreateMarket", createMsg, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("create_market: %v", err) +} +marketId := contract.DeriveMarketId(hexDecode(addr), nonce) +t.Logf("Market ID: %x", marketId) + +// Bettor A (validator) bets YES +h, _ = getHeight() +lmsrSeed := b0 - contract.FINALIZATION_BOUNTY +halfSeed := lmsrSeed / 2 +sharesA := contract.PRECISION_SCALE +costA, pe := contract.ComputeTradeCost(halfSeed, halfSeed, lmsrSeed, sharesA, true) +if pe != nil { +t.Fatalf("ComputeTradeCost A: %v", pe) +} +predMsgA := &contract.MessageSubmitPrediction{ +MarketId: marketId, +BettorAddress: hexDecode(addr), +Outcome: true, +Shares: sharesA, +MaxCost: costA * 2, +} +hash = submitTx(t, key, "submit_prediction", "MessageSubmitPrediction", predMsgA, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("prediction A (YES): %v", err) +} +t.Logf("Bettor A (YES) cost: %d uPRX", costA) + +// Bettor B (loser) bets NO +h, _ = getHeight() +sharesB := contract.PRECISION_SCALE +qYesAfterA := halfSeed + sharesA +costB, pe := contract.ComputeTradeCost(qYesAfterA, halfSeed, lmsrSeed, sharesB, false) +if pe != nil { +t.Fatalf("ComputeTradeCost B: %v", pe) +} +predMsgB := &contract.MessageSubmitPrediction{ +MarketId: marketId, +BettorAddress: hexDecode(loser.Address), +Outcome: false, +Shares: sharesB, +MaxCost: costB * 2, +} +hash = submitTx(t, loser, "submit_prediction", "MessageSubmitPrediction", predMsgB, h) +if err := waitForTx(loser.Address, hash, 60*time.Second); err != nil { +t.Fatalf("prediction B (NO): %v", err) +} +t.Logf("Bettor B (NO) cost: %d uPRX", costB) + +// Register resolver (separate address — COI-2) +resolverKey, resolverAddr := setupResolver(t, key, addr, 500_000_000_000) + +expiryTarget := createMsg.ExpiryTime + 2 +t.Logf("Waiting for expiry (height %d)...", expiryTarget) +for { +cur, _ := getHeight() +if cur >= expiryTarget { +break +} +time.Sleep(2 * time.Second) +} + +// Propose YES outcome (A wins, B loses) +h, _ = getHeight() +bond := contract.ComputeMinBond(&contract.MarketState{BEff: lmsrSeed}) +propMsg := &contract.MessageProposeOutcome{ +MarketId: marketId, +ResolverAddress: hexDecode(resolverAddr), +ProposedOutcome: true, +ProposalBond: bond, +} +hash = submitTx(t, resolverKey, "propose_outcome", "MessageProposeOutcome", propMsg, h) +if err := waitForTx(resolverAddr, hash, 60*time.Second); err != nil { +t.Fatalf("propose_outcome: %v", err) +} + +// Wait for dispute window +disputeTarget := h + contract.TEST_DISPUTE_BLOCKS + 2 +t.Logf("Waiting for dispute window (height %d)...", disputeTarget) +for { +cur, _ := getHeight() +if cur >= disputeTarget { +break +} +time.Sleep(2 * time.Second) +} + +// Finalize +h, _ = getHeight() +finMsg := &contract.MessageFinalizeMarket{ +MarketId: marketId, +CallerAddr: hexDecode(addr), +} +hash = submitTx(t, key, "finalize_market", "MessageFinalizeMarket", finMsg, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("finalize_market: %v", err) +} +t.Log("Market finalized — YES wins") + +// Bettor B (loser) claims — should get zero payout +balBBefore := getBalance(loser.Address) +h, _ = getHeight() +claimMsgB := &contract.MessageClaimWinnings{ +MarketId: marketId, +ClaimantAddress: hexDecode(loser.Address), +} +hash = submitTx(t, loser, "claim_winnings", "MessageClaimWinnings", claimMsgB, h) +if err := waitForTx(loser.Address, hash, 60*time.Second); err != nil { +t.Fatalf("claim B: %v", err) +} +balBAfter := getBalance(loser.Address) +actualPayout := int64(balBAfter) - int64(balBBefore) +t.Logf("Loser balance before: %d after: %d diff: %d", balBBefore, balBAfter, actualPayout) + +// Loser should receive nothing — zero or negative (TX fee) is acceptable +if actualPayout > 0 { +t.Errorf("Loser should not receive payout, got %d", actualPayout) +} + +t.Log("Losing bettor zero payout verified ✓") +} + + +func TestRegisterNewWalletAsResolver(t *testing.T) { + validatorKg, _ := keystoreGetKey("e7c7dad131a03f7ea0cc09a637ad096eb3495f77", "") + kg := &keyGroup{ + Address: "062b9d112eef6fbea23e3cc44670b9bc8778efad", + PublicKey: "b46e36b0d68b7574a84accfc0e101b83d3d1ba6aca035a4a3889d4ee955488deab3dc217dc9362bb691c11be96ad606c", + PrivateKey: "1645806b57847d4c3683ab27f8a1c23384c372839f994207e0e47dd7769a2a2d", + } + h, _ := getHeight() + fundMsg := &contract.MessageSend{ + FromAddress: hexDecode(validatorKg.Address), + ToAddress: hexDecode(kg.Address), + Amount: 600_000_000_000, + } + fundHash := submitSendTx(t, validatorKg, fundMsg, h) + if err := waitForTx(validatorKg.Address, fundHash, 60*time.Second); err != nil { + t.Fatalf("fund failed: %v", err) + } + h, _ = getHeight() + msg := &contract.MessageRegisterResolver{ + ResolverAddress: hexDecode(kg.Address), + StakeAmount: 500_000_000_000, + } + txHash := submitTx(t, kg, "register_resolver", "MessageRegisterResolver", msg, h) + if err := waitForTx(kg.Address, txHash, 60*time.Second); err != nil { + t.Fatalf("register resolver failed: %v", err) + } + t.Log("Resolver registered! TX:", txHash) +} + +func TestSetupNewWallet(t *testing.T) { + validatorKg, _ := keystoreGetKey("e7c7dad131a03f7ea0cc09a637ad096eb3495f77", "") + newKg := &keyGroup{ + Address: "8e14dc0ce537f1c75036f11d7495d60882aa6731", + PublicKey: "971cbbad4e8c6893b3cd1d31f733dd25058e05ef56abf2e85ba34792704c8ea5510fdc3a57711f695f4f5d45d0e5fa51", + PrivateKey: "08562bcd9856ba7b2eb9270b9dee9a6ae7a497fce0d4dcab87b3c2f05157dfaf", + } + + // Step 1: Fund new wallet + h, _ := getHeight() + sendMsg := &contract.MessageSend{ + FromAddress: hexDecode(validatorKg.Address), + ToAddress: hexDecode(newKg.Address), + Amount: 10_000_000_000_000, + } + txHash := submitSendTx(t, validatorKg, sendMsg, h) + if err := waitForTx(validatorKg.Address, txHash, 60*time.Second); err != nil { + t.Fatalf("fund failed: %v", err) + } + t.Log("Funded! TX:", txHash) + + // Step 2: Register as resolver + h, _ = getHeight() + regMsg := &contract.MessageRegisterResolver{ + ResolverAddress: hexDecode(newKg.Address), + StakeAmount: 500_000_000_000, + } + txHash = submitTx(t, newKg, "register_resolver", "MessageRegisterResolver", regMsg, h) + if err := waitForTx(newKg.Address, txHash, 60*time.Second); err != nil { + t.Fatalf("register resolver failed: %v", err) + } + t.Log("Resolver registered! TX:", txHash) +} + +// ───────────────────────────────────────────────────────────────────────────── +// COI ADVERSARIAL TESTS — Issue-19 +// These tests verify that conflict-of-interest guards reject bad transactions. +// ───────────────────────────────────────────────────────────────────────────── + +// TestCOI1ResolverWithPosition verifies that a resolver who holds a position +// in the market they are resolving is rejected by propose_outcome (COI-1). +func TestCOI1ResolverWithPosition(t *testing.T) { +validatorAddr := "e7c7dad131a03f7ea0cc09a637ad096eb3495f77" +validatorKey, _ := keystoreGetKey(validatorAddr, "") +predictorAddr := "8f8b550064ec4ee4551d1666cb0ee5d35fc5154a" +predictorKey := &keyGroup{ +Address: predictorAddr, +PublicKey: "88634c8e0fd9ee8911b362e5aff8c046154263e9b8e507fc5efe5b5d9cb6cb4fd14c3672bccb929c411e3050ccca44a9", +PrivateKey: "1c91a4882751adc1fa4f2574c4321bf144e36411ade55e099e9c6ffece87ee49", +} + +h, _ := getHeight() + +// 1. Create market +nonce := uint64(time.Now().UnixMicro()) +createMsg := &contract.MessageCreateMarket{ +CreatorAddress: hexDecode(validatorAddr), +B0: 60_000_000, +ExpiryTime: h + 150, +Nonce: nonce, +Question: "COI-1 test market", +} +txHash := submitTx(t, validatorKey, "create_market", "MessageCreateMarket", createMsg, h) +if err := waitForTx(validatorAddr, txHash, 60*time.Second); err != nil { +t.Fatalf("create market: %v", err) +} +marketId := contract.DeriveMarketId(hexDecode(validatorAddr), nonce) +t.Logf("market_id: %x", marketId) + +// 2. Fund predictor and have them bet YES +h2, _ := getHeight() +sendMsg := &contract.MessageSend{ +FromAddress: hexDecode(validatorAddr), +ToAddress: hexDecode(predictorAddr), +Amount: 10_000_000, +} +sendHash := submitSendTx(t, validatorKey, sendMsg, h2) +if err := waitForTx(validatorAddr, sendHash, 60*time.Second); err != nil { +t.Fatalf("fund predictor: %v", err) +} + +h3, _ := getHeight() +betMsg := &contract.MessageSubmitPrediction{ +MarketId: marketId, +BettorAddress: hexDecode(predictorAddr), +Outcome: true, +Shares: contract.PRECISION_SCALE, +MaxCost: 5_000_000, +} +betHash := submitTx(t, predictorKey, "submit_prediction", "MessageSubmitPrediction", betMsg, h3) +if err := waitForTx(predictorAddr, betHash, 60*time.Second); err != nil { +t.Fatalf("bet: %v", err) +} +t.Log("Predictor holds YES position") + +// 3. Register predictor as resolver — fund first, then register +h4b, _ := getHeight() +fundMsg := &contract.MessageSend{ +FromAddress: hexDecode(validatorAddr), +ToAddress: hexDecode(predictorAddr), +Amount: 600_000_000_000, +} +fundHash := submitSendTx(t, validatorKey, fundMsg, h4b) +if err := waitForTx(validatorAddr, fundHash, 60*time.Second); err != nil { +t.Fatalf("fund for stake: %v", err) +} +h4c, _ := getHeight() +regMsg := &contract.MessageRegisterResolver{ +ResolverAddress: hexDecode(predictorAddr), +StakeAmount: 500_000_000_000, +} +regHash := submitTx(t, predictorKey, "register_resolver", "MessageRegisterResolver", regMsg, h4c) +if err := waitForTx(predictorAddr, regHash, 60*time.Second); err != nil { +t.Fatalf("register resolver: %v", err) +} +t.Log("Predictor registered as resolver") + +// 4. Wait for market expiry +t.Log("Waiting for market to expire...") +for { +cur, err := getHeight() +if err == nil && cur > 0 && cur > createMsg.ExpiryTime { +break +} +time.Sleep(5 * time.Second) +} + +// 5. Attempt propose_outcome as resolver who holds a position — must fail +h5, _ := getHeight() +propMsg := &contract.MessageProposeOutcome{ +MarketId: marketId, +ResolverAddress: hexDecode(predictorAddr), +ProposedOutcome: true, +ProposalBond: 100_000_000, +} +txHash2 := submitTx(t, predictorKey, "propose_outcome", "MessageProposeOutcome", propMsg, h5) +err := waitForTx(predictorAddr, txHash2, 60*time.Second) +if err == nil { +t.Fatal("COI-1 FAILED: propose_outcome succeeded for resolver with position — expected rejection") +} +t.Logf("COI-1 PASS: propose_outcome correctly rejected resolver with position: %v", err) +} + +// TestCOI2CreatorCannotResolve verifies that the market creator cannot also +// act as the resolver for the same market (COI-2). +func TestCOI2CreatorCannotResolve(t *testing.T) { +validatorAddr := "e7c7dad131a03f7ea0cc09a637ad096eb3495f77" +validatorKey, _ := keystoreGetKey(validatorAddr, "") + +h, _ := getHeight() + +// 1. Create market — validator is the creator +nonce := uint64(time.Now().UnixMicro()) +createMsg := &contract.MessageCreateMarket{ +CreatorAddress: hexDecode(validatorAddr), +B0: 60_000_000, +ExpiryTime: h + 30, +Nonce: nonce, +Question: "COI-2 test market", +} +txHash := submitTx(t, validatorKey, "create_market", "MessageCreateMarket", createMsg, h) +if err := waitForTx(validatorAddr, txHash, 60*time.Second); err != nil { +t.Fatalf("create market: %v", err) +} +marketId := contract.DeriveMarketId(hexDecode(validatorAddr), nonce) +t.Logf("market_id: %x", marketId) + +// 2. Register creator as resolver +h2, _ := getHeight() +regMsg := &contract.MessageRegisterResolver{ +ResolverAddress: hexDecode(validatorAddr), +StakeAmount: 500_000_000_000, +} +regHash := submitTx(t, validatorKey, "register_resolver", "MessageRegisterResolver", regMsg, h2) +if err := waitForTx(validatorAddr, regHash, 60*time.Second); err != nil { +t.Fatalf("register resolver: %v", err) +} + +// 3. Wait for expiry +t.Log("Waiting for market to expire...") +for { +cur, _ := getHeight() +if cur > createMsg.ExpiryTime { +break +} +time.Sleep(5 * time.Second) +} + +// 4. Attempt propose_outcome as creator — must fail (COI-2) +h3, _ := getHeight() +propMsg := &contract.MessageProposeOutcome{ +MarketId: marketId, +ResolverAddress: hexDecode(validatorAddr), +ProposedOutcome: true, +ProposalBond: 100_000_000, +} +txHash2 := submitTx(t, validatorKey, "propose_outcome", "MessageProposeOutcome", propMsg, h3) +err := waitForTx(validatorAddr, txHash2, 60*time.Second) +if err == nil { +t.Fatal("COI-2 FAILED: propose_outcome succeeded for creator-as-resolver — expected rejection") +} +t.Logf("COI-2 PASS: propose_outcome correctly rejected creator-as-resolver: %v", err) +} + +// TestCOI3PositionCapEnforced verifies that a single address cannot accumulate +// more than 20% of the winning side shares (COI-3). +func TestCOI3PositionCapEnforced(t *testing.T) { +validatorAddr := "e7c7dad131a03f7ea0cc09a637ad096eb3495f77" +validatorKey, _ := keystoreGetKey(validatorAddr, "") + +h, _ := getHeight() + +// 1. Create market +nonce := uint64(time.Now().UnixMicro()) +createMsg := &contract.MessageCreateMarket{ +CreatorAddress: hexDecode(validatorAddr), +B0: 60_000_000, +ExpiryTime: h + 50000, +Nonce: nonce, +Question: "COI-3 cap test market", +} +txHash := submitTx(t, validatorKey, "create_market", "MessageCreateMarket", createMsg, h) +if err := waitForTx(validatorAddr, txHash, 60*time.Second); err != nil { +t.Fatalf("create market: %v", err) +} +marketId := contract.DeriveMarketId(hexDecode(validatorAddr), nonce) +t.Logf("market_id: %x", marketId) + +// 2. Submit a large YES bet that would exceed 20% of YES shares. +// Initial QYes = B0/2 - FINALIZATION_BOUNTY/2 = ~5_000_000 shares. +// Requesting 50_000_000 shares (10x the initial side) — well over 20%. +h2, _ := getHeight() +betMsg := &contract.MessageSubmitPrediction{ +MarketId: marketId, +BettorAddress: hexDecode(validatorAddr), +Outcome: true, +Shares: 50_000_000 * contract.PRECISION_SCALE, +MaxCost: 500_000_000, +} +txHash2 := submitTx(t, validatorKey, "submit_prediction", "MessageSubmitPrediction", betMsg, h2) +err := waitForTx(validatorAddr, txHash2, 60*time.Second) +if err == nil { +t.Fatal("COI-3 FAILED: oversized position was accepted — expected ErrPositionCapExceeded") +} +t.Logf("COI-3 PASS: oversized position correctly rejected: %v", err) +} + +// ───────────────────────────────────────────────────────────────────────────── +// COI ADVERSARIAL TESTS — Issue-19 +// ───────────────────────────────────────────────────────────────────────────── + + +func TestPRISFeeFlow(t *testing.T) { +addr := "e7c7dad131a03f7ea0cc09a637ad096eb3495f77" +key, err := keystoreGetKey(addr, "") +if err != nil { +t.Fatalf("get key: %v", err) +} + +// Create market — B0 = 60 PRX +h, _ := getHeight() +nonce := uint64(time.Now().UnixMicro()) +const b0 = uint64(60_000_000) +createMsg := &contract.MessageCreateMarket{ +CreatorAddress: hexDecode(addr), +B0: b0, +ExpiryTime: h + 80, +Nonce: nonce, +Question: "PRIS fee flow verification", +} +balBefore := getBalance(addr) +hash := submitTx(t, key, "create_market", "MessageCreateMarket", createMsg, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("create_market: %v", err) +} +balAfter := getBalance(addr) +marketId := contract.DeriveMarketId(hexDecode(addr), nonce) +t.Logf("Market ID: %x", marketId) +t.Logf("Creation cost: %d uPRX (B0+bond+fee)", balBefore-balAfter) + +// Submit prediction — check 2% fee on top +h, _ = getHeight() +lmsrSeed := b0 - contract.FINALIZATION_BOUNTY +halfSeed := lmsrSeed / 2 +shares := contract.PRECISION_SCALE +tradeCost, pe := contract.ComputeTradeCost(halfSeed, halfSeed, lmsrSeed, uint64(shares), true) +if pe != nil { +t.Fatalf("ComputeTradeCost: %v", pe) +} +creatorFee := tradeCost * contract.CREATOR_FEE_BPS / 10_000 +resolverFee := tradeCost * contract.RESOLVER_FEE_BPS / 10_000 +expectedTotalCost := tradeCost + creatorFee + resolverFee + testFee +t.Logf("tradeCost=%d creatorFee=%d resolverFee=%d totalExpected=%d", tradeCost, creatorFee, resolverFee, expectedTotalCost) + +bettorBalBefore := getBalance(addr) +predMsg := &contract.MessageSubmitPrediction{ +MarketId: marketId, +BettorAddress: hexDecode(addr), +Outcome: true, +Shares: uint64(shares), +MaxCost: expectedTotalCost * 2, +} +hash = submitTx(t, key, "submit_prediction", "MessageSubmitPrediction", predMsg, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("submit_prediction: %v", err) +} +bettorBalAfter := getBalance(addr) +actualCost := int64(bettorBalBefore) - int64(bettorBalAfter) +t.Logf("Bettor cost: %d (expected ~%d)", actualCost, expectedTotalCost) +if actualCost < int64(tradeCost+creatorFee+resolverFee) { +t.Errorf("PRIS fees not charged: cost %d below tradeCost+fees %d", actualCost, tradeCost+creatorFee+resolverFee) +} else { +t.Log("PRIS fees charged on top of tradeCost ✓") +} + +// Register resolver and finalize +resolverKey, resolverAddr := setupResolver(t, key, addr, 500_000_000_000) + +// Wait for expiry +expiryTarget := createMsg.ExpiryTime + 2 +for { +cur, _ := getHeight() +if cur >= expiryTarget { break } +time.Sleep(2 * time.Second) +} + +// Propose outcome +h, _ = getHeight() +bond := contract.ComputeMinBond(&contract.MarketState{BEff: lmsrSeed}) +propMsg := &contract.MessageProposeOutcome{ +MarketId: marketId, +ResolverAddress: hexDecode(resolverAddr), +ProposedOutcome: true, +ProposalBond: bond, +} +hash = submitTx(t, resolverKey, "propose_outcome", "MessageProposeOutcome", propMsg, h) +if err := waitForTx(resolverAddr, hash, 60*time.Second); err != nil { +t.Fatalf("propose_outcome: %v", err) +} + +// Wait for dispute window +disputeTarget := h + contract.TEST_DISPUTE_BLOCKS + 2 +for { +cur, _ := getHeight() +if cur >= disputeTarget { break } +time.Sleep(2 * time.Second) +} + +// Finalize — resolver should receive resolver fee pool +resolverBalBefore := getBalance(resolverAddr) +h, _ = getHeight() +finMsg := &contract.MessageFinalizeMarket{ +MarketId: marketId, +CallerAddr: hexDecode(addr), +} +hash = submitTx(t, key, "finalize_market", "MessageFinalizeMarket", finMsg, h) +if err := waitForTx(addr, hash, 60*time.Second); err != nil { +t.Fatalf("finalize_market: %v", err) +} +resolverBalAfter := getBalance(resolverAddr) +resolverGain := int64(resolverBalAfter) - int64(resolverBalBefore) +t.Logf("Resolver balance change at finalization: %d uPRX (expected ~%d resolver fee)", resolverGain, resolverFee) +if resolverGain > 0 { +t.Log("Resolver fee paid at finalization ✓") +} else { +t.Errorf("Resolver fee NOT paid at finalization: gain=%d", resolverGain) +} + +t.Log("PRIS fee flow verified ✓") +} + +func TestUnstakeResolver(t *testing.T) { +validatorAddr := "e7c7dad131a03f7ea0cc09a637ad096eb3495f77" +validatorKey, err := keystoreGetKey(validatorAddr, "") +if err != nil { +t.Fatalf("key: %v", err) +} + +// Fresh address every run — avoids UnbondingAmount stale state +import_suffix := fmt.Sprintf("%d", time.Now().UnixMicro()%9999999) +freshAddr, err2 := keystoreNewKey("unstake_test_"+import_suffix, testPassword) +if err2 != nil { + t.Fatalf("new key: %v", err2) +} +resolverKey, err3 := keystoreGetKey(freshAddr, testPassword) +if err3 != nil { + t.Fatalf("load fresh key: %v", err3) +} +resolverAddr := freshAddr +h, _ := getHeight() +fHash := "" +fundMsg := &contract.MessageSend{ + FromAddress: hexDecode(validatorAddr), + ToAddress: hexDecode(resolverAddr), + Amount: 500_000_000_000 * 4, +} +fHash = submitSendTx(t, validatorKey, fundMsg, h) +if err := waitForTx(validatorAddr, fHash, 60*time.Second); err != nil { + t.Fatalf("fund fresh resolver: %v", err) +} +h, _ = getHeight() +regMsg2 := &contract.MessageRegisterResolver{ + ResolverAddress: hexDecode(resolverAddr), + StakeAmount: 500_000_000_000, +} +regHash := submitTx(t, resolverKey, "register_resolver", "MessageRegisterResolver", regMsg2, h) +if err := waitForTx(resolverAddr, regHash, 60*time.Second); err != nil { + t.Fatalf("register fresh resolver: %v", err) +} +t.Logf("Resolver: %s", resolverAddr) + +// ── Test 1: Cannot partial unstake below MIN_RESOLVER_STAKE ────────── +h, _ = getHeight() +unstakeMsg := &contract.MessageUnstakeResolver{ +ResolverAddress: hexDecode(resolverAddr), +Amount: 1, // would leave stake at MIN-1 +} +hash := submitTx(t, resolverKey, "unstake_resolver", "MessageUnstakeResolver", unstakeMsg, h) +if err := waitForTx(resolverAddr, hash, 60*time.Second); err == nil { +t.Error("Expected rejection: partial unstake below MIN_RESOLVER_STAKE should fail") +} else { +t.Log("Correctly rejected partial unstake below MIN_RESOLVER_STAKE ✓") +} + +// ── Test 2: Partial unstake — fund extra stake first ───────────────── +// Add 100,000 PRX extra stake so partial unstake is valid +h, _ = getHeight() +extraStake := uint64(100_000_000_000) +fundMsg = &contract.MessageSend{ +FromAddress: hexDecode(validatorAddr), +ToAddress: hexDecode(resolverAddr), +Amount: extraStake * 2, +} +fHash = submitSendTx(t, validatorKey, fundMsg, h) +if err := waitForTx(validatorAddr, fHash, 60*time.Second); err != nil { +t.Fatalf("fund extra stake: %v", err) +} + +h, _ = getHeight() +addMsg := &contract.MessageRegisterResolver{ +ResolverAddress: hexDecode(resolverAddr), +StakeAmount: extraStake, +} +aHash := submitTx(t, resolverKey, "register_resolver", "MessageRegisterResolver", addMsg, h) +if err := waitForTx(resolverAddr, aHash, 60*time.Second); err != nil { +t.Fatalf("add stake: %v", err) +} +t.Logf("Added extra stake %d, total stake now %d", extraStake, 500_000_000_000+extraStake) + +// Now partial unstake the extra 100,000 PRX — leaves MIN_RESOLVER_STAKE intact +h, _ = getHeight() +partialMsg := &contract.MessageUnstakeResolver{ +ResolverAddress: hexDecode(resolverAddr), +Amount: extraStake, +} +pHash := submitTx(t, resolverKey, "unstake_resolver", "MessageUnstakeResolver", partialMsg, h) +if err := waitForTx(resolverAddr, pHash, 60*time.Second); err != nil { +t.Fatalf("partial unstake: %v", err) +} +t.Log("Partial unstake submitted ✓") + +// ── Test 3: Cannot claim unbonded stake before release height ───────── +h, _ = getHeight() +claimMsg := &contract.MessageClaimUnbondedStake{ +ResolverAddress: hexDecode(resolverAddr), +} +cHash := submitTx(t, resolverKey, "claim_unbonded_stake", "MessageClaimUnbondedStake", claimMsg, h) +if err := waitForTx(resolverAddr, cHash, 60*time.Second); err == nil { +t.Error("Expected rejection: claim before unbonding period should fail") +} else { +t.Log("Correctly rejected early claim ✓") +} + +// ── Test 4: Cannot full exit while unbonding is pending ───────────── +h, _ = getHeight() +exitMsg := &contract.MessageUnstakeResolver{ + ResolverAddress: hexDecode(resolverAddr), + Amount: 0, // 0 = full exit +} +eHash := submitTx(t, resolverKey, "unstake_resolver", "MessageUnstakeResolver", exitMsg, h) +if err := waitForTx(resolverAddr, eHash, 60*time.Second); err == nil { + t.Error("Expected rejection: cannot full exit while unbonding pending") +} else { + t.Log("Correctly rejected full exit while unbonding pending ✓") +} + +// ── Test 5: Cannot unstake again after full exit ────────────────────── +h, _ = getHeight() +reMsg := &contract.MessageUnstakeResolver{ +ResolverAddress: hexDecode(resolverAddr), +Amount: 0, +} +rHash := submitTx(t, resolverKey, "unstake_resolver", "MessageUnstakeResolver", reMsg, h) +if err := waitForTx(resolverAddr, rHash, 60*time.Second); err == nil { +t.Error("Expected rejection: cannot unstake after full exit") +} else { +t.Log("Correctly rejected double exit ✓") +} + +t.Log("TestUnstakeResolver complete ✓") +} diff --git a/plugin/go/tutorial/rpc_test.go b/plugin/go/tutorial/rpc_test.go deleted file mode 100644 index 766d4d596d..0000000000 --- a/plugin/go/tutorial/rpc_test.go +++ /dev/null @@ -1,549 +0,0 @@ -package main - -import ( - "bytes" - cryptorand "crypto/rand" - "encoding/base64" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "net/http" - "testing" - "time" - - "github.com/canopy-network/go-plugin/tutorial/contract" - "github.com/canopy-network/go-plugin/tutorial/crypto" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/anypb" -) - -// TestPluginTransactions tests the full flow of plugin transactions via RPC -// 1. Adds two accounts to the keystore -// 2. Uses faucet to add balance to one account -// 3. Does a send transaction from the fauceted account to the other account -// 4. Sends a reward from that account back to the original account -func TestPluginTransactions(t *testing.T) { - // Configuration - adjust these for your local setup - queryRPCURL := "http://localhost:50002" // Query endpoints (height, account, tx submission) - adminRPCURL := "http://localhost:50003" // Admin endpoints (keystore management) - networkID := uint64(1) - chainID := uint64(1) - testPassword := "testpassword123" - - // Step 1: Create two new accounts in the keystore - t.Log("Step 1: Creating two accounts in keystore...") - - // Use random suffixes to avoid nickname conflicts from previous test runs - suffix := randomSuffix() - account1Addr, err := keystoreNewKey(adminRPCURL, "test_faucet_1_"+suffix, testPassword) - if err != nil { - t.Fatalf("Failed to create account 1: %v", err) - } - t.Logf("Created account 1: %s", account1Addr) - - account2Addr, err := keystoreNewKey(adminRPCURL, "test_faucet_2_"+suffix, testPassword) - if err != nil { - t.Fatalf("Failed to create account 2: %v", err) - } - t.Logf("Created account 2: %s", account2Addr) - - // Get current height for transaction - height, err := getHeight(queryRPCURL) - if err != nil { - t.Fatalf("Failed to get height: %v", err) - } - t.Logf("Current height: %d", height) - - // Get account 1's key for signing - account1Key, err := keystoreGetKey(adminRPCURL, account1Addr, testPassword) - if err != nil { - t.Fatalf("Failed to get account 1 key: %v", err) - } - - // Step 2: Use faucet to add balance to account 1 - t.Log("Step 2: Using faucet to add balance to account 1...") - - faucetAmount := uint64(1000000000) // 1000 tokens - faucetFee := uint64(10000) - - faucetTxHash, err := sendFaucetTx(queryRPCURL, account1Key, account1Addr, faucetAmount, faucetFee, networkID, chainID, height) - if err != nil { - t.Fatalf("Failed to send faucet transaction: %v", err) - } - t.Logf("Faucet transaction sent: %s", faucetTxHash) - - // Wait for faucet transaction to be included in a block - t.Log("Waiting for faucet transaction to be confirmed...") - included, err := waitForTxInclusion(queryRPCURL, account1Addr, faucetTxHash, 30*time.Second) - if err != nil { - t.Fatalf("Faucet transaction not included: %v", err) - } - if !included { - t.Fatal("Faucet transaction not included within timeout") - } - t.Log("Faucet transaction confirmed!") - - // Verify no failed transactions - failedCount, err := checkTxNotFailed(queryRPCURL, account1Addr) - if err != nil { - t.Logf("Warning: Could not check failed transactions: %v", err) - } else if failedCount > 0 { - t.Fatalf("Account 1 has %d failed transactions", failedCount) - } - - // Print balances after faucet - bal1, _ := getAccountBalance(queryRPCURL, account1Addr) - bal2, _ := getAccountBalance(queryRPCURL, account2Addr) - t.Logf("Balances after faucet - Account 1: %d, Account 2: %d", bal1, bal2) - - // Step 3: Send tokens from account 1 to account 2 - t.Log("Step 3: Sending tokens from account 1 to account 2...") - - sendAmount := uint64(100000000) // 100 tokens - sendFee := uint64(10000) - - // Update height - height, _ = getHeight(queryRPCURL) - - sendTxHash, err := sendSendTx(queryRPCURL, account1Key, account1Addr, account2Addr, sendAmount, sendFee, networkID, chainID, height) - if err != nil { - t.Fatalf("Failed to send transaction: %v", err) - } - t.Logf("Send transaction sent: %s", sendTxHash) - - // Wait for send transaction to be included - t.Log("Waiting for send transaction to be confirmed...") - included, err = waitForTxInclusion(queryRPCURL, account1Addr, sendTxHash, 30*time.Second) - if err != nil { - t.Fatalf("Send transaction not included: %v", err) - } - if !included { - t.Fatal("Send transaction not included within timeout") - } - t.Log("Send transaction confirmed!") - - // Verify no failed transactions - failedCount, err = checkTxNotFailed(queryRPCURL, account1Addr) - if err != nil { - t.Logf("Warning: Could not check failed transactions: %v", err) - } else if failedCount > 0 { - t.Fatalf("Account 1 has %d failed transactions", failedCount) - } - - // Print balances after send - bal1, _ = getAccountBalance(queryRPCURL, account1Addr) - bal2, _ = getAccountBalance(queryRPCURL, account2Addr) - t.Logf("Balances after send - Account 1: %d, Account 2: %d", bal1, bal2) - - // Step 4: Send reward from account 2 back to account 1 - t.Log("Step 4: Sending reward from account 2 back to account 1...") - - // Get account 2's key for signing - account2Key, err := keystoreGetKey(adminRPCURL, account2Addr, testPassword) - if err != nil { - t.Fatalf("Failed to get account 2 key: %v", err) - } - - rewardAmount := uint64(50000000) // 50 tokens - rewardFee := uint64(10000) - - // Update height - height, _ = getHeight(queryRPCURL) - - rewardTxHash, err := sendRewardTx(queryRPCURL, account2Key, account2Addr, account1Addr, rewardAmount, rewardFee, networkID, chainID, height) - if err != nil { - t.Fatalf("Failed to send reward transaction: %v", err) - } - t.Logf("Reward transaction sent: %s", rewardTxHash) - - // Wait for reward transaction to be included - t.Log("Waiting for reward transaction to be confirmed...") - included, err = waitForTxInclusion(queryRPCURL, account2Addr, rewardTxHash, 30*time.Second) - if err != nil { - t.Fatalf("Reward transaction not included: %v", err) - } - if !included { - t.Fatal("Reward transaction not included within timeout") - } - t.Log("Reward transaction confirmed!") - - // Verify no failed transactions for account 2 - failedCount, err = checkTxNotFailed(queryRPCURL, account2Addr) - if err != nil { - t.Logf("Warning: Could not check failed transactions: %v", err) - } else if failedCount > 0 { - t.Fatalf("Account 2 has %d failed transactions", failedCount) - } - - // Print final balances after reward - bal1, _ = getAccountBalance(queryRPCURL, account1Addr) - bal2, _ = getAccountBalance(queryRPCURL, account2Addr) - t.Logf("Final balances - Account 1: %d, Account 2: %d", bal1, bal2) - - t.Log("All transactions confirmed successfully!") - - // Print tip about verifying balances via RPC - t.Log("") - t.Log("--- Verify Account Balances ---") - t.Log("You can manually check account balances at any time using the /v1/query/account RPC endpoint:") - t.Logf(` curl -X POST %s/v1/query/account -H "Content-Type: application/json" -d '{"address": "%s"}'`, queryRPCURL, account1Addr) - t.Logf(` curl -X POST %s/v1/query/account -H "Content-Type: application/json" -d '{"address": "%s"}'`, queryRPCURL, account2Addr) - t.Log("See documentation: https://github.com/canopy-network/canopy/blob/main/cmd/rpc/README.md#account") -} - -// randomSuffix generates a random hex suffix for unique nicknames -func randomSuffix() string { - b := make([]byte, 4) - cryptorand.Read(b) - return hex.EncodeToString(b) -} - -// keyGroup holds key information from the keystore -type keyGroup struct { - Address string `json:"address"` - PublicKey string `json:"publicKey"` - PrivateKey string `json:"privateKey"` -} - -// keystoreNewKey creates a new key in the keystore using raw JSON -func keystoreNewKey(rpcURL, nickname, password string) (string, error) { - reqJSON := fmt.Sprintf(`{"nickname":"%s","password":"%s"}`, nickname, password) - - respBody, err := postRawJSON(rpcURL+"/v1/admin/keystore-new-key", reqJSON) - if err != nil { - return "", err - } - - var address string - if err := json.Unmarshal(respBody, &address); err != nil { - return "", fmt.Errorf("failed to parse response: %v, body: %s", err, string(respBody)) - } - - return address, nil -} - -// keystoreGetKey gets the key info from the keystore using raw JSON -func keystoreGetKey(rpcURL, address, password string) (*keyGroup, error) { - reqJSON := fmt.Sprintf(`{"address":"%s","password":"%s"}`, address, password) - - respBody, err := postRawJSON(rpcURL+"/v1/admin/keystore-get", reqJSON) - if err != nil { - return nil, err - } - - var kg keyGroup - if err := json.Unmarshal(respBody, &kg); err != nil { - return nil, fmt.Errorf("failed to parse response: %v, body: %s", err, string(respBody)) - } - - return &kg, nil -} - -// getHeight gets the current blockchain height using raw JSON -func getHeight(rpcURL string) (uint64, error) { - respBody, err := postRawJSON(rpcURL+"/v1/query/height", "{}") - if err != nil { - return 0, err - } - - var result struct { - Height uint64 `json:"height"` - } - if err := json.Unmarshal(respBody, &result); err != nil { - return 0, fmt.Errorf("failed to parse response: %v", err) - } - - return result.Height, nil -} - -// getAccountBalance gets the balance of an account using raw JSON -func getAccountBalance(rpcURL, address string) (uint64, error) { - reqJSON := fmt.Sprintf(`{"address":"%s"}`, address) - - respBody, err := postRawJSON(rpcURL+"/v1/query/account", reqJSON) - if err != nil { - return 0, err - } - - var result struct { - Amount uint64 `json:"amount"` - } - if err := json.Unmarshal(respBody, &result); err != nil { - return 0, fmt.Errorf("failed to parse response: %v, body: %s", err, string(respBody)) - } - - return result.Amount, nil -} - -// waitForTxInclusion waits for a transaction to be included in a block -func waitForTxInclusion(rpcURL, senderAddr, txHash string, timeout time.Duration) (bool, error) { - deadline := time.Now().Add(timeout) - - for time.Now().Before(deadline) { - // Query transactions by sender - reqJSON := fmt.Sprintf(`{"address":"%s","perPage":20}`, senderAddr) - respBody, err := postRawJSON(rpcURL+"/v1/query/txs-by-sender", reqJSON) - if err != nil { - time.Sleep(1 * time.Second) - continue - } - - var result struct { - Results []struct { - TxHash string `json:"txHash"` - Height uint64 `json:"height"` - } `json:"results"` - TotalCount int `json:"totalCount"` - } - if err := json.Unmarshal(respBody, &result); err != nil { - time.Sleep(1 * time.Second) - continue - } - - // Check if our transaction is in the results - for _, tx := range result.Results { - if tx.TxHash == txHash { - return true, nil - } - } - - time.Sleep(1 * time.Second) - } - - return false, fmt.Errorf("transaction %s not included within timeout", txHash) -} - -// checkTxNotFailed verifies that a transaction is not in the failed transactions list -func checkTxNotFailed(rpcURL, senderAddr string) (int, error) { - reqJSON := fmt.Sprintf(`{"address":"%s","perPage":20}`, senderAddr) - respBody, err := postRawJSON(rpcURL+"/v1/query/failed-txs", reqJSON) - if err != nil { - return 0, err - } - - var result struct { - TotalCount int `json:"totalCount"` - } - if err := json.Unmarshal(respBody, &result); err != nil { - return 0, fmt.Errorf("failed to parse response: %v, body: %s", err, string(respBody)) - } - - return result.TotalCount, nil -} - -// hexToBase64 converts a hex string to base64 (for protojson bytes encoding) -func hexToBase64(hexStr string) string { - bytes, _ := hex.DecodeString(hexStr) - return base64.StdEncoding.EncodeToString(bytes) -} - -// sendFaucetTx sends a faucet transaction using raw JSON -func sendFaucetTx(rpcURL string, signerKey *keyGroup, recipientAddr string, amount, fee, networkID, chainID, height uint64) (string, error) { - // Create the faucet message as JSON map - // protojson expects base64 for bytes fields - faucetMsg := map[string]interface{}{ - "signerAddress": hexToBase64(signerKey.Address), - "recipientAddress": hexToBase64(recipientAddr), - "amount": float64(amount), - } - - return buildSignAndSendTx(rpcURL, signerKey, "faucet", faucetMsg, fee, networkID, chainID, height) -} - -// sendSendTx sends a send transaction using raw JSON -func sendSendTx(rpcURL string, senderKey *keyGroup, fromAddr, toAddr string, amount, fee, networkID, chainID, height uint64) (string, error) { - // Create the send message as JSON map - // protojson expects base64 for bytes fields - sendMsg := map[string]interface{}{ - "fromAddress": hexToBase64(fromAddr), - "toAddress": hexToBase64(toAddr), - "amount": float64(amount), - } - - return buildSignAndSendTx(rpcURL, senderKey, "send", sendMsg, fee, networkID, chainID, height) -} - -// sendRewardTx sends a reward transaction using raw JSON -func sendRewardTx(rpcURL string, adminKey *keyGroup, adminAddr, recipientAddr string, amount, fee, networkID, chainID, height uint64) (string, error) { - // Create the reward message as JSON map - // protojson expects base64 for bytes fields - rewardMsg := map[string]interface{}{ - "adminAddress": hexToBase64(adminAddr), - "recipientAddress": hexToBase64(recipientAddr), - "amount": float64(amount), - } - - return buildSignAndSendTx(rpcURL, adminKey, "reward", rewardMsg, fee, networkID, chainID, height) -} - -// buildSignAndSendTx builds a transaction, signs it with BLS, and sends it via raw JSON -func buildSignAndSendTx(rpcURL string, signerKey *keyGroup, msgType string, msgJSON map[string]interface{}, fee, networkID, chainID, height uint64) (string, error) { - // Build the transaction structure for signing (without signature) - txTime := uint64(time.Now().UnixMicro()) - - // For signing, we need to construct the Any message bytes - // The server uses the registered plugin schema to convert JSON to protobuf - // But for signing, we need to compute what the server will compute - - // Create sign bytes by first figuring out what the Any will look like - // The plugin registry maps message type to type URL - // For plugin messages, the type URL is: type.googleapis.com/types.Message - var typeURL string - switch msgType { - case "send": - typeURL = "type.googleapis.com/types.MessageSend" - case "reward": - typeURL = "type.googleapis.com/types.MessageReward" - case "faucet": - typeURL = "type.googleapis.com/types.MessageFaucet" - default: - return "", fmt.Errorf("unknown message type: %s", msgType) - } - - // Marshal the message to proto bytes for signing - // We need to create the actual proto message - // Addresses in msgJSON are base64-encoded (for protojson compatibility) - var msgProto proto.Message - switch msgType { - case "send": - fromAddr, _ := base64.StdEncoding.DecodeString(msgJSON["fromAddress"].(string)) - toAddr, _ := base64.StdEncoding.DecodeString(msgJSON["toAddress"].(string)) - msgProto = &contract.MessageSend{ - FromAddress: fromAddr, - ToAddress: toAddr, - Amount: uint64(msgJSON["amount"].(float64)), - } - case "reward": - adminAddr, _ := base64.StdEncoding.DecodeString(msgJSON["adminAddress"].(string)) - recipientAddr, _ := base64.StdEncoding.DecodeString(msgJSON["recipientAddress"].(string)) - msgProto = &contract.MessageReward{ - AdminAddress: adminAddr, - RecipientAddress: recipientAddr, - Amount: uint64(msgJSON["amount"].(float64)), - } - case "faucet": - signerAddr, _ := base64.StdEncoding.DecodeString(msgJSON["signerAddress"].(string)) - recipientAddr, _ := base64.StdEncoding.DecodeString(msgJSON["recipientAddress"].(string)) - msgProto = &contract.MessageFaucet{ - SignerAddress: signerAddr, - RecipientAddress: recipientAddr, - Amount: uint64(msgJSON["amount"].(float64)), - } - } - - msgBytes, err := proto.Marshal(msgProto) - if err != nil { - return "", fmt.Errorf("failed to marshal message: %v", err) - } - - // Create the Any message for signing - msgAny := &anypb.Any{ - TypeUrl: typeURL, - Value: msgBytes, - } - - // Create sign bytes - this must match the server's GetSignBytes() exactly - signBytes, err := crypto.GetSignBytes(msgType, msgAny, txTime, height, fee, "", networkID, chainID) - if err != nil { - return "", fmt.Errorf("failed to get sign bytes: %v", err) - } - - // Get the BLS private key - privKey, err := crypto.StringToBLS12381PrivateKey(signerKey.PrivateKey) - if err != nil { - return "", fmt.Errorf("failed to parse private key: %v", err) - } - - // Sign with BLS - signature := privKey.Sign(signBytes) - - // Get public key bytes - pubKeyBytes, err := hex.DecodeString(signerKey.PublicKey) - if err != nil { - return "", fmt.Errorf("failed to decode public key: %v", err) - } - - // Marshal the message to get the exact bytes for the Any.Value - msgProtoBytes, err := proto.Marshal(msgProto) - if err != nil { - return "", fmt.Errorf("failed to marshal message proto: %v", err) - } - - // Build the transaction - // For "send" (which is in RegisteredMessages), we must use "msg" field - // For plugin-only types (faucet, reward), we use msgTypeUrl/msgBytes for exact byte control - var tx map[string]interface{} - if msgType == "send" { - // "send" is in RegisteredMessages, must use msg field - tx = map[string]interface{}{ - "type": msgType, - "msg": msgJSON, - "signature": map[string]string{ - "publicKey": hex.EncodeToString(pubKeyBytes), - "signature": hex.EncodeToString(signature), - }, - "time": txTime, - "createdHeight": height, - "fee": fee, - "memo": "", - "networkID": networkID, - "chainID": chainID, - } - } else { - // Plugin-only types: use msgTypeUrl/msgBytes for exact byte control - tx = map[string]interface{}{ - "type": msgType, - "msgTypeUrl": typeURL, - "msgBytes": hex.EncodeToString(msgProtoBytes), - "signature": map[string]string{ - "publicKey": hex.EncodeToString(pubKeyBytes), - "signature": hex.EncodeToString(signature), - }, - "time": txTime, - "createdHeight": height, - "fee": fee, - "memo": "", - "networkID": networkID, - "chainID": chainID, - } - } - - txJSONBytes, err := json.MarshalIndent(tx, "", " ") - if err != nil { - return "", fmt.Errorf("failed to marshal transaction: %v", err) - } - - // Send the transaction - respBody, err := postRawJSON(rpcURL+"/v1/tx", string(txJSONBytes)) - if err != nil { - return "", fmt.Errorf("failed to send transaction: %v", err) - } - - var txHash string - if err := json.Unmarshal(respBody, &txHash); err != nil { - return "", fmt.Errorf("failed to parse response: %v, body: %s", err, string(respBody)) - } - - return txHash, nil -} - -// HTTP helper function -func postRawJSON(url string, jsonBody string) ([]byte, error) { - resp, err := http.Post(url, "application/json", bytes.NewBufferString(jsonBody)) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - if resp.StatusCode >= 400 { - return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody)) - } - - return respBody, nil -} diff --git a/sidecar/main.go b/sidecar/main.go new file mode 100644 index 0000000000..49e50999f4 --- /dev/null +++ b/sidecar/main.go @@ -0,0 +1,924 @@ +package main + +import ( + "crypto/sha1" + "crypto/sha256" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "encoding/json" + "fmt" + "log" + "math" + "net/http" + "os" + "strings" + "sync" + "time" +) + +// ── Config ──────────────────────────────────────────────────────── + +func getRPC() string { + if v := os.Getenv("PRAXIS_RPC"); v != "" { + return strings.TrimRight(v, "/") + } + return "http://localhost:50002" +} + +func getPort() string { + if v := os.Getenv("PRAXIS_SIDECAR_PORT"); v != "" { + return v + } + return "8085" +} + +func getKnownCreators() []string { + if v := os.Getenv("PRAXIS_KNOWN_CREATORS"); v != "" { + return strings.Split(v, ",") + } + return []string{"e7c7dad131a03f7ea0cc09a637ad096eb3495f77"} +} + +const ( + pollInterval = 6 * time.Second + finalizationBounty = uint64(50_000_000) +) + +// ── Domain types ────────────────────────────────────────────────── + +type MarketStatus int + +const ( + StatusOpen MarketStatus = 0 + StatusProposed MarketStatus = 4 + StatusDisputed MarketStatus = 5 + StatusFinalized MarketStatus = 6 + StatusExpired MarketStatus = 99 +) + +type Market struct { + MarketID string `json:"marketId"` + Question string `json:"question"` + Creator string `json:"creator"` + B0 uint64 `json:"b0"` + LmsrSeed uint64 `json:"lmsrSeed"` + QYes uint64 `json:"qYes"` + QNo uint64 `json:"qNo"` + BEff uint64 `json:"bEff"` + ExpiryTime uint64 `json:"expiryTime"` + Nonce uint64 `json:"nonce"` + Status MarketStatus `json:"status"` + StatusLabel string `json:"statusLabel"` + TotalPool uint64 `json:"totalPool"` + YesPct int `json:"yesPct"` + NoPct int `json:"noPct"` + CreatedAt uint64 `json:"createdAt"` + TxHash string `json:"txHash"` +} + +func (m *Market) computeDerived() { + total := m.QYes + m.QNo + if total > 0 { + m.YesPct = int(m.QYes * 100 / total) + m.NoPct = 100 - m.YesPct + } else { + m.YesPct = 50 + m.NoPct = 50 + } + m.TotalPool = total + switch m.Status { + case StatusOpen: + m.StatusLabel = "open" + case StatusProposed: + m.StatusLabel = "proposed" + case StatusDisputed: + m.StatusLabel = "disputed" + case StatusFinalized: + m.StatusLabel = "finalized" + case StatusExpired: + m.StatusLabel = "expired" + default: + m.StatusLabel = "unknown" + } +} + +// ── Store ───────────────────────────────────────────────────────── + +type Store struct { + mu sync.RWMutex + markets map[string]*Market + height uint64 +} + +func newStore() *Store { + return &Store{markets: make(map[string]*Market)} +} + +func (s *Store) getMarkets() []*Market { + s.mu.RLock() + defer s.mu.RUnlock() + out := make([]*Market, 0, len(s.markets)) + for _, m := range s.markets { + cp := *m + out = append(out, &cp) + } + return out +} + +func (s *Store) getMarket(id string) (*Market, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + m, ok := s.markets[id] + if !ok { + return nil, false + } + cp := *m + return &cp, true +} + +func (s *Store) getHeight() uint64 { + s.mu.RLock() + defer s.mu.RUnlock() + return s.height +} + +func (s *Store) setHeight(h uint64) { + s.mu.Lock() + defer s.mu.Unlock() + s.height = h +} + +func (s *Store) applyUpdate(fresh *Market) (bool, bool) { + s.mu.Lock() + defer s.mu.Unlock() + existing, ok := s.markets[fresh.MarketID] + if !ok { + s.markets[fresh.MarketID] = fresh + return true, false + } + if existing.QYes != fresh.QYes || existing.QNo != fresh.QNo || + existing.Status != fresh.Status { + s.markets[fresh.MarketID] = fresh + return false, true + } + return false, false +} + +// ── WebSocket Hub ───────────────────────────────────────────────── + +type wsClient struct { + send chan []byte + done chan struct{} +} + +type Hub struct { + mu sync.RWMutex + clients map[*wsClient]struct{} +} + +func newHub() *Hub { + return &Hub{clients: make(map[*wsClient]struct{})} +} + +func (h *Hub) register(c *wsClient) { + h.mu.Lock() + h.clients[c] = struct{}{} + h.mu.Unlock() +} + +func (h *Hub) unregister(c *wsClient) { + h.mu.Lock() + delete(h.clients, c) + h.mu.Unlock() +} + +func (h *Hub) broadcast(msg []byte) { + h.mu.RLock() + defer h.mu.RUnlock() + for c := range h.clients { + select { + case c.send <- msg: + default: + } + } +} + +func (h *Hub) clientCount() int { + h.mu.RLock() + defer h.mu.RUnlock() + return len(h.clients) +} + +func encodeWSFrame(payload []byte) []byte { + plen := len(payload) + var header []byte + if plen <= 125 { + header = []byte{0x81, byte(plen)} + } else if plen <= 65535 { + header = []byte{0x81, 126, byte(plen >> 8), byte(plen)} + } else { + header = make([]byte, 10) + header[0] = 0x81 + header[1] = 127 + binary.BigEndian.PutUint64(header[2:], uint64(plen)) + } + frame := make([]byte, len(header)+plen) + copy(frame, header) + copy(frame[len(header):], payload) + return frame +} + +func computeWSAcceptKey(clientKey string) string { + h := sha1.New() + h.Write([]byte(clientKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} + +// ── Proto varint decoder ────────────────────────────────────────── + +func decodeVarint(b []byte, pos int) (uint64, int) { + var val uint64 + var shift uint + for pos < len(b) { + byt := b[pos] + pos++ + val |= uint64(byt&0x7f) << shift + shift += 7 + if byt&0x80 == 0 { + break + } + } + return val, pos +} + +func decodeMsgBytes(hexStr string) (map[int]interface{}, error) { + b, err := hex.DecodeString(hexStr) + if err != nil { + return nil, err + } + fields := make(map[int]interface{}) + pos := 0 + for pos < len(b) { + tag, p := decodeVarint(b, pos) + if p == pos { + break + } + pos = p + fieldNum := int(tag >> 3) + wireType := tag & 0x7 + + switch wireType { + case 0: + val, p2 := decodeVarint(b, pos) + pos = p2 + fields[fieldNum] = val + case 2: + length, p2 := decodeVarint(b, pos) + pos = p2 + end := pos + int(length) + if end > len(b) { + end = len(b) + } + chunk := make([]byte, end-pos) + copy(chunk, b[pos:end]) + pos = end + fields[fieldNum] = chunk + case 1: + pos += 8 + case 5: + pos += 4 + default: + pos = len(b) + } + } + return fields, nil +} + +func getBytes(fields map[int]interface{}, n int) []byte { + v, ok := fields[n] + if !ok { + return nil + } + b, ok := v.([]byte) + if !ok { + return nil + } + return b +} + +func getUint(fields map[int]interface{}, n int) uint64 { + v, ok := fields[n] + if !ok { + return 0 + } + u, ok := v.(uint64) + if !ok { + return 0 + } + return u +} + + +// base64ToHex converts a base64-encoded 20-byte address to a 40-char hex string +func base64ToHex(b64 string) string { +decoded, err := base64.StdEncoding.DecodeString(b64) +if err != nil || len(decoded) != 20 { +return "" +} +return hex.EncodeToString(decoded) +} +func getString(fields map[int]interface{}, n int) string { + b := getBytes(fields, n) + if b == nil { + return "" + } + return string(b) +} + +// ── Market ID derivation ────────────────────────────────────────── + +func deriveMarketID(creatorHex string, nonce uint64) string { + creator, err := hex.DecodeString(creatorHex) + if err != nil || len(creator) != 20 { + return "" + } + nb := make([]byte, 8) + binary.BigEndian.PutUint64(nb, nonce) + input := append(creator, nb...) + h := sha256.Sum256(input) + return hex.EncodeToString(h[:20]) +} + +func lmsrCost(qYes, qNo, bEff uint64) float64 { + if bEff == 0 { + return 0 + } + b := float64(bEff) + y := float64(qYes) + n := float64(qNo) + ay := y / b + an := n / b + var lse float64 + if ay >= an { + lse = ay + math.Log1p(math.Exp(an-ay)) + } else { + lse = an + math.Log1p(math.Exp(ay-an)) + } + return b * lse +} + +// ── RPC client ──────────────────────────────────────────────────── + +type rpcClient struct { + base string + http *http.Client +} + +func newRPCClient(base string) *rpcClient { + return &rpcClient{ + base: base, + http: &http.Client{Timeout: 10 * time.Second}, + } +} + +func (c *rpcClient) post(path string, body interface{}) (map[string]interface{}, error) { + b, _ := json.Marshal(body) + resp, err := c.http.Post(c.base+path, "application/json", strings.NewReader(string(b))) + if err != nil { + return nil, err + } + defer resp.Body.Close() + var out map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { + return nil, err + } + return out, nil +} + +func (c *rpcClient) getHeight() (uint64, error) { + d, err := c.post("/v1/query/height", map[string]interface{}{}) + if err != nil { + return 0, err + } + if h, ok := d["height"].(float64); ok { + return uint64(h), nil + } + return 0, fmt.Errorf("height not found") +} + +func (c *rpcClient) getTxsBySender(addr string, perPage int) ([]interface{}, error) { + d, err := c.post("/v1/query/txs-by-sender", map[string]interface{}{"address": addr, "perPage": perPage}) + if err != nil { + return nil, err + } + results, _ := d["results"].([]interface{}) + return results, nil +} + +func (c *rpcClient) getFailedTxs(addr string, perPage int) ([]interface{}, error) { + d, err := c.post("/v1/query/failed-txs", map[string]interface{}{"address": addr, "perPage": perPage}) + if err != nil { + return nil, err + } + results, _ := d["results"].([]interface{}) + return results, nil +} + +// ── Indexer ─────────────────────────────────────────────────────── + +type Indexer struct { + rpc *rpcClient + store *Store + hub *Hub + creators []string + failedMu sync.Mutex + failed map[string]bool +} + +func newIndexer(rpc *rpcClient, store *Store, hub *Hub, creators []string) *Indexer { + return &Indexer{ + rpc: rpc, + store: store, + hub: hub, + creators: creators, + failed: make(map[string]bool), + } +} + +func (idx *Indexer) loadFailedTxs(addr string) { + results, _ := idx.rpc.getFailedTxs(addr, 500) + idx.failedMu.Lock() + defer idx.failedMu.Unlock() + for _, r := range results { + m, ok := r.(map[string]interface{}) + if !ok { + continue + } + if hash, ok := m["txHash"].(string); ok { + idx.failed[hash] = true + } + } +} + +func (idx *Indexer) isFailed(txHash string) bool { + idx.failedMu.Lock() + defer idx.failedMu.Unlock() + return idx.failed[txHash] +} + +func (idx *Indexer) poll() []map[string]interface{} { + height, err := idx.rpc.getHeight() + if err != nil { + log.Printf("[indexer] RPC unreachable: %v", err) + return nil + } + + prevHeight := idx.store.getHeight() + idx.store.setHeight(height) + + var events []map[string]interface{} + + if height != prevHeight { + events = append(events, map[string]interface{}{ + "type": "height", + "height": height, + }) + } + + for _, creator := range idx.creators { + idx.loadFailedTxs(creator) + } + + freshMarkets := make(map[string]*Market) + + for _, creator := range idx.creators { + txs, err := idx.rpc.getTxsBySender(creator, 500) + if err != nil { + continue + } + for _, raw := range txs { + tx, ok := raw.(map[string]interface{}) + if !ok { + continue + } + txHash, _ := tx["txHash"].(string) + if idx.isFailed(txHash) { + continue + } + t, _ := tx["transaction"].(map[string]interface{}) + if t == nil { + t = tx + } + msgType, _ := t["type"].(string) + if msgType == "" { + msgType, _ = t["messageType"].(string) + } + if msgType != "create_market" { + continue + } + + var creatorAddr string + var b0, expiry, nonce, createdAt uint64 + var question string + + if msgBytes, ok := t["msgBytes"].(string); ok && msgBytes != "" { + fields, err := decodeMsgBytes(msgBytes) + if err != nil { + continue + } + cb := getBytes(fields, 1) + if len(cb) == 20 { + creatorAddr = hex.EncodeToString(cb) + } + b0 = getUint(fields, 2) + expiry = getUint(fields, 3) + nonce = getUint(fields, 4) + question = getString(fields, 5) + } else if msg, ok := t["msg"].(map[string]interface{}); ok { + question, _ = msg["question"].(string) + if v, ok := msg["creatorAddress"].(string); ok { +creatorAddr = base64ToHex(v) +} + if v, ok := msg["b0"].(float64); ok { + b0 = uint64(v) + } + if v, ok := msg["expiryTime"].(float64); ok { + expiry = uint64(v) + } + if v, ok := msg["nonce"].(float64); ok { + nonce = uint64(v) + } + } + if creatorAddr == "" || b0 == 0 { + continue + } + if v, ok := tx["height"].(float64); ok { + createdAt = uint64(v) + } + + marketID := deriveMarketID(creatorAddr, nonce) + if marketID == "" { + continue + } + lmsrSeed := b0 + if b0 > finalizationBounty { + lmsrSeed = b0 - finalizationBounty + } + halfSeed := lmsrSeed / 2 + + m := &Market{ + MarketID: marketID, + Question: question, + Creator: creatorAddr, + B0: b0, + LmsrSeed: lmsrSeed, + QYes: halfSeed, + QNo: halfSeed, + BEff: lmsrSeed, + ExpiryTime: expiry, + Nonce: nonce, + Status: StatusOpen, + CreatedAt: createdAt, + TxHash: txHash, + } + if height > expiry && expiry > 0 { + m.Status = StatusExpired + } + freshMarkets[marketID] = m + } + } + + // Replay submit_prediction + for _, creator := range idx.creators { + txs, err := idx.rpc.getTxsBySender(creator, 500) + if err != nil { + continue + } + for _, raw := range txs { + tx, ok := raw.(map[string]interface{}) + if !ok { + continue + } + txHash, _ := tx["txHash"].(string) + if idx.isFailed(txHash) { + continue + } + t, _ := tx["transaction"].(map[string]interface{}) + if t == nil { + t = tx + } + msgType, _ := t["type"].(string) + if msgType == "" { + msgType, _ = t["messageType"].(string) + } + if msgType != "submit_prediction" { + continue + } + + var marketID string + var outcome bool + var shares uint64 + + if msgBytes, ok := t["msgBytes"].(string); ok && msgBytes != "" { + fields, err := decodeMsgBytes(msgBytes) + if err != nil { + continue + } + midBytes := getBytes(fields, 1) + if len(midBytes) == 20 { + marketID = hex.EncodeToString(midBytes) + } + outVal := getUint(fields, 3) + outcome = outVal == 1 + shares = getUint(fields, 4) + } else if msg, ok := t["msg"].(map[string]interface{}); ok { + if v, ok := msg["marketId"].(string); ok { +marketID = base64ToHex(v) +} + if v, ok := msg["outcome"].(bool); ok { + outcome = v + } + if v, ok := msg["shares"].(float64); ok { + shares = uint64(v) + } + } + if marketID == "" || shares == 0 { + continue + } + m, ok := freshMarkets[marketID] + if !ok { + continue + } + if outcome { + m.QYes += shares + } else { + m.QNo += shares + } + } + } + + // Process status-changing TXs + for _, creator := range idx.creators { + txs, _ := idx.rpc.getTxsBySender(creator, 500) + for _, raw := range txs { + tx, _ := raw.(map[string]interface{}) + txHash, _ := tx["txHash"].(string) + if idx.isFailed(txHash) { + continue + } + t, _ := tx["transaction"].(map[string]interface{}) + if t == nil { + t = tx + } + msgType, _ := t["type"].(string) + if msgType == "" { + msgType, _ = t["messageType"].(string) + } + var marketID string + if msgBytes, ok := t["msgBytes"].(string); ok { + fields, _ := decodeMsgBytes(msgBytes) + midBytes := getBytes(fields, 1) + if len(midBytes) == 20 { + marketID = hex.EncodeToString(midBytes) + } + } + if marketID == "" { + continue + } + m, ok := freshMarkets[marketID] + if !ok { + continue + } + switch msgType { + case "propose_outcome": + m.Status = StatusProposed + case "file_dispute": + m.Status = StatusDisputed + case "finalize_market": + m.Status = StatusFinalized + } + } + } + + for _, m := range freshMarkets { + m.computeDerived() + isNew, changed := idx.store.applyUpdate(m) + if isNew { + b, _ := json.Marshal(map[string]interface{}{ + "type": "new_market", + "marketId": m.MarketID, + "question": m.Question, + "creator": m.Creator, + "b0": m.B0, + "lmsrSeed": m.LmsrSeed, + "qYes": m.QYes, + "qNo": m.QNo, + "bEff": m.BEff, + "expiryTime": m.ExpiryTime, + "status": m.StatusLabel, + "yesPct": m.YesPct, + "noPct": m.NoPct, + "totalPool": m.TotalPool, + "height": height, + }) + events = append(events, map[string]interface{}{"_raw": b}) + } else if changed { + b, _ := json.Marshal(map[string]interface{}{ + "type": "market_update", + "marketId": m.MarketID, + "qYes": m.QYes, + "qNo": m.QNo, + "status": m.StatusLabel, + "yesPct": m.YesPct, + "noPct": m.NoPct, + "totalPool": m.TotalPool, + "height": height, + }) + events = append(events, map[string]interface{}{"_raw": b}) + } + } + return events +} + +// ── HTTP handlers ───────────────────────────────────────────────── + +func jsonResp(w http.ResponseWriter, v interface{}) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + json.NewEncoder(w).Encode(v) +} + +func handleHealth(store *Store) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + jsonResp(w, map[string]interface{}{ + "status": "ok", + "height": store.getHeight(), + "markets": len(store.getMarkets()), + }) + } +} + +func handleMarkets(store *Store) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + jsonResp(w, map[string]interface{}{ + "height": store.getHeight(), + "markets": store.getMarkets(), + "count": len(store.getMarkets()), + }) + } +} + +func handleMarket(store *Store) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.URL.Query().Get("id") + if id == "" { + http.Error(w, `{"error":"missing id param"}`, http.StatusBadRequest) + return + } + m, ok := store.getMarket(id) + if !ok { + http.Error(w, `{"error":"market not found"}`, http.StatusNotFound) + return + } + jsonResp(w, m) + } +} + +func handleWS(hub *Hub, store *Store) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !strings.EqualFold(r.Header.Get("Upgrade"), "websocket") { + http.Error(w, "expected websocket upgrade", http.StatusBadRequest) + return + } + key := r.Header.Get("Sec-Websocket-Key") + if key == "" { + http.Error(w, "missing Sec-Websocket-Key", http.StatusBadRequest) + return + } + + hj, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "hijack not supported", http.StatusInternalServerError) + return + } + conn, bufrw, err := hj.Hijack() + if err != nil { + log.Printf("[ws] hijack failed: %v", err) + return + } + defer conn.Close() + + acceptKey := computeWSAcceptKey(key) + resp := "HTTP/1.1 101 Switching Protocols\r\n" + + "Upgrade: websocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Accept: " + acceptKey + "\r\n\r\n" + bufrw.WriteString(resp) + bufrw.Flush() + + client := &wsClient{send: make(chan []byte, 64), done: make(chan struct{})} + hub.register(client) + defer hub.unregister(client) + + markets := store.getMarkets() + snapshot, _ := json.Marshal(map[string]interface{}{ + "type": "snapshot", + "markets": markets, + "height": store.getHeight(), + }) + bufrw.Write(encodeWSFrame(snapshot)) + bufrw.Flush() + + go func() { + for { + select { + case msg := <-client.send: + bufrw.Write(encodeWSFrame(msg)) + bufrw.Flush() + case <-client.done: + return + } + } + }() + + buf := make([]byte, 4096) + for { + conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + n, err := conn.Read(buf) + if err != nil || n == 0 { + break + } + if n >= 2 { + opcode := buf[0] & 0x0f + if opcode == 0x8 { + break + } + if opcode == 0x9 { + pong := []byte{0x8a, 0x00} + bufrw.Write(pong) + bufrw.Flush() + } + } + } + close(client.done) + } +} + +// ── Main ────────────────────────────────────────────────────────── + +func main() { + rpcBase := getRPC() + port := getPort() + creators := getKnownCreators() + + log.Printf("[praxis-sidecar] starting on :%s", port) + log.Printf("[praxis-sidecar] RPC: %s", rpcBase) + log.Printf("[praxis-sidecar] creators: %v", creators) + + rpc := newRPCClient(rpcBase) + store := newStore() + hub := newHub() + idx := newIndexer(rpc, store, hub, creators) + + idx.poll() + + go func() { + ticker := time.NewTicker(pollInterval) + defer ticker.Stop() + for range ticker.C { + events := idx.poll() + for _, ev := range events { + if raw, ok := ev["_raw"]; ok { + if b, ok := raw.([]byte); ok { + hub.broadcast(b) + } + } else { + b, _ := json.Marshal(ev) + hub.broadcast(b) + } + } + } + }() + + mux := http.NewServeMux() + mux.HandleFunc("/v1/praxis/health", handleHealth(store)) + mux.HandleFunc("/v1/praxis/markets", handleMarkets(store)) + mux.HandleFunc("/v1/praxis/market", handleMarket(store)) + mux.HandleFunc("/v1/praxis/ws", handleWS(hub, store)) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + mux.ServeHTTP(w, r) + }) + + log.Printf("[praxis-sidecar] listening on :%s", port) + if err := http.ListenAndServe(":"+port, handler); err != nil { + log.Fatalf("server failed: %v", err) + } +} diff --git a/sidecar/main.go.bak b/sidecar/main.go.bak new file mode 100644 index 0000000000..49e50999f4 --- /dev/null +++ b/sidecar/main.go.bak @@ -0,0 +1,924 @@ +package main + +import ( + "crypto/sha1" + "crypto/sha256" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "encoding/json" + "fmt" + "log" + "math" + "net/http" + "os" + "strings" + "sync" + "time" +) + +// ── Config ──────────────────────────────────────────────────────── + +func getRPC() string { + if v := os.Getenv("PRAXIS_RPC"); v != "" { + return strings.TrimRight(v, "/") + } + return "http://localhost:50002" +} + +func getPort() string { + if v := os.Getenv("PRAXIS_SIDECAR_PORT"); v != "" { + return v + } + return "8085" +} + +func getKnownCreators() []string { + if v := os.Getenv("PRAXIS_KNOWN_CREATORS"); v != "" { + return strings.Split(v, ",") + } + return []string{"e7c7dad131a03f7ea0cc09a637ad096eb3495f77"} +} + +const ( + pollInterval = 6 * time.Second + finalizationBounty = uint64(50_000_000) +) + +// ── Domain types ────────────────────────────────────────────────── + +type MarketStatus int + +const ( + StatusOpen MarketStatus = 0 + StatusProposed MarketStatus = 4 + StatusDisputed MarketStatus = 5 + StatusFinalized MarketStatus = 6 + StatusExpired MarketStatus = 99 +) + +type Market struct { + MarketID string `json:"marketId"` + Question string `json:"question"` + Creator string `json:"creator"` + B0 uint64 `json:"b0"` + LmsrSeed uint64 `json:"lmsrSeed"` + QYes uint64 `json:"qYes"` + QNo uint64 `json:"qNo"` + BEff uint64 `json:"bEff"` + ExpiryTime uint64 `json:"expiryTime"` + Nonce uint64 `json:"nonce"` + Status MarketStatus `json:"status"` + StatusLabel string `json:"statusLabel"` + TotalPool uint64 `json:"totalPool"` + YesPct int `json:"yesPct"` + NoPct int `json:"noPct"` + CreatedAt uint64 `json:"createdAt"` + TxHash string `json:"txHash"` +} + +func (m *Market) computeDerived() { + total := m.QYes + m.QNo + if total > 0 { + m.YesPct = int(m.QYes * 100 / total) + m.NoPct = 100 - m.YesPct + } else { + m.YesPct = 50 + m.NoPct = 50 + } + m.TotalPool = total + switch m.Status { + case StatusOpen: + m.StatusLabel = "open" + case StatusProposed: + m.StatusLabel = "proposed" + case StatusDisputed: + m.StatusLabel = "disputed" + case StatusFinalized: + m.StatusLabel = "finalized" + case StatusExpired: + m.StatusLabel = "expired" + default: + m.StatusLabel = "unknown" + } +} + +// ── Store ───────────────────────────────────────────────────────── + +type Store struct { + mu sync.RWMutex + markets map[string]*Market + height uint64 +} + +func newStore() *Store { + return &Store{markets: make(map[string]*Market)} +} + +func (s *Store) getMarkets() []*Market { + s.mu.RLock() + defer s.mu.RUnlock() + out := make([]*Market, 0, len(s.markets)) + for _, m := range s.markets { + cp := *m + out = append(out, &cp) + } + return out +} + +func (s *Store) getMarket(id string) (*Market, bool) { + s.mu.RLock() + defer s.mu.RUnlock() + m, ok := s.markets[id] + if !ok { + return nil, false + } + cp := *m + return &cp, true +} + +func (s *Store) getHeight() uint64 { + s.mu.RLock() + defer s.mu.RUnlock() + return s.height +} + +func (s *Store) setHeight(h uint64) { + s.mu.Lock() + defer s.mu.Unlock() + s.height = h +} + +func (s *Store) applyUpdate(fresh *Market) (bool, bool) { + s.mu.Lock() + defer s.mu.Unlock() + existing, ok := s.markets[fresh.MarketID] + if !ok { + s.markets[fresh.MarketID] = fresh + return true, false + } + if existing.QYes != fresh.QYes || existing.QNo != fresh.QNo || + existing.Status != fresh.Status { + s.markets[fresh.MarketID] = fresh + return false, true + } + return false, false +} + +// ── WebSocket Hub ───────────────────────────────────────────────── + +type wsClient struct { + send chan []byte + done chan struct{} +} + +type Hub struct { + mu sync.RWMutex + clients map[*wsClient]struct{} +} + +func newHub() *Hub { + return &Hub{clients: make(map[*wsClient]struct{})} +} + +func (h *Hub) register(c *wsClient) { + h.mu.Lock() + h.clients[c] = struct{}{} + h.mu.Unlock() +} + +func (h *Hub) unregister(c *wsClient) { + h.mu.Lock() + delete(h.clients, c) + h.mu.Unlock() +} + +func (h *Hub) broadcast(msg []byte) { + h.mu.RLock() + defer h.mu.RUnlock() + for c := range h.clients { + select { + case c.send <- msg: + default: + } + } +} + +func (h *Hub) clientCount() int { + h.mu.RLock() + defer h.mu.RUnlock() + return len(h.clients) +} + +func encodeWSFrame(payload []byte) []byte { + plen := len(payload) + var header []byte + if plen <= 125 { + header = []byte{0x81, byte(plen)} + } else if plen <= 65535 { + header = []byte{0x81, 126, byte(plen >> 8), byte(plen)} + } else { + header = make([]byte, 10) + header[0] = 0x81 + header[1] = 127 + binary.BigEndian.PutUint64(header[2:], uint64(plen)) + } + frame := make([]byte, len(header)+plen) + copy(frame, header) + copy(frame[len(header):], payload) + return frame +} + +func computeWSAcceptKey(clientKey string) string { + h := sha1.New() + h.Write([]byte(clientKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} + +// ── Proto varint decoder ────────────────────────────────────────── + +func decodeVarint(b []byte, pos int) (uint64, int) { + var val uint64 + var shift uint + for pos < len(b) { + byt := b[pos] + pos++ + val |= uint64(byt&0x7f) << shift + shift += 7 + if byt&0x80 == 0 { + break + } + } + return val, pos +} + +func decodeMsgBytes(hexStr string) (map[int]interface{}, error) { + b, err := hex.DecodeString(hexStr) + if err != nil { + return nil, err + } + fields := make(map[int]interface{}) + pos := 0 + for pos < len(b) { + tag, p := decodeVarint(b, pos) + if p == pos { + break + } + pos = p + fieldNum := int(tag >> 3) + wireType := tag & 0x7 + + switch wireType { + case 0: + val, p2 := decodeVarint(b, pos) + pos = p2 + fields[fieldNum] = val + case 2: + length, p2 := decodeVarint(b, pos) + pos = p2 + end := pos + int(length) + if end > len(b) { + end = len(b) + } + chunk := make([]byte, end-pos) + copy(chunk, b[pos:end]) + pos = end + fields[fieldNum] = chunk + case 1: + pos += 8 + case 5: + pos += 4 + default: + pos = len(b) + } + } + return fields, nil +} + +func getBytes(fields map[int]interface{}, n int) []byte { + v, ok := fields[n] + if !ok { + return nil + } + b, ok := v.([]byte) + if !ok { + return nil + } + return b +} + +func getUint(fields map[int]interface{}, n int) uint64 { + v, ok := fields[n] + if !ok { + return 0 + } + u, ok := v.(uint64) + if !ok { + return 0 + } + return u +} + + +// base64ToHex converts a base64-encoded 20-byte address to a 40-char hex string +func base64ToHex(b64 string) string { +decoded, err := base64.StdEncoding.DecodeString(b64) +if err != nil || len(decoded) != 20 { +return "" +} +return hex.EncodeToString(decoded) +} +func getString(fields map[int]interface{}, n int) string { + b := getBytes(fields, n) + if b == nil { + return "" + } + return string(b) +} + +// ── Market ID derivation ────────────────────────────────────────── + +func deriveMarketID(creatorHex string, nonce uint64) string { + creator, err := hex.DecodeString(creatorHex) + if err != nil || len(creator) != 20 { + return "" + } + nb := make([]byte, 8) + binary.BigEndian.PutUint64(nb, nonce) + input := append(creator, nb...) + h := sha256.Sum256(input) + return hex.EncodeToString(h[:20]) +} + +func lmsrCost(qYes, qNo, bEff uint64) float64 { + if bEff == 0 { + return 0 + } + b := float64(bEff) + y := float64(qYes) + n := float64(qNo) + ay := y / b + an := n / b + var lse float64 + if ay >= an { + lse = ay + math.Log1p(math.Exp(an-ay)) + } else { + lse = an + math.Log1p(math.Exp(ay-an)) + } + return b * lse +} + +// ── RPC client ──────────────────────────────────────────────────── + +type rpcClient struct { + base string + http *http.Client +} + +func newRPCClient(base string) *rpcClient { + return &rpcClient{ + base: base, + http: &http.Client{Timeout: 10 * time.Second}, + } +} + +func (c *rpcClient) post(path string, body interface{}) (map[string]interface{}, error) { + b, _ := json.Marshal(body) + resp, err := c.http.Post(c.base+path, "application/json", strings.NewReader(string(b))) + if err != nil { + return nil, err + } + defer resp.Body.Close() + var out map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { + return nil, err + } + return out, nil +} + +func (c *rpcClient) getHeight() (uint64, error) { + d, err := c.post("/v1/query/height", map[string]interface{}{}) + if err != nil { + return 0, err + } + if h, ok := d["height"].(float64); ok { + return uint64(h), nil + } + return 0, fmt.Errorf("height not found") +} + +func (c *rpcClient) getTxsBySender(addr string, perPage int) ([]interface{}, error) { + d, err := c.post("/v1/query/txs-by-sender", map[string]interface{}{"address": addr, "perPage": perPage}) + if err != nil { + return nil, err + } + results, _ := d["results"].([]interface{}) + return results, nil +} + +func (c *rpcClient) getFailedTxs(addr string, perPage int) ([]interface{}, error) { + d, err := c.post("/v1/query/failed-txs", map[string]interface{}{"address": addr, "perPage": perPage}) + if err != nil { + return nil, err + } + results, _ := d["results"].([]interface{}) + return results, nil +} + +// ── Indexer ─────────────────────────────────────────────────────── + +type Indexer struct { + rpc *rpcClient + store *Store + hub *Hub + creators []string + failedMu sync.Mutex + failed map[string]bool +} + +func newIndexer(rpc *rpcClient, store *Store, hub *Hub, creators []string) *Indexer { + return &Indexer{ + rpc: rpc, + store: store, + hub: hub, + creators: creators, + failed: make(map[string]bool), + } +} + +func (idx *Indexer) loadFailedTxs(addr string) { + results, _ := idx.rpc.getFailedTxs(addr, 500) + idx.failedMu.Lock() + defer idx.failedMu.Unlock() + for _, r := range results { + m, ok := r.(map[string]interface{}) + if !ok { + continue + } + if hash, ok := m["txHash"].(string); ok { + idx.failed[hash] = true + } + } +} + +func (idx *Indexer) isFailed(txHash string) bool { + idx.failedMu.Lock() + defer idx.failedMu.Unlock() + return idx.failed[txHash] +} + +func (idx *Indexer) poll() []map[string]interface{} { + height, err := idx.rpc.getHeight() + if err != nil { + log.Printf("[indexer] RPC unreachable: %v", err) + return nil + } + + prevHeight := idx.store.getHeight() + idx.store.setHeight(height) + + var events []map[string]interface{} + + if height != prevHeight { + events = append(events, map[string]interface{}{ + "type": "height", + "height": height, + }) + } + + for _, creator := range idx.creators { + idx.loadFailedTxs(creator) + } + + freshMarkets := make(map[string]*Market) + + for _, creator := range idx.creators { + txs, err := idx.rpc.getTxsBySender(creator, 500) + if err != nil { + continue + } + for _, raw := range txs { + tx, ok := raw.(map[string]interface{}) + if !ok { + continue + } + txHash, _ := tx["txHash"].(string) + if idx.isFailed(txHash) { + continue + } + t, _ := tx["transaction"].(map[string]interface{}) + if t == nil { + t = tx + } + msgType, _ := t["type"].(string) + if msgType == "" { + msgType, _ = t["messageType"].(string) + } + if msgType != "create_market" { + continue + } + + var creatorAddr string + var b0, expiry, nonce, createdAt uint64 + var question string + + if msgBytes, ok := t["msgBytes"].(string); ok && msgBytes != "" { + fields, err := decodeMsgBytes(msgBytes) + if err != nil { + continue + } + cb := getBytes(fields, 1) + if len(cb) == 20 { + creatorAddr = hex.EncodeToString(cb) + } + b0 = getUint(fields, 2) + expiry = getUint(fields, 3) + nonce = getUint(fields, 4) + question = getString(fields, 5) + } else if msg, ok := t["msg"].(map[string]interface{}); ok { + question, _ = msg["question"].(string) + if v, ok := msg["creatorAddress"].(string); ok { +creatorAddr = base64ToHex(v) +} + if v, ok := msg["b0"].(float64); ok { + b0 = uint64(v) + } + if v, ok := msg["expiryTime"].(float64); ok { + expiry = uint64(v) + } + if v, ok := msg["nonce"].(float64); ok { + nonce = uint64(v) + } + } + if creatorAddr == "" || b0 == 0 { + continue + } + if v, ok := tx["height"].(float64); ok { + createdAt = uint64(v) + } + + marketID := deriveMarketID(creatorAddr, nonce) + if marketID == "" { + continue + } + lmsrSeed := b0 + if b0 > finalizationBounty { + lmsrSeed = b0 - finalizationBounty + } + halfSeed := lmsrSeed / 2 + + m := &Market{ + MarketID: marketID, + Question: question, + Creator: creatorAddr, + B0: b0, + LmsrSeed: lmsrSeed, + QYes: halfSeed, + QNo: halfSeed, + BEff: lmsrSeed, + ExpiryTime: expiry, + Nonce: nonce, + Status: StatusOpen, + CreatedAt: createdAt, + TxHash: txHash, + } + if height > expiry && expiry > 0 { + m.Status = StatusExpired + } + freshMarkets[marketID] = m + } + } + + // Replay submit_prediction + for _, creator := range idx.creators { + txs, err := idx.rpc.getTxsBySender(creator, 500) + if err != nil { + continue + } + for _, raw := range txs { + tx, ok := raw.(map[string]interface{}) + if !ok { + continue + } + txHash, _ := tx["txHash"].(string) + if idx.isFailed(txHash) { + continue + } + t, _ := tx["transaction"].(map[string]interface{}) + if t == nil { + t = tx + } + msgType, _ := t["type"].(string) + if msgType == "" { + msgType, _ = t["messageType"].(string) + } + if msgType != "submit_prediction" { + continue + } + + var marketID string + var outcome bool + var shares uint64 + + if msgBytes, ok := t["msgBytes"].(string); ok && msgBytes != "" { + fields, err := decodeMsgBytes(msgBytes) + if err != nil { + continue + } + midBytes := getBytes(fields, 1) + if len(midBytes) == 20 { + marketID = hex.EncodeToString(midBytes) + } + outVal := getUint(fields, 3) + outcome = outVal == 1 + shares = getUint(fields, 4) + } else if msg, ok := t["msg"].(map[string]interface{}); ok { + if v, ok := msg["marketId"].(string); ok { +marketID = base64ToHex(v) +} + if v, ok := msg["outcome"].(bool); ok { + outcome = v + } + if v, ok := msg["shares"].(float64); ok { + shares = uint64(v) + } + } + if marketID == "" || shares == 0 { + continue + } + m, ok := freshMarkets[marketID] + if !ok { + continue + } + if outcome { + m.QYes += shares + } else { + m.QNo += shares + } + } + } + + // Process status-changing TXs + for _, creator := range idx.creators { + txs, _ := idx.rpc.getTxsBySender(creator, 500) + for _, raw := range txs { + tx, _ := raw.(map[string]interface{}) + txHash, _ := tx["txHash"].(string) + if idx.isFailed(txHash) { + continue + } + t, _ := tx["transaction"].(map[string]interface{}) + if t == nil { + t = tx + } + msgType, _ := t["type"].(string) + if msgType == "" { + msgType, _ = t["messageType"].(string) + } + var marketID string + if msgBytes, ok := t["msgBytes"].(string); ok { + fields, _ := decodeMsgBytes(msgBytes) + midBytes := getBytes(fields, 1) + if len(midBytes) == 20 { + marketID = hex.EncodeToString(midBytes) + } + } + if marketID == "" { + continue + } + m, ok := freshMarkets[marketID] + if !ok { + continue + } + switch msgType { + case "propose_outcome": + m.Status = StatusProposed + case "file_dispute": + m.Status = StatusDisputed + case "finalize_market": + m.Status = StatusFinalized + } + } + } + + for _, m := range freshMarkets { + m.computeDerived() + isNew, changed := idx.store.applyUpdate(m) + if isNew { + b, _ := json.Marshal(map[string]interface{}{ + "type": "new_market", + "marketId": m.MarketID, + "question": m.Question, + "creator": m.Creator, + "b0": m.B0, + "lmsrSeed": m.LmsrSeed, + "qYes": m.QYes, + "qNo": m.QNo, + "bEff": m.BEff, + "expiryTime": m.ExpiryTime, + "status": m.StatusLabel, + "yesPct": m.YesPct, + "noPct": m.NoPct, + "totalPool": m.TotalPool, + "height": height, + }) + events = append(events, map[string]interface{}{"_raw": b}) + } else if changed { + b, _ := json.Marshal(map[string]interface{}{ + "type": "market_update", + "marketId": m.MarketID, + "qYes": m.QYes, + "qNo": m.QNo, + "status": m.StatusLabel, + "yesPct": m.YesPct, + "noPct": m.NoPct, + "totalPool": m.TotalPool, + "height": height, + }) + events = append(events, map[string]interface{}{"_raw": b}) + } + } + return events +} + +// ── HTTP handlers ───────────────────────────────────────────────── + +func jsonResp(w http.ResponseWriter, v interface{}) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + json.NewEncoder(w).Encode(v) +} + +func handleHealth(store *Store) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + jsonResp(w, map[string]interface{}{ + "status": "ok", + "height": store.getHeight(), + "markets": len(store.getMarkets()), + }) + } +} + +func handleMarkets(store *Store) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + jsonResp(w, map[string]interface{}{ + "height": store.getHeight(), + "markets": store.getMarkets(), + "count": len(store.getMarkets()), + }) + } +} + +func handleMarket(store *Store) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := r.URL.Query().Get("id") + if id == "" { + http.Error(w, `{"error":"missing id param"}`, http.StatusBadRequest) + return + } + m, ok := store.getMarket(id) + if !ok { + http.Error(w, `{"error":"market not found"}`, http.StatusNotFound) + return + } + jsonResp(w, m) + } +} + +func handleWS(hub *Hub, store *Store) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !strings.EqualFold(r.Header.Get("Upgrade"), "websocket") { + http.Error(w, "expected websocket upgrade", http.StatusBadRequest) + return + } + key := r.Header.Get("Sec-Websocket-Key") + if key == "" { + http.Error(w, "missing Sec-Websocket-Key", http.StatusBadRequest) + return + } + + hj, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "hijack not supported", http.StatusInternalServerError) + return + } + conn, bufrw, err := hj.Hijack() + if err != nil { + log.Printf("[ws] hijack failed: %v", err) + return + } + defer conn.Close() + + acceptKey := computeWSAcceptKey(key) + resp := "HTTP/1.1 101 Switching Protocols\r\n" + + "Upgrade: websocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Accept: " + acceptKey + "\r\n\r\n" + bufrw.WriteString(resp) + bufrw.Flush() + + client := &wsClient{send: make(chan []byte, 64), done: make(chan struct{})} + hub.register(client) + defer hub.unregister(client) + + markets := store.getMarkets() + snapshot, _ := json.Marshal(map[string]interface{}{ + "type": "snapshot", + "markets": markets, + "height": store.getHeight(), + }) + bufrw.Write(encodeWSFrame(snapshot)) + bufrw.Flush() + + go func() { + for { + select { + case msg := <-client.send: + bufrw.Write(encodeWSFrame(msg)) + bufrw.Flush() + case <-client.done: + return + } + } + }() + + buf := make([]byte, 4096) + for { + conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + n, err := conn.Read(buf) + if err != nil || n == 0 { + break + } + if n >= 2 { + opcode := buf[0] & 0x0f + if opcode == 0x8 { + break + } + if opcode == 0x9 { + pong := []byte{0x8a, 0x00} + bufrw.Write(pong) + bufrw.Flush() + } + } + } + close(client.done) + } +} + +// ── Main ────────────────────────────────────────────────────────── + +func main() { + rpcBase := getRPC() + port := getPort() + creators := getKnownCreators() + + log.Printf("[praxis-sidecar] starting on :%s", port) + log.Printf("[praxis-sidecar] RPC: %s", rpcBase) + log.Printf("[praxis-sidecar] creators: %v", creators) + + rpc := newRPCClient(rpcBase) + store := newStore() + hub := newHub() + idx := newIndexer(rpc, store, hub, creators) + + idx.poll() + + go func() { + ticker := time.NewTicker(pollInterval) + defer ticker.Stop() + for range ticker.C { + events := idx.poll() + for _, ev := range events { + if raw, ok := ev["_raw"]; ok { + if b, ok := raw.([]byte); ok { + hub.broadcast(b) + } + } else { + b, _ := json.Marshal(ev) + hub.broadcast(b) + } + } + } + }() + + mux := http.NewServeMux() + mux.HandleFunc("/v1/praxis/health", handleHealth(store)) + mux.HandleFunc("/v1/praxis/markets", handleMarkets(store)) + mux.HandleFunc("/v1/praxis/market", handleMarket(store)) + mux.HandleFunc("/v1/praxis/ws", handleWS(hub, store)) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + mux.ServeHTTP(w, r) + }) + + log.Printf("[praxis-sidecar] listening on :%s", port) + if err := http.ListenAndServe(":"+port, handler); err != nil { + log.Fatalf("server failed: %v", err) + } +} diff --git a/sidecar/praxis-sidecar b/sidecar/praxis-sidecar new file mode 100755 index 0000000000..2d800c4a61 Binary files /dev/null and b/sidecar/praxis-sidecar differ diff --git a/ui/app.js b/ui/app.js new file mode 100644 index 0000000000..e8153d5b69 --- /dev/null +++ b/ui/app.js @@ -0,0 +1,2855 @@ + +// ═══════════════════════════════════════════ +// BLS +// ═══════════════════════════════════════════ +let bls12_381 = null; +(async () => { + for (const url of ['https://esm.sh/@noble/curves@1.4.2/bls12-381','https://cdn.skypack.dev/@noble/curves@1.4.2/bls12-381']) { + try { const m = await import(url); bls12_381 = m.bls12_381; break; } catch {} + } + if (!bls12_381) toast('BLS library failed to load — check internet', true); +})(); + +// ═══════════════════════════════════════════ +// CONFIG & STATE +// ═══════════════════════════════════════════ +const getRPCHost = () => localStorage.getItem('praxis_rpc_host') || 'localhost'; +const getRPC = () => `http://${getRPCHost()}:50002`; + +let currentHeight = 0; +let currentNetworkID = 1; +let currentChainID = 1; +let selectedOut = true; +let propOut = true; +let revOut = true; +let signerPrivKey = null, signerPubKey = null, signerAddress = null; + +// ═══════════════════════════════════════════ +// PROTO ENCODER +// ═══════════════════════════════════════════ +function encV(value) { + const out = []; let v = typeof value==='bigint'?value:BigInt(value); + while(v>127n){out.push(Number((v&0x7fn)|0x80n));v>>=7n;}out.push(Number(v));return new Uint8Array(out); +} +function cat(...a){const t=a.reduce((s,x)=>s+x.length,0);const o=new Uint8Array(t);let off=0;for(const x of a){o.set(x,off);off+=x.length;}return o;} +function tag(f,w){return encV((BigInt(f)<<3n)|BigInt(w));} +function vf(f,v){const x=typeof v==='bigint'?v:BigInt(v);if(x===0n)return new Uint8Array(0);return cat(tag(f,0),encV(x));} +function bf(f,b){if(!b||!b.length)return new Uint8Array(0);return cat(tag(f,2),encV(b.length),b);} +function sf(f,s){if(!s||!s.length)return new Uint8Array(0);const e=new TextEncoder().encode(s);return cat(tag(f,2),encV(e.length),e);} +function ef(f,m){if(!m||!m.length)return new Uint8Array(0);return cat(tag(f,2),encV(m.length),m);} +function boolF(f,v){return cat(tag(f,0),new Uint8Array([v?1:0]));} +function hexToBytes(hex){const b=new Uint8Array(hex.length/2);for(let i=0;ix.toString(16).padStart(2,"0")).join("");} + +// ═══════════════════════════════════════════ +// HELPERS +// ═══════════════════════════════════════════ +function h2b(hex){hex=hex.trim().toLowerCase();if(hex.length%2)throw new Error('Odd hex');const o=new Uint8Array(hex.length/2);for(let i=0;ix.toString(16).padStart(2,'0')).join('');} +function fmtA(n){if(!n&&n!==0)return'—';const x=Number(n);if(x>=1e9)return(x/1e9).toFixed(2)+'B';if(x>=1e6)return(x/1e6).toFixed(2)+'M';if(x>=1e3)return(x/1e3).toFixed(1)+'k';return String(x);} +function fmtPRX(n){if(!n&&n!==0)return'—';const x=Number(n)/1_000_000;if(x>=1e9)return(x/1e9).toFixed(2)+'B';if(x>=1e6)return(x/1e6).toFixed(2)+'M';if(x>=1000)return(x/1000).toFixed(2)+'k';if(x>=1)return x.toFixed(2);return x.toFixed(6);} +function esc(s){return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,''');} +function addr40(s,label){if(!s||s.length!==40)throw new Error(`${label||'Address'} must be 40 hex chars`);} +function mid40(s){addr40(s,'Market ID');} + +// ═══════════════════════════════════════════ +// google.protobuf.Any +// ═══════════════════════════════════════════ +function encAny(typeUrl,inner){return cat(sf(1,typeUrl),bf(2,inner));} + +function getSelectedCat() { + const el = document.querySelector('#c_cat_pick .cpick.active'); + return el ? el.getAttribute('data-cat') : 'other'; +} + +// ═══════════════════════════════════════════ +// INNER MESSAGE ENCODERS — field numbers match tx.proto +// ═══════════════════════════════════════════ +function encSend(from,to,amt){return cat(bf(1,h2b(from)),bf(2,h2b(to)),vf(3,amt));} +function encCreate(creator,b0,expiry,nonce,question,rules){return cat(bf(1,h2b(creator)),vf(2,b0),vf(3,expiry),vf(4,nonce),sf(5,question),sf(6,rules||''));} +function encPredict(mid,bettor,outcome,shares,maxcost){return cat(bf(1,h2b(mid)),bf(2,h2b(bettor)),boolF(3,outcome),vf(4,shares),vf(5,maxcost));} +function encResolve(mid,resolver,outcome){return cat(bf(1,h2b(mid)),bf(2,h2b(resolver)),boolF(3,outcome));} +function encClaim(mid,claimant){return cat(bf(1,h2b(mid)),bf(2,h2b(claimant)));} +function encReclaim(mid,claimant){return cat(bf(1,h2b(mid)),bf(2,h2b(claimant)));} +function encRegister(addr,stake){return cat(bf(1,h2b(addr)),vf(2,stake));} +function encPropose(mid,resolver,outcome,bond){return cat(bf(1,h2b(mid)),bf(2,h2b(resolver)),boolF(3,outcome),vf(4,bond));} +function encDispute(mid,addr,bond){return cat(bf(1,h2b(mid)),bf(2,h2b(addr)),vf(3,bond));} +function encCommit(mid,voter,hash){return cat(bf(1,h2b(mid)),bf(2,h2b(voter)),bf(3,h2b(hash)));} +function encReveal(mid,voter,vote,nonce){return cat(bf(1,h2b(mid)),bf(2,h2b(voter)),boolF(3,vote),bf(4,h2b(nonce)));} +function encTally(mid,addr){return cat(bf(1,h2b(mid)),bf(2,h2b(addr)));} +function encFinalize(mid,addr){return cat(bf(1,h2b(mid)),bf(2,h2b(addr)));} +function encSlash(mid,addr){return cat(bf(1,h2b(mid)),bf(2,h2b(addr)));} +function encForfeit(mid,resolver){return cat(bf(1,h2b(mid)),bf(2,h2b(resolver)));} +function encUnstakeResolver(addr,amount){return cat(bf(1,h2b(addr)),vf(2,amount));} +function encClaimUnbonded(addr){return cat(bf(1,h2b(addr)));} + +// ═══════════════════════════════════════════ +// TX SIGN BYTES ENCODER +// ═══════════════════════════════════════════ +function encSignBytes(msgType,typeUrl,inner,{txTime,fee,height,memo,netId,chainId}){ + const any=encAny(typeUrl,inner); + return cat( + sf(1,msgType),ef(2,any), + vf(4,height||currentHeight),vf(5,txTime),vf(6,fee||10000), + memo?sf(7,memo):new Uint8Array(0), + vf(8,netId||1),vf(9,chainId||1), + ); +} + +// ═══════════════════════════════════════════ +// BLS SIGN +// ═══════════════════════════════════════════ +async function blsSign(msg){ + if(!signerPrivKey)throw new Error('No key loaded — go to Signer'); + if(!bls12_381)throw new Error('BLS library not loaded'); + return await bls12_381.sign(msg,signerPrivKey); +} + +// ═══════════════════════════════════════════ +// BASE64 HELPER (for proto JSON encoding) +// ═══════════════════════════════════════════ +function b2b64(bytes){ + let s='';for(let i=0;iel.style.display='none',5000); +}; + +// ═══════════════════════════════════════════ +// NAVIGATION +// ═══════════════════════════════════════════ +window.showPage=function(id,btn){ + document.querySelectorAll('.page').forEach(p=>p.classList.remove('active')); + document.getElementById('page-'+id).classList.add('active'); + document.querySelectorAll('#deskNav .ni').forEach(b=>b.classList.remove('active')); + const dm=document.querySelector(`#deskNav [data-p="${id}"]`);if(dm)dm.classList.add('active'); + document.querySelectorAll('#bnav .btab').forEach(b=>b.classList.remove('active')); + const bm=document.querySelector(`#bnav [data-p="${id}"]`);if(bm)bm.classList.add('active'); + if(id==='markets')loadMarkets(); + if(id==='wallet'){refreshBalance();loadMyPredictions();} + if(id==='create'){updateCreateBreakdown();setTimeout(initExpiryDate,50);} + if(id==='predict')updatePredictBreakdown(); + if(id==='resolvers')loadResolvers(); + closeNav(); +}; + +// ═══════════════════════════════════════════ +// MOBILE NAV +// ═══════════════════════════════════════════ +window.openNav=function(){document.getElementById('deskNav').classList.add('open');document.getElementById('mobNav').classList.add('open');};window.closeNav=function(e){if(!e||e.target===document.getElementById('mobNav')||e.currentTarget===document.getElementById('mobNav')){document.getElementById('deskNav').classList.remove('open');document.getElementById('mobNav').classList.remove('open');}}; + +function buildMobNav(){ + const body=document.getElementById('mobNavBody'); + if(!body)return; + body.innerHTML=document.getElementById('deskNav').innerHTML; + body.querySelectorAll('.ni').forEach(item=>{ + const p=item.getAttribute('data-p'); + if(p)item.setAttribute('onclick',`showPage('${p}',this)`); + }); +} + +// ═══════════════════════════════════════════ +// THEME +// ═══════════════════════════════════════════ +window.toggleTheme=function(){ + const html=document.documentElement; + const d=html.getAttribute('data-theme')==='dark'; + html.setAttribute('data-theme',d?'light':'dark'); + localStorage.setItem('praxis_theme',d?'light':'dark'); + updateTL(); +}; +function updateTL(){ + const d=document.documentElement.getAttribute('data-theme')==='dark'; + const lbl=d?'Light mode':'Dark mode'; + ['tlD','tlM'].forEach(id=>{const e=document.getElementById(id);if(e)e.textContent=lbl;}); +} +const st=localStorage.getItem('praxis_theme'); +if(st)document.documentElement.setAttribute('data-theme',st); +updateTL(); + +// ═══════════════════════════════════════════ +// RPC STATUS +// ═══════════════════════════════════════════ +window.checkRPC=async function(){ + try{ + const d=await rpc('/v1/query/height',{});currentHeight=d.height||0; + currentNetworkID=d.network_id||d.networkID||currentNetworkID; + currentChainID=d.chain_id||d.chainID||currentChainID; + ['rpcDot','rpcDotM'].forEach(id=>{const e=document.getElementById(id);if(e)e.className='dot live';}); + const el=document.getElementById('rpcStatus');if(el)el.textContent='live'; + const hb=document.getElementById('hBadge');if(hb)hb.textContent=`block ${currentHeight}`; + const hm=document.getElementById('hbM');if(hm)hm.textContent=`#${currentHeight}`; + ['ni_height'].forEach(id=>{const e=document.getElementById(id);if(e)e.textContent=currentHeight;}); + const ns=document.getElementById('ni_status');if(ns)ns.textContent='connected'; + const nr=document.getElementById('ni_rpc');if(nr)nr.textContent=getRPC(); + const sh=document.getElementById('sb_h');if(sh)sh.textContent=currentHeight; + updateExpiryFromDate(); + const nonceEl=document.getElementById('c_nonce');if(nonceEl&&!nonceEl.value)nonceEl.value=BigInt(Date.now())*1000n; + }catch{ + ['rpcDot','rpcDotM'].forEach(id=>{const e=document.getElementById(id);if(e)e.className='dot';}); + const el=document.getElementById('rpcStatus');if(el)el.textContent='offline'; + const ns=document.getElementById('ni_status');if(ns)ns.textContent='offline'; + } +}; +window.applyHost=function(){const h=document.getElementById('ni_host').value.trim();if(h)localStorage.setItem('praxis_rpc_host',h);checkRPC();toast('Connecting to '+h+'…');}; + +// ═══════════════════════════════════════════ +// OUTCOME TOGGLES +// ═══════════════════════════════════════════ +window.setOut=function(v){selectedOut=v;document.getElementById('btn_yes').className='obtn yes'+(v?' active':'');document.getElementById('btn_no').className='obtn no'+(!v?' active':'');}; +window.setPropOut=function(v){propOut=v;document.getElementById('pbtn_yes').className='obtn yes'+(v?' active':'');document.getElementById('pbtn_no').className='obtn no'+(!v?' active':'');}; +window.setRevOut=function(v){revOut=v;document.getElementById('rvbtn_yes').className='obtn yes'+(v?' active':'');document.getElementById('rvbtn_no').className='obtn no'+(!v?' active':'');}; + +// ═══════════════════════════════════════════ +// SIGNER +// ═══════════════════════════════════════════ +window.loadKey=async function(){ + const hex=document.getElementById('sk_input').value.trim().toLowerCase(); + if(hex.length!==64)return toast('Private key must be exactly 64 hex chars',true); + try{ + if(!bls12_381)throw new Error('BLS library not loaded'); + signerPrivKey=h2b(hex); + signerPubKey=bls12_381.getPublicKey(signerPrivKey); + const hb=await crypto.subtle.digest('SHA-256',signerPubKey); + signerAddress=b2h(new Uint8Array(hb).slice(0,20)); + document.getElementById('keyStatus').className='kstat loaded'; + document.getElementById('keyStatus').textContent='✓ loaded — '+signerAddress.slice(0,16)+'…'; + document.getElementById('sk_derived').style.display='block'; + document.getElementById('sk_pub').textContent=b2h(signerPubKey); + document.getElementById('sk_addr').textContent=signerAddress; + ['c_creator','p_bettor','r_resolver','cl_addr','s_from','w_addr','ft_addr', + 'reg_addr','pr_resolver','dis_addr','cv_voter','rv_voter','tal_addr','fin_addr','sl_addr', + 'fo_resolver','rc_addr','ccf_addr','can_addr','unst_addr','cub_addr'].forEach(id=>{ + const el=document.getElementById(id);if(el&&!el.value)el.value=signerAddress; + }); + const _ski=document.getElementById('sk_input');if(_ski)_ski.value=''; + refreshBalance(); + loadMyPredictions(); + toast('Key loaded — '+signerAddress); + }catch(e){signerPrivKey=signerPubKey=signerAddress=null;toast('Key load failed: '+e.message,true);} +}; +window.clearKey=function(){ + localStorage.removeItem('praxis_keystore'); + signerPrivKey=signerPubKey=signerAddress=null; + document.getElementById('keyStatus').className='kstat'; + document.getElementById('keyStatus').textContent='○ No key loaded'; + document.getElementById('sk_derived').style.display='none'; + const _ski=document.getElementById('sk_input');if(_ski)_ski.value=''; + toast('Key cleared'); +}; + +// ═══════════════════════════════════════════ +// ACCOUNT QUERY +// ═══════════════════════════════════════════ +window.queryAccount=async function(){ + const addr=document.getElementById('w_addr').value.trim().toLowerCase(); + addr40(addr,'Address'); + try{ + const d=await rpc('/v1/query/account',{address:addr}); + document.getElementById('w_result').style.display='block'; + document.getElementById('w_balance').textContent=fmtPRX(d.amount||0); + document.getElementById('w_addrD').textContent=addr; + }catch(e){toast('Query failed: '+e.message,true);} +}; + +// ═══════════════════════════════════════════ +// FAILED TX +// ═══════════════════════════════════════════ +window.checkFailedTxs=async function(){ + const addr=document.getElementById('ft_addr').value.trim().toLowerCase(); + addr40(addr,'Address'); + try{ + const d=await rpc('/v1/query/failed-txs',{address:addr,perPage:20}); + const c=d.totalCount||0;const el=document.getElementById('ft_result');el.style.display='block'; + if(c===0){el.innerHTML=`
✓ No failed transactions for ${addr.slice(0,12)}…
`;return;} + const rows=(d.results||[]).map(r=>`
${esc(r.error?.msg||'?')} (${r.error?.code})
${r.txHash?.slice(0,24)}…
`).join(''); + el.innerHTML=`
⚠ ${c} failed tx(s)
${rows}`; + }catch(e){toast('Query failed: '+e.message,true);} +}; + +// ═══════════════════════════════════════════ +// PENDING HELPER +// ═══════════════════════════════════════════ +function setPend(btnId,pendId,on){ + const b=document.getElementById(btnId);const p=document.getElementById(pendId); + if(b)b.disabled=on;if(p)p.style.display=on?'flex':'none'; +} + +async function doSubmit(msgType,typeUrl,inner,meta,btnId,pendId){ + if(!signerPrivKey)return toast('Load a private key in Signer first',true); + if(!currentHeight)return toast('Node not connected',true); + setPend(btnId,pendId,true); + try{ + const tx=await buildSigned(msgType,typeUrl,inner,meta); + const hash=await submitTxRPC(tx); + toast('⏳ Broadcasting — confirming in ~25s…'); + checkRPC(); + if(msgType==='create_market')setTimeout(loadMarkets,3000); + setTimeout(async()=>{ + try{ + const d=await rpc('/v1/query/failed-txs',{address:signerAddress,perPage:20}); + const failed=(d.results||[]).find(r=>r.txHash===hash); + if(failed){ + const code=failed.error?.code; + const msg=failed.error?.msg||'Transaction failed'; + toast('✗ Failed — '+friendlyError(code,msg),true); + } else { + toast('✓ Confirmed — '+(hash.length>20?hash.slice(0,20)+'…':hash)); + if(msgType==='create_market'||msgType==='finalize_market')loadMarkets(); + } + }catch(e){toast('✓ Submitted — could not confirm status',false);} + },25000); + }catch(e){toast(friendlyError(null,e.message),true);} + finally{setPend(btnId,pendId,false);} +} + +function showPL(outId,payId,tx){ + document.getElementById(outId).style.display='block'; + document.getElementById(payId).value=JSON.stringify(tx,null,2); +} + +// ═══════════════════════════════════════════ +// MY PREDICTIONS +// ═══════════════════════════════════════════ +async function refreshBalance(){ + if(!signerAddress)return; + try{ + const d=await rpc('/v1/query/account',{address:signerAddress}); + const bal=Number(d.amount||0); + const wbal=document.getElementById('w_balance');if(wbal)wbal.textContent=fmtPRX(bal); + const wres=document.getElementById('w_result');if(wres)wres.style.display='block'; + const wadr=document.getElementById('w_addrD');if(wadr)wadr.textContent=signerAddress; + const waddr=document.getElementById('w_addr');if(waddr&&!waddr.value)waddr.value=signerAddress; + }catch{} +} + +window.loadMyPredictions = async function () { + const el = document.getElementById('myPredictions'); + if (!signerAddress) { + el.innerHTML = '
Load wallet to see predictions
'; + return; + } + el.innerHTML = '
▪▪▪ loading predictions
'; + try { + const data = await rpc('/v1/query/txs-by-sender', { address: signerAddress, perPage: 200 }); + const results = data.results || []; + const seen = {}; + const predictions = []; + + for (const tx of results) { + const t = tx.transaction || tx; + const type = t.type || t.messageType || ''; + if (type !== 'submit_prediction') continue; + const msg = t.msg || t; + let marketId = '', outcome = false, shares = 0n, maxCost = 0n; + if (t.msgBytes) { + const bytes = h2b(t.msgBytes); + let pos = 0; + while (pos < bytes.length) { + const { v: tagV, p: p1 } = decVarint(bytes, pos); pos = p1; + const fn = Number(tagV >> 3n), wt = Number(tagV & 7n); + if (fn === 3 && wt === 0) { const { v, p: p2 } = decVarint(bytes, pos); pos = p2; outcome = v === 1n; } + else if (wt === 0) { const { v: _, p: p2 } = decVarint(bytes, pos); pos = p2; if (fn === 4) shares = _; if (fn === 5) maxCost = _; } + else if (wt === 2) { const { v: lenV, p: p2 } = decVarint(bytes, pos); pos = p2 + Number(lenV); if (fn === 1) marketId = b2h(bytes.slice(p2 - Number(lenV), pos)); } + else if (wt === 1) { pos += 8; } else if (wt === 5) { pos += 4; } else break; + } + } else { + marketId = msg.marketId || ''; + outcome = msg.outcome === true || msg.outcome === 'true' || msg.outcome === 1; + shares = BigInt(msg.shares || 0); + maxCost = BigInt(msg.maxCost || msg.max_cost || 0); + } + const key = marketId || tx.txHash; + if (!seen[key]) { + seen[key] = true; + predictions.push({ marketId: marketId || tx.txHash, outcome, shares, maxCost, height: tx.height || 0 }); + } + } + + if (predictions.length === 0) { + el.innerHTML = '
No predictions yet
'; + return; + } + + el.innerHTML = predictions.map(p => { + const m = _allMarkets.find(x => x.id === p.marketId); + let payoutHtml = ''; + if (m && m.status === 6) { + // finalized — compute expected payout + const totalPool = m.qYes + m.qNo; + const winPool = p.outcome ? m.qYes : m.qNo; + const won = m.proposedOutcome === p.outcome; + if (won && winPool > 0n) { + const payout = totalPool * p.shares / winPool; + payoutHtml = '
✓ Est. payout: ' + fmtPRX(payout) + ' PRX
'; + } else if (!won) { + payoutHtml = '
✗ Lost
'; + } + } else if (m && m.status === 4) { + payoutHtml = '
⏳ Awaiting finalization
'; + } + return '
' + + '
' + + '
' + + '
MKT ' + p.marketId.slice(0,12) + '…
' + + '
' + + '' + (p.outcome ? 'YES' : 'NO') + '' + + 'Shares: ' + fmtPRX(p.shares) + '' + + 'Max: ' + fmtPRX(p.maxCost) + ' PRX' + + '
' + + '
' + + '#' + p.height + '' + + '
' + + payoutHtml + + '
'; + }).join(''); + } catch (e) { + el.innerHTML = '
Error: ' + esc(e.message) + '
'; + } +}; + +// ═══════════════════════════════════════════ +// RENDER MARKET CARDS — Premium Design +// ═══════════════════════════════════════════ +function resolverTier(addr) { + const r = _resolverRegistry.get(addr); + if (!r) return null; + const estRRS = Math.min(r.proposalCount * 10, 999); + if (estRRS >= 200) return {label:'Gold', color:'#FFD700', icon:'★'}; + if (estRRS >= 50) return {label:'Silver', color:'#C0C0C0', icon:'◆'}; + if (estRRS >= 1) return {label:'Bronze', color:'#CD7F32', icon:'▲'}; + return {label:'Registered', color:'var(--text3)', icon:'○'}; +} + +window.renderMarketCards = function(markets) { + if (!markets || markets.length === 0) return '
No markets found
'; + + // category filter — tag-based first, keyword fallback + let filtered = markets; + if (window._activeCat && window._activeCat !== 'all') { + const cat = window._activeCat; + filtered = markets.filter(m => { + const tag = extractCat(m.rules || ''); + if (tag && tag !== 'other') return tag === cat; + // keyword fallback for untagged markets + const q = (m.question || '').toLowerCase(); + if (cat === 'crypto') return /btc|eth|crypto|bitcoin|ethereum|solana|token|defi|nft|blockchain/.test(q); + if (cat === 'sports') return /nba|nfl|fifa|soccer|football|tennis|golf|sports|league|match|game|win/.test(q); + if (cat === 'politics') return /election|president|vote|congress|senate|government|policy|law|bill/.test(q); + if (cat === 'finance') return /stock|market|fed|rate|gdp|inflation|s&p|nasdaq|economy|oil|gold/.test(q); + return cat === 'other'; + }); + } + + if (filtered.length === 0) return '
No markets in this category
'; + + return '
' + filtered.map(m => { + const total = m.qYes + m.qNo; + const yesPct = total > 0n ? Number((m.qYes * 100n) / total) : 50; + const noPct = 100 - yesPct; + const mid = m.marketId || m.txHash || ''; + const vol = total > 0n ? fmtPRX(total) : '—'; + const exp = m.expiry ? '#' + m.expiry.toString() : '—'; + const creator = (m.creator || '').slice(0,8) + '…'; + + let statusHtml = ''; + let cardClass = ''; + if (m.status === 0) { + statusHtml = 'LIVE'; + } else if (m.status === 4) { + statusHtml = 'PROPOSED'; + cardClass = 'mexp'; + } else if (m.status === 5) { + statusHtml = 'DISPUTED'; + cardClass = 'mexp'; + } else if (m.status === 6) { + statusHtml = 'FINALIZED'; + cardClass = 'mfin'; + } else if (m.status === 1) { + statusHtml = 'CANCELLED'; + cardClass = 'mcan'; + } else if (m.status === 8) { + statusHtml = 'EXPIRED'; + cardClass = 'mexp'; + } + + const showBtns = m.status === 0; + + const catIcon = {'crypto':'🪙','sports':'⚽','politics':'🗳','finance':'📈','other':'◈'}[extractCat(m.rules||'')] || '◈'; + const catName = (CAT_LABELS[extractCat(m.rules||'')] || 'Other').replace(/[🪙⚽🗳📈◈]\s*/,''); + const hasBanner = !!extractImg(m.rules||''); + const yesMulti = m.qYes > 0n ? (Number(m.qYes + m.qNo) / Number(m.qYes)).toFixed(2) : '—'; + const noMulti = m.qNo > 0n ? (Number(m.qYes + m.qNo) / Number(m.qNo)).toFixed(2) : '—'; + return `
+ ${mkBannerImg(m.rules)}
+
${catIcon} ${catName}  ${statusHtml}
+
${esc(m.question || '(no question)')}
+
+ ${yesPct}% ${yesMulti}x + · + ${noPct}% ${noMulti}x +
+
+
+
+
Vol${vol}
+
Exp${exp}
+
Creator${creator}
+
`; + }).join('') + '
'; +}; + +// ── Volume chip updater ── + +// store markets globally for detail view +let _allMarkets = []; +let _resolverRegistry = new Map(); +let _detailMarketId = null; // address -> {stake, proposalCount} + +window.showDetail = function(marketId) { + const m = _allMarkets.find(x => x.marketId === marketId || x.txHash === marketId); + if (!m) return; + const open = m.status === 0; + const expired = m.status === 8; + const cancelled = m.status === 1; + const proposed = m.status === 4; + const disputed = m.status === 5; + const finalized = m.status === 6; + const voided = m.status === 7; + const resolved = m.status === 2; + const total = m.qYes + m.qNo; + const yesPct = total > 0n ? Number(m.qYes * 100n / total) : 50; + const noPct = 100 - yesPct; + const mid = m.marketId || m.txHash; + + document.getElementById('det-question').textContent = m.question; + document.getElementById('det-qyes').textContent = fmtPRX(m.qYes) + ' PRX'; + document.getElementById('det-qno').textContent = fmtPRX(m.qNo) + ' PRX'; + document.getElementById('det-yes-pct').textContent = yesPct + '%'; + document.getElementById('det-no-pct').textContent = noPct + '%'; + document.getElementById('det-bar').style.width = yesPct + '%'; + document.getElementById('det-mid').textContent = mid; + document.getElementById('det-creator').textContent = m.creator || '—'; + document.getElementById('det-total').textContent = fmtPRX(m.qYes + m.qNo) + ' PRX'; + if (m.expiry) { + const blk = Number(m.expiry); + const blocksLeft = blk - currentHeight; + const msLeft = blocksLeft * 5000; + const expDate = new Date(Date.now() + msLeft); + const dateStr = expDate.toLocaleDateString('en-US', {month:'short', day:'numeric', year:'numeric'}); + const timeStr = expDate.toLocaleTimeString('en-US', {hour:'2-digit', minute:'2-digit'}); + document.getElementById('det-expiry').textContent = 'blk #' + blk + ' (' + dateStr + ' ' + timeStr + ')'; + } else { + document.getElementById('det-expiry').textContent = '—'; + } + // Banner image + const imgUrl = extractImg(m.rules || ''); + const bannerDiv = document.getElementById('det-img-banner'); + const bannerImg = document.getElementById('det-img-banner-img'); + if (bannerDiv && bannerImg) { + if (imgUrl) { + bannerImg.src = imgUrl; + bannerDiv.style.display = ''; + bannerImg.onerror = () => { bannerDiv.style.display = 'none'; }; + } else { + bannerDiv.style.display = 'none'; + } + } + + const rulesRow = document.getElementById('det-rules-row'); + const rulesEl = document.getElementById('det-rules'); + const catBadge = document.getElementById('det-cat-badge'); + if (rulesRow && rulesEl) { + const rawRules = m.rules || ''; + const cat = extractCat(rawRules); + const stripped = stripImgTag(stripCatPrefix(rawRules)).trim(); + const displayRules = stripped || (cat !== 'other' ? 'No resolution criteria specified.' : ''); + if (displayRules || cat !== 'other') { + rulesEl.textContent = displayRules || 'No resolution criteria specified.'; + rulesRow.style.display = ''; + if (catBadge) { + catBadge.textContent = CAT_LABELS[cat] || '◈ Other'; + const catColors = { crypto:'#f7931a', sports:'#22c55e', politics:'#3b82f6', finance:'#a855f7', other:'var(--text3)' }; + catBadge.style.background = 'var(--surf2)'; + catBadge.style.color = catColors[cat] || 'var(--text3)'; + catBadge.style.border = '1px solid var(--border)'; + } + } else { + rulesRow.style.display = 'none'; + } + } + + const resolverRow = document.getElementById('det-resolver-row'); + if (m.resolver) { + resolverRow.style.display = ''; + const tier = m.resolver ? resolverTier(m.resolver) : null; + const tierHtml = tier ? ' ' + tier.icon + ' ' + tier.label + '' : ''; + document.getElementById('det-resolver').innerHTML = (m.resolver || '—') + tierHtml + (m.proposedOutcome !== undefined ? ' → proposed ' + (m.proposedOutcome ? 'YES' : 'NO') : ''); + } else { + resolverRow.style.display = 'none'; + } + + const statusLabels = {0:'Open',1:'Cancelled',2:'Resolved',3:'Expired',4:'Proposed',5:'Disputed',6:'Finalized',7:'Voided',8:'Expired'}; + const statusClasses = {0:'sp-o',1:'sp-d',2:'sp-f',3:'sp-e',4:'sp-e',5:'sp-d',6:'sp-f',7:'sp-e',8:'sp-e'}; + document.getElementById('det-status-pill').innerHTML = '
' + (statusLabels[m.status]||'Closed') + '
'; + + const yesBtn = document.getElementById('det-bet-yes'); + const noBtn = document.getElementById('det-bet-no'); + if (open) { + yesBtn.removeAttribute('disabled'); yesBtn.setAttribute('onclick', 'fillP(' + JSON.stringify(mid) + ', true)'); + noBtn.removeAttribute('disabled'); noBtn.setAttribute('onclick', 'fillP(' + JSON.stringify(mid) + ', false)'); + } else { + yesBtn.setAttribute('disabled',''); noBtn.setAttribute('disabled',''); + } + + const proposeBtn = document.getElementById('det-propose-btn'); + const claimBtn = document.getElementById('det-claim-btn'); + if (proposeBtn) { + if (m.status === 8) { + // COI-1: hide propose if signer is market creator + const signerIsCreator = signerAddress && m.creator && signerAddress.toLowerCase() === m.creator.toLowerCase(); + // COI-2: hide propose if signer holds a position in this market + const signerHasPosition = (() => { + try { + const txs = JSON.parse(localStorage.getItem('praxis_tx_cache') || '[]'); + return txs.some(tx => + tx.messageType === 'submit_prediction' && + tx.sender && tx.sender.toLowerCase() === (signerAddress||'').toLowerCase() && + tx.transaction && tx.transaction.msg && + (() => { try { return b2h(Uint8Array.from(atob(tx.transaction.msg.marketId||''), c=>c.charCodeAt(0))) === mid; } catch { return false; } })() + ); + } catch { return false; } + })(); + if (signerIsCreator) { + proposeBtn.style.display = ''; + proposeBtn.disabled = true; + proposeBtn.title = 'Market creators cannot propose outcomes for their own markets'; + proposeBtn.textContent = '⚖ Cannot Propose (Creator)'; + } else if (signerHasPosition) { + proposeBtn.style.display = ''; + proposeBtn.disabled = true; + proposeBtn.title = 'Forfeit your position before proposing'; + proposeBtn.textContent = '⚖ Forfeit Position First'; + } else { + proposeBtn.style.display = ''; + proposeBtn.disabled = false; + proposeBtn.textContent = '⚖ Propose Outcome'; + proposeBtn.setAttribute('onclick', 'fillPropose(' + JSON.stringify(mid) + ')'); + } + } else { + proposeBtn.style.display = 'none'; + proposeBtn.disabled = false; + proposeBtn.textContent = '⚖ Propose Outcome'; + } + } + if (claimBtn) { + if (m.status === 6) { + claimBtn.style.display = ''; + claimBtn.textContent = '◎ Claim Winnings'; + claimBtn.setAttribute('onclick', 'fillC(' + JSON.stringify(mid) + ')'); + } else if (m.status === 1) { + claimBtn.style.display = ''; + claimBtn.textContent = '◎ Claim Refund'; + claimBtn.setAttribute('onclick', 'fillC(' + JSON.stringify(mid) + ')'); + } + } + + const reclaimBtn = document.getElementById('det-reclaim-btn'); + if (reclaimBtn) { + if (m.status === 8 && currentHeight > Number(m.expiry) + 300) { + reclaimBtn.style.display = ''; + reclaimBtn.setAttribute('onclick', 'fillReclaim(' + JSON.stringify(mid) + ')'); + } else { + reclaimBtn.style.display = 'none'; + } + } + + const forfeitBtn = document.getElementById('det-forfeit-btn'); + if (forfeitBtn) { + if (m.status === 0 && signerAddress && signerAddress !== m.creator) { + forfeitBtn.style.display = ''; + forfeitBtn.setAttribute('onclick', 'fillForfeit(' + JSON.stringify(mid) + ')'); + } else { + forfeitBtn.style.display = 'none'; + } + } + + const bannerCard = document.getElementById('det-banner-card'); + if (m.status === 8) { + bannerCard.style.display = ''; + bannerCard.innerHTML = '
Awaiting resolver proposal
'; + } else if (m.status === 4) { + bannerCard.style.display = ''; + bannerCard.innerHTML = '
🔎 Resolver: ' + (m.resolver ? m.resolver.slice(0,8) + '…' : '?') + ' — proposed ' + (m.proposedOutcome ? 'YES' : 'NO') + '
'; + } else if (m.status === 1) { + bannerCard.style.display = ''; + bannerCard.innerHTML = '
Market cancelled — reclaim your stake
'; + } else if (m.status === 7) { + bannerCard.style.display = ''; + bannerCard.innerHTML = '
Market voided — full refund available
'; + } else { + bannerCard.style.display = 'none'; + } + + _detailMarketId = mid; + showPage('detail', null); + setTimeout(()=>switchDetailTab('activity'), 50); +}; +window.openDetail = window.showDetail; + +window.fillP = (id, outcome) => { + document.getElementById('p_mid').value = id; + if (outcome !== undefined) { setOut(outcome); } + showPage('predict', null); +}; +window.fillC = id => { document.getElementById('cl_mid').value = id; showPage('claim', null); }; + +// ═══════════════════════════════════════════ +// MARKETS PAGE +// ═══════════════════════════════════════════ +function decVarint(buf,pos){let r=0n,s=0n;while(pos b.classList.remove('active')); + const btn = document.getElementById('tab-' + tab); + if (btn) btn.classList.add('active'); + renderCurrentTab(); +}; + +function renderCurrentTab() { + const el = document.getElementById('marketsList'); + if (!_allMarkets.length) return; + + let markets; + if (_activeTab === 'live') { + markets = _allMarkets.filter(m => m.status === 0); + } else if (_activeTab === 'proposed') { + markets = _allMarkets.filter(m => m.status === 4 || m.status === 5); + } else { + // closed — rolling window of last CLOSED_WINDOW blocks + markets = _allMarkets.filter(m => + (m.status === 8 || m.status === 1 || m.status === 6 || m.status === 7 || m.status === 2 || m.status === 3) && + m.expiry && Number(m.expiry) >= (currentHeight - CLOSED_WINDOW) + ); + } + + const countEl = document.getElementById('sb_c'); + if (countEl) countEl.textContent = _allMarkets.filter(m => m.status === 0).length; + + if (markets.length === 0) { + const labels = {live:'No open markets yet', proposed:'No markets awaiting resolution', closed:'No recently closed markets'}; + el.innerHTML = '
' + (labels[_activeTab] || 'No markets') + '
'; + return; + } + el.innerHTML = window.renderMarketCards(markets); +} + +window.loadMarkets = async function () { + const el = document.getElementById('marketsList'); + const countEl = document.getElementById('sb_c'); + el.innerHTML = '
▪ ▪ ▪  loading markets from chain
'; + try { + await checkRPC(); + + const heightResp = await rpc('/v1/query/height', {}); + const tipHeight = Number(heightResp.height || currentHeight || 1); + const BATCH = 100; + const CACHE_KEY = 'praxis_tx_cache'; + const CACHE_HEIGHT_KEY = 'praxis_scan_height'; + + // load cache + let allTxs = []; + let scanFrom = 1; + try { + const cached = localStorage.getItem(CACHE_KEY); + const cachedHeight = parseInt(localStorage.getItem(CACHE_HEIGHT_KEY) || '0'); + if (cached && cachedHeight > 0) { + allTxs = JSON.parse(cached); + scanFrom = cachedHeight + 1; + el.innerHTML = '
▪ ▪ ▪  Cache loaded to block ' + cachedHeight + ' — scanning new blocks…
'; + } + } catch(e) { allTxs = []; scanFrom = 1; } + + // scan only new blocks + if (scanFrom <= tipHeight) { + for (let h = scanFrom; h <= tipHeight; h += BATCH) { + const pct = Math.round(((h - scanFrom) / Math.max(tipHeight - scanFrom, 1)) * 100); + el.innerHTML = '
▪ ▪ ▪  Scanning blocks ' + h + ' / ' + tipHeight + '  ' + pct + '%
'; + const batchPromises = []; + for (let bh = h; bh < h + BATCH && bh <= tipHeight; bh++) { + batchPromises.push( + rpc('/v1/query/txs-by-height', { height: bh, perPage: 50 }) + .then(d => allTxs.push(...(d.results || []))) + .catch(() => {}) + ); + } + await Promise.all(batchPromises); + } + // save cache + try { + localStorage.setItem(CACHE_KEY, JSON.stringify(allTxs)); + localStorage.setItem(CACHE_HEIGHT_KEY, String(tipHeight)); + } catch(e) {} + } + + const marketsMap = new Map(); + + for (const tx of allTxs) { + if (tx.messageType !== 'create_market') continue; + const msg = (tx.transaction && tx.transaction.msg) || {}; + const question = msg.question || ''; + const rules = msg.rules || ''; + const creator = tx.sender || ''; + const b0 = BigInt(msg.b0 || 0); + const expiry = BigInt(msg.expiryTime || msg.expiry_time || 0); + const nonce = BigInt(msg.nonce || 0); + const lmsrSeed = b0 > 50000000n ? b0 - 50000000n : b0; + + let marketId = tx.txHash || (creator + String(nonce)); + try { + const creatorBytes = /^[0-9a-fA-F]{40}$/.test(creator) + ? h2b(creator) + : (() => { const bin = atob(creator); return new Uint8Array([...bin].map(c => c.charCodeAt(0))); })(); + const nonceBytes = new Uint8Array(8); + let n = nonce; + for (let i = 7; i >= 0; i--) { nonceBytes[i] = Number(n & 0xffn); n >>= 8n; } + const input = new Uint8Array(creatorBytes.length + 8); + input.set(creatorBytes); input.set(nonceBytes, 20); + const hash = await crypto.subtle.digest('SHA-256', input); + marketId = b2h(new Uint8Array(hash).slice(0, 20)); + } catch (e) {} + + if (!marketsMap.has(marketId)) { + marketsMap.set(marketId, { + txHash: tx.txHash || '', + marketId, + question: question || '(no question)', + rules: rules || '', + creator, + b0, + lmsrSeed, + expiry, + nonce, + status: 0, + qYes: lmsrSeed / 2n, + qNo: lmsrSeed / 2n, + }); + } + } + + for (const tx of allTxs) { + if (tx.messageType !== 'submit_prediction') continue; + const msg = (tx.transaction && tx.transaction.msg) || {}; + const rawMid = msg.marketId || msg.market_id || ''; + let marketId = rawMid; + try { const b = Uint8Array.from(atob(rawMid), c => c.charCodeAt(0)); marketId = b2h(b); } catch(e) {} + const outcome = msg.outcome === true || msg.outcome === 'true' || msg.outcome === 1; + const amount = BigInt(msg.shares || msg.amount || 0); + if (!marketId || !marketsMap.has(marketId)) continue; + const m = marketsMap.get(marketId); + if (outcome) { m.qYes += amount; } else { m.qNo += amount; } + } + + for (const tx of allTxs) { + if (tx.messageType !== 'propose_outcome') continue; + const msg = (tx.transaction && tx.transaction.msg) || {}; + const rawMid = msg.marketId || ''; + let marketId = rawMid; + try { const b = Uint8Array.from(atob(rawMid), c => c.charCodeAt(0)); marketId = b2h(b); } catch(e) {} + if (!marketId || !marketsMap.has(marketId)) continue; + const m = marketsMap.get(marketId); + let resolver = tx.sender || ''; + try { const rb = Uint8Array.from(atob(msg.resolverAddress || ''), c => c.charCodeAt(0)); resolver = b2h(rb); } catch(e) {} + m.status = 4; + m.resolver = resolver; + m.proposedOutcome = msg.proposedOutcome; + } + + // Build resolver registry + for (const tx of allTxs) { + if (tx.messageType !== 'register_resolver') continue; + const msg = (tx.transaction && tx.transaction.msg) || {}; + const addr = tx.sender || ''; + let stake = BigInt(msg.stakeAmount || msg.stake_amount || 0); + if (!_resolverRegistry.has(addr)) { + _resolverRegistry.set(addr, { stake, proposalCount: 0, rrs: 10 }); + } else { + _resolverRegistry.get(addr).stake = stake; + } + } + for (const tx of allTxs) { + if (tx.messageType !== 'propose_outcome') continue; + const addr = tx.sender || ''; + if (_resolverRegistry.has(addr)) { + _resolverRegistry.get(addr).proposalCount++; + } + } + // sync rrs estimate into registry + _resolverRegistry.forEach(r => { r.rrs = Math.min(10 + r.proposalCount * 10, 999); }); + + for (const tx of allTxs) { + if (tx.messageType !== 'finalize_market') continue; + const msg = (tx.transaction && tx.transaction.msg) || {}; + const rawMid = msg.marketId || ''; + let marketId = rawMid; + try { const b = Uint8Array.from(atob(rawMid), c => c.charCodeAt(0)); marketId = b2h(b); } catch(e) {} + if (!marketId || !marketsMap.has(marketId)) continue; + marketsMap.get(marketId).status = 6; + } + + for (const tx of allTxs) { + if (tx.messageType !== 'file_dispute') continue; + const msg = (tx.transaction && tx.transaction.msg) || {}; + const rawMid = msg.marketId || ''; + let marketId = rawMid; + try { const b = Uint8Array.from(atob(rawMid), c => c.charCodeAt(0)); marketId = b2h(b); } catch(e) {} + if (!marketId || !marketsMap.has(marketId)) continue; + marketsMap.get(marketId).status = 5; + } + + for (const tx of allTxs) { + if (tx.messageType !== 'cancel_market') continue; + const msg = (tx.transaction && tx.transaction.msg) || {}; + const rawMid = msg.marketId || ''; + let marketId = rawMid; + try { const b = Uint8Array.from(atob(rawMid), c => c.charCodeAt(0)); marketId = b2h(b); } catch(e) {} + if (!marketId || !marketsMap.has(marketId)) continue; + marketsMap.get(marketId).status = 1; + } + + const markets = [...marketsMap.values()]; + for (const m of markets) { + if (m.expiry && currentHeight > Number(m.expiry) && m.status === 0) m.status = 8; + } + + _allMarkets = markets; + checkRoles(); + renderCurrentTab(); + + } catch (e) { + el.innerHTML = '
⚠ Cannot reach node at ' + getRPC() + '
' + esc(e.message) + '
'; + } +}; + +// ═══════════════════════════════════════════ +// ── SEND +// ═══════════════════════════════════════════ +window.build_send=function(){try{ + const from=document.getElementById('s_from').value.trim().toLowerCase(); + const to=document.getElementById('s_to').value.trim().toLowerCase(); + const amt=parseInt(document.getElementById('s_amount').value)*1000000; + const fee=parseInt(document.getElementById('s_fee').value)||10000; + addr40(from,'From');addr40(to,'To');if(!amt||amt<=0)throw new Error('Amount > 0 required'); + showPL('so','sp',buildUnsigned('send','type.googleapis.com/types.MessageSend',encSend(from,to,amt),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.signAndSubmit_send=async function(){try{ + const from=document.getElementById('s_from').value.trim().toLowerCase(); + const to=document.getElementById('s_to').value.trim().toLowerCase(); + const amt=parseInt(document.getElementById('s_amount').value)*1000000; + const fee=parseInt(document.getElementById('s_fee').value)||10000; + addr40(from,'From');addr40(to,'To');if(!amt||amt<=0)throw new Error('Amount > 0'); + await doSubmit('send','type.googleapis.com/types.MessageSend',encSend(from,to,amt),{fee},'btn_send','pend_send'); +}catch(e){toast(e.message,true);}}; + +// ── CREATE MARKET +window.build_create=function(){try{ + const _cat=getSelectedCat(); + const q=document.getElementById('c_question').value.trim(); + const cr=document.getElementById('c_creator').value.trim().toLowerCase(); + const b0=parseInt(document.getElementById('c_b0').value)*1000000; + const exp=parseInt(document.getElementById('c_expiry').value)||currentHeight+1000; + const fee=parseInt(document.getElementById('c_fee').value)||10000; + let nonce=document.getElementById('c_nonce').value; + if(!nonce)nonce=BigInt(Date.now())*1000n; + else nonce=parseInt(nonce); + const rules=document.getElementById('c_rules').value.trim(); + const _imgUrl=document.getElementById('c_img')?.value.trim()||''; + if(!q)throw new Error('Question required');addr40(cr,'Creator'); + showPL('co','cp',buildUnsigned('create_market','type.googleapis.com/types.MessageCreateMarket',encCreate(cr,b0,exp,nonce,q,buildRulesWithImg(buildRulesWithCat(_cat,rules),_imgUrl)),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.updateCreateBreakdown=function(){ + const b0=parseInt(document.getElementById('c_b0')?.value||0); + const fee=parseInt(document.getElementById('c_fee')?.value||10000); + const bond=5000; + const total=b0+bond+(fee/1000000); + const el=document.getElementById('create_breakdown'); + if(!el)return; + el.innerHTML= + '
B0 liquidity seed'+b0.toLocaleString()+' PRX
'+ + '
Creator bond (locked)5,000 PRX
'+ + '
TX fee'+fee.toLocaleString()+' uPRX
'+ + '
Total deducted'+(b0+bond).toLocaleString()+' PRX
'; +}; +window.signAndSubmit_create=async function(){try{ + const _cat=getSelectedCat(); + const q=document.getElementById('c_question').value.trim(); + const cr=document.getElementById('c_creator').value.trim().toLowerCase(); + const b0=parseInt(document.getElementById('c_b0').value)*1000000; + const exp=parseInt(document.getElementById('c_expiry').value)||currentHeight+1000; + const fee=parseInt(document.getElementById('c_fee').value)||10000; + let nonce=document.getElementById('c_nonce').value; + if(!nonce)nonce=BigInt(Date.now())*1000n; + else nonce=parseInt(nonce); + const rules=document.getElementById('c_rules').value.trim(); + const _imgUrl=document.getElementById('c_img')?.value.trim()||''; + if(!q)throw new Error('Question required');addr40(cr,'Creator'); + await doSubmit('create_market','type.googleapis.com/types.MessageCreateMarket',encCreate(cr,b0,exp,nonce,q,buildRulesWithImg(buildRulesWithCat(_cat,rules),_imgUrl)),{fee},'btn_create','pend_create'); +}catch(e){toast(e.message,true);}}; + +// ── SUBMIT PREDICTION +window.build_predict=function(){try{ + const mid=document.getElementById('p_mid').value.trim().toLowerCase();mid40(mid); + const bettor=document.getElementById('p_bettor').value.trim().toLowerCase();addr40(bettor,'Bettor'); + const sharesInput=parseInt(document.getElementById("p_shares").value); + const shares=sharesInput*1000000; + const mc=parseInt(document.getElementById('p_maxcost').value)*1000000; + const fee=parseInt(document.getElementById('p_fee').value)||10000; + if(sharesInput<1)throw new Error("Shares min 1 PRX"); + showPL('po','pp',buildUnsigned('submit_prediction','type.googleapis.com/types.MessageSubmitPrediction',encPredict(mid,bettor,selectedOut,shares,mc),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.updatePredictBreakdown=function(){ + const shares=parseInt(document.getElementById('p_shares')?.value||0); + const fee=parseInt(document.getElementById('p_fee')?.value||10000); + const slipPct=parseFloat(document.getElementById('p_slippage')?.value||5); + const el=document.getElementById('predict_breakdown'); + const slipLbl=document.getElementById('p_slip_lbl'); + if(slipLbl)slipLbl.textContent=slipPct.toFixed(1)+'%'; + if(!el)return; + const tradeCost=shares; + const creatorFee=Math.ceil(shares*0.01); + const resolverFee=Math.ceil(shares*0.01); + const total=tradeCost+creatorFee+resolverFee; + const maxCost=Math.ceil(total*(1+slipPct/100)); + const mcEl=document.getElementById('p_maxcost'); + if(mcEl)mcEl.value=maxCost; + el.innerHTML= + '
Trade cost'+tradeCost.toLocaleString()+' PRX
'+ + '
Market fee (2%)'+(creatorFee+resolverFee).toLocaleString()+' PRX
'+ + '
TX fee'+fee.toLocaleString()+' uPRX
'+ + '
Max cost ('+slipPct.toFixed(1)+'% slippage)'+maxCost.toLocaleString()+' PRX
'; +}; +window.signAndSubmit_predict=async function(){try{ + const mid=document.getElementById('p_mid').value.trim().toLowerCase();mid40(mid); + const bettor=document.getElementById('p_bettor').value.trim().toLowerCase();addr40(bettor,'Bettor'); + const sharesInput=parseInt(document.getElementById("p_shares").value); + const shares=sharesInput*1000000; + const mc=parseInt(document.getElementById('p_maxcost').value)*1000000; + const fee=parseInt(document.getElementById('p_fee').value)||10000; + if(sharesInput<1)throw new Error("Shares min 1 PRX"); + await doSubmit('submit_prediction','type.googleapis.com/types.MessageSubmitPrediction',encPredict(mid,bettor,selectedOut,shares,mc),{fee},'btn_predict','pend_predict'); +}catch(e){toast(e.message,true);}}; + +// ── CLAIM WINNINGS +window.build_claim=function(){try{ + const mid=document.getElementById('cl_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('cl_addr').value.trim().toLowerCase();addr40(addr,'Claimant'); + const fee=parseInt(document.getElementById('cl_fee').value)||10000; + showPL('clo','clp',buildUnsigned('claim_winnings','type.googleapis.com/types.MessageClaimWinnings',encClaim(mid,addr),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.signAndSubmit_claim=async function(){try{ + const mid=document.getElementById('cl_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('cl_addr').value.trim().toLowerCase();addr40(addr,'Claimant'); + const fee=parseInt(document.getElementById('cl_fee').value)||10000; + await doSubmit('claim_winnings','type.googleapis.com/types.MessageClaimWinnings',encClaim(mid,addr),{fee},'btn_claim','pend_claim'); +}catch(e){toast(e.message,true);}}; + +// ── REGISTER RESOLVER +window.build_register=function(){try{ + const addr=document.getElementById('reg_addr').value.trim().toLowerCase();addr40(addr,'Resolver'); + const stake=parseInt(document.getElementById('reg_stake').value)*1000000; + const fee=parseInt(document.getElementById('reg_fee').value)||10000; + if(stake<500000000000)throw new Error('Stake min 500,000 PRX'); + showPL('rego','regp',buildUnsigned('register_resolver','type.googleapis.com/types.MessageRegisterResolver',encRegister(addr,stake),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.signAndSubmit_register=async function(){try{ + const addr=document.getElementById('reg_addr').value.trim().toLowerCase();addr40(addr,'Resolver'); + const stake=parseInt(document.getElementById('reg_stake').value)*1000000; + const fee=parseInt(document.getElementById('reg_fee').value)||10000; + if(stake<500000000000)throw new Error('Stake min 500,000 PRX'); + await doSubmit('register_resolver','type.googleapis.com/types.MessageRegisterResolver',encRegister(addr,stake),{fee},'btn_register','pend_register'); +}catch(e){toast(e.message,true);}}; + +// ── PROPOSE OUTCOME +window.build_propose=function(){try{ + const mid=document.getElementById('pr_mid').value.trim().toLowerCase();mid40(mid); + const res=document.getElementById('pr_resolver').value.trim().toLowerCase();addr40(res,'Resolver'); + const bond=parseInt(document.getElementById('pr_bond').value)*1000000; + const fee=parseInt(document.getElementById('pr_fee').value)||10000; + showPL('propo','propp',buildUnsigned('propose_outcome','type.googleapis.com/types.MessageProposeOutcome',encPropose(mid,res,propOut,bond),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.signAndSubmit_propose=async function(){try{ + const mid=document.getElementById('pr_mid').value.trim().toLowerCase();mid40(mid); + const res=document.getElementById('pr_resolver').value.trim().toLowerCase();addr40(res,'Resolver'); + const bond=parseInt(document.getElementById('pr_bond').value)*1000000; + const fee=parseInt(document.getElementById('pr_fee').value)||10000; + await doSubmit('propose_outcome','type.googleapis.com/types.MessageProposeOutcome',encPropose(mid,res,propOut,bond),{fee},'btn_propose','pend_propose'); +}catch(e){toast(e.message,true);}}; + +// ── FILE DISPUTE +window.build_dispute=function(){try{ + const mid=document.getElementById('dis_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('dis_addr').value.trim().toLowerCase();addr40(addr,'Disputer'); + const bond=parseInt(document.getElementById('dis_bond').value)*1000000; + const fee=parseInt(document.getElementById('dis_fee').value)||10000; + showPL('diso','disp',buildUnsigned('file_dispute','type.googleapis.com/types.MessageFileDispute',encDispute(mid,addr,bond),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.signAndSubmit_dispute=async function(){try{ + const mid=document.getElementById('dis_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('dis_addr').value.trim().toLowerCase();addr40(addr,'Disputer'); + const bond=parseInt(document.getElementById('dis_bond').value)*1000000; + const fee=parseInt(document.getElementById('dis_fee').value)||10000; + await doSubmit('file_dispute','type.googleapis.com/types.MessageFileDispute',encDispute(mid,addr,bond),{fee},'btn_dispute','pend_dispute'); +}catch(e){toast(e.message,true);}}; + +// ── COMMIT VOTE +window.build_commit=function(){try{ + const mid=document.getElementById('cv_mid').value.trim().toLowerCase();mid40(mid); + const voter=document.getElementById('cv_voter').value.trim().toLowerCase();addr40(voter,'Voter'); + const hash=document.getElementById('cv_hash').value.trim().toLowerCase();if(hash.length!==64)throw new Error('Commit hash must be 64 hex chars'); + const fee=parseInt(document.getElementById('cv_fee').value)||10000; + showPL('cvo','cvp',buildUnsigned('commit_vote','type.googleapis.com/types.MessageCommitVote',encCommit(mid,voter,hash),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.signAndSubmit_commit=async function(){try{ + const mid=document.getElementById('cv_mid').value.trim().toLowerCase();mid40(mid); + const voter=document.getElementById('cv_voter').value.trim().toLowerCase();addr40(voter,'Voter'); + const hash=document.getElementById('cv_hash').value.trim().toLowerCase();if(hash.length!==64)throw new Error('Commit hash must be 64 hex chars'); + const fee=parseInt(document.getElementById('cv_fee').value)||10000; + await doSubmit('commit_vote','type.googleapis.com/types.MessageCommitVote',encCommit(mid,voter,hash),{fee},'btn_commit','pend_commit'); +}catch(e){toast(e.message,true);}}; + +// ── REVEAL VOTE +window.build_reveal=function(){try{ + const mid=document.getElementById('rv_mid').value.trim().toLowerCase();mid40(mid); + const voter=document.getElementById('rv_voter').value.trim().toLowerCase();addr40(voter,'Voter'); + const nonce=document.getElementById('rv_nonce').value.trim().toLowerCase();if(nonce.length!==64)throw new Error('Nonce must be 64 hex chars'); + const fee=parseInt(document.getElementById('rv_fee').value)||10000; + showPL('rvo','rvp',buildUnsigned('reveal_vote','type.googleapis.com/types.MessageRevealVote',encReveal(mid,voter,revOut,nonce),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.signAndSubmit_reveal=async function(){try{ + const mid=document.getElementById('rv_mid').value.trim().toLowerCase();mid40(mid); + const voter=document.getElementById('rv_voter').value.trim().toLowerCase();addr40(voter,'Voter'); + const nonce=document.getElementById('rv_nonce').value.trim().toLowerCase();if(nonce.length!==64)throw new Error('Nonce must be 64 hex chars'); + const fee=parseInt(document.getElementById('rv_fee').value)||10000; + await doSubmit('reveal_vote','type.googleapis.com/types.MessageRevealVote',encReveal(mid,voter,revOut,nonce),{fee},'btn_reveal','pend_reveal'); +}catch(e){toast(e.message,true);}}; + +// ── TALLY VOTES +window.build_tally=function(){try{ + const mid=document.getElementById('tal_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('tal_addr').value.trim().toLowerCase();addr40(addr,'Caller'); + const fee=parseInt(document.getElementById('tal_fee').value)||10000; + showPL('talo','talp',buildUnsigned('tally_votes','type.googleapis.com/types.MessageTallyVotes',encTally(mid,addr),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.signAndSubmit_tally=async function(){try{ + const mid=document.getElementById('tal_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('tal_addr').value.trim().toLowerCase();addr40(addr,'Caller'); + const fee=parseInt(document.getElementById('tal_fee').value)||10000; + await doSubmit('tally_votes','type.googleapis.com/types.MessageTallyVotes',encTally(mid,addr),{fee},'btn_tally','pend_tally'); +}catch(e){toast(e.message,true);}}; + +// ── FINALIZE MARKET +window.build_finalize=function(){try{ + const mid=document.getElementById('fin_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('fin_addr').value.trim().toLowerCase();addr40(addr,'Caller'); + const fee=parseInt(document.getElementById('fin_fee').value)||10000; + showPL('fino','finp',buildUnsigned('finalize_market','type.googleapis.com/types.MessageFinalizeMarket',encFinalize(mid,addr),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.signAndSubmit_finalize=async function(){try{ + const mid=document.getElementById('fin_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('fin_addr').value.trim().toLowerCase();addr40(addr,'Caller'); + const fee=parseInt(document.getElementById('fin_fee').value)||10000; + await doSubmit('finalize_market','type.googleapis.com/types.MessageFinalizeMarket',encFinalize(mid,addr),{fee},'btn_finalize','pend_finalize'); +}catch(e){toast(e.message,true);}}; + +// ── CLAIM SLASH +window.build_slash=function(){try{ + const mid=document.getElementById('sl_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('sl_addr').value.trim().toLowerCase();addr40(addr,'Claimant'); + const fee=parseInt(document.getElementById('sl_fee').value)||10000; + showPL('slo','slp',buildUnsigned('claim_slash','type.googleapis.com/types.MessageClaimSlash',encSlash(mid,addr),{fee}));toast('Payload built'); +}catch(e){toast(e.message,true);}}; +window.signAndSubmit_slash=async function(){try{ + const mid=document.getElementById('sl_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('sl_addr').value.trim().toLowerCase();addr40(addr,'Claimant'); + const fee=parseInt(document.getElementById('sl_fee').value)||10000; + await doSubmit('claim_slash','type.googleapis.com/types.MessageClaimSlash',encSlash(mid,addr),{fee},'btn_slash','pend_slash'); +}catch(e){toast(e.message,true);}}; + +// ═══════════════════════════════════════════ +// MAINNET POLISH — UI ONLY, NO CHAIN LOGIC +// ═══════════════════════════════════════════ + +// PRX denomination — 1 PRX = 1 PRX (no micro conversion) + +// Copy to clipboard +window.copyText = async function(text, btn) { + try { + await navigator.clipboard.writeText(text); + if (btn) { btn.textContent = '✓'; btn.classList.add('ok'); setTimeout(() => { btn.textContent = '⎘'; btn.classList.remove('ok'); }, 1800); } + toast('Copied'); + } catch { toast('Copy failed', true); } +}; + +// Wire copy buttons to derived address and pubkey after key load +function wireCopyBtns() { + const pairs = [ + ['sk_addr', 'copy_sk_addr'], + ['sk_pub', 'copy_sk_pub'], + ]; + pairs.forEach(([srcId, btnId]) => { + const btn = document.getElementById(btnId); + if (!btn) return; + btn.onclick = function() { + const el = document.getElementById(srcId); + copyText(el ? el.textContent.trim() : '', this); + }; + }); + // payload boxes + document.querySelectorAll('.payload-box textarea').forEach(ta => { + const box = ta.closest('.payload-box'); + if (!box || box.querySelector('.copy-payload-btn')) return; + const b = document.createElement('button'); + b.className = 'btn bg bsm copy-payload-btn'; + b.style.cssText = 'margin-top:6px;font-size:10px'; + b.textContent = '⎘ Copy payload'; + b.onclick = function() { copyText(ta.value, this); }; + box.appendChild(b); + }); +} + +// Inject copy buttons into derived key display +function injectKeyboardCopyBtns() { + const addrEl = document.getElementById('sk_addr'); + const pubEl = document.getElementById('sk_pub'); + if (addrEl && !document.getElementById('copy_sk_addr')) { + const wrap = document.createElement('div'); + wrap.className = 'cwrap'; + addrEl.parentNode.insertBefore(wrap, addrEl); + wrap.appendChild(addrEl); + const btn = document.createElement('button'); + btn.id = 'copy_sk_addr'; btn.className = 'cbtn'; btn.textContent = '⎘'; + btn.title = 'Copy address'; + wrap.appendChild(btn); + } + if (pubEl && !document.getElementById('copy_sk_pub')) { + const wrap = document.createElement('div'); + wrap.className = 'cwrap'; + pubEl.parentNode.insertBefore(wrap, pubEl); + wrap.appendChild(pubEl); + const btn = document.createElement('button'); + btn.id = 'copy_sk_pub'; btn.className = 'cbtn'; btn.textContent = '⎘'; + btn.title = 'Copy pubkey'; + wrap.appendChild(btn); + } + wireCopyBtns(); +} + +// Confirm modal +let _confirmResolve = null; +window.closeConfirm = function() { + document.getElementById('confOverlay').classList.remove('open'); + if (_confirmResolve) { _confirmResolve(false); _confirmResolve = null; } +}; +document.getElementById('confOk').onclick = function() { + document.getElementById('confOverlay').classList.remove('open'); + if (_confirmResolve) { _confirmResolve(true); _confirmResolve = null; } +}; +document.getElementById('confOverlay').addEventListener('click', function(e) { + if (e.target === this) closeConfirm(); +}); + +function showConfirm(title, rows) { + return new Promise(resolve => { + _confirmResolve = resolve; + document.getElementById('confTitle').textContent = title; + document.getElementById('confSub').textContent = 'review before signing · canopy network'; + const rowsEl = document.getElementById('confRows'); + rowsEl.innerHTML = rows.map(([l, v, cls]) => + `
${l}${v}
` + ).join(''); + document.getElementById('confOverlay').classList.add('open'); + }); +} + +// Patch signAndSubmit_* functions with confirm gate +// We wrap — originals are preserved, just called after confirmation +(function() { + const v = id => parseInt(document.getElementById(id)?.value)||0; + const patches = { + signAndSubmit_create: () => [ + 'Create Market', [ + ['Question', document.getElementById('c_question')?.value || '—', ''], + ['B0 Liquidity', v('c_b0').toLocaleString()+' PRX', 'g'], + ['Fee', v('c_fee')+' PRX', ''], + ] + ], + signAndSubmit_predict: () => [ + 'Submit Prediction', [ + ['Market ID', (document.getElementById('p_mid')?.value||'').slice(0,16)+'…', ''], + ['Outcome', (window._selectedOut!==false?'YES':'NO'), window._selectedOut!==false?'green':'red'], + ['Shares', v('p_shares').toLocaleString()+' PRX', ''], + ['Max Cost', v('p_maxcost').toLocaleString()+' PRX', ''], + ] + ], + signAndSubmit_claim: () => [ + 'Claim Winnings', [ + ['Market ID', (document.getElementById('cl_mid')?.value||'').slice(0,16)+'…', ''], + ['Claimant', (document.getElementById('cl_addr')?.value||'').slice(0,16)+'…', ''], + ] + ], + signAndSubmit_register: () => [ + 'Register Resolver', [ + ['Address', (document.getElementById('reg_addr')?.value||'').slice(0,16)+'…', ''], + ['Stake', (parseInt(document.getElementById('reg_stake')?.value||0)).toLocaleString()+' PRX', 'g'], + ] + ], + signAndSubmit_propose: () => [ + 'Propose Outcome', [ + ['Market ID', (document.getElementById('pr_mid')?.value||'').slice(0,16)+'…', ''], + ['Outcome', (window._propOut!==false?'YES':'NO'), window._propOut!==false?'green':'red'], + ['Bond', v('prop_bond').toLocaleString()+' PRX', ''], + ] + ], + signAndSubmit_dispute: () => [ + 'File Dispute', [ + ['Market ID', (document.getElementById('dis_mid')?.value||'').slice(0,16)+'…', ''], + ['Bond', v('dis_bond').toLocaleString()+' PRX', ''], + ] + ], + signAndSubmit_commit: () => [ + 'Commit Vote', [ + ['Market ID', (document.getElementById('cv_mid')?.value||'').slice(0,16)+'…', ''], + ['Commit Hash', (document.getElementById('cv_hash')?.value||'').slice(0,16)+'…', ''], + ] + ], + signAndSubmit_reveal: () => [ + 'Reveal Vote', [ + ['Market ID', (document.getElementById('rv_mid')?.value||'').slice(0,16)+'…', ''], + ['Vote', (window._revOut!==false?'YES':'NO'), window._revOut!==false?'green':'red'], + ] + ], + signAndSubmit_tally: () => [ + 'Tally Votes', [ + ['Market ID', (document.getElementById('tal_mid')?.value||'').slice(0,16)+'…', ''], + ] + ], + signAndSubmit_finalize: () => [ + 'Finalize Market', [ + ['Market ID', (document.getElementById('fin_mid')?.value||'').slice(0,16)+'…', ''], + ] + ], + signAndSubmit_slash: () => [ + 'Claim Slash', [ + ['Market ID', (document.getElementById('sl_mid')?.value||'').slice(0,16)+'…', ''], + ['Claimant', (document.getElementById('sl_addr')?.value||'').slice(0,16)+'…', ''], + ] + ], + signAndSubmit_claimcreator: () => [ + 'Claim Creator Fee', [ + ['Market ID', (document.getElementById('ccf_mid')?.value||'').slice(0,16)+'…', ''], + ['Creator', (document.getElementById('ccf_addr')?.value||'').slice(0,16)+'…', ''], + ] + ], + signAndSubmit_cancel: () => [ + 'Cancel Market', [ + ['Market ID', (document.getElementById('can_mid')?.value||'').slice(0,16)+'…', ''], + ['Creator', (document.getElementById('can_addr')?.value||'').slice(0,16)+'…', ''], + ] + ], + signAndSubmit_unstake_resolver: () => [ + 'Unstake Resolver', [ + ['Resolver', (document.getElementById('unst_addr')?.value||'').slice(0,16)+'…', ''], + ['Amount', (parseInt(document.getElementById('unst_amount')?.value||0)).toLocaleString()+' PRX (0 = full exit)', ''], + ] + ], + signAndSubmit_claim_unbonded: () => [ + 'Claim Unbonded Stake', [ + ['Resolver', (document.getElementById('cub_addr')?.value||'').slice(0,16)+'…', ''], + ] + ], + signAndSubmit_send: () => [ + 'Send $PRX', [ + ['To', (document.getElementById('s_to')?.value||'').slice(0,16)+'…', ''], + ['Amount', v('s_amount').toLocaleString()+' PRX', 'g'], + ] + ], + }; + + // Expose outcome vars so patches can read them + // (they already exist as module-level vars; we shadow-expose via a getter trick) + Object.defineProperty(window, '_selectedOut', { get: () => typeof selectedOut !== 'undefined' ? selectedOut : true }); + Object.defineProperty(window, '_resolveOut', { get: () => typeof resolveOut !== 'undefined' ? resolveOut : true }); + Object.defineProperty(window, '_propOut', { get: () => typeof propOut !== 'undefined' ? propOut : true }); + Object.defineProperty(window, '_revOut', { get: () => typeof revOut !== 'undefined' ? revOut : true }); + + Object.keys(patches).forEach(name => { + const orig = window[name]; + if (!orig) return; + window[name] = async function() { + const [title, rows] = patches[name](); + const ok = await showConfirm(title, rows); + if (ok) await orig(); + }; + }); +})(); + +// Offline banner wired to RPC status +const _origCheckRPC = window.checkRPC; +window.checkRPC = async function() { + try { + await _origCheckRPC(); + document.getElementById('offBanner').classList.remove('show'); + } catch { + document.getElementById('offBanner').classList.add('show'); + } +}; + +// Session badge visibility +const _origLoadKey = window.loadKey; +window.loadKey = async function() { + await _origLoadKey(); + const badge = document.getElementById('sessBadge'); + if (badge) badge.classList.remove('hidden'); + injectKeyboardCopyBtns(); + setTimeout(wireCopyBtns, 100); +}; +const _origClearKey = window.clearKey; +window.clearKey = function() { + _origClearKey(); + const badge = document.getElementById('sessBadge'); + if (badge) badge.classList.add('hidden'); +}; + +// Wire payload copy buttons when pages are shown +const _origShowPage = window.showPage; +window.showPage = function(id, btn) { + _origShowPage(id, btn); + setTimeout(wireCopyBtns, 50); +}; + +// Init copy btn injection +injectKeyboardCopyBtns(); + +// ═══════════════════════════════════════════ +// INIT +// ═══════════════════════════════════════════ +const _niHost=document.getElementById('ni_host');if(_niHost)_niHost.value=getRPCHost(); +buildMobNav(); +checkRPC(); +setInterval(checkRPC,12000); + +// ═══════════════════════════════════════════ +// KEYSTORE — AES-GCM + Argon2id (Canopy official format) +// Uses argon2-bundled.min.js (must be served alongside app.js) +// ═══════════════════════════════════════════ + +// Argon2id params matching Canopy CLI keystore +const ARGON2_TIME = 3; +const ARGON2_MEM = 65536; // 64 MB +const ARGON2_THREADS = 4; +const ARGON2_KEYLEN = 32; + +async function deriveKeyArgon2(password, salt) { + // argon2-bundled exposes window.argon2 + if (!window.argon2) throw new Error('Argon2 library not loaded — ensure argon2-bundled.min.js is present'); + const result = await window.argon2.hash({ + pass: password, + salt: salt, // Uint8Array + time: window._argon2Override?.time || ARGON2_TIME, + mem: window._argon2Override?.mem || ARGON2_MEM, + hashLen: window._argon2Override?.keylen || ARGON2_KEYLEN, + parallelism: window._argon2Override?.threads || ARGON2_THREADS, + type: window.argon2.ArgonType.Argon2id, + }); + // result.hash is Uint8Array of 32 bytes — import as AES-GCM key + return crypto.subtle.importKey('raw', result.hash, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']); +} + +async function encryptKey(privKeyBytes, password) { + const salt = crypto.getRandomValues(new Uint8Array(16)); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const key = await deriveKeyArgon2(password, salt); + const enc = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, privKeyBytes); + return { + kdf: 'argon2id', + salt: b2h(salt), + iv: b2h(iv), + encrypted: b2h(new Uint8Array(enc)), + argon2: { time: ARGON2_TIME, mem: ARGON2_MEM, threads: ARGON2_THREADS, keylen: ARGON2_KEYLEN }, + }; +} + +async function decryptKey(encrypted, iv, salt, password, kdf) { + let key, nonce; + if (kdf === 'canopy') { + // Canopy CLI format: Argon2i (not id), mem=32MB, keyLen=32, nonce=key[:12] + if (!window.argon2) throw new Error('Argon2 library not loaded'); + const result = await window.argon2.hash({ + pass: password, salt: h2b(salt), + time: 3, mem: 32768, hashLen: 32, + parallelism: 4, type: window.argon2.ArgonType.Argon2i, + }); + const keyBytes = result.hash; // 32 bytes + nonce = keyBytes.slice(0, 12); // nonce = key[:12] + key = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['decrypt']); + } else if (!kdf || kdf === 'argon2id') { + key = await deriveKeyArgon2(password, h2b(salt)); + nonce = h2b(iv); + } else { + // legacy PBKDF2 fallback + const enc = new TextEncoder(); + const km = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveKey']); + key = await crypto.subtle.deriveKey( + { name: 'PBKDF2', salt: h2b(salt), iterations: 200000, hash: 'SHA-256' }, + km, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'] + ); + nonce = h2b(iv); + } + const dec = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: nonce }, key, h2b(encrypted)); + return new Uint8Array(dec); +} + +window.createKeystore = async function() { + const pw = document.getElementById('ks_new_pw').value; + const pw2 = document.getElementById('ks_new_pw2').value; + if (!pw) return toast('Enter a password', true); + if (pw !== pw2) return toast('Passwords do not match', true); + if (pw.length < 8) return toast('Password must be at least 8 characters', true); + + try { + // generate new BLS private key (valid scalar) + const privBytes = bls12_381.utils.randomPrivateKey(); + const pubKey = bls12_381.getPublicKey(privBytes); + const hash = await crypto.subtle.digest('SHA-256', pubKey); + const address = b2h(new Uint8Array(hash).slice(0, 20)); + + const { salt, iv, encrypted } = await encryptKey(privBytes, pw); + + const keystore = { + version: 1, + kdf: 'argon2id', + publicKey: b2h(pubKey), + keyAddress: address, + salt, iv, encrypted, + argon2: { time: ARGON2_TIME, mem: ARGON2_MEM, threads: ARGON2_THREADS, keylen: ARGON2_KEYLEN }, + }; + + // download + const blob = new Blob([JSON.stringify(keystore, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; a.download = 'praxis-keystore-' + address.slice(0,8) + '.json'; + a.click(); URL.revokeObjectURL(url); + + // auto-load into session + signerPrivKey = privBytes; + signerPubKey = pubKey; + signerAddress = address; + updateSignerUI(); + toast('Keystore created and loaded'); + document.getElementById('ks_new_pw').value = ''; + document.getElementById('ks_new_pw2').value = ''; + } catch(e) { toast('Create failed: ' + e.message, true); } +}; + +window.checkSavedKeystore = function() { + const saved = localStorage.getItem('praxis_keystore'); + const wrap = document.getElementById('ks_quick_wrap'); + if (!wrap) return; + if (saved) { + const raw = JSON.parse(saved); + const addr = raw.keyAddress || '?'; + document.getElementById('ks_quick_addr').textContent = addr.slice(0,8) + '…' + addr.slice(-6); + wrap.style.display = ''; + } else { + wrap.style.display = 'none'; + } +}; + +window.quickUnlock = async function() { + const pw = document.getElementById('ks_quick_pw').value; + if (!pw) return toast('Enter password', true); + const saved = localStorage.getItem('praxis_keystore'); + if (!saved) return toast('No saved keystore', true); + try { + const raw = JSON.parse(saved); + if (!raw.encrypted || !raw.salt || !raw.iv || !raw.publicKey) throw new Error('Invalid saved keystore'); + if (raw.argon2) { window._argon2Override = raw.argon2; } else { window._argon2Override = null; } + let privBytes; + try { + privBytes = await decryptKey(raw.encrypted, raw.iv, raw.salt, pw, raw.kdf || 'argon2id'); + } catch(e) { + privBytes = await decryptKey(raw.encrypted, raw.iv, raw.salt, pw, 'pbkdf2'); + } + let pubKey = bls12_381.getPublicKey(privBytes); + if (b2h(pubKey) !== raw.publicKey) { + try { privBytes = await decryptKey(raw.encrypted, raw.iv, raw.salt, pw, 'pbkdf2'); pubKey = bls12_381.getPublicKey(privBytes); } catch(e2) {} + } + if (b2h(pubKey) !== raw.publicKey) throw new Error('Wrong password'); + const hash = await crypto.subtle.digest('SHA-256', pubKey); + const address = b2h(new Uint8Array(hash).slice(0, 20)); + signerPrivKey = privBytes; + signerPubKey = pubKey; + signerAddress = address; + updateSignerUI(); + toast('Session restored — ' + address.slice(0,8) + '…'); + document.getElementById('ks_quick_pw').value = ''; + } catch(e) { toast('Unlock failed: ' + e.message, true); } +}; + +window.importKeystore = async function() { + const pw = document.getElementById('ks_imp_pw').value; + const file = document.getElementById('ks_imp_file').files[0]; + if (!file) return toast('Select a keystore file', true); + if (!pw) return toast('Enter password', true); + + try { + const text = await file.text(); + const raw = JSON.parse(text); + + // Praxis flat format + if (!raw.encrypted || !raw.salt || !raw.iv || !raw.publicKey) throw new Error('Invalid keystore file'); + if (raw.argon2) { window._argon2Override = raw.argon2; } else { window._argon2Override = null; } + let privBytes; + try { + privBytes = await decryptKey(raw.encrypted, raw.iv, raw.salt, pw, raw.kdf || 'argon2id'); + } catch(e) { + privBytes = await decryptKey(raw.encrypted, raw.iv, raw.salt, pw, 'pbkdf2'); + } + let pubKey = bls12_381.getPublicKey(privBytes); + if (b2h(pubKey) !== raw.publicKey) { + // try pbkdf2 fallback + try { + privBytes = await decryptKey(raw.encrypted, raw.iv, raw.salt, pw, 'pbkdf2'); + pubKey = bls12_381.getPublicKey(privBytes); + } catch(e2) {} + } + if (b2h(pubKey) !== raw.publicKey) throw new Error('Wrong password or corrupted keystore'); + const hash = await crypto.subtle.digest('SHA-256', pubKey); + const address = b2h(new Uint8Array(hash).slice(0, 20)); + signerPrivKey = privBytes; + signerPubKey = pubKey; + signerAddress = address; + updateSignerUI(); + toast('Keystore unlocked — ' + address.slice(0,8) + '…'); + localStorage.setItem('praxis_keystore', JSON.stringify(raw)); + document.getElementById('ks_imp_pw').value = ''; + document.getElementById('ks_imp_file').value = ''; + checkSavedKeystore(); + } catch(e) { console.error('Import failed full error:', e); toast('Import failed: ' + e.message, true); } +}; + +function updateSignerUI() { + document.getElementById('keyStatus').className = 'kstat loaded'; + document.getElementById('keyStatus').textContent = '✓ loaded — ' + signerAddress.slice(0,16) + '…'; + document.getElementById('sk_derived').style.display = 'block'; + document.getElementById('sk_pub').textContent = b2h(signerPubKey); + document.getElementById('sk_addr').textContent = signerAddress; + ['c_creator','p_bettor','r_resolver','cl_addr','s_from','w_addr','ft_addr', + 'reg_addr','pr_resolver','dis_addr','cv_voter','rv_voter','tal_addr','fin_addr','sl_addr', + 'fo_resolver','rc_addr','ccf_addr','can_addr','unst_addr','cub_addr'].forEach(id => { + const el = document.getElementById(id); if (el && !el.value) el.value = signerAddress; + }); + const badge = document.getElementById('sessBadge'); + if (badge) badge.classList.remove('hidden'); + refreshBalance(); + loadMyPredictions(); + injectKeyboardCopyBtns(); + setTimeout(wireCopyBtns, 100); +} + +// ═══════════════════════════════════════════ +// METAMASK +// ═══════════════════════════════════════════ +let mmAddress = null; + +window.connectMetaMask = async function() { + if (!window.ethereum) return toast('MetaMask not detected — install MetaMask first', true); + try { + const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); + if (!accounts || accounts.length === 0) return toast('No accounts returned', true); + mmAddress = accounts[0].toLowerCase(); + updateMMUI(true); + toast('MetaMask connected — ' + mmAddress.slice(0,8) + '…'); + } catch(e) { + toast('MetaMask connection failed: ' + e.message, true); + } +}; + +window.disconnectMetaMask = function() { + mmAddress = null; + updateMMUI(false); + toast('MetaMask disconnected'); +}; + +function updateMMUI(connected) { + const disc = document.getElementById('mm_disconnected'); + const conn = document.getElementById('mm_connected'); + const addr = document.getElementById('mm_addr'); + const status = document.getElementById('mm_status'); + if (!disc || !conn) return; + if (connected) { + disc.style.display = 'none'; + conn.style.display = 'block'; + if (addr) addr.textContent = mmAddress; + if (status) status.textContent = '✓ connected — ' + mmAddress.slice(0,8) + '…'; + } else { + disc.style.display = 'block'; + conn.style.display = 'none'; + } +} + +// auto-reconnect if already authorized +(async () => { + if (!window.ethereum) return; + try { + const accounts = await window.ethereum.request({ method: 'eth_accounts' }); + if (accounts && accounts.length > 0) { + mmAddress = accounts[0].toLowerCase(); + updateMMUI(true); + } + } catch {} +})(); + +// listen for account changes +if (window.ethereum) { + window.ethereum.on('accountsChanged', (accounts) => { + if (accounts.length === 0) { mmAddress = null; updateMMUI(false); } + else { mmAddress = accounts[0].toLowerCase(); updateMMUI(true); } + }); +} + +// ═══════════════════════════════════════════ +// SCAN CACHE +// ═══════════════════════════════════════════ +window.clearScanCache = function() { + localStorage.removeItem('praxis_tx_cache'); + localStorage.removeItem('praxis_scan_height'); + toast('Cache cleared — full rescan on next refresh'); + loadMarkets(); +}; + +// ═══════════════════════════════════════════ +// ERROR CODES +// ═══════════════════════════════════════════ +const PRAXIS_ERRORS = { + 124: 'Market has not expired yet — propose_outcome is only callable after expiry.', + 181: 'Cannot finalize — dispute window is still open. Wait for the dispute period to close.', + 4001: 'Resolver has an open position in this market. Use Forfeit Position before proposing.', + 4002: 'Market creator cannot act as resolver for their own market.', + 4003: 'This prediction exceeds the 20% position cap for one side. Try a smaller amount.', + 4010: 'Storage error — please try again or contact support.', + 195: 'Dispute panel could not be formed', + 196: 'This market is not eligible for reclaim', + 197: "Reclaim window hasn't opened yet — wait 300 blocks after expiry", + 198: 'Nothing to reclaim for this wallet', + 199: 'You hold a position in this market and cannot act as resolver. Transfer or forfeit your shares first.', + 200: 'The market creator cannot resolve their own market.', + 201: 'This prediction would exceed the 20% per-address position cap for this market. Try a smaller amount.', + 202: 'Resolver stake below minimum — 500,000 PRX required.', + 203: 'Cooldown period has not elapsed yet.', + 204: 'Pool is empty — nothing to claim.', + 205: 'Market is not finalized.', + 207: 'Resolver RRS is zero — not eligible for rewards.', + 208: 'No successful resolutions in this epoch.', + 210: 'Active proposal exists — unstake not allowed.', + 211: 'Resolver is not active.', + 212: 'No unbonding stake to claim.', + 213: 'Unbonding period not complete.', + 214: 'Resolver record not found.', + 215: 'Market has expired.', + 216: 'Market has positions — cannot cancel.', + 217: 'Unbonding already pending.', +}; + +function friendlyError(code, msg) { + if (!code && msg) { const m = msg.match(/"code":(\d+)/); if (m) code = parseInt(m[1]); } + if (code && PRAXIS_ERRORS[code]) return PRAXIS_ERRORS[code]; + return msg || 'Unknown error'; +} + +// ═══════════════════════════════════════════ +// RECLAIM STAKE +// ═══════════════════════════════════════════ +window.build_reclaim = function() { + try { + const mid = document.getElementById('rc_mid').value.trim().toLowerCase(); + const addr = document.getElementById('rc_addr').value.trim().toLowerCase(); + const fee = parseInt(document.getElementById('rc_fee').value) || 10000; + addr40(mid, 'Market ID'); addr40(addr, 'Claimant Address'); + showPL('rco','rcp', buildUnsigned('reclaim_stake','type.googleapis.com/types.MessageReclaimStake', encReclaim(mid,addr),{fee})); + toast('Payload built'); + } catch(e) { toast(e.message, true); } +}; + +window.signAndSubmit_reclaim = async function() { + const mid = document.getElementById('rc_mid').value.trim().toLowerCase(); + const addr = document.getElementById('rc_addr').value.trim().toLowerCase(); + const fee = parseInt(document.getElementById('rc_fee').value) || 10000; + try { + addr40(mid,'Market ID'); addr40(addr,'Claimant Address'); + } catch(e) { return toast(e.message, true); } + await doSubmit('reclaim_stake','type.googleapis.com/types.MessageReclaimStake', encReclaim(mid,addr),{fee},'btn_reclaim','pend_reclaim'); +}; + +window.fillReclaim = function(id) { + document.getElementById('rc_mid').value = id; + if (signerAddress) document.getElementById('rc_addr').value = signerAddress; + showPage('reclaim', null); +}; + +// ═══════════════════════════════════════════ +// ROLE-BASED SIDEBAR +// ═══════════════════════════════════════════ +async function checkRoles() { + if (!signerAddress) return; + + // Superadmin — full access regardless of role + const SUPERADMIN = '8e14dc0ce537f1c75036f11d7495d60882aa6731'; + if (signerAddress.toLowerCase() === SUPERADMIN) { + document.getElementById('nav-resolver-section').style.display = ''; + document.querySelectorAll('.nav-resolver-item').forEach(el => el.style.display = ''); + document.getElementById('nav-admin-section').style.display = ''; + document.querySelectorAll('.nav-admin-item').forEach(el => el.style.display = ''); + return; + } + + // Admin section — superadmin only (handled above) + document.getElementById('nav-admin-section').style.display = 'none'; + document.querySelectorAll('.nav-admin-item').forEach(el => el.style.display = 'none'); + + // Check RESOLVER — has a register_resolver tx in scanned data + const isResolver = _allMarkets.length >= 0 && (() => { + const cache = localStorage.getItem('praxis_tx_cache'); + if (!cache) return false; + try { + const txs = JSON.parse(cache); + return txs.some(tx => + tx.messageType === 'register_resolver' && + tx.sender && tx.sender.toLowerCase() === signerAddress.toLowerCase() + ); + } catch { return false; } + })(); + + document.getElementById('nav-resolver-section').style.display = isResolver ? '' : 'none'; + document.querySelectorAll('.nav-resolver-item').forEach(el => el.style.display = isResolver ? '' : 'none'); +} + +// Run role check after key load and after markets load +const _origUpdateSignerUI = updateSignerUI; +updateSignerUI = function() { + _origUpdateSignerUI(); + checkRoles(); +}; + +// ═══════════════════════════════════════════ +// COI-3 POSITION CAP CHECK +// ═══════════════════════════════════════════ +window.checkPositionCap = async function() { + const mid = document.getElementById('p_mid').value.trim().toLowerCase(); + const bettor = document.getElementById('p_bettor').value.trim().toLowerCase(); + const mc = (parseInt(document.getElementById('p_maxcost').value) || 0)*1000000; + const capEl = document.getElementById('cap_indicator'); + const btn = document.getElementById('btn_predict'); + + if (!capEl) return; + if (!mid || mid.length !== 40 || !bettor || bettor.length !== 40) { + capEl.style.display = 'none'; + return; + } + + // find market in _allMarkets + const m = _allMarkets.find(x => x.marketId === mid || x.txHash === mid); + if (!m) { capEl.style.display = 'none'; return; } + + const pool = Number(m.qYes + m.qNo); + const cap = Math.floor(pool * 2000 / 10000); // 20% + + // try to get user's current cost paid from chain + let costPaid = 0; + try { + const d = await rpc('/v1/query/account', { address: bettor }); + // costPaid not available without plugin query — use 0 for now + costPaid = 0; + } catch {} + + const newTotal = costPaid + mc; + const remaining = cap - costPaid; + const pct = pool > 0 ? Math.round((newTotal / pool) * 100) : 0; + const over = newTotal > cap; + + capEl.style.display = ''; + if (over) { + capEl.style.background = 'rgba(255,61,90,.08)'; + capEl.style.border = '1px solid rgba(255,61,90,.3)'; + capEl.style.color = 'var(--red)'; + capEl.textContent = '⚠ Exceeds 20% position cap — max ' + fmtPRX(remaining) + ' PRX remaining'; + if (btn) btn.setAttribute('disabled', ''); + } else { + capEl.style.background = 'rgba(0,232,122,.05)'; + capEl.style.border = '1px solid rgba(0,232,122,.15)'; + capEl.style.color = 'var(--text2)'; + capEl.textContent = 'Position: ' + fmtPRX(newTotal) + ' PRX / Cap: ' + fmtPRX(cap) + ' PRX (' + pct + '% of pool)'; + if (btn) btn.removeAttribute('disabled'); + } +}; + +// ═══════════════════════════════════════════ +// FORFEIT POSITION +// ═══════════════════════════════════════════ +window.build_forfeit = function() { + const mid = document.getElementById('fo_mid').value.trim().toLowerCase(); + const resolver = document.getElementById('fo_resolver').value.trim().toLowerCase(); + const fee = parseInt(document.getElementById('fo_fee').value) || 10000; + mid40(mid); addr40(resolver, 'Resolver Address'); + const inner = encForfeit(mid, resolver); + showPL('foo','fop', buildUnsigned('forfeit_position','type.googleapis.com/types.MessageForfeitPosition',inner,{fee})); + toast('Payload built'); +}; + +window.signAndSubmit_forfeit = async function() { + const mid = document.getElementById('fo_mid').value.trim().toLowerCase(); + const resolver = document.getElementById('fo_resolver').value.trim().toLowerCase(); + const fee = parseInt(document.getElementById('fo_fee').value) || 10000; + try { mid40(mid); addr40(resolver, 'Resolver Address'); } catch(e) { return toast(e.message, true); } + const inner = encForfeit(mid, resolver); + await doSubmit('forfeit_position','type.googleapis.com/types.MessageForfeitPosition',inner,{fee},'btn_forfeit','pend_forfeit'); +}; + +window.fillForfeit = function(id) { + document.getElementById('fo_mid').value = id; + if (signerAddress) document.getElementById('fo_resolver').value = signerAddress; + showPage('forfeit', null); +}; +window.fillPropose = function(id) { + document.getElementById('pr_mid').value = id; + if (signerAddress) document.getElementById('pr_resolver').value = signerAddress; + showPage('propose', null); + setTimeout(() => { updateMinBondHint(); updateProposeRisk(); }, 50); +}; + +window.updateMinBondHint = function() { + const mid = document.getElementById('pr_mid').value.trim().toLowerCase(); + const hint = document.getElementById('prop_bond_hint'); + const bondEl = document.getElementById('pr_bond'); + if (!hint) return; + if (!mid || mid.length !== 40) { + hint.textContent = 'Enter Market ID to compute min bond'; + hint.style.color = ''; + return; + } + const m = _allMarkets.find(x => x.marketId === mid || x.txHash === mid); + if (!m) { + hint.textContent = 'Market not found in cache — browse Markets first'; + hint.style.color = 'var(--red)'; + return; + } + // BEff = current pool size (qYes + qNo) + const beff = Number(m.qYes + m.qNo) / 1_000_000; + const onePct = beff * 0.01; + const minBond = Math.max(onePct, 60); + hint.textContent = 'Min bond: ' + minBond.toFixed(2) + ' PRX (max(1% of pool, 60 PRX) — deducted from resolver stake)'; + hint.style.color = 'var(--amber)'; + const bondEl2 = document.getElementById('pr_bond'); if (bondEl2 && parseFloat(bondEl2.value) < minBond) bondEl2.value = Math.ceil(minBond); +}; +checkSavedKeystore(); + + +// ═══════════════════════════════════════════ +// ELEVATED RISK / PANEL SIZE INDICATOR +// ═══════════════════════════════════════════ +const ELEVATED_RISK_THRESHOLD = 25_000_000_000n; // 25,000 PRX in uPRX +const STANDARD_PANEL_SIZE = 5; +const ELEVATED_PANEL_SIZE = 7; + +function getRiskInfo(mid) { + if (!mid || mid.length !== 40) return null; + const m = (_allMarkets || []).find(x => x.marketId === mid || x.txHash === mid); + if (!m) return null; + const pool = m.qYes + m.qNo; + const elevated = pool >= ELEVATED_RISK_THRESHOLD; + const poolPRX = (Number(pool) / 1_000_000).toFixed(2); + return { elevated, pool, poolPRX, panelSize: elevated ? ELEVATED_PANEL_SIZE : STANDARD_PANEL_SIZE }; +} + +function renderRiskBox(boxEl, info) { + if (!info) { + boxEl.style.display = 'none'; + return; + } + const { elevated, poolPRX, panelSize } = info; + boxEl.style.display = ''; + if (elevated) { + boxEl.style.background = 'rgba(255,64,96,.08)'; + boxEl.style.border = '1px solid rgba(255,64,96,.25)'; + boxEl.style.color = 'var(--red)'; + boxEl.innerHTML = '⚠ ELEVATED RISK MARKET
Pool: ' + poolPRX + ' PRX (>= 25,000 PRX threshold)
Panel size: ' + panelSize + ' resolvers (extended panel)'; + } else { + boxEl.style.background = 'var(--gdim)'; + boxEl.style.border = '1px solid rgba(0,232,122,.15)'; + boxEl.style.color = 'var(--text2)'; + boxEl.innerHTML = '✓ Standard market
Pool: ' + poolPRX + ' PRX
Panel size: ' + panelSize + ' resolvers (standard panel)'; + } +} + +window.updateProposeRisk = function() { + const mid = (document.getElementById('pr_mid')?.value || '').trim().toLowerCase(); + const box = document.getElementById('pr_risk_box'); + if (!box) return; + renderRiskBox(box, getRiskInfo(mid)); + // also update bond hint + if (typeof updateMinBondHint === 'function') updateMinBondHint(); +}; + +window.updateDisputeRisk = function() { + const mid = (document.getElementById('di_mid')?.value || '').trim().toLowerCase(); + const box = document.getElementById('di_risk_box'); + if (!box) return; + renderRiskBox(box, getRiskInfo(mid)); +}; + + + + +// ═══════════════════════════════════════════ +// MARKET BANNER IMAGE SYSTEM +// ═══════════════════════════════════════════ + +function mkBannerImg(rules) { + const u = extractImg(rules||''); + if (!u) return ''; + return ''; +} + +function extractImg(rules) { + if (!rules) return ''; + const m = rules.match(/\[IMG:([^\]]+)\]/); + return m ? m[1].trim() : ''; +} + +function stripImgTag(rules) { + if (!rules) return ''; + return rules.replace(/\[IMG:[^\]]+\]\s*/g, '').trim(); +} + +function buildRulesWithImg(rules, imgUrl) { + const stripped = stripImgTag(rules); + if (!imgUrl) return stripped; + return stripped + (stripped ? ' ' : '') + '[IMG:' + imgUrl.trim() + ']'; +} + +window.previewBanner = function() { + const url = (document.getElementById('c_img')?.value || '').trim(); + const preview = document.getElementById('c_img_preview'); + const img = document.getElementById('c_img_preview_img'); + const hint = document.getElementById('c_img_hint'); + if (!preview || !img) return; + if (url && (url.startsWith('http') || url.startsWith('ipfs'))) { + img.src = url; + preview.style.display = ''; + img.onload = () => { if (hint) { hint.textContent = '✓ Image loaded'; hint.style.color = 'var(--green)'; } }; + img.onerror = () => { + preview.style.display = 'none'; + if (hint) { hint.textContent = '✗ Could not load image — check URL or CORS policy'; hint.style.color = 'var(--red)'; } + }; + } else { + preview.style.display = 'none'; + if (hint) { hint.textContent = 'Image will be stored on-chain via IPFS or direct URL. Recommended: 16:9, min 800x450px.'; hint.style.color = ''; } + } +}; + +// ═══════════════════════════════════════════ +// EXPIRY DATE → BLOCK HEIGHT CONVERTER +// ═══════════════════════════════════════════ +const BLOCK_TIME_MS = 5000; // 5s per block + +function blocksFromNow(ms) { + return Math.ceil(ms / BLOCK_TIME_MS); +} + +function fmtDuration(ms) { + const s = Math.floor(ms / 1000); + const d = Math.floor(s / 86400); + const h = Math.floor((s % 86400) / 3600); + const m = Math.floor((s % 3600) / 60); + if (d > 0) return d + 'd ' + h + 'h'; + if (h > 0) return h + 'h ' + m + 'm'; + return m + 'm'; +} + +window.updateExpiryFromDate = function() { + const dtEl = document.getElementById('c_expiry_dt'); + const hidden = document.getElementById('c_expiry'); + const hint = document.getElementById('c_expiry_hint'); + if (!dtEl || !hidden || !hint) return; + + const val = dtEl.value; + if (!val) { + hint.textContent = 'Select a date to compute block height'; + hint.style.color = ''; + hidden.value = ''; + return; + } + + const targetMs = new Date(val).getTime(); + const nowMs = Date.now(); + const diffMs = targetMs - nowMs; + + if (diffMs <= 0) { + hint.textContent = 'Date must be in the future'; + hint.style.color = 'var(--red)'; + hidden.value = ''; + return; + } + + const blocksNeeded = blocksFromNow(diffMs); + const blockHeight = currentHeight + blocksNeeded; + hidden.value = blockHeight; + + const dur = fmtDuration(diffMs); + hint.textContent = 'Block #' + blockHeight + ' (~' + dur + ' from now, ' + blocksNeeded + ' blocks)'; + hint.style.color = blocksNeeded < 100 ? 'var(--red)' : 'var(--amber)'; +}; + +// Set default expiry to 7 days from now when page loads +window.initExpiryDate = function() { + const dtEl = document.getElementById('c_expiry_dt'); + if (!dtEl || dtEl.value) return; + const d = new Date(Date.now() + 7 * 24 * 3600 * 1000); + // Format: YYYY-MM-DDTHH:MM + const pad = n => String(n).padStart(2, '0'); + dtEl.value = d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) + 'T' + pad(d.getHours()) + ':' + pad(d.getMinutes()); + updateExpiryFromDate(); +}; + +// ═══════════════════════════════════════════ +// CATEGORY SYSTEM +// ═══════════════════════════════════════════ +const CAT_LABELS = { + crypto: '🪙 Crypto', sports: '⚽ Sports', politics: '🗳 Politics', + finance: '📈 Finance', other: '◈ Other' +}; + +function extractCat(rules) { + if (!rules) return 'other'; + const m = rules.match(/^\[CAT:(\w+)\]/); + return m ? m[1] : 'other'; +} + +function stripCatPrefix(rules) { + if (!rules) return ''; + return rules.replace(/^\[CAT:\w+\]\s*/, ''); +} + +function buildRulesWithCat(cat, rules) { + const stripped = stripCatPrefix(rules); + return '[CAT:' + cat + '] ' + stripped; +} + +window.pickCat = function(el) { + document.querySelectorAll('#c_cat_pick .cpick').forEach(e => e.classList.remove('active')); + el.classList.add('active'); +}; + + + +// ═══════════════════════════════════════════ +// CLAIM CREATOR FEE +// ═══════════════════════════════════════════ +function encClaimCreatorFee(mid,creator){return cat(bf(1,h2b(mid)),bf(2,h2b(creator)));} + +window.build_claimcreator=function(){ + try{ + const mid=document.getElementById('ccf_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('ccf_addr').value.trim().toLowerCase();addr40(addr,'Creator'); + const fee=parseInt(document.getElementById('ccf_fee').value)||10000; + showPL('ccfo','ccfp',buildUnsigned('claim_creator_fee','type.googleapis.com/types.MessageClaimCreatorFee',encClaimCreatorFee(mid,addr),{fee})); + toast('Payload built'); + }catch(e){toast(e.message,true);} +}; +window.signAndSubmit_claimcreator=async function(){ + try{ + const mid=document.getElementById('ccf_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('ccf_addr').value.trim().toLowerCase();addr40(addr,'Creator'); + const fee=parseInt(document.getElementById('ccf_fee').value)||10000; + await doSubmit('claim_creator_fee','type.googleapis.com/types.MessageClaimCreatorFee',encClaimCreatorFee(mid,addr),{fee},'btn_claimcreator','pend_claimcreator'); + }catch(e){toast(e.message,true);} +}; +window.fillClaimCreator=function(id){ + document.getElementById('ccf_mid').value=id; + if(signerAddress)document.getElementById('ccf_addr').value=signerAddress; + showPage('claimcreator',null); +}; + +// ═══════════════════════════════════════════ +// CANCEL MARKET +// ═══════════════════════════════════════════ +function encCancelMarket(mid,creator){return cat(bf(1,h2b(mid)),bf(2,h2b(creator)));} + +window.build_cancel=function(){ + try{ + const mid=document.getElementById('can_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('can_addr').value.trim().toLowerCase();addr40(addr,'Creator'); + const fee=parseInt(document.getElementById('can_fee').value)||10000; + showPL('cano','canp',buildUnsigned('cancel_market','type.googleapis.com/types.MessageCancelMarket',encCancelMarket(mid,addr),{fee})); + toast('Payload built'); + }catch(e){toast(e.message,true);} +}; +window.signAndSubmit_cancel=async function(){ + try{ + const mid=document.getElementById('can_mid').value.trim().toLowerCase();mid40(mid); + const addr=document.getElementById('can_addr').value.trim().toLowerCase();addr40(addr,'Creator'); + const fee=parseInt(document.getElementById('can_fee').value)||10000; + await doSubmit('cancel_market','type.googleapis.com/types.MessageCancelMarket',encCancelMarket(mid,addr),{fee},'btn_cancel','pend_cancel'); + }catch(e){toast(e.message,true);} +}; + +// ═══════════════════════════════════════════ +// UNSTAKE RESOLVER +// ═══════════════════════════════════════════ +window.build_unstake_resolver=function(){ + try{ + const addr=document.getElementById('unst_addr').value.trim().toLowerCase();addr40(addr,'Resolver'); + const amount=parseInt(document.getElementById('unst_amount').value||'0'); + const amountU=BigInt(amount)*1000000n; + const fee=parseInt(document.getElementById('unst_fee').value)||10000; + showPL('unsto','unstp',buildUnsigned('unstake_resolver','type.googleapis.com/types.MessageUnstakeResolver',encUnstakeResolver(addr,amountU),{fee})); + toast('Payload built'); + }catch(e){toast(e.message,true);} +}; +window.signAndSubmit_unstake_resolver=async function(){ + try{ + const addr=document.getElementById('unst_addr').value.trim().toLowerCase();addr40(addr,'Resolver'); + const amount=parseInt(document.getElementById('unst_amount').value||'0'); + const amountU=BigInt(amount)*1000000n; + const fee=parseInt(document.getElementById('unst_fee').value)||10000; + await doSubmit('unstake_resolver','type.googleapis.com/types.MessageUnstakeResolver',encUnstakeResolver(addr,amountU),{fee},'btn_unstake_resolver','pend_unstake_resolver'); + }catch(e){toast(e.message,true);} +}; + +// ═══════════════════════════════════════════ +// CLAIM UNBONDED STAKE +// ═══════════════════════════════════════════ +window.build_claim_unbonded=function(){ + try{ + const addr=document.getElementById('cub_addr').value.trim().toLowerCase();addr40(addr,'Resolver'); + const fee=parseInt(document.getElementById('cub_fee').value)||10000; + showPL('cubo','cubp',buildUnsigned('claim_unbonded_stake','type.googleapis.com/types.MessageClaimUnbondedStake',encClaimUnbonded(addr),{fee})); + toast('Payload built'); + }catch(e){toast(e.message,true);} +}; +window.signAndSubmit_claim_unbonded=async function(){ + try{ + const addr=document.getElementById('cub_addr').value.trim().toLowerCase();addr40(addr,'Resolver'); + const fee=parseInt(document.getElementById('cub_fee').value)||10000; + await doSubmit('claim_unbonded_stake','type.googleapis.com/types.MessageClaimUnbondedStake',encClaimUnbonded(addr),{fee},'btn_claim_unbonded','pend_claim_unbonded'); + }catch(e){toast(e.message,true);} +}; + +// ═══════════════════════════════════════════ +// RESOLVER RECORD STATE QUERY +// prefix 0x16 + len + addr bytes +// ═══════════════════════════════════════════ +function buildResolverKey(addrHex){ + const addr=h2b(addrHex); + const key=new Uint8Array(1+1+addr.length); + key[0]=0x16; key[1]=addr.length; key.set(addr,2); + return b2h(key); +} + +function decodeResolverRecord(hexData){ + const buf=h2b(hexData); + let pos=0; + const rec={stake:0n,rrs:0n,registeredAt:0n,successfulResolutions:0n,lastClaimedEpoch:0n}; + while(pos>3n),wt=Number(tagV&7n); + if(wt===2){const {v:lenV,p:p2}=decVarint(buf,pos);pos=p2+Number(lenV);} + else if(wt===0){ + const {v,p:p2}=decVarint(buf,pos);pos=p2; + if(fn===2)rec.stake=v; + if(fn===3)rec.rrs=v; + if(fn===4)rec.registeredAt=v; + if(fn===5)rec.successfulResolutions=v; + if(fn===6)rec.lastClaimedEpoch=v; + } else if(wt===1){pos+=8;} else if(wt===5){pos+=4;} else break; + } + return rec; +} + +async function fetchResolverRecord(addrHex){ + try{ + const key=buildResolverKey(addrHex); + const resp=await fetch(getRPC()+'/v1/query/state',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({key})}); + if(!resp.ok)return null; + const data=await resp.json(); + const hex=data.value||data.result||''; + if(!hex)return null; + return decodeResolverRecord(hex); + }catch{return null;} +} + +// ═══════════════════════════════════════════ +// BROWSE RESOLVERS +// ═══════════════════════════════════════════ +window.loadResolvers=async function(){ + const el=document.getElementById('resolversList'); + if(!el)return; + el.innerHTML='
▪ ▪ ▪  loading resolvers
'; + try{ + // auto-scan if no cache or registry empty + if(!localStorage.getItem('praxis_tx_cache')||_resolverRegistry.size===0){ + await loadMarkets(); + } + const cache=localStorage.getItem('praxis_tx_cache'); + if(!cache){el.innerHTML='
No chain data — node may be offline
';return;} + const txs=JSON.parse(cache); + const resolvers=new Map(); + for(const tx of txs){ + if(tx.messageType!=='register_resolver')continue; + const msg=(tx.transaction&&tx.transaction.msg)||{}; + const addr=tx.sender||''; + const stake=BigInt(msg.stakeAmount||msg.stake_amount||0); + if(!resolvers.has(addr)){ + // use rrs from _resolverRegistry if available (already computed in loadMarkets) + const regEntry=_resolverRegistry.get(addr); + const rrs=regEntry?regEntry.rrs:10; + resolvers.set(addr,{addr,stake,proposals:regEntry?regEntry.proposalCount:0,height:tx.height||0,rrs}); + } else { + const r=resolvers.get(addr); + r.stake=stake; + if((tx.height||0)>r.height)r.height=tx.height; + } + } + for(const tx of txs){ + if(tx.messageType!=='propose_outcome')continue; + const addr=tx.sender||''; + if(resolvers.has(addr)&&!_resolverRegistry.has(addr))resolvers.get(addr).proposals++; + } + if(resolvers.size===0){el.innerHTML='
No registered resolvers found
';return;} + const list=[...resolvers.values()]; + // if registry has data, rrs already set; otherwise estimate + list.forEach(r=>{ if(!_resolverRegistry.has(r.addr)) r.rrs=Math.min(10+r.proposals*10,999); }); + list.sort((a,b)=>b.rrs-a.rrs); + el.innerHTML=list.map(r=>{ + const rrs=r.rrs||0; + let tier,tcolor,ticon; + if(rrs<10){tier='Suspended';tcolor='var(--red)';ticon='✕';} + else if(rrs>=100){tier='Gold';tcolor='#FFD700';ticon='★';} + else if(rrs>=50){tier='Silver';tcolor='#C0C0C0';ticon='◆';} + else{tier='Bronze';tcolor='#CD7F32';ticon='▲';} + return '
'+ + '
'+ + '
'+r.addr.slice(0,8)+'\u2026'+r.addr.slice(-6)+'
'+ + ''+ticon+' '+tier+''+ + '
'+ + '
'+ + '
Stake
'+fmtPRX(r.stake)+' PRX
'+ + '
Est. RRS
'+rrs+'
'+ + '
Proposals
'+r.proposals+'
'+ + '
Since block
#'+r.height+'
'+ + '
'+ + '
'+r.addr+'
'+ + '
'; + }).join(''); + }catch(e){el.innerHTML='
Error: '+esc(e.message)+'
';} +}; +// ═══════════════════════════════════════════ +// MARKET DETAIL — ACTIVITY FEED + TOP HOLDERS +// ═══════════════════════════════════════════ +window.switchDetailTab = function(tab) { + ['activity','holders'].forEach(t => { + const btn = document.getElementById('dtab-'+t); + const pane = document.getElementById('dpane-'+t); + if(btn) { + btn.style.borderBottomColor = t===tab ? 'var(--green)' : 'transparent'; + btn.style.color = t===tab ? 'var(--text2)' : 'var(--text3)'; + } + if(pane) pane.style.display = t===tab ? '' : 'none'; + }); + if(tab==='activity') renderActivityFeed(_detailMarketId); + if(tab==='holders') renderTopHolders(_detailMarketId); +}; + +window.renderActivityFeed = function(mid) { + const el = document.getElementById('dpane-activity'); + if(!el||!mid) return; + try { + const txs = JSON.parse(localStorage.getItem('praxis_tx_cache')||'[]'); + const relevant = txs.filter(tx => { + const msg = (tx.transaction&&tx.transaction.msg)||{}; + const rawMid = msg.marketId||msg.market_id||''; + if(!rawMid && tx.messageType==='create_market') { + // match by derived marketId + return false; // handled below + } + if(!rawMid) return false; + let txMid = rawMid; + try { txMid = b2h(Uint8Array.from(atob(rawMid),c=>c.charCodeAt(0))); } catch{} + return txMid === mid; + }); + + // also include the create_market TX for this market + const createTx = txs.find(tx => tx.messageType==='create_market' && _allMarkets.find(m=>m.marketId===mid&&m.txHash===tx.txHash)); + if(createTx && !relevant.includes(createTx)) relevant.unshift(createTx); + + relevant.sort((a,b)=>(b.height||0)-(a.height||0)); + + if(!relevant.length){ + el.innerHTML='
No activity found
'; + return; + } + + const typeIcon = {create_market:'◎',submit_prediction:'⚡',propose_outcome:'⚖',finalize_market:'✓',cancel_market:'✕',claim_winnings:'◈',forfeit_position:'↩',resolve_market:'⚑'}; + const typeColor = {create_market:'var(--text2)',submit_prediction:'var(--green)',propose_outcome:'#FFD700',finalize_market:'var(--green)',cancel_market:'var(--red)',claim_winnings:'var(--green)',forfeit_position:'var(--red)',resolve_market:'#C0C0C0'}; + + el.innerHTML = relevant.map(tx => { + const msg = (tx.transaction&&tx.transaction.msg)||{}; + const type = tx.messageType||'unknown'; + const icon = typeIcon[type]||'▪'; + const color = typeColor[type]||'var(--text3)'; + const sender = tx.sender||''; + const height = tx.height||0; + let detail = ''; + if(type==='submit_prediction'){ + const outcome = msg.outcome===true||msg.outcome==='true'||msg.outcome===1; + const shares = BigInt(msg.shares||msg.amount||0); + detail = ''+(outcome?'YES':'NO')+'  '+fmtPRX(shares)+' PRX'; + } else if(type==='propose_outcome'){ + const outcome = msg.proposedOutcome===true||msg.proposedOutcome==='true'||msg.proposedOutcome===1; + detail = 'Proposed '+( outcome?'YES':'NO')+''; + } else if(type==='create_market'){ + const b0=BigInt(msg.b0||0); + detail='Market created · B0 '+fmtPRX(b0)+' PRX'; + } else if(type==='finalize_market'){detail='Market finalized';} + else if(type==='cancel_market'){detail='Market cancelled';} + else if(type==='claim_winnings'){detail='Claimed winnings';} + else if(type==='forfeit_position'){detail='Position forfeited';} + return '
'+ + '
'+icon+'
'+ + '
'+ + '
'+ + ''+type.replace(/_/g,' ')+''+ + 'blk #'+height+''+ + '
'+ + '
'+ + (sender?sender.slice(0,8)+'…'+sender.slice(-6):'')+ + '
'+ + (detail?'
'+detail+'
':'')+ + '
'+ + '
'; + }).join(''); + } catch(e) { + el.innerHTML='
Error: '+esc(e.message)+'
'; + } +}; + +window.renderTopHolders = function(mid) { + const el = document.getElementById('dpane-holders'); + if(!el||!mid) return; + try { + const txs = JSON.parse(localStorage.getItem('praxis_tx_cache')||'[]'); + const holders = new Map(); + for(const tx of txs){ + if(tx.messageType!=='submit_prediction') continue; + const msg=(tx.transaction&&tx.transaction.msg)||{}; + const rawMid=msg.marketId||msg.market_id||''; + let txMid=rawMid; + try{txMid=b2h(Uint8Array.from(atob(rawMid),c=>c.charCodeAt(0)));}catch{} + if(txMid!==mid) continue; + const addr=tx.sender||''; + const outcome=msg.outcome===true||msg.outcome==='true'||msg.outcome===1; + const shares=BigInt(msg.shares||msg.amount||0); + if(!holders.has(addr)) holders.set(addr,{addr,yes:0n,no:0n,txCount:0}); + const h=holders.get(addr); + if(outcome) h.yes+=shares; else h.no+=shares; + h.txCount++; + } + if(!holders.size){ + el.innerHTML='
No positions yet
'; + return; + } + const list=[...holders.values()].sort((a,b)=>Number((b.yes+b.no)-(a.yes+a.no))); + const totalYes=list.reduce((s,h)=>s+h.yes,0n); + const totalNo=list.reduce((s,h)=>s+h.no,0n); + const grand=totalYes+totalNo; + el.innerHTML= + '
'+ + '
'+ + '
TOTAL YES
'+ + '
'+fmtPRX(totalYes)+' PRX
'+ + '
'+ + '
TOTAL NO
'+ + '
'+fmtPRX(totalNo)+' PRX
'+ + '
'+ + list.map((h,i)=>{ + const total=h.yes+h.no; + const pct=grand>0n?Number(total*100n/grand):0; + const yesPct=total>0n?Number(h.yes*100n/total):0; + const bias=h.yes>h.no?'YES':h.no>h.yes?'NO':'EVEN'; + const bc=bias==='YES'?'var(--green)':bias==='NO'?'var(--red)':'var(--text3)'; + return '
'+ + '
'+ + '
'+ + '#'+(i+1)+''+ + ''+h.addr.slice(0,8)+'…'+h.addr.slice(-6)+''+ + '
'+ + ''+bias+''+ + '
'+ + '
'+ + '
YES
'+ + '
'+fmtPRX(h.yes)+'
'+ + '
NO
'+ + '
'+fmtPRX(h.no)+'
'+ + '
SHARE
'+ + '
'+pct+'%
'+ + '
'+ + '
'+ + '
'+ + '
'+ + '
'; + }).join(''); + } catch(e){ + el.innerHTML='
Error: '+esc(e.message)+'
'; + } +}; + +// ═══════════════════════════════════════════ +// PRIS REWARD PAGES +// ═══════════════════════════════════════════ + +const EPOCH_BLOCKS = 1000; +const AUTHORIZED_BUILDER = '954378ba109c5ca45b23bfa284f3ac70e2671b87'; +const AUTHORIZED_COMMUNITY = '15e658698d2510799339273f6fccb0484c4f4b6f'; +const AUTHORIZED_INVESTOR = '125c1bb803a2dd9194dca40d77445cf75647cb12'; +const AUTHORIZED_PROTOCOL = 'c1764f10ad672558afe1a3b666185fd141ae1ea8'; + +// Encoding +function encRewardResolver(addr, epoch){ return cat(bf(1,h2b(addr)), vf(2, BigInt(epoch))); } +function encRewardBuilder(addr){ return cat(bf(1,h2b(addr))); } +function encRewardCommunity(addr){ return cat(bf(1,h2b(addr))); } +function encRewardInvestor(addr){ return cat(bf(1,h2b(addr))); } +function encRewardProtocol(addr){ return cat(bf(1,h2b(addr))); } + +// Auth guard helper +function checkRewardAuth(pageId, contentId, unauthId, authorizedAddr) { + const authed = !authorizedAddr || (signerAddress && signerAddress.toLowerCase() === authorizedAddr.toLowerCase()); + document.getElementById(contentId).style.display = authed ? '' : 'none'; + document.getElementById(unauthId).style.display = authed ? 'none' : ''; +} + +// Auto-fill address fields when reward page opens + +// Generic pool stat loader (reads from chain via admin RPC) +async function loadPoolStat(elId, key) { + try { + const el = document.getElementById(elId); + if (!el) return; + // Pool data comes from plugin state — show epoch estimate for now + const epoch = currentHeight ? Math.floor(currentHeight / EPOCH_BLOCKS) : 0; + el.textContent = 'Epoch #' + epoch; + } catch(e) {} +} + +// Resolver reward data +async function loadResolverRewardData() { + try { + const epoch = currentHeight ? Math.floor(currentHeight / EPOCH_BLOCKS) : 0; + document.getElementById('rrw-pool').textContent = 'Epoch #' + epoch; + + // Pull resolver info from the resolvers map if already loaded + if (signerAddress && window._resolvers) { + const r = window._resolvers.get(signerAddress.toLowerCase()); + if (r) { + const rrs = r.rrs || 10; + const proposals = r.proposalCount || 0; + document.getElementById('rrw-rrs').textContent = rrs; + document.getElementById('rrw-rrs2').textContent = rrs; + document.getElementById('rrw-resolutions').textContent = proposals; + + // Tier + let tier = 'bronze', tierLabel = '🥉 Bronze', tierClass = 'rrs-bronze'; + if (rrs >= 100) { tier = 'gold'; tierLabel = 'Gold'; tierClass = 'rrs-gold'; } + else if (rrs >= 50) { tier = 'silver'; tierLabel = 'Silver'; tierClass = 'rrs-silver'; } + const weight = rrs >= 100 ? 3 : rrs >= 50 ? 2 : 1; + + const badge = document.getElementById('rrw-tier-badge'); + badge.className = 'rrs-badge ' + tierClass; + badge.innerHTML = tierLabel + ' — RRS ' + rrs + ''; + document.getElementById('rrw-share').textContent = weight + '× weight'; + + // Epoch history table (last 5 epochs) + let rows = ''; + for (let i = Math.max(0, epoch - 4); i <= epoch; i++) { + const isCurrent = i === epoch; + rows += ` + #${i} + ${isCurrent ? 'In progress' : '—'} + — + ${isCurrent ? 'Current' : 'Claimable'} + `; + } + document.querySelector('#rrw-history tbody').innerHTML = rows; + } + } + } catch(e) {} +} + +// Builder reward data +async function loadBuilderRewardData() { + try { + const epoch = currentHeight ? Math.floor(currentHeight / EPOCH_BLOCKS) : 0; + document.getElementById('brw-pool').textContent = 'Epoch #' + epoch; + let rows = ''; + for (let i = Math.max(0, epoch - 4); i <= epoch; i++) { + const isCurrent = i === epoch; + rows += ` + #${i} + ${isCurrent ? 'In progress' : '—'} + — + ${isCurrent ? 'Current' : 'Claimable'} + `; + } + document.querySelector('#brw-history tbody').innerHTML = rows; + } catch(e) {} +} + +// ── Submit handlers ── + +window.signAndSubmit_rewardResolver = async function() { + try { + const addr = document.getElementById('rrw-addr').value.trim(); + const epochVal = document.getElementById('rrw-epoch').value.trim(); + const epoch = epochVal ? parseInt(epochVal) : Math.floor((currentHeight||0) / EPOCH_BLOCKS); + const fee = BigInt(document.getElementById('rrw-fee').value||10000); + if (!addr || addr.length !== 40) return toast('Invalid resolver address', true); + await doSubmit('claim_resolver_reward','type.googleapis.com/types.MessageClaimResolverReward',encRewardResolver(addr,epoch),{fee},'btn_rrw','pend_rrw'); + } catch(e) { toast(friendlyError(null,e.message),true); } +}; + +window.build_rewardResolver = function() { + try { + const addr = document.getElementById('rrw-addr').value.trim(); + const epochVal = document.getElementById('rrw-epoch').value.trim(); + const epoch = epochVal ? parseInt(epochVal) : Math.floor((currentHeight||0) / EPOCH_BLOCKS); + const fee = BigInt(document.getElementById('rrw-fee').value||10000); + if (!addr || addr.length !== 40) return toast('Invalid resolver address', true); + showPL('rrwo','rrwp',buildUnsigned('claim_resolver_reward','type.googleapis.com/types.MessageClaimResolverReward',encRewardResolver(addr,epoch),{fee})); + toast('Payload built'); + } catch(e) { toast(friendlyError(null,e.message),true); } +}; + +window.signAndSubmit_rewardBuilder = async function() { + try { + const addr = document.getElementById('brw-addr').value.trim(); + const fee = BigInt(document.getElementById('brw-fee').value||10000); + if (!addr || addr.length !== 40) return toast('Invalid address', true); + await doSubmit('claim_builder_reward','type.googleapis.com/types.MessageClaimBuilderReward',encRewardBuilder(addr),{fee},'btn_brw','pend_brw'); + } catch(e) { toast(friendlyError(null,e.message),true); } +}; + +window.build_rewardBuilder = function() { + try { + const addr = document.getElementById('brw-addr').value.trim(); + const fee = BigInt(document.getElementById('brw-fee').value||10000); + if (!addr || addr.length !== 40) return toast('Invalid address', true); + showPL('brwo','brwp',buildUnsigned('claim_builder_reward','type.googleapis.com/types.MessageClaimBuilderReward',encRewardBuilder(addr),{fee})); + toast('Payload built'); + } catch(e) { toast(friendlyError(null,e.message),true); } +}; + +window.signAndSubmit_rewardCommunity = async function() { + try { + const addr = document.getElementById('crw-addr').value.trim(); + const fee = BigInt(document.getElementById('crw-fee').value||10000); + if (!addr || addr.length !== 40) return toast('Invalid address', true); + await doSubmit('claim_community_reward','type.googleapis.com/types.MessageClaimCommunityReward',encRewardCommunity(addr),{fee},'btn_crw','pend_crw'); + } catch(e) { toast(friendlyError(null,e.message),true); } +}; + +window.build_rewardCommunity = function() { + try { + const addr = document.getElementById('crw-addr').value.trim(); + const fee = BigInt(document.getElementById('crw-fee').value||10000); + if (!addr || addr.length !== 40) return toast('Invalid address', true); + showPL('crwo','crwp',buildUnsigned('claim_community_reward','type.googleapis.com/types.MessageClaimCommunityReward',encRewardCommunity(addr),{fee})); + toast('Payload built'); + } catch(e) { toast(friendlyError(null,e.message),true); } +}; + +window.signAndSubmit_rewardInvestor = async function() { + try { + const addr = document.getElementById('irw-addr').value.trim(); + const fee = BigInt(document.getElementById('irw-fee').value||10000); + if (!addr || addr.length !== 40) return toast('Invalid address', true); + await doSubmit('claim_investor_reward','type.googleapis.com/types.MessageClaimInvestorReward',encRewardInvestor(addr),{fee},'btn_irw','pend_irw'); + } catch(e) { toast(friendlyError(null,e.message),true); } +}; + +window.build_rewardInvestor = function() { + try { + const addr = document.getElementById('irw-addr').value.trim(); + const fee = BigInt(document.getElementById('irw-fee').value||10000); + if (!addr || addr.length !== 40) return toast('Invalid address', true); + showPL('irwo','irwp',buildUnsigned('claim_investor_reward','type.googleapis.com/types.MessageClaimInvestorReward',encRewardInvestor(addr),{fee})); + toast('Payload built'); + } catch(e) { toast(friendlyError(null,e.message),true); } +}; + +window.signAndSubmit_rewardProtocol = async function() { + try { + const addr = document.getElementById('prw-addr').value.trim(); + const fee = BigInt(document.getElementById('prw-fee').value||10000); + if (!addr || addr.length !== 40) return toast('Invalid address', true); + await doSubmit('claim_protocol_reward','type.googleapis.com/types.MessageClaimProtocolReward',encRewardProtocol(addr),{fee},'btn_prw','pend_prw'); + } catch(e) { toast(friendlyError(null,e.message),true); } +}; + +window.build_rewardProtocol = function() { + try { + const addr = document.getElementById('prw-addr').value.trim(); + const fee = BigInt(document.getElementById('prw-fee').value||10000); + if (!addr || addr.length !== 40) return toast('Invalid address', true); + showPL('prwo','prwp',buildUnsigned('claim_protocol_reward','type.googleapis.com/types.MessageClaimProtocolReward',encRewardProtocol(addr),{fee})); + toast('Payload built'); + } catch(e) { toast(friendlyError(null,e.message),true); } +}; + +// expose resolvers map for reward page +const _origBrowseResolvers = window.loadResolvers; +if (typeof loadResolvers === 'function') { + const __orig = loadResolvers; + window.loadResolvers = async function() { + await __orig(); + // cache resolvers map reference + window._resolvers = resolvers; + }; +} + +// ═══════════════════════════════════════════ +// SEARCH PAGE +// ═══════════════════════════════════════════ +let _srchCat = 'all'; + +window.srchCat = function(el) { + document.querySelectorAll('#srch-cats .cpick').forEach(e => e.classList.remove('active')); + el.classList.add('active'); + _srchCat = el.getAttribute('data-cat') || 'all'; + runSearch(); +}; + +window.runSearch = function() { + const q = (document.getElementById('srch-input')?.value || '').trim().toLowerCase(); + const out = document.getElementById('srch-results'); + if (!out) return; + const markets = _allMarkets || []; + if (!q && _srchCat === 'all') { + out.innerHTML = '
Type to search markets
'; + return; + } + let filtered = markets.filter(m => { + const catMatch = _srchCat === 'all' || extractCat(m.rules || '') === _srchCat; + const textMatch = !q || + (m.question || '').toLowerCase().includes(q) || + (m.marketId || '').toLowerCase().includes(q) || + (m.creator || '').toLowerCase().includes(q) || + stripCatPrefix(m.rules || '').toLowerCase().includes(q); + return catMatch && textMatch; + }); + if (filtered.length === 0) { + out.innerHTML = '
No markets found
'; + return; + } + out.innerHTML = '
' + filtered.map(m => { + const mid = m.marketId || m.txHash || ''; + const total = m.qYes + m.qNo; + const yesPct = total > 0n ? Number((m.qYes * 100n) / total) : 50; + const noPct = 100 - yesPct; + const vol = fmtPRX(total) + ' PRX'; + const exp = m.expiry ? '#' + m.expiry.toString() : '—'; + const catIcon = {'crypto':'🪙','sports':'⚽','politics':'🗳','finance':'📈','other':'◈'}[extractCat(m.rules||'')] || '◈'; + const catName = (CAT_LABELS[extractCat(m.rules||'')] || 'Other').replace(/[🪙⚽🗳📈◈]\s*/,''); + const statusMap = {0:'● LIVE',2:'◆ PROPOSED',3:'⚠ DISPUTED',4:'✓ FINALIZED',1:'✕ CANCELLED',8:'⏱ EXPIRED'}; + const statusHtml = statusMap[m.status] || ''; + const hasBanner = !!extractImg(m.rules||''); + const yesMulti = m.qYes > 0n ? (Number(m.qYes + m.qNo) / Number(m.qYes)).toFixed(2) : '—'; + const noMulti = m.qNo > 0n ? (Number(m.qYes + m.qNo) / Number(m.qNo)).toFixed(2) : '—'; + return `
+ ${mkBannerImg(m.rules)}
+
${catIcon} ${catName}  ${statusHtml}
+
${esc(m.question || '(no question)')}
+
+ ${yesPct}% ${yesMulti}x + · + ${noPct}% ${noMulti}x +
+
+
+
+
Vol${vol}
+
Exp${exp}
+
`; + }).join('') + '
'; +}; diff --git a/ui/argon2-bundled.min.js b/ui/argon2-bundled.min.js new file mode 100644 index 0000000000..607e16f59b --- /dev/null +++ b/ui/argon2-bundled.min.js @@ -0,0 +1 @@ +!function(A,I){"object"==typeof exports&&"object"==typeof module?module.exports=I():"function"==typeof define&&define.amd?define([],I):"object"==typeof exports?exports.argon2=I():A.argon2=I()}(this,(function(){return(()=>{var A,I,g={773:(A,I,g)=>{var B,Q="undefined"!=typeof self&&void 0!==self.Module?self.Module:{},C={};for(B in Q)Q.hasOwnProperty(B)&&(C[B]=Q[B]);var E,i,o,D,e=[];E="object"==typeof window,i="function"==typeof importScripts,o="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,D=!E&&!o&&!i;var n,t,a,r,s,y="";o?(y=i?g(967).dirname(y)+"/":"//",n=function(A,I){return r||(r=g(145)),s||(s=g(967)),A=s.normalize(A),r.readFileSync(A,I?null:"utf8")},a=function(A){var I=n(A,!0);return I.buffer||(I=new Uint8Array(I)),G(I.buffer),I},process.argv.length>1&&process.argv[1].replace(/\\/g,"/"),e=process.argv.slice(2),A.exports=Q,process.on("uncaughtException",(function(A){if(!(A instanceof V))throw A})),process.on("unhandledRejection",u),Q.inspect=function(){return"[Emscripten Module object]"}):D?("undefined"!=typeof read&&(n=function(A){return read(A)}),a=function(A){var I;return"function"==typeof readbuffer?new Uint8Array(readbuffer(A)):(G("object"==typeof(I=read(A,"binary"))),I)},"undefined"!=typeof scriptArgs?e=scriptArgs:void 0!==arguments&&(e=arguments),"undefined"!=typeof print&&("undefined"==typeof console&&(console={}),console.log=print,console.warn=console.error="undefined"!=typeof printErr?printErr:print)):(E||i)&&(i?y=self.location.href:"undefined"!=typeof document&&document.currentScript&&(y=document.currentScript.src),y=0!==y.indexOf("blob:")?y.substr(0,y.lastIndexOf("/")+1):"",n=function(A){var I=new XMLHttpRequest;return I.open("GET",A,!1),I.send(null),I.responseText},i&&(a=function(A){var I=new XMLHttpRequest;return I.open("GET",A,!1),I.responseType="arraybuffer",I.send(null),new Uint8Array(I.response)}),t=function(A,I,g){var B=new XMLHttpRequest;B.open("GET",A,!0),B.responseType="arraybuffer",B.onload=function(){200==B.status||0==B.status&&B.response?I(B.response):g()},B.onerror=g,B.send(null)}),Q.print||console.log.bind(console);var F,c,w=Q.printErr||console.warn.bind(console);for(B in C)C.hasOwnProperty(B)&&(Q[B]=C[B]);C=null,Q.arguments&&(e=Q.arguments),Q.thisProgram&&Q.thisProgram,Q.quit&&Q.quit,Q.wasmBinary&&(F=Q.wasmBinary),Q.noExitRuntime,"object"!=typeof WebAssembly&&u("no native wasm support detected");var h=!1;function G(A,I){A||u("Assertion failed: "+I)}var N,R,f="undefined"!=typeof TextDecoder?new TextDecoder("utf8"):void 0;function U(A){N=A,Q.HEAP8=new Int8Array(A),Q.HEAP16=new Int16Array(A),Q.HEAP32=new Int32Array(A),Q.HEAPU8=R=new Uint8Array(A),Q.HEAPU16=new Uint16Array(A),Q.HEAPU32=new Uint32Array(A),Q.HEAPF32=new Float32Array(A),Q.HEAPF64=new Float64Array(A)}Q.INITIAL_MEMORY;var M,Y=[],S=[],H=[],d=0,k=null,J=null;function u(A){throw Q.onAbort&&Q.onAbort(A),w(A+=""),h=!0,A="abort("+A+"). Build with -s ASSERTIONS=1 for more info.",new WebAssembly.RuntimeError(A)}function p(A){return A.startsWith("data:application/octet-stream;base64,")}function L(A){return A.startsWith("file://")}Q.preloadedImages={},Q.preloadedAudios={};var l,K="argon2.wasm";function q(A){try{if(A==K&&F)return new Uint8Array(F);if(a)return a(A);throw"both async and sync fetching of the wasm failed"}catch(A){u(A)}}function b(A){for(;A.length>0;){var I=A.shift();if("function"!=typeof I){var g=I.func;"number"==typeof g?void 0===I.arg?M.get(g)():M.get(g)(I.arg):g(void 0===I.arg?null:I.arg)}else I(Q)}}function x(A){try{return c.grow(A-N.byteLength+65535>>>16),U(c.buffer),1}catch(A){}}p(K)||(l=K,K=Q.locateFile?Q.locateFile(l,y):y+l);var m,X={a:function(A,I,g){R.copyWithin(A,I,I+g)},b:function(A){var I,g=R.length,B=2147418112;if((A>>>=0)>B)return!1;for(var Q=1;Q<=4;Q*=2){var C=g*(1+.2/Q);if(C=Math.min(C,A+100663296),x(Math.min(B,((I=Math.max(A,C))%65536>0&&(I+=65536-I%65536),I))))return!0}return!1}},W=(function(){var A={a:X};function I(A,I){var g,B=A.exports;Q.asm=B,U((c=Q.asm.c).buffer),M=Q.asm.k,g=Q.asm.d,S.unshift(g),function(A){if(d--,Q.monitorRunDependencies&&Q.monitorRunDependencies(d),0==d&&(null!==k&&(clearInterval(k),k=null),J)){var I=J;J=null,I()}}()}function g(A){I(A.instance)}function B(I){return function(){if(!F&&(E||i)){if("function"==typeof fetch&&!L(K))return fetch(K,{credentials:"same-origin"}).then((function(A){if(!A.ok)throw"failed to load wasm binary file at '"+K+"'";return A.arrayBuffer()})).catch((function(){return q(K)}));if(t)return new Promise((function(A,I){t(K,(function(I){A(new Uint8Array(I))}),I)}))}return Promise.resolve().then((function(){return q(K)}))}().then((function(I){return WebAssembly.instantiate(I,A)})).then(I,(function(A){w("failed to asynchronously prepare wasm: "+A),u(A)}))}if(d++,Q.monitorRunDependencies&&Q.monitorRunDependencies(d),Q.instantiateWasm)try{return Q.instantiateWasm(A,I)}catch(A){return w("Module.instantiateWasm callback failed with error: "+A),!1}F||"function"!=typeof WebAssembly.instantiateStreaming||p(K)||L(K)||"function"!=typeof fetch?B(g):fetch(K,{credentials:"same-origin"}).then((function(I){return WebAssembly.instantiateStreaming(I,A).then(g,(function(A){return w("wasm streaming compile failed: "+A),w("falling back to ArrayBuffer instantiation"),B(g)}))}))}(),Q.___wasm_call_ctors=function(){return(Q.___wasm_call_ctors=Q.asm.d).apply(null,arguments)},Q._argon2_hash=function(){return(Q._argon2_hash=Q.asm.e).apply(null,arguments)},Q._malloc=function(){return(W=Q._malloc=Q.asm.f).apply(null,arguments)}),T=(Q._free=function(){return(Q._free=Q.asm.g).apply(null,arguments)},Q._argon2_verify=function(){return(Q._argon2_verify=Q.asm.h).apply(null,arguments)},Q._argon2_error_message=function(){return(Q._argon2_error_message=Q.asm.i).apply(null,arguments)},Q._argon2_encodedlen=function(){return(Q._argon2_encodedlen=Q.asm.j).apply(null,arguments)},Q._argon2_hash_ext=function(){return(Q._argon2_hash_ext=Q.asm.l).apply(null,arguments)},Q._argon2_verify_ext=function(){return(Q._argon2_verify_ext=Q.asm.m).apply(null,arguments)},Q.stackAlloc=function(){return(T=Q.stackAlloc=Q.asm.n).apply(null,arguments)});function V(A){this.name="ExitStatus",this.message="Program terminated with exit("+A+")",this.status=A}function j(A){function I(){m||(m=!0,Q.calledRun=!0,h||(b(S),Q.onRuntimeInitialized&&Q.onRuntimeInitialized(),function(){if(Q.postRun)for("function"==typeof Q.postRun&&(Q.postRun=[Q.postRun]);Q.postRun.length;)A=Q.postRun.shift(),H.unshift(A);var A;b(H)}()))}A=A||e,d>0||(function(){if(Q.preRun)for("function"==typeof Q.preRun&&(Q.preRun=[Q.preRun]);Q.preRun.length;)A=Q.preRun.shift(),Y.unshift(A);var A;b(Y)}(),d>0||(Q.setStatus?(Q.setStatus("Running..."),setTimeout((function(){setTimeout((function(){Q.setStatus("")}),1),I()}),1)):I()))}if(Q.allocate=function(A,I){var g;return g=1==I?T(A.length):W(A.length),A.subarray||A.slice?R.set(A,g):R.set(new Uint8Array(A),g),g},Q.UTF8ToString=function(A,I){return A?function(A,I,g){for(var B=I+g,Q=I;A[Q]&&!(Q>=B);)++Q;if(Q-I>16&&A.subarray&&f)return f.decode(A.subarray(I,Q));for(var C="";I>10,56320|1023&D)}}else C+=String.fromCharCode((31&E)<<6|i)}else C+=String.fromCharCode(E)}return C}(R,A,I):""},Q.ALLOC_NORMAL=0,J=function A(){m||j(),m||(J=A)},Q.run=j,Q.preInit)for("function"==typeof Q.preInit&&(Q.preInit=[Q.preInit]);Q.preInit.length>0;)Q.preInit.pop()();j(),A.exports=Q,Q.unloadRuntime=function(){"undefined"!=typeof self&&delete self.Module,Q=c=M=N=R=void 0,delete A.exports}},631:function(A,I,g){var B,Q;"undefined"!=typeof self&&self,void 0===(Q="function"==typeof(B=function(){const A="undefined"!=typeof self?self:this,I={Argon2d:0,Argon2i:1,Argon2id:2};function B(I){if(B._promise)return B._promise;if(B._module)return Promise.resolve(B._module);let C;return C=A.process&&A.process.versions&&A.process.versions.node?Q().then((A=>new Promise((I=>{A.postRun=()=>I(A)})))):(A.loadArgon2WasmBinary?A.loadArgon2WasmBinary():Promise.resolve(g(721)).then((A=>function(A){const I=atob(A),g=new Uint8Array(new ArrayBuffer(I.length));for(let A=0;Afunction(I,g){return new Promise((B=>(A.Module={wasmBinary:I,wasmMemory:g,postRun(){B(Module)}},Q())))}(g,I?function(A){const I=1024,g=64*I,B=(1024*I*1024*2-64*I)/g,Q=Math.min(Math.max(Math.ceil(A*I/g),256)+256,B);return new WebAssembly.Memory({initial:Q,maximum:B})}(I):void 0))),B._promise=C,C.then((A=>(B._module=A,delete B._promise,A)))}function Q(){return A.loadArgon2WasmModule?A.loadArgon2WasmModule():Promise.resolve(g(773))}function C(A,I){return A.allocate(I,"i8",A.ALLOC_NORMAL)}function E(A,I){return C(A,new Uint8Array([...I,0]))}function i(A){if("string"!=typeof A)return A;if("function"==typeof TextEncoder)return(new TextEncoder).encode(A);if("function"==typeof Buffer)return Buffer.from(A);throw new Error("Don't know how to encode UTF8")}return{ArgonType:I,hash:function(A){const g=A.mem||1024;return B(g).then((B=>{const Q=A.time||1,o=A.parallelism||1,D=i(A.pass),e=E(B,D),n=D.length,t=i(A.salt),a=E(B,t),r=t.length,s=A.type||I.Argon2d,y=B.allocate(new Array(A.hashLen||24),"i8",B.ALLOC_NORMAL),F=A.secret?C(B,A.secret):0,c=A.secret?A.secret.byteLength:0,w=A.ad?C(B,A.ad):0,h=A.ad?A.ad.byteLength:0,G=A.hashLen||24,N=B._argon2_encodedlen(Q,g,o,r,G,s),R=B.allocate(new Array(N+1),"i8",B.ALLOC_NORMAL);let f,U,M;try{U=B._argon2_hash_ext(Q,g,o,e,n,a,r,y,G,R,N,s,F,c,w,h,19)}catch(A){f=A}if(0!==U||f){try{f||(f=B.UTF8ToString(B._argon2_error_message(U)))}catch(A){}M={message:f,code:U}}else{let A="";const I=new Uint8Array(G);for(let g=0;g{const B=i(A.pass),Q=E(g,B),o=B.length,D=A.secret?C(g,A.secret):0,e=A.secret?A.secret.byteLength:0,n=A.ad?C(g,A.ad):0,t=A.ad?A.ad.byteLength:0,a=E(g,i(A.encoded));let r,s,y,F=A.type;if(void 0===F){let g=A.encoded.split("$")[1];g&&(g=g.replace("a","A"),F=I[g]||I.Argon2d)}try{s=g._argon2_verify_ext(a,Q,o,D,e,n,t,F)}catch(A){r=A}if(s||r){try{r||(r=g.UTF8ToString(g._argon2_error_message(s)))}catch(A){}y={message:r,code:s}}try{g._free(Q),g._free(a)}catch(A){}if(r)throw y;return y}))},unloadRuntime:function(){B._module&&(B._module.unloadRuntime(),delete B._promise,delete B._module)}}})?B.apply(I,[]):B)||(A.exports=Q)},721:function(A,I){A.exports="AGFzbQEAAAABkwESYAN/f38Bf2ABfwF/YAJ/fwBgAn9/AX9gAX8AYAR/f39/AX9gA39/fwBgBH9/f38AYAJ/fgBgAn5/AX5gAn5+AX5gBX9/f39/AGAGf3x/f39/AX9gAABgCH9/f39/f39/AX9gEX9/f39/f39/f39/f39/f39/AX9gBn9/f39/fwF/YA1/f39/f39/f39/f39/AX8CDQIBYQFhAAABYQFiAAEDPDsJCgIAAAIEAQEAAQsGAQAHAAIBAwICAwIIBQECAwEHDQMBBgQGAQEFBQEAAAIEAAAIAQAODwQQAQURAwQFAXABAwMFBwEBgAL//wEGCQF/AUGQo8ACCwcxDAFjAgABZAAhAWUAOwFmAAkBZwAIAWgAOgFpADkBagA4AWsBAAFsADYBbQA1AW4AMwkIAQBBAQsCCzQKwbMBOwgAIAAgAa2KCx4AIAAgAXwgAEIBhkL+////H4MgAUL/////D4N+fAsXAEHwHCgCAEUgAEVyRQRAIAAgARAdCwuDBAEDfyACQYAETwRAIAAgASACEAAaIAAPCyAAIAJqIQMCQCAAIAFzQQNxRQRAAkAgAEEDcUUEQCAAIQIMAQsgAkEBSARAIAAhAgwBCyAAIQIDQCACIAEtAAA6AAAgAUEBaiEBIAJBAWoiAkEDcUUNASACIANJDQALCwJAIANBfHEiBEHAAEkNACACIARBQGoiBUsNAANAIAIgASgCADYCACACIAEoAgQ2AgQgAiABKAIINgIIIAIgASgCDDYCDCACIAEoAhA2AhAgAiABKAIUNgIUIAIgASgCGDYCGCACIAEoAhw2AhwgAiABKAIgNgIgIAIgASgCJDYCJCACIAEoAig2AiggAiABKAIsNgIsIAIgASgCMDYCMCACIAEoAjQ2AjQgAiABKAI4NgI4IAIgASgCPDYCPCABQUBrIQEgAkFAayICIAVNDQALCyACIARPDQEDQCACIAEoAgA2AgAgAUEEaiEBIAJBBGoiAiAESQ0ACwwBCyADQQRJBEAgACECDAELIAAgA0EEayIESwRAIAAhAgwBCyAAIQIDQCACIAEtAAA6AAAgAiABLQABOgABIAIgAS0AAjoAAiACIAEtAAM6AAMgAUEEaiEBIAJBBGoiAiAETQ0ACwsgAiADSQRAA0AgAiABLQAAOgAAIAFBAWohASACQQFqIgIgA0cNAAsLIAALzwEBA38CQCACRQ0AQX8hAyAARSABRXINACAAKQNQQgBSDQACQCAAKALgASIDIAJqQYEBSQ0AIABB4ABqIgUgA2ogAUGAASADayIEEAUaIABCgAEQGiAAIAUQGUEAIQMgAEEANgLgASABIARqIQEgAiAEayICQYEBSQ0AA0AgAEKAARAaIAAgARAZIAFBgAFqIQEgAkGAAWsiAkGAAUsNAAsgACgC4AEhAwsgACADakHgAGogASACEAUaIAAgACgC4AEgAmo2AuABQQAhAwsgAwsJACAAIAE2AAALpwwBB38CQCAARQ0AIABBCGsiAyAAQQRrKAIAIgFBeHEiAGohBQJAIAFBAXENACABQQNxRQ0BIAMgAygCACIBayIDQbAfKAIASQ0BIAAgAWohACADQbQfKAIARwRAIAFB/wFNBEAgAygCCCICIAFBA3YiBEEDdEHIH2pGGiACIAMoAgwiAUYEQEGgH0GgHygCAEF+IAR3cTYCAAwDCyACIAE2AgwgASACNgIIDAILIAMoAhghBgJAIAMgAygCDCIBRwRAIAMoAggiAiABNgIMIAEgAjYCCAwBCwJAIANBFGoiAigCACIEDQAgA0EQaiICKAIAIgQNAEEAIQEMAQsDQCACIQcgBCIBQRRqIgIoAgAiBA0AIAFBEGohAiABKAIQIgQNAAsgB0EANgIACyAGRQ0BAkAgAyADKAIcIgJBAnRB0CFqIgQoAgBGBEAgBCABNgIAIAENAUGkH0GkHygCAEF+IAJ3cTYCAAwDCyAGQRBBFCAGKAIQIANGG2ogATYCACABRQ0CCyABIAY2AhggAygCECICBEAgASACNgIQIAIgATYCGAsgAygCFCICRQ0BIAEgAjYCFCACIAE2AhgMAQsgBSgCBCIBQQNxQQNHDQBBqB8gADYCACAFIAFBfnE2AgQgAyAAQQFyNgIEIAAgA2ogADYCAA8LIAMgBU8NACAFKAIEIgFBAXFFDQACQCABQQJxRQRAIAVBuB8oAgBGBEBBuB8gAzYCAEGsH0GsHygCACAAaiIANgIAIAMgAEEBcjYCBCADQbQfKAIARw0DQagfQQA2AgBBtB9BADYCAA8LIAVBtB8oAgBGBEBBtB8gAzYCAEGoH0GoHygCACAAaiIANgIAIAMgAEEBcjYCBCAAIANqIAA2AgAPCyABQXhxIABqIQACQCABQf8BTQRAIAUoAggiAiABQQN2IgRBA3RByB9qRhogAiAFKAIMIgFGBEBBoB9BoB8oAgBBfiAEd3E2AgAMAgsgAiABNgIMIAEgAjYCCAwBCyAFKAIYIQYCQCAFIAUoAgwiAUcEQCAFKAIIIgJBsB8oAgBJGiACIAE2AgwgASACNgIIDAELAkAgBUEUaiICKAIAIgQNACAFQRBqIgIoAgAiBA0AQQAhAQwBCwNAIAIhByAEIgFBFGoiAigCACIEDQAgAUEQaiECIAEoAhAiBA0ACyAHQQA2AgALIAZFDQACQCAFIAUoAhwiAkECdEHQIWoiBCgCAEYEQCAEIAE2AgAgAQ0BQaQfQaQfKAIAQX4gAndxNgIADAILIAZBEEEUIAYoAhAgBUYbaiABNgIAIAFFDQELIAEgBjYCGCAFKAIQIgIEQCABIAI2AhAgAiABNgIYCyAFKAIUIgJFDQAgASACNgIUIAIgATYCGAsgAyAAQQFyNgIEIAAgA2ogADYCACADQbQfKAIARw0BQagfIAA2AgAPCyAFIAFBfnE2AgQgAyAAQQFyNgIEIAAgA2ogADYCAAsgAEH/AU0EQCAAQQN2IgFBA3RByB9qIQACf0GgHygCACICQQEgAXQiAXFFBEBBoB8gASACcjYCACAADAELIAAoAggLIQIgACADNgIIIAIgAzYCDCADIAA2AgwgAyACNgIIDwtBHyECIANCADcCECAAQf///wdNBEAgAEEIdiIBIAFBgP4/akEQdkEIcSIBdCICIAJBgOAfakEQdkEEcSICdCIEIARBgIAPakEQdkECcSIEdEEPdiABIAJyIARyayIBQQF0IAAgAUEVanZBAXFyQRxqIQILIAMgAjYCHCACQQJ0QdAhaiEBAkACQAJAQaQfKAIAIgRBASACdCIHcUUEQEGkHyAEIAdyNgIAIAEgAzYCACADIAE2AhgMAQsgAEEAQRkgAkEBdmsgAkEfRht0IQIgASgCACEBA0AgASIEKAIEQXhxIABGDQIgAkEddiEBIAJBAXQhAiAEIAFBBHFqIgdBEGooAgAiAQ0ACyAHIAM2AhAgAyAENgIYCyADIAM2AgwgAyADNgIIDAELIAQoAggiACADNgIMIAQgAzYCCCADQQA2AhggAyAENgIMIAMgADYCCAtBwB9BwB8oAgBBAWsiAEF/IAAbNgIACwuULQEMfyMAQRBrIgwkAAJAAkACQAJAAkACQAJAAkACQAJAAkACQCAAQfQBTQRAQaAfKAIAIgVBECAAQQtqQXhxIABBC0kbIghBA3YiAnYiAUEDcQRAIAFBf3NBAXEgAmoiA0EDdCIBQdAfaigCACIEQQhqIQACQCAEKAIIIgIgAUHIH2oiAUYEQEGgHyAFQX4gA3dxNgIADAELIAIgATYCDCABIAI2AggLIAQgA0EDdCIBQQNyNgIEIAEgBGoiASABKAIEQQFyNgIEDA0LIAhBqB8oAgAiCk0NASABBEACQEECIAJ0IgBBACAAa3IgASACdHEiAEEAIABrcUEBayIAIABBDHZBEHEiAnYiAUEFdkEIcSIAIAJyIAEgAHYiAUECdkEEcSIAciABIAB2IgFBAXZBAnEiAHIgASAAdiIBQQF2QQFxIgByIAEgAHZqIgNBA3QiAEHQH2ooAgAiBCgCCCIBIABByB9qIgBGBEBBoB8gBUF+IAN3cSIFNgIADAELIAEgADYCDCAAIAE2AggLIARBCGohACAEIAhBA3I2AgQgBCAIaiICIANBA3QiASAIayIDQQFyNgIEIAEgBGogAzYCACAKBEAgCkEDdiIBQQN0QcgfaiEHQbQfKAIAIQQCfyAFQQEgAXQiAXFFBEBBoB8gASAFcjYCACAHDAELIAcoAggLIQEgByAENgIIIAEgBDYCDCAEIAc2AgwgBCABNgIIC0G0HyACNgIAQagfIAM2AgAMDQtBpB8oAgAiBkUNASAGQQAgBmtxQQFrIgAgAEEMdkEQcSICdiIBQQV2QQhxIgAgAnIgASAAdiIBQQJ2QQRxIgByIAEgAHYiAUEBdkECcSIAciABIAB2IgFBAXZBAXEiAHIgASAAdmpBAnRB0CFqKAIAIgEoAgRBeHEgCGshAyABIQIDQAJAIAIoAhAiAEUEQCACKAIUIgBFDQELIAAoAgRBeHEgCGsiAiADIAIgA0kiAhshAyAAIAEgAhshASAAIQIMAQsLIAEgCGoiCSABTQ0CIAEoAhghCyABIAEoAgwiBEcEQCABKAIIIgBBsB8oAgBJGiAAIAQ2AgwgBCAANgIIDAwLIAFBFGoiAigCACIARQRAIAEoAhAiAEUNBCABQRBqIQILA0AgAiEHIAAiBEEUaiICKAIAIgANACAEQRBqIQIgBCgCECIADQALIAdBADYCAAwLC0F/IQggAEG/f0sNACAAQQtqIgBBeHEhCEGkHygCACIJRQ0AQQAgCGshAwJAAkACQAJ/QQAgCEGAAkkNABpBHyAIQf///wdLDQAaIABBCHYiACAAQYD+P2pBEHZBCHEiAnQiACAAQYDgH2pBEHZBBHEiAXQiACAAQYCAD2pBEHZBAnEiAHRBD3YgASACciAAcmsiAEEBdCAIIABBFWp2QQFxckEcagsiBUECdEHQIWooAgAiAkUEQEEAIQAMAQtBACEAIAhBAEEZIAVBAXZrIAVBH0YbdCEBA0ACQCACKAIEQXhxIAhrIgcgA08NACACIQQgByIDDQBBACEDIAIhAAwDCyAAIAIoAhQiByAHIAIgAUEddkEEcWooAhAiAkYbIAAgBxshACABQQF0IQEgAg0ACwsgACAEckUEQEEAIQRBAiAFdCIAQQAgAGtyIAlxIgBFDQMgAEEAIABrcUEBayIAIABBDHZBEHEiAnYiAUEFdkEIcSIAIAJyIAEgAHYiAUECdkEEcSIAciABIAB2IgFBAXZBAnEiAHIgASAAdiIBQQF2QQFxIgByIAEgAHZqQQJ0QdAhaigCACEACyAARQ0BCwNAIAAoAgRBeHEgCGsiASADSSECIAEgAyACGyEDIAAgBCACGyEEIAAoAhAiAQR/IAEFIAAoAhQLIgANAAsLIARFDQAgA0GoHygCACAIa08NACAEIAhqIgYgBE0NASAEKAIYIQUgBCAEKAIMIgFHBEAgBCgCCCIAQbAfKAIASRogACABNgIMIAEgADYCCAwKCyAEQRRqIgIoAgAiAEUEQCAEKAIQIgBFDQQgBEEQaiECCwNAIAIhByAAIgFBFGoiAigCACIADQAgAUEQaiECIAEoAhAiAA0ACyAHQQA2AgAMCQsgCEGoHygCACICTQRAQbQfKAIAIQMCQCACIAhrIgFBEE8EQEGoHyABNgIAQbQfIAMgCGoiADYCACAAIAFBAXI2AgQgAiADaiABNgIAIAMgCEEDcjYCBAwBC0G0H0EANgIAQagfQQA2AgAgAyACQQNyNgIEIAIgA2oiACAAKAIEQQFyNgIECyADQQhqIQAMCwsgCEGsHygCACIGSQRAQawfIAYgCGsiATYCAEG4H0G4HygCACICIAhqIgA2AgAgACABQQFyNgIEIAIgCEEDcjYCBCACQQhqIQAMCwtBACEAIAhBL2oiCQJ/QfgiKAIABEBBgCMoAgAMAQtBhCNCfzcCAEH8IkKAoICAgIAENwIAQfgiIAxBDGpBcHFB2KrVqgVzNgIAQYwjQQA2AgBB3CJBADYCAEGAIAsiAWoiBUEAIAFrIgdxIgIgCE0NCkHYIigCACIEBEBB0CIoAgAiAyACaiIBIANNIAEgBEtyDQsLQdwiLQAAQQRxDQUCQAJAQbgfKAIAIgMEQEHgIiEAA0AgAyAAKAIAIgFPBEAgASAAKAIEaiADSw0DCyAAKAIIIgANAAsLQQAQDCIBQX9GDQYgAiEFQfwiKAIAIgNBAWsiACABcQRAIAIgAWsgACABakEAIANrcWohBQsgBSAITSAFQf7///8HS3INBkHYIigCACIEBEBB0CIoAgAiAyAFaiIAIANNIAAgBEtyDQcLIAUQDCIAIAFHDQEMCAsgBSAGayAHcSIFQf7///8HSw0FIAUQDCIBIAAoAgAgACgCBGpGDQQgASEACyAAQX9GIAhBMGogBU1yRQRAQYAjKAIAIgEgCSAFa2pBACABa3EiAUH+////B0sEQCAAIQEMCAsgARAMQX9HBEAgASAFaiEFIAAhAQwIC0EAIAVrEAwaDAULIAAiAUF/Rw0GDAQLAAtBACEEDAcLQQAhAQwFCyABQX9HDQILQdwiQdwiKAIAQQRyNgIACyACQf7///8HSw0BIAIQDCIBQX9GQQAQDCIAQX9GciAAIAFNcg0BIAAgAWsiBSAIQShqTQ0BC0HQIkHQIigCACAFaiIANgIAQdQiKAIAIABJBEBB1CIgADYCAAsCQAJAAkBBuB8oAgAiBwRAQeAiIQADQCABIAAoAgAiAyAAKAIEIgJqRg0CIAAoAggiAA0ACwwCC0GwHygCACIAQQAgACABTRtFBEBBsB8gATYCAAtBACEAQeQiIAU2AgBB4CIgATYCAEHAH0F/NgIAQcQfQfgiKAIANgIAQewiQQA2AgADQCAAQQN0IgNB0B9qIANByB9qIgI2AgAgA0HUH2ogAjYCACAAQQFqIgBBIEcNAAtBrB8gBUEoayIDQXggAWtBB3FBACABQQhqQQdxGyIAayICNgIAQbgfIAAgAWoiADYCACAAIAJBAXI2AgQgASADakEoNgIEQbwfQYgjKAIANgIADAILIAAtAAxBCHEgAyAHS3IgASAHTXINACAAIAIgBWo2AgRBuB8gB0F4IAdrQQdxQQAgB0EIakEHcRsiAGoiAjYCAEGsH0GsHygCACAFaiIBIABrIgA2AgAgAiAAQQFyNgIEIAEgB2pBKDYCBEG8H0GIIygCADYCAAwBC0GwHygCACABSwRAQbAfIAE2AgALIAEgBWohAkHgIiEAAkACQAJAAkACQAJAA0AgAiAAKAIARwRAIAAoAggiAA0BDAILCyAALQAMQQhxRQ0BC0HgIiEAA0AgByAAKAIAIgJPBEAgAiAAKAIEaiIEIAdLDQMLIAAoAgghAAwACwALIAAgATYCACAAIAAoAgQgBWo2AgQgAUF4IAFrQQdxQQAgAUEIakEHcRtqIgkgCEEDcjYCBCACQXggAmtBB3FBACACQQhqQQdxG2oiBSAIIAlqIgZrIQIgBSAHRgRAQbgfIAY2AgBBrB9BrB8oAgAgAmoiADYCACAGIABBAXI2AgQMAwsgBUG0HygCAEYEQEG0HyAGNgIAQagfQagfKAIAIAJqIgA2AgAgBiAAQQFyNgIEIAAgBmogADYCAAwDCyAFKAIEIgBBA3FBAUYEQCAAQXhxIQcCQCAAQf8BTQRAIAUoAggiAyAAQQN2IgBBA3RByB9qRhogAyAFKAIMIgFGBEBBoB9BoB8oAgBBfiAAd3E2AgAMAgsgAyABNgIMIAEgAzYCCAwBCyAFKAIYIQgCQCAFIAUoAgwiAUcEQCAFKAIIIgAgATYCDCABIAA2AggMAQsCQCAFQRRqIgAoAgAiAw0AIAVBEGoiACgCACIDDQBBACEBDAELA0AgACEEIAMiAUEUaiIAKAIAIgMNACABQRBqIQAgASgCECIDDQALIARBADYCAAsgCEUNAAJAIAUgBSgCHCIDQQJ0QdAhaiIAKAIARgRAIAAgATYCACABDQFBpB9BpB8oAgBBfiADd3E2AgAMAgsgCEEQQRQgCCgCECAFRhtqIAE2AgAgAUUNAQsgASAINgIYIAUoAhAiAARAIAEgADYCECAAIAE2AhgLIAUoAhQiAEUNACABIAA2AhQgACABNgIYCyAFIAdqIQUgAiAHaiECCyAFIAUoAgRBfnE2AgQgBiACQQFyNgIEIAIgBmogAjYCACACQf8BTQRAIAJBA3YiAEEDdEHIH2ohAgJ/QaAfKAIAIgFBASAAdCIAcUUEQEGgHyAAIAFyNgIAIAIMAQsgAigCCAshACACIAY2AgggACAGNgIMIAYgAjYCDCAGIAA2AggMAwtBHyEAIAJB////B00EQCACQQh2IgAgAEGA/j9qQRB2QQhxIgN0IgAgAEGA4B9qQRB2QQRxIgF0IgAgAEGAgA9qQRB2QQJxIgB0QQ92IAEgA3IgAHJrIgBBAXQgAiAAQRVqdkEBcXJBHGohAAsgBiAANgIcIAZCADcCECAAQQJ0QdAhaiEEAkBBpB8oAgAiA0EBIAB0IgFxRQRAQaQfIAEgA3I2AgAgBCAGNgIAIAYgBDYCGAwBCyACQQBBGSAAQQF2ayAAQR9GG3QhACAEKAIAIQEDQCABIgMoAgRBeHEgAkYNAyAAQR12IQEgAEEBdCEAIAMgAUEEcWoiBCgCECIBDQALIAQgBjYCECAGIAM2AhgLIAYgBjYCDCAGIAY2AggMAgtBrB8gBUEoayIDQXggAWtBB3FBACABQQhqQQdxGyIAayICNgIAQbgfIAAgAWoiADYCACAAIAJBAXI2AgQgASADakEoNgIEQbwfQYgjKAIANgIAIAcgBEEnIARrQQdxQQAgBEEna0EHcRtqQS9rIgAgACAHQRBqSRsiAkEbNgIEIAJB6CIpAgA3AhAgAkHgIikCADcCCEHoIiACQQhqNgIAQeQiIAU2AgBB4CIgATYCAEHsIkEANgIAIAJBGGohAANAIABBBzYCBCAAQQhqIQEgAEEEaiEAIAEgBEkNAAsgAiAHRg0DIAIgAigCBEF+cTYCBCAHIAIgB2siBEEBcjYCBCACIAQ2AgAgBEH/AU0EQCAEQQN2IgBBA3RByB9qIQICf0GgHygCACIBQQEgAHQiAHFFBEBBoB8gACABcjYCACACDAELIAIoAggLIQAgAiAHNgIIIAAgBzYCDCAHIAI2AgwgByAANgIIDAQLQR8hACAHQgA3AhAgBEH///8HTQRAIARBCHYiACAAQYD+P2pBEHZBCHEiAnQiACAAQYDgH2pBEHZBBHEiAXQiACAAQYCAD2pBEHZBAnEiAHRBD3YgASACciAAcmsiAEEBdCAEIABBFWp2QQFxckEcaiEACyAHIAA2AhwgAEECdEHQIWohAwJAQaQfKAIAIgJBASAAdCIBcUUEQEGkHyABIAJyNgIAIAMgBzYCACAHIAM2AhgMAQsgBEEAQRkgAEEBdmsgAEEfRht0IQAgAygCACEBA0AgASICKAIEQXhxIARGDQQgAEEddiEBIABBAXQhACACIAFBBHFqIgMoAhAiAQ0ACyADIAc2AhAgByACNgIYCyAHIAc2AgwgByAHNgIIDAMLIAMoAggiACAGNgIMIAMgBjYCCCAGQQA2AhggBiADNgIMIAYgADYCCAsgCUEIaiEADAULIAIoAggiACAHNgIMIAIgBzYCCCAHQQA2AhggByACNgIMIAcgADYCCAtBrB8oAgAiACAITQ0AQawfIAAgCGsiATYCAEG4H0G4HygCACICIAhqIgA2AgAgACABQQFyNgIEIAIgCEEDcjYCBCACQQhqIQAMAwtB3B5BMDYCAEEAIQAMAgsCQCAFRQ0AAkAgBCgCHCICQQJ0QdAhaiIAKAIAIARGBEAgACABNgIAIAENAUGkHyAJQX4gAndxIgk2AgAMAgsgBUEQQRQgBSgCECAERhtqIAE2AgAgAUUNAQsgASAFNgIYIAQoAhAiAARAIAEgADYCECAAIAE2AhgLIAQoAhQiAEUNACABIAA2AhQgACABNgIYCwJAIANBD00EQCAEIAMgCGoiAEEDcjYCBCAAIARqIgAgACgCBEEBcjYCBAwBCyAEIAhBA3I2AgQgBiADQQFyNgIEIAMgBmogAzYCACADQf8BTQRAIANBA3YiAEEDdEHIH2ohAgJ/QaAfKAIAIgFBASAAdCIAcUUEQEGgHyAAIAFyNgIAIAIMAQsgAigCCAshACACIAY2AgggACAGNgIMIAYgAjYCDCAGIAA2AggMAQtBHyEAIANB////B00EQCADQQh2IgAgAEGA/j9qQRB2QQhxIgJ0IgAgAEGA4B9qQRB2QQRxIgF0IgAgAEGAgA9qQRB2QQJxIgB0QQ92IAEgAnIgAHJrIgBBAXQgAyAAQRVqdkEBcXJBHGohAAsgBiAANgIcIAZCADcCECAAQQJ0QdAhaiECAkACQCAJQQEgAHQiAXFFBEBBpB8gASAJcjYCACACIAY2AgAgBiACNgIYDAELIANBAEEZIABBAXZrIABBH0YbdCEAIAIoAgAhCANAIAgiASgCBEF4cSADRg0CIABBHXYhAiAAQQF0IQAgASACQQRxaiICKAIQIggNAAsgAiAGNgIQIAYgATYCGAsgBiAGNgIMIAYgBjYCCAwBCyABKAIIIgAgBjYCDCABIAY2AgggBkEANgIYIAYgATYCDCAGIAA2AggLIARBCGohAAwBCwJAIAtFDQACQCABKAIcIgJBAnRB0CFqIgAoAgAgAUYEQCAAIAQ2AgAgBA0BQaQfIAZBfiACd3E2AgAMAgsgC0EQQRQgCygCECABRhtqIAQ2AgAgBEUNAQsgBCALNgIYIAEoAhAiAARAIAQgADYCECAAIAQ2AhgLIAEoAhQiAEUNACAEIAA2AhQgACAENgIYCwJAIANBD00EQCABIAMgCGoiAEEDcjYCBCAAIAFqIgAgACgCBEEBcjYCBAwBCyABIAhBA3I2AgQgCSADQQFyNgIEIAMgCWogAzYCACAKBEAgCkEDdiIAQQN0QcgfaiEEQbQfKAIAIQICf0EBIAB0IgAgBXFFBEBBoB8gACAFcjYCACAEDAELIAQoAggLIQAgBCACNgIIIAAgAjYCDCACIAQ2AgwgAiAANgIIC0G0HyAJNgIAQagfIAM2AgALIAFBCGohAAsgDEEQaiQAIAALfwEDfyAAIQECQCAAQQNxBEADQCABLQAARQ0CIAFBAWoiAUEDcQ0ACwsDQCABIgJBBGohASACKAIAIgNBf3MgA0GBgoQIa3FBgIGChHhxRQ0ACyADQf8BcUUEQCACIABrDwsDQCACLQABIQMgAkEBaiIBIQIgAw0ACwsgASAAawvyAgICfwF+AkAgAkUNACAAIAJqIgNBAWsgAToAACAAIAE6AAAgAkEDSQ0AIANBAmsgAToAACAAIAE6AAEgA0EDayABOgAAIAAgAToAAiACQQdJDQAgA0EEayABOgAAIAAgAToAAyACQQlJDQAgAEEAIABrQQNxIgRqIgMgAUH/AXFBgYKECGwiATYCACADIAIgBGtBfHEiBGoiAkEEayABNgIAIARBCUkNACADIAE2AgggAyABNgIEIAJBCGsgATYCACACQQxrIAE2AgAgBEEZSQ0AIAMgATYCGCADIAE2AhQgAyABNgIQIAMgATYCDCACQRBrIAE2AgAgAkEUayABNgIAIAJBGGsgATYCACACQRxrIAE2AgAgBCADQQRxQRhyIgRrIgJBIEkNACABrUKBgICAEH4hBSADIARqIQEDQCABIAU3AxggASAFNwMQIAEgBTcDCCABIAU3AwAgAUEgaiEBIAJBIGsiAkEfSw0ACwsgAAtPAQJ/QdgeKAIAIgEgAEEDakF8cSICaiEAAkAgAkEAIAAgAU0bDQAgAD8AQRB0SwRAIAAQAUUNAQtB2B4gADYCACABDwtB3B5BMDYCAEF/C20BAX8jAEGAAmsiBSQAIARBgMAEcSACIANMckUEQCAFIAFB/wFxIAIgA2siAkGAAiACQYACSSIBGxALGiABRQRAA0AgACAFQYACEA4gAkGAAmsiAkH/AUsNAAsLIAAgBSACEA4LIAVBgAJqJAALnQIBA38gAC0AAEEgcUUEQAJAIAEhBAJAIAIgACIBKAIQIgAEfyAABQJ/IAEiACABLQBKIgNBAWsgA3I6AEogASgCACIDQQhxBEAgACADQSByNgIAQX8MAQsgAEIANwIEIAAgACgCLCIDNgIcIAAgAzYCFCAAIAMgACgCMGo2AhBBAAsNASABKAIQCyABKAIUIgVrSwRAIAEgBCACIAEoAiQRAAAaDAILAn8gASwAS0F/SgRAIAIhAANAIAIgACIDRQ0CGiAEIANBAWsiAGotAABBCkcNAAsgASAEIAMgASgCJBEAACADSQ0CIAMgBGohBCABKAIUIQUgAiADawwBCyACCyEAIAUgBCAAEAUaIAEgASgCFCAAajYCFAsLCwsKACAAQTBrQQpJC2MBAn8gAkUEQEEADwsCfyAALQAAIgMEQANAAkACQCABLQAAIgRFDQAgAkEBayICRQ0AIAMgBEYNAQsgAwwDCyABQQFqIQEgAC0AASEDIABBAWohACADDQALC0EACyABLQAAawucDQIQfhB/IwBBgBBrIhQkACAUQYAIaiABEBcgFEGACGogABAWIBQgFEGACGoQFyADBEAgFCACEBYLQQAhAEEAIQEDQCAUQYAIaiABQQd0IgNBwAByaiIVKQMAIBRBgAhqIANB4AByaiIWKQMAIBRBgAhqIANqIhcpAwAgFEGACGogA0EgcmoiGCkDACIIEAMiBIVBIBACIgUQAyIGIAiFQRgQAiEIIAggBiAFIAQgCBADIgeFQRAQAiIKEAMiEYVBPxACIQggFEGACGogA0HIAHJqIhkpAwAgFEGACGogA0HoAHJqIhopAwAgFEGACGogA0EIcmoiGykDACAUQYAIaiADQShyaiIcKQMAIgQQAyIFhUEgEAIiBhADIgsgBIVBGBACIQQgBCALIAYgBSAEEAMiC4VBEBACIhIQAyIThUE/EAIhBCAUQYAIaiADQdAAcmoiHSkDACAUQYAIaiADQfAAcmoiHikDACAUQYAIaiADQRByaiIfKQMAIBRBgAhqIANBMHJqIiApAwAiBRADIgaFQSAQAiIMEAMiDSAFhUEYEAIhBSAFIA0gDCAGIAUQAyINhUEQEAIiDBADIg6FQT8QAiEFIBRBgAhqIANB2AByaiIhKQMAIBRBgAhqIANB+AByaiIiKQMAIBRBgAhqIANBGHJqIiMpAwAgFEGACGogA0E4cmoiAykDACIGEAMiD4VBIBACIgkQAyIQIAaFQRgQAiEGIAYgECAJIA8gBhADIg+FQRAQAiIJEAMiEIVBPxACIQYgFyAHIAQQAyIHIAQgDiAHIAmFQSAQAiIHEAMiDoVBGBACIgQQAyIJNwMAICIgByAJhUEQEAIiBzcDACAdIA4gBxADIgc3AwAgHCAEIAeFQT8QAjcDACAbIAsgBRADIgQgBSAQIAQgCoVBIBACIgQQAyIHhUEYEAIiBRADIgo3AwAgFiAEIAqFQRAQAiIENwMAICEgByAEEAMiBDcDACAgIAQgBYVBPxACNwMAIB8gDSAGEAMiBCAGIBEgBCAShUEgEAIiBBADIgWFQRgQAiIGEAMiBzcDACAaIAQgB4VBEBACIgQ3AwAgFSAFIAQQAyIENwMAIAMgBCAGhUE/EAI3AwAgIyAPIAgQAyIEIAggEyAEIAyFQSAQAiIEEAMiBYVBGBACIggQAyIGNwMAIB4gBCAGhUEQEAIiBDcDACAZIAUgBBADIgQ3AwAgGCAEIAiFQT8QAjcDACABQQFqIgFBCEcNAAsDQCAAQQR0IgMgFEGACGpqIgEiFUGABGopAwAgASkDgAYgASkDACABKQOAAiIIEAMiBIVBIBACIgUQAyIGIAiFQRgQAiEIIAggBiAFIAQgCBADIgeFQRAQAiIKEAMiEYVBPxACIQggASkDiAQgASkDiAYgFEGACGogA0EIcmoiAykDACABKQOIAiIEEAMiBYVBIBACIgYQAyILIASFQRgQAiEEIAQgCyAGIAUgBBADIguFQRAQAiISEAMiE4VBPxACIQQgASkDgAUgASkDgAcgASkDgAEgASkDgAMiBRADIgaFQSAQAiIMEAMiDSAFhUEYEAIhBSAFIA0gDCAGIAUQAyINhUEQEAIiDBADIg6FQT8QAiEFIAEpA4gFIAEpA4gHIAEpA4gBIAEpA4gDIgYQAyIPhUEgEAIiCRADIhAgBoVBGBACIQYgBiAQIAkgDyAGEAMiD4VBEBACIgkQAyIQhUE/EAIhBiABIAcgBBADIgcgBCAOIAcgCYVBIBACIgcQAyIOhUEYEAIiBBADIgk3AwAgASAHIAmFQRAQAiIHNwOIByABIA4gBxADIgc3A4AFIAEgBCAHhUE/EAI3A4gCIAMgCyAFEAMiBCAFIBAgBCAKhUEgEAIiBBADIgeFQRgQAiIFEAMiCjcDACABIAQgCoVBEBACIgQ3A4AGIAEgByAEEAMiBDcDiAUgASAEIAWFQT8QAjcDgAMgASANIAYQAyIEIAYgESAEIBKFQSAQAiIEEAMiBYVBGBACIgYQAyIHNwOAASABIAQgB4VBEBACIgQ3A4gGIBUgBSAEEAMiBDcDgAQgASAEIAaFQT8QAjcDiAMgASAPIAgQAyIEIAggEyAEIAyFQSAQAiIEEAMiBYVBGBACIggQAyIGNwOIASABIAQgBoVBEBACIgQ3A4AHIAEgBSAEEAMiBDcDiAQgASAEIAiFQT8QAjcDgAIgAEEBaiIAQQhHDQALIAIgFBAXIAIgFEGACGoQFiAUQYAQaiQAC8MBAQN/IwBBQGoiAyQAIANBAEHAABALIQRBfyEDAkAgAEUgAUVyDQAgACgC5AEgAksNACAAKQNQQgBSDQAgACAANQLgARAaIAAQJUEAIQMgAEHgAGoiAiAAKALgASIFakEAQYABIAVrEAsaIAAgAhAZA0AgBCADQQN0IgVqIAAgBWopAwAQMiADQQFqIgNBCEcNAAsgASAEIAAoAuQBEAUaIARBwAAQBCACQYABEAQgAEHAABAEQQAhAwsgBEFAayQAIAML1AMBBn8jAEEQayIEJAAgBCABNgIMIwBBoAFrIgMkACADQQhqQYAYQZABEAUaIAMgADYCNCADIAA2AhwgA0F+IABrIgJB/////wcgAkH/////B0kbIgU2AjggAyAAIAVqIgA2AiQgAyAANgIYIANBCGohACMAQdABayICJAAgAiABNgLMASACQaABakEAQSgQCxogAiACKALMATYCyAECQEEAIAJByAFqIAJB0ABqIAJBoAFqEBtBAEgNACAAKAJMQQBOIQYgACgCACEBIAAsAEpBAEwEQCAAIAFBX3E2AgALIAFBIHEhBwJ/IAAoAjAEQCAAIAJByAFqIAJB0ABqIAJBoAFqEBsMAQsgAEHQADYCMCAAIAJB0ABqNgIQIAAgAjYCHCAAIAI2AhQgACgCLCEBIAAgAjYCLCAAIAJByAFqIAJB0ABqIAJBoAFqEBsgAUUNABogAEEAQQAgACgCJBEAABogAEEANgIwIAAgATYCLCAAQQA2AhwgAEEANgIQIAAoAhQaIABBADYCFEEACxogACAAKAIAIAdyNgIAIAZFDQALIAJB0AFqJAAgBQRAIAMoAhwiACAAIAMoAhhGa0EAOgAACyADQaABaiQAIARBEGokAAs0AQF/QQEhAQJAIABBCkkNAEECIQEDQCAAQeQASQ0BIAFBAWohASAAQQpuIQAMAAsACyABC4UBAQd/AkAgAC0AACIGQTBrQf8BcUEJSw0AIAYhAgNAIAQhByADQZmz5swBSw0BIAJB/wFxQTBrIgIgA0EKbCIEQX9zSw0BIAIgBGohAyAAIAdBAWoiBGoiCC0AACICQTBrQf8BcUEKSQ0ACyAGQTBGQQAgBxsNACABIAM2AgAgCCEFCyAFCzEBA38DQCAAIAJBA3QiA2oiBCAEKQMAIAEgA2opAwCFNwMAIAJBAWoiAkGAAUcNAAsLDAAgACABQYAIEAUaC14BAn8jAEFAaiICJABBfyEDAkAgAEUNACABQQFrQcAATwRAIAAQNwwBCyACQQE6AAMgAkGAAjsAASACIAE6AAAgAkEEckEAQTwQCxogACACEDwhAwsgAkFAayQAIAMLpAoCA38RfiMAQYACayIDJAADQCACQQN0IgQgA0GAAWpqIAEgBGopAAA3AwAgAkEBaiICQRBHDQALIAMgAEHAABAFIQEgACkDWEL5wvibkaOz8NsAhSELIAApA1BC6/qG2r+19sEfhSEMIAApA0hCn9j52cKR2oKbf4UhDSAAKQNAQtGFmu/6z5SH0QCFIQ5C8e30+KWn/aelfyEPQqvw0/Sv7ry3PCESQrvOqqbY0Ouzu38hEEKIkvOd/8z5hOoAIQVBACEDIAEpAzghBiABKQMYIRQgASkDMCEHIAEpAxAhFSABKQMoIQggASkDCCERIAEpAyAhCSABKQMAIQoDQCAJIAUgDiABQYABaiADQQZ0IgJBwAhqKAIAQQN0aikDACAJIAp8fCIKhUEgEAIiDnwiE4VBGBACIQUgBSATIA4gAUGAAWogAkHECGooAgBBA3RqKQMAIAUgCnx8IgqFQRAQAiIOfCIThUE/EAIhCSAIIBAgDSABQYABaiACQcgIaigCAEEDdGopAwAgCCARfHwiEYVBIBACIg18IhCFQRgQAiEFIAUgECANIAFBgAFqIAJBzAhqKAIAQQN0aikDACAFIBF8fCIRhUEQEAIiDXwiEIVBPxACIQUgEiAMIAFBgAFqIAJB0AhqKAIAQQN0aikDACAHIBV8fCIIhUEgEAIiDHwiEiAHhUEYEAIhByAHIBIgDCABQYABaiACQdQIaigCAEEDdGopAwAgByAIfHwiFYVBEBACIgx8IgiFQT8QAiEHIA8gCyABQYABaiACQdgIaigCAEEDdGopAwAgBiAUfHwiEoVBIBACIgt8Ig8gBoVBGBACIQYgBiALIAFBgAFqIAJB3AhqKAIAQQN0aikDACAGIBJ8fCIUhUEQEAIiCyAPfCIPhUE/EAIhBiAFIAggCyABQYABaiACQeAIaigCAEEDdGopAwAgBSAKfHwiCoVBIBACIgt8IgiFQRgQAiEFIAUgCCALIAFBgAFqIAJB5AhqKAIAQQN0aikDACAFIAp8fCIKhUEQEAIiC3wiEoVBPxACIQggByAPIA4gAUGAAWogAkHoCGooAgBBA3RqKQMAIAcgEXx8Ig+FQSAQAiIOfCIRhUEYEAIhBSAFIBEgDiABQYABaiACQewIaigCAEEDdGopAwAgBSAPfHwiEYVBEBACIg58Ig+FQT8QAiEHIAYgDSABQYABaiACQfAIaigCAEEDdGopAwAgBiAVfHwiBYVBIBACIg0gE3wiE4VBGBACIQYgBiATIA0gAUGAAWogAkH0CGooAgBBA3RqKQMAIAUgBnx8IhWFQRAQAiINfCIFhUE/EAIhBiAJIBAgDCABQYABaiACQfgIaigCAEEDdGopAwAgCSAUfHwiEIVBIBACIgx8IhOFQRgQAiEJIAkgEyAMIAFBgAFqIAJB/AhqKAIAQQN0aikDACAJIBB8fCIUhUEQEAIiDHwiEIVBPxACIQkgA0EBaiIDQQxHDQALIAEgDjcDYCABIAk3AyAgASANNwNoIAEgCDcDKCABIBE3AwggASAQNwNIIAEgDDcDcCABIAc3AzAgASAVNwMQIAEgEjcDUCABIAs3A3ggASAGNwM4IAEgFDcDGCABIA83A1ggASAFNwNAIAEgCjcDACAAIAogACkDAIUgBYU3AwBBASECA0AgACACQQN0IgNqIgQgASADaiIDKQMAIAQpAwCFIANBQGspAwCFNwMAIAJBAWoiAkEIRw0ACyABQYACaiQACyYBAX4gACABIAApA0AiAXwiAjcDQCAAIAApA0ggASACVq18NwNIC6AUAhB/An4jAEHQAGsiBiQAIAZByg42AkwgBkE3aiETIAZBOGohEANAAkAgDkEASA0AQf////8HIA5rIARIBEBB3B5BPTYCAEF/IQ4MAQsgBCAOaiEOCyAGKAJMIgchBAJAAkACQAJAAkACQAJAAkAgBgJ/AkAgBy0AACIFBEADQAJAAkAgBUH/AXEiBUUEQCAEIQUMAQsgBUElRw0BIAQhBQNAIAQtAAFBJUcNASAGIARBAmoiCDYCTCAFQQFqIQUgBC0AAiELIAghBCALQSVGDQALCyAFIAdrIQQgAARAIAAgByAEEA4LIAQNDSAGKAJMLAABEA8hBSAGKAJMIQQgBUUNAyAELQACQSRHDQMgBCwAAUEwayEPQQEhESAEQQNqDAQLIAYgBEEBaiIINgJMIAQtAAEhBSAIIQQMAAsACyAOIQwgAA0IIBFFDQJBASEEA0AgAyAEQQJ0aigCACIABEAgAiAEQQN0aiAAIAEQJEEBIQwgBEEBaiIEQQpHDQEMCgsLQQEhDCAEQQpPDQgDQCADIARBAnRqKAIADQggBEEBaiIEQQpHDQALDAgLQX8hDyAEQQFqCyIENgJMQQAhCAJAIAQsAAAiDUEgayIFQR9LDQBBASAFdCIFQYnRBHFFDQADQAJAIAYgBEEBaiIINgJMIAQsAAEiDUEgayIEQSBPDQBBASAEdCIEQYnRBHFFDQAgBCAFciEFIAghBAwBCwsgCCEEIAUhCAsCQCANQSpGBEAgBgJ/AkAgBCwAARAPRQ0AIAYoAkwiBC0AAkEkRw0AIAQsAAFBAnQgA2pBwAFrQQo2AgAgBCwAAUEDdCACakGAA2soAgAhCkEBIREgBEEDagwBCyARDQhBACERQQAhCiAABEAgASABKAIAIgRBBGo2AgAgBCgCACEKCyAGKAJMQQFqCyIENgJMIApBf0oNAUEAIAprIQogCEGAwAByIQgMAQsgBkHMAGoQIyIKQQBIDQYgBigCTCEEC0F/IQkCQCAELQAAQS5HDQAgBC0AAUEqRgRAAkAgBCwAAhAPRQ0AIAYoAkwiBC0AA0EkRw0AIAQsAAJBAnQgA2pBwAFrQQo2AgAgBCwAAkEDdCACakGAA2soAgAhCSAGIARBBGoiBDYCTAwCCyARDQcgAAR/IAEgASgCACIEQQRqNgIAIAQoAgAFQQALIQkgBiAGKAJMQQJqIgQ2AkwMAQsgBiAEQQFqNgJMIAZBzABqECMhCSAGKAJMIQQLQQAhBQNAIAUhEkF/IQwgBCwAAEHBAGtBOUsNByAGIARBAWoiDTYCTCAELAAAIQUgDSEEIAUgEkE6bGpBzxhqLQAAIgVBAWtBCEkNAAsgBUETRg0CIAVFDQYgD0EATgRAIAMgD0ECdGogBTYCACAGIAIgD0EDdGopAwA3A0AMBAsgAA0BC0EAIQwMBQsgBkFAayAFIAEQJCAGKAJMIQ0MAgsgD0F/Sg0DC0EAIQQgAEUNBAsgCEH//3txIgsgCCAIQYDAAHEbIQVBACEMQcAOIQ8gECEIAkACQAJAAn8CQAJAAkACQAJ/AkACQAJAAkACQAJAAkAgDUEBaywAACIEQV9xIAQgBEEPcUEDRhsgBCASGyIEQdgAaw4hBBISEhISEhISDhIPBg4ODhIGEhISEgIFAxISCRIBEhIEAAsCQCAEQcEAaw4HDhILEg4ODgALIARB0wBGDQkMEQsgBikDQCEUQcAODAULQQAhBAJAAkACQAJAAkACQAJAIBJB/wFxDggAAQIDBBcFBhcLIAYoAkAgDjYCAAwWCyAGKAJAIA42AgAMFQsgBigCQCAOrDcDAAwUCyAGKAJAIA47AQAMEwsgBigCQCAOOgAADBILIAYoAkAgDjYCAAwRCyAGKAJAIA6sNwMADBALIAlBCCAJQQhLGyEJIAVBCHIhBUH4ACEECyAQIQcgBEEgcSELIAYpA0AiFFBFBEADQCAHQQFrIgcgFKdBD3FB4BxqLQAAIAtyOgAAIBRCD1YhDSAUQgSIIRQgDQ0ACwsgBUEIcUUgBikDQFByDQMgBEEEdkHADmohD0ECIQwMAwsgECEEIAYpA0AiFFBFBEADQCAEQQFrIgQgFKdBB3FBMHI6AAAgFEIHViEHIBRCA4ghFCAHDQALCyAEIQcgBUEIcUUNAiAJIBAgB2siBEEBaiAEIAlIGyEJDAILIAYpA0AiFEJ/VwRAIAZCACAUfSIUNwNAQQEhDEHADgwBCyAFQYAQcQRAQQEhDEHBDgwBC0HCDkHADiAFQQFxIgwbCyEPIBAhBAJAIBRCgICAgBBUBEAgFCEVDAELA0AgBEEBayIEIBQgFEIKgCIVQgp+fadBMHI6AAAgFEL/////nwFWIQcgFSEUIAcNAAsLIBWnIgcEQANAIARBAWsiBCAHIAdBCm4iC0EKbGtBMHI6AAAgB0EJSyENIAshByANDQALCyAEIQcLIAVB//97cSAFIAlBf0obIQUgBikDQCIUQgBSIAlyRQRAQQAhCSAQIQcMCgsgCSAUUCAQIAdraiIEIAQgCUgbIQkMCQsCfyAJIgRBAEchCAJAAkACQCAGKAJAIgVB4xYgBRsiByIFQQNxRSAERXINAANAIAUtAABFDQIgBEEBayIEQQBHIQggBUEBaiIFQQNxRQ0BIAQNAAsLIAhFDQELAkAgBS0AAEUgBEEESXINAANAIAUoAgAiCEF/cyAIQYGChAhrcUGAgYKEeHENASAFQQRqIQUgBEEEayIEQQNLDQALCyAERQ0AA0AgBSAFLQAARQ0CGiAFQQFqIQUgBEEBayIEDQALC0EACyIEIAcgCWogBBshCCALIQUgBCAHayAJIAQbIQkMCAsgCQRAIAYoAkAMAgtBACEEIABBICAKQQAgBRANDAILIAZBADYCDCAGIAYpA0A+AgggBiAGQQhqNgJAQX8hCSAGQQhqCyEIQQAhBAJAA0AgCCgCACIHRQ0BIAZBBGogBxAiIgdBAEgiCyAHIAkgBGtLckUEQCAIQQRqIQggCSAEIAdqIgRLDQEMAgsLQX8hDCALDQULIABBICAKIAQgBRANIARFBEBBACEEDAELQQAhCCAGKAJAIQ0DQCANKAIAIgdFDQEgBkEEaiAHECIiByAIaiIIIARKDQEgACAGQQRqIAcQDiANQQRqIQ0gBCAISw0ACwsgAEEgIAogBCAFQYDAAHMQDSAKIAQgBCAKSBshBAwFCyAAIAYrA0AgCiAJIAUgBEEAEQwAIQQMBAsgBiAGKQNAPAA3QQEhCSATIQcgCyEFDAILQX8hDAsgBkHQAGokACAMDwsgAEEgIAwgCCAHayILIAkgCSALSBsiCWoiCCAKIAggCkobIgQgCCAFEA0gACAPIAwQDiAAQTAgBCAIIAVBgIAEcxANIABBMCAJIAtBABANIAAgByALEA4gAEEgIAQgCCAFQYDAAHMQDQwACwALkwIBAn8gAEUEQEFnDwsgACgCAEUEQEF/DwsCQAJ/QX4gACgCBEEESQ0AGiAAKAIIRQRAQW4gACgCDA0BGgsgACgCFCEBIAAoAhBFDQFBeiABQQhJDQAaIAAoAhhFBEBBbCAAKAIcDQEaCyAAKAIgRQRAQWsgACgCJA0BGgtBciAAKAIsIgFBCEkNABpBcSABQYCAgAFLDQAaQXIgASAAKAIwIgJBA3RJDQAaIAAoAihFBEBBdA8LIAJFBEBBcA8LQW8gAkH///8HSw0AGiAAKAI0IgFFBEBBZA8LQWMgAUH///8HSw0AGiAAKAJAIQECQCAAKAI8BEAgAQ0BQWkPC0FoIAENARoLQQALDwtBbUF6IAEbCzgBAX8jAEEQayICJAAgAiAANgIMIAIgATYCCCACKAIMQQAgAigCCEH8FygCABEAABogAkEQaiQAC4MSAhN/An4jAEEwayIJJAACQCAAEBwiBA0AQWYhBCABQQJLDQAgACgCLCEDIAAoAjAhBCAAKAI4IQIgCUEANgIAIAkgAjYCBCAAKAIoIQIgCSAENgIYIAkgAjYCCCAJIARBA3QiAiADIAIgA0sbIARBAnQiAm4iAzYCECAJIANBAnQ2AhQgCSACIANsNgIMIAAoAjQhAyAJIAE2AiAgCSADNgIcIAMgBEsEQCAJIAQ2AhwLIwBB0ABrIgskAEFnIQQCQCAJIgFFIAAiA0VyDQAgASADNgIoIAMhBSABKAIMIQZBaiECAkAgASIERQ0AIAatQgqGIhVCIIinDQAgFachAgJAIAUoAjwiBQRAIAQgAiAFEQMAGiAEKAIAIQIMAQsgBCACEAkiAjYCAAtBAEFqIAIbIQILIAIiBA0AIAEoAiAhBSMAQYACayICJAAgA0UgCyIERXJFBEAgAkEQakHAABAYGiACQQxqIAMoAjAQByACQRBqIAJBDGpBBBAGGiACQQxqIAMoAgQQByACQRBqIAJBDGpBBBAGGiACQQxqIAMoAiwQByACQRBqIAJBDGpBBBAGGiACQQxqIAMoAigQByACQRBqIAJBDGpBBBAGGiACQQxqIAMoAjgQByACQRBqIAJBDGpBBBAGGiACQQxqIAUQByACQRBqIAJBDGpBBBAGGiACQQxqIAMoAgwQByACQRBqIAJBDGpBBBAGGgJAIAMoAggiBUUNACACQRBqIAUgAygCDBAGGiADLQBEQQFxRQ0AIAMoAgggAygCDBAdIANBADYCDAsgAkEMaiADKAIUEAcgAkEQaiACQQxqQQQQBhogAygCECIFBEAgAkEQaiAFIAMoAhQQBhoLIAJBDGogAygCHBAHIAJBEGogAkEMakEEEAYaAkAgAygCGCIFRQ0AIAJBEGogBSADKAIcEAYaIAMtAERBAnFFDQAgAygCGCADKAIcEB0gA0EANgIcCyACQQxqIAMoAiQQByACQRBqIAJBDGpBBBAGGiADKAIgIgUEQCACQRBqIAUgAygCJBAGGgsgAkEQaiAEQcAAEBIaCyACQYACaiQAIAtBQGtBCBAEQQAhAiMAQYAIayIDJAAgASgCGARAIARBxABqIQYgBEFAayEFA0AgBUEAEAcgBiACEAcgA0GACCAEQcgAECAgASgCACABKAIUIAJsQQp0aiADEC4gBUEBEAcgA0GACCAEQcgAECAgASgCACABKAIUIAJsQQp0akGACGogAxAuIAJBAWoiAiABKAIYSQ0ACwsgA0GACBAEIANBgAhqJAAgC0HIABAEQQAhBAsgC0HQAGokACAEDQBBZyEEAkAgCUUNACABKAIYRQ0AIwBBIGsiBSQAIAEiCygCCARAIAsoAhghBANAIAQhA0EAIQ8DQEEAIRBBACECIAMEQANAIAUgDzoAGCAFQQA2AhwgBSAFKQMYNwMIIAUgEjYCECAFIBA2AhQgBSAFKQMQNwMAIAUhBEEAIREjAEGAGGsiByQAAkAgCyIDRQ0AAkACQAJAAn8CfwJAAkACQCADKAIgQQFrDgICAQALIAQoAgAhCEEADAMLIAQoAgANA0EAIAQtAAgiDEECSQ0BGiAELQAIIghFQQF0IQwMBQsgBC0ACCEMIAQoAgALIQggBxAvIAdBgAhqEC8gByAIrTcDgAggBDUCBCEVIAcgDK1C/wGDNwOQCCAHIBU3A4gIIAcgAzUCDDcDmAggByADNQIINwOgCCAHIAM1AiA3A6gIQQELIREgCEUNAQsgBC0ACCEIQQAhDAwBCyAELQAIIghFQQF0IQwgCCARRXINACAHQYAQaiAHQYAIaiAHECZBAiEMQQAhCAsgDCADKAIQIgZPDQBBfyADKAIUIgJBAWsgAiAEKAIEbCAMaiAGIAhB/wFxbGoiCCACcBsgCGohBgNAIAhBAWsgBiAIIAJwQQFGGyEOAn8gEQRAIAxB/wBxIgJFBEAgB0GAEGogB0GACGogBxAmCyAHQYAQaiACQQN0agwBCyADKAIAIA5BCnRqCyECIAMoAhghCiACKQMAIRUgBCAMNgIMIAMhBiAVpyEUIBVCIIinIApwrSIVIBUgBDUCBCIVIAQtAAgbIAQoAgAbIhYgFVEhCgJ+IAQiAigCAEUEQCACLQAIIg1FBEAgAigCDEEBayEKQgAMAgsgBigCECANbCENIAIoAgwhAiAKBEAgAiANakEBayEKQgAMAgsgDSACRWshCkIADAELIAYoAhAhDSAGKAIUIRMCfyAKBEAgAigCDCATIA1Bf3NqagwBCyATIA1rIAIoAgxFawshCkIAIAItAAgiAkEDRg0AGiANIAJBAWpsrQshFSAVIApBAWutfCAKrSAUrSIVIBV+QiCIfkIgiH0gBjUCFIKnIQYgAygCACICIAMoAhQgFqdsQQp0aiAGQQp0aiEGIAIgCEEKdGohCgJAIAMoAgRBEEYEQCACIA5BCnRqIAYgCkEAEBEMAQsgAiAOQQp0aiECIAQoAgBFBEAgAiAGIApBABARDAELIAIgBiAKQQEQEQsgDEEBaiIMIAMoAhBPDQEgCEEBaiEIIA5BAWohBiADKAIUIQIMAAsACyAHQYAYaiQAIAsoAhgiBCECIBBBAWoiECAESQ0ACwsgAiEDIA9BAWoiD0EERw0ACyASQQFqIhIgCygCCEkNAAsLIAVBIGokAEEAIQQLIAQNACMAQYAQayIDJAAgAEUgCUVyRQRAIANBgAhqIAEoAgAgASgCFEEKdGpBgAhrEBcgASgCGEECTwRAQQEhBANAIANBgAhqIAEoAgAgASgCFCICIAIgBGxqQQp0akGACGsQFiAEQQFqIgQgASgCGEkNAAsLIAMiAkGACGohC0EAIQQDQCACIARBA3QiBWogBSALaikDABAyIARBAWoiBEGAAUcNAAsgACgCACAAKAIEIANBgAgQICADQYAIakGACBAEIANBgAgQBCABKAIAIgQgASgCDEEKdCIBEAQCQCAAKAJAIgAEQCAEIAEgABECAAwBCyAEEAgLCyADQYAQaiQAQQAhBAsgCUEwaiQAIAQLJwEBfwJAAkACQAJAIAAOAwABAgMLQdATDwtBixEPC0GeEyEBCyABC48DAQF/IwBBgANrIgQkACAEQQA2AowBIARBjAFqIAEQBwJAIAFBwABNBEAgBEGQAWogARAYQQBIDQEgBEGQAWogBEGMAWpBBBAGQQBIDQEgBEGQAWogAiADEAZBAEgNASAEQZABaiAAIAEQEhoMAQsgBEGQAWpBwAAQGEEASA0AIARBkAFqIARBjAFqQQQQBkEASA0AIARBkAFqIAIgAxAGQQBIDQAgBEGQAWogBEFAa0HAABASQQBIDQAgACAEKQNANwAAIAAgBCkDSDcACCAAIAQpA1g3ABggACAEKQNQNwAQIABBIGohACABQSBrIgJBwQBPBEADQCAEIARBQGtBwAAQBSIBQUBrQcAAIAEQMUEASA0CIAAgASkDQDcAACAAIAEpA0g3AAggACAEKQNYNwAYIAAgBCkDUDcAECAAQSBqIQAgAkEgayICQcAASw0ACwsgBCAEQUBrQcAAEAUiAUFAayACIAEQMUEASA0AIAAgAUFAayACEAUaCyAEQZABakHwARAEIARBgANqJAALAwABC5kCACAARQRAQQAPCwJ/AkAgAAR/IAFB/wBNDQECQEGgHigCACgCAEUEQCABQYB/cUGAvwNGDQMMAQsgAUH/D00EQCAAIAFBP3FBgAFyOgABIAAgAUEGdkHAAXI6AABBAgwECyABQYCwA09BACABQYBAcUGAwANHG0UEQCAAIAFBP3FBgAFyOgACIAAgAUEMdkHgAXI6AAAgACABQQZ2QT9xQYABcjoAAUEDDAQLIAFBgIAEa0H//z9NBEAgACABQT9xQYABcjoAAyAAIAFBEnZB8AFyOgAAIAAgAUEGdkE/cUGAAXI6AAIgACABQQx2QT9xQYABcjoAAUEEDAQLC0HcHkEZNgIAQX8FQQELDAELIAAgAToAAEEBCwtQAQN/AkAgACgCACwAABAPRQRADAELA0AgACgCACICLAAAIQMgACACQQFqNgIAIAEgA2pBMGshASACLAABEA9FDQEgAUEKbCEBDAALAAsgAQu7AgACQCABQRRLDQACQAJAAkACQAJAAkACQAJAAkACQCABQQlrDgoAAQIDBAUGBwgJCgsgAiACKAIAIgFBBGo2AgAgACABKAIANgIADwsgAiACKAIAIgFBBGo2AgAgACABNAIANwMADwsgAiACKAIAIgFBBGo2AgAgACABNQIANwMADwsgAiACKAIAQQdqQXhxIgFBCGo2AgAgACABKQMANwMADwsgAiACKAIAIgFBBGo2AgAgACABMgEANwMADwsgAiACKAIAIgFBBGo2AgAgACABMwEANwMADwsgAiACKAIAIgFBBGo2AgAgACABMAAANwMADwsgAiACKAIAIgFBBGo2AgAgACABMQAANwMADwsgAiACKAIAQQdqQXhxIgFBCGo2AgAgACABKwMAOQMADwsgACACQQARAgALCxkAIAAtAOgBBEAgAEJ/NwNYCyAAQn83A1ALIwAgASABKQMwQgF8NwMwIAIgASAAQQAQESACIAAgAEEAEBELOQECfyAAQQNuIgJBAnQhAQJAAkACQCACQQNsQX9zIABqDgIBAAILIAFBAXIhAQsgAUECaiEBCyABC3oBAn8gAEHA/wBzQQFqQQh2QX9zQS9xIABBwf8Ac0EBakEIdkF/c0ErcSAAQeb/A2pBCHZB/wFxIgEgAEHBAGpxcnIgAEHM/wNqQQh2IgIgAEHHAGpxIAFB/wFzcXIgAEH8AWogAEHC/wNqQQh2cSACQX9zcUH/AXFyC9YBAQV/QX8hBCADQQNuIgZBAnQhBQJAAkACQCAGQQNsQX9zIANqDgIBAAILIAVBAXIhBQsgBUECaiEFCyABIAVLBH8CQCADRQ0AQQAhAUEIIQQDQCABIAItAAAiCHIhBwNAIAAiASAHIAQiBkEGayIEdkE/cRAoOgAAIAFBAWohACAEQQVLDQALIANBAWsiAwRAIAJBAWohAiAHQQh0IQEgBEEIaiEEDAELCyAERQ0AIAEgCEEMIAZrdEE/cRAoOgABIAFBAmohAAsgAEEAOgAAIAUFIAQLC8oEAQN/IwBB4ABrIgQkACADEB8hBSACEBwhAwJAAkAgBUUNACADDQEgAUECSQ0AIABBJDsAACABQQFrIgMgBRAKIgFNDQAgAEEBaiAFIAFBAWoQBSEAIAMgAWsiA0EESQ0AIAAgAWoiAUGk7PUBNgAAIAQgAigCODYCMCAEQUBrIARBMGoQEyADQQNrIgMgBEFAaxAKIgBNDQAgAUEDaiAEQUBrIABBAWoQBSEBIAMgAGsiA0EESQ0AIAAgAWoiAUGk2vUBNgAAIAQgAigCLDYCICAEQUBrIARBIGoQEyADQQNrIgMgBEFAaxAKIgBNDQAgAUEDaiAEQUBrIABBAWoQBSEBIAMgAGsiA0EESQ0AIAAgAWoiAUGs6PUBNgAAIAQgAigCKDYCECAEQUBrIARBEGoQEyADQQNrIgMgBEFAaxAKIgBNDQAgAUEDaiAEQUBrIABBAWoQBSEBIAMgAGsiA0EESQ0AIAAgAWoiAUGs4PUBNgAAIAQgAigCMDYCACAEQUBrIAQQEyADQQNrIgMgBEFAaxAKIgBNDQAgAUEDaiAEQUBrIABBAWoQBSEBIAMgAGsiA0ECSQ0AIAAgAWoiAEEkOwAAIABBAWoiACADQQFrIgYgAigCECACKAIUECkiAUF/RiIFDQBBYSEDIAZBACABIAUbayIGQQJJDQEgACAAIAFqIAUbIgBBJDsAACAAQQFqIAZBAWsgAigCACACKAIEECkhACAEQeAAaiQAQWFBACAAQX9GGw8LQWEhAwsgBEHgAGokACADC7gBAQF/QQAgAEEEaiAAQdD/A2pBCHZBf3NxQTkgAGtBCHZBf3NxQf8BcSAAQcEAayIBIAFBCHZBf3NxQdoAIABrQQh2QX9zcUH/AXEgAEG5AWogAEGf/wNqQQh2QX9zcUH6ACAAa0EIdkF/c3FB/wFxIABB0P8Ac0EBakEIdkF/c0E/cSAAQdT/AHNBAWpBCHZBf3NBPnFycnJyIgFrQQh2QX9zIABBvv8Dc0EBakEIdnFB/wFxIAFyC64BAQR/An8CfyACLAAAECsiBkH/AUYEQEF/DAELA0AgBCAGaiEEAkAgA0EGaiIGQQhJBEAgBiEDDAELIAEoAgAgBU0EQEEADwsgACAEIANBAmsiA3Y6AAAgAEEBaiEAIAVBAWohBQsgAkEBaiICLAAAECsiBkH/AUcEQCAEQQZ0IQQMAQsLQQAgA0EESw0BGkF/IAN0CyEDQQAgBCADQX9zcQ0AGiABIAU2AgAgAgsLrAMBBX8jAEEQayIDJAAgACgCBCEGIAAoAhQhBwJAIAIQHyIERQRAQWYhAgwBC0FgIQIgAS0AACIFQSRHDQAgAUEBaiABIAVBJEYbIgEgBCAEEAoiBBAQIgUNACAAQRA2AjggASABIARqIgEgBRsiBEHfFEEDEBBFBEAgBEEDaiADQQxqEBUiAUUNASAAIAMoAgw2AjgLIAFB6xRBAxAQDQAgAUEDaiADQQxqEBUiAUUNACAAIAMoAgw2AiwgAUHjFEEDEBANACABQQNqIANBDGoQFSIBRQ0AIAAgAygCDDYCKCABQecUQQMQEA0AIAFBA2ogA0EMahAVIgFFDQAgACADKAIMIgQ2AjAgACAENgI0IAEtAABBJEcNACADIAc2AgwgACgCECADQQxqIAFBAWoQLCIBRQ0AIAAgAygCDDYCFCABLQAAQSRHDQAgAyAGNgIMIAAoAgAgA0EMaiABQQFqECwiAUUNACAAIAMoAgw2AgQgAEEANgJEIABCADcCPCAAQgA3AhggAEIANwIgIAAQHCICDQBBYEEAIAEtAAAbIQILIANBEGokACACCykBAn8DQCAAIAJBA3QiA2ogASADaikAADcDACACQQFqIgJBgAFHDQALCwwAIABBAEGACBALGgtlAQJ/IAAgAhAeIgIEfyACBUFdQQACfyAAKAIAIQRBACECIAAoAgQiAAR/A0AgAyACIARqLQAAIAEgAmotAABzciEDIAJBAWoiAiAARw0ACyADQQFrQQh2QQFxQQFrBUEACwsbCwtdAQJ/IwBB8AFrIgMkAEF/IQQCQCACRSAARSABRXJyIAFBwABLcg0AIAMgARAYQQBIDQAgAyACQcAAEAZBAEgNACADIAAgARASIQQLIANB8AEQBCADQfABaiQAIAQLCQAgACABNwAACxAAIwAgAGtBcHEiACQAIAALMwEBfyAAKAIUIgMgASACIAAoAhAgA2siASABIAJLGyIBEAUaIAAgACgCFCABajYCFCACC9oBAQR/IwBB0ABrIggkAAJAIABFBEBBYCEADAELIAggABAKIgk2AgwgCCAJNgIcIAggCRAJIgo2AhggCCAJEAkiCzYCCEEAIQkCQAJAIApFIAtFcg0AIAggAjYCFCAIIAE2AhAgCEEIaiAAIAcQLSIADQEgCCgCCCEJIAggCCgCDBAJIgA2AgggAEUNACAIIAY2AiwgCCAFNgIoIAggBDYCJCAIIAM2AiAgCEEIaiAJIAcQMCEADAELQWohAAsgCCgCGBAIIAgoAggQCCAJEAgLIAhB0ABqJAAgAAuQAgEDfyMAQdAAayIRJABBfiETAkAgCEEESQ0AIAgQCSISRQRAQWohEwwBCyARQQA2AkwgEUIANwJEIBEgAjYCPCARIAI2AjggESABNgI0IBEgADYCMCARIA82AiwgESAONgIoIBEgDTYCJCARIAw2AiAgESAGNgIcIBEgBTYCGCARIAQ2AhQgESADNgIQIBEgCDYCDCARIBI2AgggESAQNgJAAkAgEUEIaiALEB4iEwRAIBIgCBAEDAELIAcEQCAHIBIgCBAFGgsCQCAJRSAKRXINACAJIAogEUEIaiALECpFDQAgEiAIEAQgCSAKEARBYSETDAELIBIgCBAEQQAhEwsgEhAICyARQdAAaiQAIBMLDQAgAEHwARAEIAAQJQspACAFEB8QCiAAEBRqIAEQFGogAhAUaiADECdqIAQQJ2pBExAUakEQagsfACAAQSNqIgBBI00EQCAAQQJ0QewWaigCAA8LQYsTC74BAQR/IwBB0ABrIgQkAAJAIABFBEBBYCEADAELIAQgABAKIgU2AgwgBCAFNgIcIAQgBRAJIgY2AhggBCAFEAkiBzYCCEEAIQUCQAJAIAZFIAdFcg0AIAQgAjYCFCAEIAE2AhAgBEEIaiAAIAMQLSIADQEgBCgCCCEFIAQgBCgCDBAJIgA2AgggAEUNACAEQQhqIAUgAxAwIQAMAQtBaiEACyAEKAIYEAggBCgCCBAIIAUQCAsgBEHQAGokACAAC4ICAQN/IwBB0ABrIg0kAEF+IQ8CQCAIQQRJDQAgCBAJIg5FBEBBaiEPDAELIA1CADcDKCANQgA3AyAgDSAGNgIcIA0gBTYCGCANIAQ2AhQgDSADNgIQIA0gCDYCDCANIA42AgggDUEANgJMIA1CADcCRCANIAI2AjwgDSACNgI4IA0gATYCNCANIAA2AjAgDSAMNgJAAkAgDUEIaiALEB4iDwRAIA4gCBAEDAELIAcEQCAHIA4gCBAFGgsCQCAJRSAKRXINACAJIAogDUEIaiALECpFDQAgDiAIEAQgCSAKEARBYSEPDAELIA4gCBAEQQAhDwsgDhAICyANQdAAaiQAIA8LYgEDfyABRSAARXIEf0F/BSAAQUBrQQBBsAEQCxogAEGACEHAABAFGgNAIAAgAkEDdCIDaiIEIAEgA2opAAAgBCkDAIU3AwAgAkEBaiICQQhHDQALIAAgAS0AADYC5AFBAAsLC/ISFABBgAgLuQUIybzzZ+YJajunyoSFrme7K/iU/nLzbjzxNh1fOvVPpdGC5q1/Ug5RH2w+K4xoBZtrvUH7q9mDH3khfhMZzeBbAAAAAAEAAAACAAAAAwAAAAQAAAAFAAAABgAAAAcAAAAIAAAACQAAAAoAAAALAAAADAAAAA0AAAAOAAAADwAAAA4AAAAKAAAABAAAAAgAAAAJAAAADwAAAA0AAAAGAAAAAQAAAAwAAAAAAAAAAgAAAAsAAAAHAAAABQAAAAMAAAALAAAACAAAAAwAAAAAAAAABQAAAAIAAAAPAAAADQAAAAoAAAAOAAAAAwAAAAYAAAAHAAAAAQAAAAkAAAAEAAAABwAAAAkAAAADAAAAAQAAAA0AAAAMAAAACwAAAA4AAAACAAAABgAAAAUAAAAKAAAABAAAAAAAAAAPAAAACAAAAAkAAAAAAAAABQAAAAcAAAACAAAABAAAAAoAAAAPAAAADgAAAAEAAAALAAAADAAAAAYAAAAIAAAAAwAAAA0AAAACAAAADAAAAAYAAAAKAAAAAAAAAAsAAAAIAAAAAwAAAAQAAAANAAAABwAAAAUAAAAPAAAADgAAAAEAAAAJAAAADAAAAAUAAAABAAAADwAAAA4AAAANAAAABAAAAAoAAAAAAAAABwAAAAYAAAADAAAACQAAAAIAAAAIAAAACwAAAA0AAAALAAAABwAAAA4AAAAMAAAAAQAAAAMAAAAJAAAABQAAAAAAAAAPAAAABAAAAAgAAAAGAAAAAgAAAAoAAAAGAAAADwAAAA4AAAAJAAAACwAAAAMAAAAAAAAACAAAAAwAAAACAAAADQAAAAcAAAABAAAABAAAAAoAAAAFAAAACgAAAAIAAAAIAAAABAAAAAcAAAAGAAAAAQAAAAUAAAAPAAAACwAAAAkAAAAOAAAAAwAAAAwAAAANAEHEDQu5CgEAAAACAAAAAwAAAAQAAAAFAAAABgAAAAcAAAAIAAAACQAAAAoAAAALAAAADAAAAA0AAAAOAAAADwAAAA4AAAAKAAAABAAAAAgAAAAJAAAADwAAAA0AAAAGAAAAAQAAAAwAAAAAAAAAAgAAAAsAAAAHAAAABQAAAAMAAAAtKyAgIDBYMHgAJWx1AE91dHB1dCBpcyB0b28gc2hvcnQAU2FsdCBpcyB0b28gc2hvcnQAU2VjcmV0IGlzIHRvbyBzaG9ydABQYXNzd29yZCBpcyB0b28gc2hvcnQAQXNzb2NpYXRlZCBkYXRhIGlzIHRvbyBzaG9ydABTb21lIG9mIGVuY29kZWQgcGFyYW1ldGVycyBhcmUgdG9vIGxvbmcgb3IgdG9vIHNob3J0AE1pc3NpbmcgYXJndW1lbnRzAFRvbyBtYW55IGxhbmVzAFRvbyBmZXcgbGFuZXMAVG9vIG1hbnkgdGhyZWFkcwBOb3QgZW5vdWdoIHRocmVhZHMATWVtb3J5IGFsbG9jYXRpb24gZXJyb3IATWVtb3J5IGNvc3QgaXMgdG9vIHNtYWxsAFRpbWUgY29zdCBpcyB0b28gc21hbGwAYXJnb24yaQBBcmdvbjJpAFRoZSBwYXNzd29yZCBkb2VzIG5vdCBtYXRjaCB0aGUgc3VwcGxpZWQgaGFzaABPdXRwdXQgcG9pbnRlciBtaXNtYXRjaABPdXRwdXQgaXMgdG9vIGxvbmcAU2FsdCBpcyB0b28gbG9uZwBTZWNyZXQgaXMgdG9vIGxvbmcAUGFzc3dvcmQgaXMgdG9vIGxvbmcAQXNzb2NpYXRlZCBkYXRhIGlzIHRvbyBsb25nAFRocmVhZGluZyBmYWlsdXJlAE1lbW9yeSBjb3N0IGlzIHRvbyBsYXJnZQBUaW1lIGNvc3QgaXMgdG9vIGxhcmdlAFVua25vd24gZXJyb3IgY29kZQBhcmdvbjJpZABBcmdvbjJpZABFbmNvZGluZyBmYWlsZWQARGVjb2RpbmcgZmFpbGVkAGFyZ29uMmQAQXJnb24yZABBcmdvbjJfQ29udGV4dCBjb250ZXh0IGlzIE5VTEwAT3V0cHV0IHBvaW50ZXIgaXMgTlVMTABUaGUgYWxsb2NhdGUgbWVtb3J5IGNhbGxiYWNrIGlzIE5VTEwAVGhlIGZyZWUgbWVtb3J5IGNhbGxiYWNrIGlzIE5VTEwAT0sAJHY9ACx0PQAscD0AJG09AFRoZXJlIGlzIG5vIHN1Y2ggdmVyc2lvbiBvZiBBcmdvbjIAU2FsdCBwb2ludGVyIGlzIE5VTEwsIGJ1dCBzYWx0IGxlbmd0aCBpcyBub3QgMABTZWNyZXQgcG9pbnRlciBpcyBOVUxMLCBidXQgc2VjcmV0IGxlbmd0aCBpcyBub3QgMABQYXNzd29yZCBwb2ludGVyIGlzIE5VTEwsIGJ1dCBwYXNzd29yZCBsZW5ndGggaXMgbm90IDAAQXNzb2NpYXRlZCBkYXRhIHBvaW50ZXIgaXMgTlVMTCwgYnV0IGFkIGxlbmd0aCBpcyBub3QgMAAobnVsbCkAAACbCAAAuwcAAEkJAADACQAAsAkAAPAHAAAfCAAAMAgAAMkIAABvCgAA4AkAABYKAAA7CgAAQwgAACsLAADBCgAAkgoAAPQKAAACCAAAEQgAAFsJAABbCAAAdAkAAHQIAAAFCQAAdAcAAC0JAACeBwAA9AgAAGIHAAAYCQAAiAcAAOEIAABOBwAA/wkAAFwKAAABAEGkGAsBAgBByxgLBf//////AEGQGQtBEQAKABEREQAAAAAFAAAAAAAACQAAAAALAAAAAAAAAAARAA8KERERAwoHAAEACQsLAAAJBgsAAAsABhEAAAAREREAQeEZCyELAAAAAAAAAAARAAoKERERAAoAAAIACQsAAAAJAAsAAAsAQZsaCwEMAEGnGgsVDAAAAAAMAAAAAAkMAAAAAAAMAAAMAEHVGgsBDgBB4RoLFQ0AAAAEDQAAAAAJDgAAAAAADgAADgBBjxsLARAAQZsbCx4PAAAAAA8AAAAACRAAAAAAABAAABAAABIAAAASEhIAQdIbCw4SAAAAEhISAAAAAAAACQBBgxwLAQsAQY8cCxUKAAAAAAoAAAAACQsAAAAAAAsAAAsAQb0cCwEMAEHJHAsnDAAAAAAMAAAAAAkMAAAAAAAMAAAMAAAwMTIzNDU2Nzg5QUJDREVGAEHwHAsBAQBBoB4LAogPAEHYHgsDkBFQ"},145:()=>{},967:()=>{}},B={};function Q(A){var I=B[A];if(void 0!==I)return I.exports;var C=B[A]={exports:{}};return g[A].call(C.exports,C,C.exports,Q),C.exports}return I=Object.getPrototypeOf?A=>Object.getPrototypeOf(A):A=>A.__proto__,Q.t=function(g,B){if(1&B&&(g=this(g)),8&B)return g;if("object"==typeof g&&g){if(4&B&&g.__esModule)return g;if(16&B&&"function"==typeof g.then)return g}var C=Object.create(null);Q.r(C);var E={};A=A||[null,I({}),I([]),I(I)];for(var i=2&B&&g;"object"==typeof i&&!~A.indexOf(i);i=I(i))Object.getOwnPropertyNames(i).forEach((A=>E[A]=()=>g[A]));return E.default=()=>g,Q.d(C,E),C},Q.d=(A,I)=>{for(var g in I)Q.o(I,g)&&!Q.o(A,g)&&Object.defineProperty(A,g,{enumerable:!0,get:I[g]})},Q.o=(A,I)=>Object.prototype.hasOwnProperty.call(A,I),Q.r=A=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(A,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(A,"__esModule",{value:!0})},Q(631)})()})); \ No newline at end of file diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000000..b0535a50bb --- /dev/null +++ b/ui/index.html @@ -0,0 +1,1573 @@ + + + + + + + + + +Praxis — Prediction Markets + + + + + + +
⚠   Node offline — check Settings
+
+ + +
+ + +
+ +
+
+ +
+
+ + +
+
+ +
+
+
+ + +
+ + + + + +
+
+
Live on Canopy
+
Prediction Markets
+
Browse open markets and place your predictions using $PRX
+
+ +
+
Markets
+
Block
+
Volume
+ + +
+ +
+
All
+
🪙 Crypto
+
⚽ Sports
+
🗳 Politics
+
📈 Finance
+
◈ Other
+
+ +
+ + + +
+ +
▪ ▪ ▪  loading markets
+
+ + +
+
+
Market Detail
+
+
+
+ + +
+
+ +
+
+
+
YES
+
+
+
+
+
NO
+
+
+
+
+ + + + + +
+
+ + +
+
+ +
+
+ +
+ +
+
+
+
+ + +
+
+ + + + +
+
+
+ + +
+
+
Market Info
+
Market ID
+
Creator
+
Total Pool
+
Expiry
+ +
Resolver
+
+
+
+
+
+ + +
+
Resolver Action
Forfeit Position
Exit your position before proposing an outcome — required for COI-1 compliance
+
⚠ Forfeiting permanently exits your shares in this market. This cannot be undone.
+
+
// forfeit_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
Place Bet
Submit Prediction
Buy YES or NO shares — cost determined by LMSR pricing
+
+
// prediction_parameters
+
+
YES
NO
+
+
Min 1 PRX
+
1%10%
+
+
+
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
Collect Payout
Claim Winnings
Proportional payout from losing pool after finalization
+
+
// claim_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
Recover Funds
Reclaim Stake
Recover funds from expired markets with no resolver
+
Only available if no resolver proposed an outcome within the resolution window.
+
+
// reclaim_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
Network
Browse Resolvers
Active resolvers staking $PRX to guarantee market outcomes
+
▪ ▪ ▪  loading resolvers
+
+ + +
+
Creator
Claim Creator Fee
Collect accumulated fees from markets you created
+
+
// creator_fee_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
Admin
Cancel Market
Cancel an open market before expiry — creator bond returned
+
⚠ Cancellation is irreversible. All bettors can reclaim their stakes.
+
+
// cancel_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
Account
Wallet
Check balances, send $PRX, and view your predictions
+
+
// metamask_identity
+
+
Connect MetaMask to verify your identity. Your BLS keystore handles transaction signing.
+
+
+ +
+
+
// balance_lookup
+
+
+ +
+
+
// send_prx
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+
// my_predictions
+
Load wallet to see predictions
+
+
+ + +
+
BLS12-381
Signer
Encrypted keystore — private key held in RAM only, never stored or sent
+ +
+
// session_status
+
○ No key loaded — transactions cannot be signed
+ + +
+ + + +
+
// import_keystore
+
+ + +
Upload your encrypted Praxis keystore JSON
+
+
+ + +
+
+ + +
+
+ +
+
// create_keystore
+
Generates a new BLS12-381 keypair and downloads an encrypted keystore file.
+
+ + +
+
+ + +
+
+ +
+
+ +
+
// signing_spec
+
+ 1. sign_bytes = proto.Marshal(Transaction{…, signature:nil})
+ 2. time = BigInt(Date.now()) × 1000n — microseconds
+ 3. BLS12-381 G2 signature (96 bytes) — @noble/curves
+ 4. address = SHA256(pubKey).slice(0,20)
+ 5. Keystore = AES-GCM + PBKDF2 (200k iterations) +
+
+
+ + +
+
Resolver
Register as Resolver
Stake $PRX to earn resolution fees — minimum 500,000 PRX
+
+
// register_parameters
+
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Resolver
Propose Outcome
Submit your resolution after market expiry
+
+
// propose_parameters
+
+
YES
NO
+ +
Enter Market ID to compute min bond
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Resolver
File Dispute
Challenge a proposed outcome during the dispute window
+
+
// dispute_parameters
+
+ +
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Resolver
Commit Vote
Submit a blinded commitment during the voting phase
+
+
// commit_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Resolver
Reveal Vote
Reveal your committed vote during the reveal phase
+
+
// reveal_parameters
+
+
YES
NO
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Resolver
Tally Votes
Trigger vote tallying after the reveal phase ends
+
+
// tally_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Resolver
Claim Slash
Claim slashed stake from a penalized resolver
+
+
// slash_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Resolver
Unstake Resolver
Begin 120,960-block unbonding period — partial or full exit
+
+
// unstake_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Resolver
Claim Unbonded Stake
Release tokens after the 120,960-block unbonding period
+
+
// claim_unbonded_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
Admin
Create Market
Deploy a new prediction market on Praxis
+
+
// market_parameters
+
+
+
🪙 Crypto
+
⚽ Sports
+
🗳 Politics
+
📈 Finance
+
◈ Other
+
+
+
+
+
+
+ + +
Select a date to compute block height
+ +
+
+
+
+
+ + + +
Image will be stored on-chain via IPFS or direct URL. Recommended: 16:9, min 800x450px.
+
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Admin
Finalize Market
Collect finalization bounty after the dispute window closes
+
+
// finalize_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + + +
+
Rewards
Resolver Rewards
Epoch-weighted rewards based on your RRS tier and resolution activity
+
+
Claimable Now
PRX
+
Total Earned
PRX all time
+
Current Epoch Pool
PRX this epoch
+
+
+
// resolver_status
+
Bronze — RRS 10
+
+
Resolutions 0
+
RRS Score 10
+
Weight 1x
+
+
+ Share formula: your_weight / total_weight x epoch_pool
+ Weight = 1x Bronze (RRS 1-49) / 3x Silver (RRS 50-199) / 7x Gold (RRS 200+) +
+
+
+
// epoch_history
+
+ + + +
EpochPool (PRX)Your ShareStatus
Connect signer to load history
+
+
+
+
// claim_resolver_reward
+
+
+
+
+
+
+
... broadcasting...
+
// unsigned_payload
+
+
+ + +
+
Rewards
Builder Rewards
Protocol rewards for market creators — 120,960 block vesting window
+ +
+
+
Claimable Now
PRX
+
Total Earned
PRX all time
+
Current Pool
PRX this epoch
+
+
Vesting window: 120,960 blocks (~14 days). Rewards vest linearly.
+
+
// epoch_history
+
+ + + +
EpochPool (PRX)Your ShareStatus
Connect signer to load history
+
+
+
+
// claim_builder_reward
+
+
+
+
... broadcasting...
+
// unsigned_payload
+
+
+
+ + +
+
Rewards
Community Rewards
Participation rewards for active predictors
+ +
+
Claimable Now
PRX
+
Total Earned
PRX all time
+
Current Pool
PRX this epoch
+
+
+
// claim_community_reward
+
+
+
+
... broadcasting...
+
// unsigned_payload
+
+
+
+ + +
+
Rewards
Investor Rewards
Liquidity provision rewards — 241,920 block vesting window
+ +
+
Claimable Now
PRX
+
Total Earned
PRX all time
+
Current Pool
PRX this epoch
+
+
Vesting window: 241,920 blocks (~28 days). Rewards vest linearly.
+
+
// claim_investor_reward
+
+
+
+
... broadcasting...
+
// unsigned_payload
+
+
+
+ + +
+
Rewards
Protocol Rewards
Governance and staking rewards from protocol treasury
+ +
+
+
Claimable Now
PRX
+
Total Earned
PRX all time
+
Current Pool
PRX this epoch
+
+
+
// claim_protocol_reward
+
+
+
+
... broadcasting...
+
// unsigned_payload
+
+
+
+ +
+
Admin
Node Settings
Configure RPC endpoint and network connection
+
+
// rpc_configuration
+
+
+ +
+
+
// admin_unlock
+
Enter your admin address to unlock Creator and Resolver sections.
+
+
+
+
+ + + + +
+
+
Confirm Transaction
Review before signing
+
+
+ + +
+
+
+ +
+ + + + diff --git a/ui/index.html.bak b/ui/index.html.bak new file mode 100644 index 0000000000..4294ada746 --- /dev/null +++ b/ui/index.html.bak @@ -0,0 +1,1606 @@ + + + + + + +PRAXIS — Prediction Markets + + + + + +
⚠   Node offline — check Settings
+
+ + + +
+ +
+
+ +
+
+
+
+
+ +
+ +
+
Live on Canopy
Prediction Markets
Browse open markets and place your predictions using $PRX
+
Markets
Block
+
▪ ▪ ▪  loading markets
+
+ +
+
Place Bet
Submit Prediction
Buy YES or NO shares — cost determined by LMSR pricing
+
+
// prediction_parameters
+
+
YES
NO
+
1 share = 1,000,000
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Collect Payout
Claim Winnings
Proportional payout from losing pool after finalization
+
+
// claim_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Account
Wallet
Check balances, send $PRX, and view your predictions
+
+
// balance_lookup
+
+
+ +
+
+
// send_prx
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+
// my_predictions
+
Load wallet to see predictions
+
+
+ +
+
BLS12-381
Signer
Load your private key — held in RAM only, never stored or sent
+
+
// key_management
+
○ No key loaded — transactions cannot be signed
+
⚠ Only enter on trusted instances. Clears on page reload.
+
+ + +
+
+
// signing_spec
+
+ 1. sign_bytes = proto.Marshal(Transaction{…, signature:nil})
+ 2. time = BigInt(Date.now()) × 1000n — microseconds
+ 3. BLS12-381 G2 signature (96 bytes) — @noble/curves
+ 4. address = SHA256(pubKey).slice(0,20)
+ 5. Plugin format: msgTypeUrl + msgBytes (hex) +
+
+
+ + +
+
Resolver Role
Register as Resolver
Stake $PRX to gain eligibility to propose market outcomes
+
Min stake 100,000,000 uPRX. Dishonest proposals result in bond slashing.
+
+
// resolver_registration
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Resolver Action
Propose Outcome
Propose the winning outcome after market expiry
+
Only callable after ExpiryTime. Bond locked until finalization.
+
+
// proposal_parameters
+
+
YES
NO
+
min = ComputeMinBond(market)
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Dispute System
File Dispute
Challenge a proposed outcome within the dispute window
+
⚠ Dishonest disputes result in bond slashing to the protocol treasury.
+
+
// dispute_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Panel Voting
Commit Vote
Submit blinded vote hash during the commit phase
+
commit_hash = SHA256(vote_byte ‖ nonce ‖ voter_addr) — 0x01=YES, 0x00=NO
+
+
// commit_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Panel Voting
Reveal Vote
Reveal your committed vote during the reveal phase
+
+
// reveal_parameters
+
+
YES
NO
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Panel Voting
Tally Votes
Permissionless — trigger vote tally after reveal phase ends
+
+
// tally_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Enforcement
Claim Slash
Claim reward from a dishonest resolver's forfeited bond
+
+
// slash_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ + +
+
Admin · Market Creation
Create Market
Deploy a new YES/NO prediction market with LMSR liquidity
+
B0 minimum 60,000,000 uPRX (50M bounty reserve + 10M LMSR seed).
+
+
// market_parameters
+
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Admin · Finalization
Finalize Market
Permissionless — seal market after dispute window. Caller receives 50 PRX bounty.
+
+
// finalize_parameters
+
+
+
+
▪▪▪ broadcasting…
+
// unsigned_payload
+
+
+ +
+
Admin · Configuration
Settings
Node connection and chain diagnostics
+
+
// rpc_connection
+
Public RPC → :50002  ·  Admin → :50003 (local only)
+
+
+
+
// chain_status
+
+
Height
+
Status
+
RPC Endpoint
+
+
+
+
// failed_tx_checker
+
+
+ +
+
+ +
+
+ + +
+
+ Markets
+
+ Predict
+
+ Wallet
+
+ Signer
+
+ Resolve
+
+ +
+
+
+
Confirm Transaction
review before signing
+
+
+
+
+ + + + diff --git a/ui/style.css b/ui/style.css new file mode 100644 index 0000000000..11a23f96a9 --- /dev/null +++ b/ui/style.css @@ -0,0 +1,401 @@ +/* TOKENS */ +:root{ + --bg:#07090d;--bg2:#0b0e16;--surface:#0f1320;--surf2:#141929; + --border:#1c2236;--border2:#263048; + --text:#c4d4ec;--text2:#6a7d9c;--text3:#2e3d58; + --green:#00e87a;--gdim:rgba(0,232,122,.07);--gglow:rgba(0,232,122,.22); + --red:#ff3d5a;--rdim:rgba(255,61,90,.07); + --amber:#f59e0b;--adim:rgba(245,158,11,.08); + --cyan:#22d3ee;--cdim:rgba(34,211,238,.07); + --purple:#a78bfa;--pdim:rgba(167,139,250,.08); + --shadow:rgba(0,0,0,.55); + --font-mono:'JetBrains Mono',monospace; + --font-ui:'Space Grotesk',sans-serif; + --font-d:'Unbounded',sans-serif; +} +[data-theme="light"]{ + --bg:#eef1f8;--bg2:#e4e8f2;--surface:#fff;--surf2:#f5f7fc; + --border:#dae0ee;--border2:#c4cedf; + --text:#18253a;--text2:#526070;--text3:#a0b0c8; + --green:#00a352;--gdim:rgba(0,163,82,.07);--gglow:rgba(0,163,82,.18); + --red:#e02040;--rdim:rgba(224,32,64,.07); + --amber:#b45309;--adim:rgba(180,83,9,.07); + --cyan:#0891b2;--cdim:rgba(8,145,178,.07); + --purple:#7c3aed;--pdim:rgba(124,58,237,.07); + --shadow:rgba(0,0,0,.1); +} +*{box-sizing:border-box;margin:0;padding:0} +html{scroll-behavior:smooth} +body{background:var(--bg);color:var(--text);font-family:var(--font-ui);font-size:14px;line-height:1.6;min-height:100vh;overflow-x:hidden;transition:background .25s,color .25s} +[data-theme="dark"] body::after{content:'';position:fixed;inset:0;background-image:linear-gradient(rgba(0,232,122,.01) 1px,transparent 1px),linear-gradient(90deg,rgba(0,232,122,.01) 1px,transparent 1px);background-size:44px 44px;pointer-events:none;z-index:0} +.shell{display:flex;min-height:100vh;position:relative;z-index:1} + +/* SIDEBAR */ + +.logo{padding:18px 16px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:11px} +.logo-hex{width:36px;height:36px;flex-shrink:0;display:flex;align-items:center;justify-content:center} +.logo-hex span{font-family:var(--font-d);font-size:10px;font-weight:900;color:#000;letter-spacing:-1px} +.logo-text .wm{font-family:var(--font-d);font-size:13px;font-weight:900;letter-spacing:2px;color:var(--green);text-shadow:0 0 20px var(--gglow)} +.logo-text .tg{font-family:var(--font-mono);font-size:8px;color:var(--text3);letter-spacing:2px;text-transform:uppercase;margin-top:1px} +.nav{padding:4px 0;flex:1;overflow-y:auto} +.nav::-webkit-scrollbar{width:3px} +.nav::-webkit-scrollbar-thumb{background:var(--border2)} +.ns{font-family:var(--font-mono);font-size:8px;letter-spacing:3px;text-transform:uppercase;color:var(--text3);padding:13px 16px 4px;display:flex;align-items:center;gap:8px} +.ns::after{content:'';flex:1;height:1px;background:var(--border)} +.ni{display:flex;align-items:center;gap:8px;padding:7px 16px;cursor:pointer;color:var(--text2);font-size:12px;font-weight:500;transition:all .12s;border-left:2px solid transparent;user-select:none} +.ni:hover{color:var(--text);background:var(--gdim)} +.ni.active{color:var(--green);border-left-color:var(--green);background:var(--gdim)} +.ni-ic{width:14px;text-align:center;font-size:13px;flex-shrink:0;opacity:.7} +.ni.active .ni-ic{opacity:1} +.rb{font-family:var(--font-mono);font-size:7px;letter-spacing:1px;text-transform:uppercase;padding:1px 5px;margin-left:auto;flex-shrink:0} +.rb-r{background:var(--adim);color:var(--amber);border:1px solid rgba(245,158,11,.2)} +.rb-a{background:var(--pdim);color:var(--purple);border:1px solid rgba(167,139,250,.2)} +.sbfoot{padding:11px 14px;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:7px} +.conn-row{display:flex;align-items:center;gap:6px;font-family:var(--font-mono);font-size:10px;color:var(--text2)} +.dot{width:6px;height:6px;border-radius:50%;background:var(--text3);flex-shrink:0;transition:all .4s} +.dot.live{background:var(--green);box-shadow:0 0 8px var(--green);animation:pulse 2s ease-in-out infinite} +@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}} +#hBadge{font-family:var(--font-mono);font-size:9px;color:var(--text3);margin-left:auto} +.theme-btn{display:flex;align-items:center;gap:7px;cursor:pointer;padding:5px 9px;background:var(--bg2);border:1px solid var(--border);font-family:var(--font-mono);font-size:9px;color:var(--text2);transition:all .15s} +.theme-btn:hover{border-color:var(--green);color:var(--green)} +.ttrack{width:24px;height:12px;background:var(--border2);border-radius:6px;position:relative;flex-shrink:0;transition:background .2s} +[data-theme="light"] .ttrack{background:var(--green)} +.tthumb{position:absolute;top:2px;left:2px;width:8px;height:8px;border-radius:50%;background:var(--text2);transition:transform .2s,background .2s} +[data-theme="light"] .tthumb{transform:translateX(12px);background:#fff} + +/* MOBILE */ +.mobhead{display:flex;width:100%;position:sticky;top:0;z-index:100;background:var(--surface);border-bottom:1px solid var(--border);padding:11px 16px;align-items:center;justify-content:space-between} +.moblogo{font-family:var(--font-d);font-size:13px;font-weight:900;letter-spacing:2px;color:var(--green)} +.ham{background:none;border:none;cursor:pointer;color:var(--text);font-size:20px;padding:2px} +.mobnav{display:none;position:fixed;inset:0;z-index:200;background:rgba(0,0,0,.7)} +.mobnav.open{display:block} +.mobpanel{position:absolute;left:0;top:0;bottom:0;width:260px;background:var(--surface);border-right:1px solid var(--border);overflow-y:auto;transform:translateX(-100%);transition:transform .25s} +.mobnav.open .mobpanel{transform:translateX(0)} +.mobclose{position:absolute;top:13px;right:13px;background:none;border:none;cursor:pointer;font-size:18px;color:var(--text2)} + +/* SECTION ROLE HEADERS */ +.role-banner{padding:9px 16px;font-family:var(--font-mono);font-size:9px;letter-spacing:2px;text-transform:uppercase;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px} +.rb-resolver{background:rgba(245,158,11,.04);color:var(--amber)} +.rb-admin-b{background:rgba(167,139,250,.04);color:var(--purple)} + +/* MAIN */ +.main{flex:1;overflow-y:auto;padding:26px 32px 80px;max-width:950px} + +/* PAGE HEADER */ +.ph{margin-bottom:22px;padding-bottom:16px;border-bottom:1px solid var(--border)} +.ph-eye{font-family:var(--font-mono);font-size:9px;letter-spacing:3px;text-transform:uppercase;margin-bottom:7px;display:flex;align-items:center;gap:9px} +.ph-eye::before{content:'';width:18px;height:1px} +.ph-g .ph-eye{color:var(--green)}.ph-g .ph-eye::before{background:var(--green)} +.ph-r .ph-eye{color:var(--amber)}.ph-r .ph-eye::before{background:var(--amber)} +.ph-a .ph-eye{color:var(--purple)}.ph-a .ph-eye::before{background:var(--purple)} +.ph-title{font-family:var(--font-d);font-size:20px;font-weight:900;letter-spacing:-.5px;line-height:1.2} +.ph-sub{font-size:13px;color:var(--text2);margin-top:5px} + +/* STAT BAR */ +.sbar{display:flex;gap:8px;margin-bottom:20px;flex-wrap:wrap;align-items:center} +.chip{background:var(--surface);border:1px solid var(--border);padding:5px 12px;font-family:var(--font-mono);font-size:10px;color:var(--text2);display:flex;align-items:center;gap:5px} +.chip b{color:var(--text);font-family:var(--font-d);font-size:13px;font-weight:700} + +/* CARD */ +.card{background:var(--surface);border:1px solid var(--border);margin-bottom:13px;position:relative;overflow:hidden;transition:background .25s} +[data-theme="dark"] .card::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,var(--green),transparent);opacity:.15} +[data-theme="dark"] .ph-r ~ .card::before,[data-theme="dark"] .card.r-card::before{background:linear-gradient(90deg,var(--amber),transparent)!important} +[data-theme="dark"] .ph-a ~ .card::before,[data-theme="dark"] .card.a-card::before{background:linear-gradient(90deg,var(--purple),transparent)!important} +.ci{padding:18px} +.ct{font-family:var(--font-mono);font-size:9px;color:var(--text3);letter-spacing:2px;text-transform:uppercase;margin-bottom:15px;padding-bottom:10px;border-bottom:1px solid var(--border)} + +/* FORM */ +.field{margin-bottom:12px} +.field label{display:block;font-family:var(--font-mono);font-size:9px;color:var(--text2);letter-spacing:2px;text-transform:uppercase;margin-bottom:5px} +.field input,.field textarea,.field select{width:100%;background:var(--bg);border:1px solid var(--border2);padding:8px 11px;color:var(--text);font-family:var(--font-mono);font-size:12px;outline:none;transition:border-color .15s,box-shadow .15s;appearance:none} +.field input:focus,.field textarea:focus{border-color:var(--green);box-shadow:0 0 0 3px var(--gdim)} +.field textarea{resize:vertical;min-height:70px;line-height:1.6} +.hint{font-family:var(--font-mono);font-size:9px;color:var(--text3);margin-top:3px;line-height:1.5} +.r2{display:grid;grid-template-columns:1fr 1fr;gap:12px} +.r3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px} + +/* BUTTONS */ +.btn{display:inline-flex;align-items:center;gap:6px;padding:9px 17px;border:none;cursor:pointer;font-family:var(--font-ui);font-size:13px;font-weight:600;letter-spacing:.2px;transition:all .15s;position:relative} +.bp{background:var(--green);color:#000} +.bp:hover{filter:brightness(1.1);transform:translateY(-1px);box-shadow:0 4px 20px var(--gglow)} +.bp:active{transform:translateY(0)} +.bp:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none} +.bsec{background:transparent;border:1px solid var(--border2);color:var(--text2)} +.bsec:hover{border-color:var(--green);color:var(--green);background:var(--gdim)} +.bamb{background:var(--amber);color:#000} +.bamb:hover{filter:brightness(1.08);transform:translateY(-1px);box-shadow:0 4px 16px rgba(245,158,11,.3)} +.bamb:disabled{opacity:.4;cursor:not-allowed;transform:none;box-shadow:none} +.bsm{padding:6px 11px;font-size:11px} +.brow{display:flex;gap:8px;flex-wrap:wrap;margin-top:15px;align-items:center} + +/* OUTCOME TOGGLE */ +.otog{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-bottom:12px} +.obtn{padding:10px;border:1px solid var(--border2);background:transparent;color:var(--text2);cursor:pointer;font-family:var(--font-d);font-size:9px;font-weight:700;letter-spacing:2px;transition:all .15s;text-align:center} +.obtn.yes:hover{border-color:var(--green);color:var(--green)} +.obtn.no:hover{border-color:var(--red);color:var(--red)} +.obtn.yes.active{background:var(--gdim);border-color:var(--green);color:var(--green)} +.obtn.no.active{background:var(--rdim);border-color:var(--red);color:var(--red)} + +/* ALERTS */ +.alert{padding:10px 13px;font-family:var(--font-mono);font-size:10px;margin-bottom:13px;line-height:1.7} +.ag{background:var(--gdim);border:1px solid rgba(0,232,122,.15);color:var(--green)} +.ar{background:var(--rdim);border:1px solid rgba(255,61,90,.15);color:var(--red)} +.ay{background:var(--adim);border:1px solid rgba(245,158,11,.2);color:var(--amber)} +.ac{background:var(--cdim);border:1px solid rgba(34,211,238,.15);color:var(--cyan)} +.ap{background:var(--pdim);border:1px solid rgba(167,139,250,.2);color:var(--purple)} + +/* PAYLOAD */ +.pbox{display:none;margin-top:13px} +.plbl{font-family:var(--font-mono);font-size:9px;color:var(--text3);letter-spacing:2px;text-transform:uppercase;margin-bottom:4px} +.pbox textarea{width:100%;background:var(--bg);border:1px solid var(--border2);padding:10px;color:var(--text2);font-family:var(--font-mono);font-size:10px;resize:vertical;min-height:120px;outline:none;line-height:1.7} + +/* PENDING */ +.pend{display:none;align-items:center;gap:8px;font-family:var(--font-mono);font-size:10px;color:var(--amber);padding:8px 12px;background:var(--adim);border:1px solid rgba(245,158,11,.2);margin-top:11px} +.blink{animation:blink 1.2s step-end infinite} +@keyframes blink{0%,100%{opacity:1}50%{opacity:.1}} +.div{height:1px;background:var(--border);margin:15px 0} +.loading{padding:52px;text-align:center;color:var(--text3);font-family:var(--font-mono);font-size:11px;letter-spacing:1px} + +/* TOAST */ +#toast{position:fixed;bottom:20px;right:20px;background:var(--surf2);border:1px solid var(--border2);padding:10px 15px;font-family:var(--font-mono);font-size:11px;color:var(--text);z-index:9999;display:none;max-width:360px;box-shadow:0 12px 40px var(--shadow);letter-spacing:.3px;line-height:1.5} +#toast.ok{border-color:var(--green);color:var(--green)} +#toast.err{border-color:var(--red);color:var(--red)} + +/* PAGES */ +.page{display:none}.page.active{display:block;animation:fu .18s ease} +@keyframes fu{from{opacity:0;transform:translateY(7px)}to{opacity:1;transform:translateY(0)}} + +/* INFO GRID */ +.igrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:8px} +.icell{background:var(--bg);border:1px solid var(--border);padding:12px} +.ilbl{font-family:var(--font-mono);font-size:8px;color:var(--text3);letter-spacing:2px;text-transform:uppercase;margin-bottom:4px} +.ival{font-family:var(--font-mono);font-size:14px;color:var(--text)} + +/* KEY */ +.kstat{display:flex;align-items:center;gap:8px;padding:9px 12px;border:1px solid var(--border2);font-family:var(--font-mono);font-size:11px;margin-bottom:13px;color:var(--text2)} +.kstat.loaded{border-color:var(--green);background:var(--gdim);color:var(--green)} +.addr-mono{font-family:var(--font-mono);font-size:10px;color:var(--cyan);word-break:break-all;padding:8px 11px;background:var(--bg);border:1px solid var(--border2);margin-top:8px} +.bal-wrap{padding:16px;background:var(--bg);border:1px solid var(--border);margin-bottom:13px} +.bal-num{font-family:var(--font-d);font-size:30px;font-weight:900;color:var(--green);line-height:1;margin-bottom:2px} +.bal-unit{font-family:var(--font-mono);font-size:8px;color:var(--text3);letter-spacing:2px} +.sinfo{font-family:var(--font-mono);font-size:10px;color:var(--text2);line-height:2} +.sinfo em{color:var(--green);font-style:normal} +code{font-family:var(--font-mono);font-size:11px;background:var(--bg2);padding:1px 5px;border:1px solid var(--border);color:var(--cyan)} +.cwrap{display:flex;align-items:center;gap:5px} +.cbtn{background:var(--bg2);border:1px solid var(--border);color:var(--text3);padding:3px 7px;font-size:11px;cursor:pointer;transition:all .15s} +.cbtn:hover{border-color:var(--green);color:var(--green)} +.cbtn.ok{color:var(--green);border-color:var(--green)} +.sbadge{display:inline-flex;align-items:center;gap:5px;padding:4px 11px;background:var(--rdim);border:1px solid rgba(255,61,90,.2);font-family:var(--font-mono);font-size:9px;color:var(--red);cursor:pointer;transition:all .15s;margin-top:7px} +.sbadge:hover{background:var(--red);color:#fff} +.sbadge.hidden{display:none} + +/* OFFLINE */ +#offBanner{display:none;position:sticky;top:0;z-index:50;background:rgba(255,61,90,.07);border-bottom:1px solid rgba(255,61,90,.2);padding:7px 18px;font-family:var(--font-mono);font-size:10px;color:var(--red);text-align:center} +#offBanner.show{display:block} + +/* CONFIRM */ +#confOverlay{display:none;position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.75);backdrop-filter:blur(4px);align-items:center;justify-content:center} +#confOverlay.open{display:flex} +.cm{background:var(--surface);border:1px solid var(--border2);width:min(420px,90vw);max-height:80vh;overflow-y:auto} +.cm-h{padding:18px;border-bottom:1px solid var(--border)} +.cm-t{font-family:var(--font-d);font-size:15px;font-weight:900} +.cm-s{font-family:var(--font-mono);font-size:8px;color:var(--text3);letter-spacing:2px;text-transform:uppercase;margin-top:3px} +.cm-rows{padding:14px 18px} +.cm-row{display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--border);font-family:var(--font-mono);font-size:11px} +.cm-row:last-child{border:none} +.cm-l{color:var(--text3)}.cm-v{color:var(--text);word-break:break-all;text-align:right;max-width:60%} +.cm-v.g{color:var(--green)}.cm-v.r{color:var(--red)} +.cm-btns{display:flex;border-top:1px solid var(--border)} +.cm-btns button{flex:1;padding:13px;border:none;cursor:pointer;font-family:var(--font-d);font-size:13px;font-weight:700;transition:all .15s} +#confCancel{background:transparent;color:var(--text2);border-right:1px solid var(--border)} +#confCancel:hover{color:var(--text);background:var(--bg2)} +#confOk{background:var(--green);color:#000} +#confOk:hover{filter:brightness(1.08)} + +/* MARKET CARDS */ +.mgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px} +.mcard{background:var(--surface);border:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden;transition:border-color .25s,box-shadow .25s,transform .2s;position:relative;border-radius:2px} +[data-theme="dark"] .mcard::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,var(--green),transparent);opacity:.4} +[data-theme="dark"] .mcard.sp-live::after{content:'';position:absolute;inset:0;border-radius:2px;box-shadow:0 0 0 1px rgba(0,232,122,.06) inset;pointer-events:none} +.mcard:hover{border-color:var(--green);box-shadow:0 8px 32px rgba(0,0,0,.6),0 0 0 1px rgba(0,232,122,.08);transform:translateY(-2px)} +.mcard.mexp::before{background:linear-gradient(90deg,var(--amber),transparent)!important} +.mcard.mexp:hover{border-color:var(--amber);box-shadow:0 8px 32px rgba(0,0,0,.6),0 0 0 1px rgba(245,158,11,.08)} +.mcard.mfin{opacity:.65}.mcard.mfin::before{background:linear-gradient(90deg,#3a3a3a,transparent)!important} +.mcard.mfin:hover{border-color:var(--border2);box-shadow:0 4px 16px rgba(0,0,0,.4);transform:none} +.mcard.mcan{opacity:.8}.mcard.mcan::before{background:linear-gradient(90deg,var(--red),transparent)!important} +.mcard.mcan:hover{border-color:var(--red);box-shadow:0 8px 32px rgba(0,0,0,.6),0 0 0 1px rgba(255,61,90,.08)} +.mc-head{padding:14px 15px 12px;display:flex;align-items:flex-start;justify-content:space-between;gap:10px;cursor:pointer;transition:background .15s} +.mc-head:hover{background:rgba(255,255,255,.02)} +.mc-q{font-family:var(--font-d);font-size:13px;font-weight:700;color:var(--text);line-height:1.45;flex:1;letter-spacing:-.01em} +.spill{flex-shrink:0;display:flex;align-items:center;gap:4px;padding:3px 8px;font-size:8px;font-weight:700;letter-spacing:.15em;text-transform:uppercase;font-family:var(--font-mono);border-radius:1px} +.sp-o{background:rgba(0,232,122,.1);color:var(--green);border:1px solid rgba(0,232,122,.25)} +.sp-o .dot{background:var(--green);box-shadow:0 0 6px var(--green);animation:pulse 2s infinite} +.sp-e{background:rgba(245,158,11,.1);color:var(--amber);border:1px solid rgba(245,158,11,.25)} +.sp-e .dot{background:var(--amber)} +.sp-d{background:rgba(255,61,90,.1);color:var(--red);border:1px solid rgba(255,61,90,.25)} +.sp-d .dot{background:var(--red)} +.sp-f{background:rgba(100,100,100,.08);color:#666;border:1px solid rgba(100,100,100,.15)} +.sp-f .dot{background:#555} +.mc-prob{padding:16px 15px 12px} +.mc-prob-nums{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:10px} +.mc-prob-side{display:flex;flex-direction:column} +.pvy{font-family:var(--font-d);font-size:36px;font-weight:900;color:var(--green);letter-spacing:-.04em;line-height:1;text-shadow:0 0 24px rgba(0,232,122,.35)} +.pvn{font-family:var(--font-d);font-size:36px;font-weight:900;color:var(--red);letter-spacing:-.04em;line-height:1;text-shadow:0 0 24px rgba(255,61,90,.25)} +.pvl{font-family:var(--font-mono);font-size:8px;color:var(--text3);letter-spacing:2px;text-transform:uppercase;display:block;margin-top:4px} +.btrack{height:4px;background:rgba(255,255,255,.04);overflow:hidden;display:flex;border-radius:2px} +.byes{height:100%;background:var(--green);box-shadow:0 0 8px rgba(0,232,122,.5);transition:width .5s cubic-bezier(.4,0,.2,1)} +.bno{height:100%;background:var(--red);flex:1} +.mc-pools{display:flex;border-top:1px solid var(--border)} +.pc{flex:1;padding:10px 14px;font-family:var(--font-mono)} +.pc:first-child{border-right:1px solid var(--border)} +.pc-lbl{font-size:8px;color:var(--text3);letter-spacing:1.5px;text-transform:uppercase;margin-bottom:3px} +.pc-val{font-size:13px;font-weight:600;letter-spacing:-.01em} +.pcy .pc-val{color:var(--green)}.pcn .pc-val{color:var(--red)} +.mc-banner{padding:8px 14px;font-family:var(--font-mono);font-size:9px;display:flex;align-items:center;gap:8px;border-top:1px solid var(--border);letter-spacing:.03em} +.bnr{background:rgba(245,158,11,.06);color:var(--amber);border-left:2px solid var(--amber)} +.bnd{background:rgba(255,61,90,.06);color:var(--red);border-left:2px solid var(--red)} +.bnf{background:rgba(0,232,122,.06);color:var(--green);border-left:2px solid var(--green)} +.mc-acts{display:flex;padding:10px 14px;gap:8px;border-top:1px solid var(--border)} +.byes2{flex:1;padding:9px 8px;background:var(--gdim);border:1px solid rgba(0,232,122,.2);color:var(--green);font-family:var(--font-d);font-size:9px;font-weight:700;letter-spacing:2px;cursor:pointer;transition:all .15s;border-radius:1px} +.byes2:hover{background:var(--green);color:#000;box-shadow:0 0 16px rgba(0,232,122,.3);border-color:var(--green)} +.byes2:disabled{opacity:.25;cursor:not-allowed} +.bno2{flex:1;padding:9px 8px;background:var(--rdim);border:1px solid rgba(255,61,90,.2);color:var(--red);font-family:var(--font-d);font-size:9px;font-weight:700;letter-spacing:2px;cursor:pointer;transition:all .15s;border-radius:1px} +.bno2:hover{background:var(--red);color:#fff;box-shadow:0 0 16px rgba(255,61,90,.3);border-color:var(--red)} +.bno2:disabled{opacity:.25;cursor:not-allowed} +.mc-foot{padding:9px 15px;display:flex;justify-content:space-between;align-items:center;border-top:1px solid var(--border);background:var(--bg2)} +.mc-foot-item{display:flex;flex-direction:column;gap:2px} +.mc-propose-btn{flex:1;padding:9px 8px;background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.25);color:var(--amber);font-family:var(--font-d);font-size:9px;font-weight:700;letter-spacing:2px;cursor:pointer;transition:all .15s;border-radius:1px} +.mc-propose-btn:hover{background:var(--amber);color:#000;box-shadow:0 0 16px rgba(245,158,11,.3)} +.mitems{display:flex;gap:14px;flex-wrap:wrap} +.mitem{display:flex;flex-direction:column} +.mlbl{font-family:var(--font-mono);font-size:7px;color:var(--text3);letter-spacing:2px;text-transform:uppercase;margin-bottom:2px} +.mval{font-family:var(--font-mono);font-size:10px;color:var(--text2)} +.mval.g{color:var(--green)} +.midb{font-family:var(--font-mono);font-size:8px;color:var(--text3)} + +/* PREDICTIONS */ +.pitem{background:var(--bg);border:1px solid var(--border);padding:11px 13px;margin-bottom:7px;display:flex;justify-content:space-between;align-items:center;transition:border-color .15s} +.pitem:hover{border-color:var(--border2)} +.pmid{font-family:var(--font-mono);font-size:10px;color:var(--text3);margin-bottom:3px} +.pmeta{display:flex;gap:12px;align-items:center} +.poc{font-family:var(--font-mono);font-size:11px;font-weight:500} +.pst{font-family:var(--font-mono);font-size:10px;color:var(--text2)} +.pht{font-family:var(--font-mono);font-size:9px;color:var(--text3)} + +/* BOTTOM NAV */ +.bnav{display:none;position:fixed;bottom:0;left:0;right:0;background:var(--surface);border-top:1px solid var(--border);z-index:100;padding:0 4px;justify-content:space-around;align-items:center;height:54px} +.bni{display:flex;flex-direction:column;align-items:center;gap:2px;padding:4px 5px;cursor:pointer;color:var(--text3);font-size:8px;font-family:var(--font-mono);letter-spacing:.3px;transition:color .15s;flex:1;text-align:center;user-select:none} +.bni.active{color:var(--green)} +.bni svg{width:17px;height:17px} + +@media(max-width:900px){ + .mobhead{display:flex}.main{padding:14px 14px 70px;width:100%;min-width:0}.bnav{display:flex} + .r2,.r3{grid-template-columns:1fr}.mgrid{grid-template-columns:1fr}.ph-title{font-size:17px} + .sbar{flex-wrap:wrap;gap:6px;padding:8px 0;justify-content:flex-start} + .hide-mob{display:none} + .sbar .chip{font-size:10px} + .sbar .bsm{font-size:9px;padding:4px 10px;flex:1} + .mtabs{padding:0 0 12px;gap:6px} + .mtab{flex:1;text-align:center;padding:6px 8px} + .shell{display:block} +} + +/* Market Tabs */ +.mtabs{display:flex;gap:8px;padding:0 16px 12px;border-bottom:1px solid var(--border);margin-bottom:16px} +.mtab{padding:6px 16px;font-family:var(--font-mono);font-size:10px;letter-spacing:1px;border:1px solid var(--border);background:transparent;color:var(--text3);cursor:pointer;transition:all .15s;border-radius:2px} +.mtab:hover{border-color:var(--green);color:var(--green)} +.mtab.active{background:var(--gdim);border-color:var(--green);color:var(--green)} + +/* Keystore account card selector */ +.ks-acct-card:hover { border-color: var(--green) !important; } +.ks-acct-active { border-color: var(--green) !important; } +.ks-acct-active div:last-child { background: var(--green) !important; } + +/* NEW CARD DESIGN — inline script classes */ +.spill.sp-live{background:rgba(0,232,122,.1);color:var(--green);border:1px solid rgba(0,232,122,.25)} +.spill.sp-live .sp-dot{background:var(--green);box-shadow:0 0 6px var(--green);animation:pulse 2s infinite} +.spill.sp-proposed{background:rgba(245,158,11,.1);color:var(--amber);border:1px solid rgba(245,158,11,.25)} +.spill.sp-disputed{background:rgba(255,61,90,.1);color:var(--red);border:1px solid rgba(255,61,90,.25)} +.spill.sp-finalized{background:rgba(100,100,100,.08);color:#666;border:1px solid rgba(100,100,100,.15)} +.spill.sp-cancelled{background:rgba(255,61,90,.1);color:var(--red);border:1px solid rgba(255,61,90,.25)} +.spill.sp-expired{background:rgba(245,158,11,.1);color:var(--amber);border:1px solid rgba(245,158,11,.25)} +.sp-dot{width:6px;height:6px;border-radius:50%;background:currentColor;flex-shrink:0;display:inline-block} + +.mcard-top{padding:16px 15px 12px;display:flex;flex-direction:column;gap:10px;cursor:pointer} +.mcard-cat{font-family:var(--font-mono);font-size:8px;letter-spacing:2px;color:var(--text3);display:flex;align-items:center;gap:8px} +.mcard-cat-dot{width:5px;height:5px;border-radius:50%;background:var(--green);flex-shrink:0} +.mcard-q{font-family:var(--font-d);font-size:13px;font-weight:700;color:var(--text);line-height:1.45;letter-spacing:-.01em} +.mcard-probs{display:flex;align-items:flex-end;justify-content:space-between} +.prob-yes{font-family:var(--font-d);font-size:36px;font-weight:900;color:var(--green);letter-spacing:-.04em;line-height:1;text-shadow:0 0 24px rgba(0,232,122,.35)} +.prob-no{font-family:var(--font-d);font-size:36px;font-weight:900;color:var(--red);letter-spacing:-.04em;line-height:1;text-shadow:0 0 24px rgba(255,61,90,.25)} +.prob-labels{display:flex;gap:4px;margin-top:4px} +.prob-lbl{font-family:var(--font-mono);font-size:8px;color:var(--text3);letter-spacing:2px;text-transform:uppercase} +.prob-divider{width:1px;background:var(--border);align-self:stretch;margin:0 8px} + +.mcard-meta{display:flex;justify-content:space-between;padding:9px 15px;border-top:1px solid var(--border);background:var(--bg2)} +.meta-item{display:flex;flex-direction:column;gap:2px} +.meta-lbl{font-family:var(--font-mono);font-size:7px;color:var(--text3);letter-spacing:2px;text-transform:uppercase} +.meta-val{font-family:var(--font-mono);font-size:10px;color:var(--text2)} +.meta-val.g{color:var(--green)} + +.mcard-actions{display:flex;gap:8px;padding:10px 14px;border-top:1px solid var(--border)} +.mcard-btn{flex:1;padding:9px 8px;font-family:var(--font-d);font-size:9px;font-weight:700;letter-spacing:2px;cursor:pointer;transition:all .15s;border-radius:1px;border:1px solid} +.mcard-btn-yes{background:var(--gdim);border-color:rgba(0,232,122,.2);color:var(--green)} +.mcard-btn-yes:hover{background:var(--green);color:#000;box-shadow:0 0 16px rgba(0,232,122,.3);border-color:var(--green)} +.mcard-btn-no{background:var(--rdim);border-color:rgba(255,61,90,.2);color:var(--red)} +.mcard-btn-no:hover{background:var(--red);color:#fff;box-shadow:0 0 16px rgba(255,61,90,.3);border-color:var(--red)} + +/* RESOLVER CARDS */ +.res-card{display:flex;align-items:center;gap:14px;padding:14px 16px;border-bottom:1px solid var(--border);transition:background .15s} +.res-card:hover{background:var(--gdim)} +.res-avatar{width:38px;height:38px;border-radius:50%;background:var(--surf2);border:1px solid var(--border2);display:flex;align-items:center;justify-content:center;font-family:var(--font-d);font-size:13px;font-weight:900;color:var(--green);flex-shrink:0} +.res-info{flex:1;min-width:0} +.res-addr{font-family:var(--font-mono);font-size:10px;color:var(--text2);margin-bottom:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.res-stats{display:flex;gap:16px} +.res-stat{display:flex;flex-direction:column;gap:1px} +.res-stat-lbl{font-family:var(--font-mono);font-size:7px;color:var(--text3);letter-spacing:2px;text-transform:uppercase} +.res-stat-val{font-family:var(--font-mono);font-size:11px;color:var(--text)} +.tier-badge{font-family:var(--font-mono);font-size:8px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;padding:3px 8px;border-radius:2px;flex-shrink:0} +.tier-gold{background:rgba(255,215,0,.1);color:#FFD700;border:1px solid rgba(255,215,0,.25)} +.tier-silver{background:rgba(192,192,192,.1);color:#C0C0C0;border:1px solid rgba(192,192,192,.25)} +.tier-bronze{background:rgba(205,127,50,.1);color:#CD7F32;border:1px solid rgba(205,127,50,.25)} + +/* DETAIL PAGE */ +.det-grid{display:grid;grid-template-columns:1fr 340px;gap:16px;align-items:start} +.det-main{display:flex;flex-direction:column;gap:14px} +.det-sidebar{display:flex;flex-direction:column;gap:14px;position:sticky;top:16px} +.prob-big{display:grid;grid-template-columns:1fr 1fr;gap:1px;background:var(--border);border:1px solid var(--border);margin-bottom:12px} +.prob-side{padding:20px 18px;background:var(--surface);display:flex;flex-direction:column;gap:4px} +.prob-side-yes{border-right:none} +.prob-side-no{text-align:right} +.prob-side-pct{font-family:var(--font-d);font-size:42px;font-weight:900;letter-spacing:-.04em;line-height:1} +.prob-side-yes .prob-side-pct{color:var(--green);text-shadow:0 0 30px rgba(0,232,122,.4)} +.prob-side-no .prob-side-pct{color:var(--red);text-shadow:0 0 30px rgba(255,61,90,.3)} +.prob-side-lbl{font-family:var(--font-mono);font-size:8px;letter-spacing:3px;text-transform:uppercase;color:var(--text3)} +.prob-side-pool{font-family:var(--font-mono);font-size:11px;color:var(--text2);margin-top:6px} + +@media(max-width:768px){ + .det-grid{grid-template-columns:1fr} + .det-sidebar{position:static} + .prob-side-pct{font-size:32px} +} + +/* MOBILE CARD OVERRIDES */ +@media(max-width:768px){ + .mgrid{grid-template-columns:1fr;gap:10px} + .mcard-top{padding:12px 12px 10px} + .prob-yes,.prob-no{font-size:28px} + .mcard-q{font-size:12px} + .mcard-meta{padding:7px 12px} + .meta-lbl{font-size:7px} + .meta-val{font-size:10px} + .mcard-actions{padding:8px 12px;gap:6px} + .mcard-btn{padding:8px 6px;font-size:8px} + .sbar .chip{font-size:9px;padding:4px 8px} + .cat{font-size:9px;padding:5px 10px} + .ph-title{font-size:22px} + .ph-sub{font-size:12px} +} + + +