0xacfba7ce…4082·#25,829,677·view on Etherscan
!==0){before=0;break}
for(let k=j+7;k<j+11;k++)if(k>=SZ||get(i,k)!==0){after=0;break}
if(before||after)p+=40}};
hit((i,j)=>m[i][j]);hit((i,j)=>m[j][i]);
let dark=0;for(let i=0;i<SZ;i++)for(let j=0;j<SZ;j++)dark+=m[i][j];
p+=Math.floor(Math.abs(dark*100/(SZ*SZ)-50)/5)*10;
return p}
function build(str){
const cw=encode(str);
let best=null,bestP=Infinity,bestK=0;
for(let k=0;k<8;k++){
const{m,fn}=frame();
place(m,fn,cw);
for(let r=0;r<SZ;r++)for(let c=0;c<SZ;c++)
if(!fn[r][c]&&maskFn(k,r,c))m[r][c]^=1;
const f=fmtBits(k);
for(let i=0;i<8;i++){
const b=(f>>i)&1;
if(i<6)m[i][8]=b;else if(i===6)m[7][8]=b;else m[8][8]=b;
m[8][SZ-1-i]=b}
for(let i=8;i<15;i++){
const b=(f>>i)&1,c=14-i;
if(c<6)m[8][c]=b;else m[8][7]=b;
m[SZ-1-c][8]=b}
m[SZ-8][8]=1;
const p=penalty(m);
if(p<bestP){bestP=p;best=m;bestK=k}}
return{size:SZ,modules:best,mask:bestK}}
return{build,_encode:encode};
})();
const qrSvg=m=>{let d="";
for(let r=0;r<m.size;r++)for(let c=0;c<m.size;c++)if(m.modules[r][c])d+="M"+c+" "+r+"h1v1h-1z";
return '<svg viewBox="-4 -4 '+(m.size+8)+" "+(m.size+8)+'" role="img" aria-label="WalletConnect pairing code" style="width:100%;height:auto;background:#fff;border-radius:.3em"><path fill="#000" d="'+d+'"/></svg>'};
/* -------------------------------------------------------------- WALLET */
const NODE_KEY="poidh:rpc";
let PROV=null,account=null;
const readNode=()=>{try{return localStorage.getItem(NODE_KEY)||""}catch{return""}};
const NO_WALLET="No wallet, and no read node set.";
const nodeCall=async(method,params)=>{
const url=readNode();
if(!url)throw Object.assign(Error(NO_WALLET),{noWallet:true});
const r=await fetch(url,{method:"POST",headers:{"content-type":"application/json"},
body:JSON.stringify({id:1,jsonrpc:"2.0",method,params:params||[]})});
const j=await r.json();
if(j.error)throw Error(j.error.message||"node error");
return j.result};
const rpc=(method,params)=>{
const e=PROV||window.ethereum;
return e?e.request({method,params}):nodeCall(method,params)};
const rpcRead=async(method,params,tries=3)=>{
let last;
for(let i=0;i<tries;i++){
try{return await rpc(method,params)}
catch(e){last=e;if(e&&e.noWallet)throw e;await new Promise(r=>setTimeout(r,250*(i+1)))}}
throw last};
const checkWallet=async()=>{
if(!account)throw Error("Connect a wallet first.");
const[c,a]=await Promise.all([rpc(I,[]),rpc("eth_accounts",[])]);
if(+c!==CHAIN)throw Error("Switch to Ethereum mainnet.");
if((a[0]||"").toLowerCase()!==account.toLowerCase())throw Error("The wallet changed accounts.")};
const waitTx=async hash=>{
const end=Date.now()+600000;let wait=1500;
while(Date.now()<end){
try{const r=await rpc("eth_getTransactionReceipt",[hash]);
if(r){if(r.status!=="0x1")throw Error("Transaction reverted.");return r}
wait=Math.min(wait+500,5000)}
catch(e){if(/reverted/.test(e.message||""))throw e}
await new Promise(r=>setTimeout(r,wait))}
throw Error("Not mined in time; check your wallet.")};
const providers=[];
addEventListener("eip6963:announceProvider",e=>{
const d=e.detail;if(!d||!d.provider)return;
if(providers.some(p=>p.info.uuid===d.info.uuid))return;
providers.push(d)});
try{dispatchEvent(new Event("eip6963:requestProvider"))}catch{}
/* --------------------------------------------------------------- NAMES
Ported from zSwap, with the same preferences. Forward resolution picks the
registry by suffix: .wei -> WNS, .gwei -> GNS (a WNS NameNFT fork, same
interface), .eth -> ENS. Reverse resolution asks all three at once and
prefers WNS, then GNS, then ENS.
A REVERSE RECORD IS A CLAIM, NOT A FACT: anybody may point one at any name.
None of them counts until the name resolves FORWARD to the same address,
which is why every candidate below is checked before it is shown. */
const WNS="0x0000000000696760e15f265e828db644a0c242eb";
const GNS="0x9d51d507bc7264d4fe8ad1cf7fe191933a0a81d6";
const ENSREG="0x00000000000c2e074ec69a0dfb2997ba6c7d2e1e";
/* computeId(string) then resolve(uint256) on WNS/GNS; the third is their
reverse lookup. ENS keeps resolver(bytes32), addr(bytes32), name(bytes32),
plus resolve(bytes,bytes) for wildcard resolvers. */
const SEL_CID="fb021939",SEL_NRES="4f896d4f",SEL_REV="9af8b7aa";
const SEL_RSLV="0178b8bf",SEL_EADDR="3b3b57de",SEL_ENAME="691f3431";
const SEL_ERESOLVE="9061b923",SEL_SUPPORTS="01ffc9a7";
const NAME_OFFCHAIN="That name keeps its records off chain, and this page reads only on-chain state.";
const hexToBytes=h=>{const t=strip0x(h),o=new Uint8Array(t.length/2);
for(let i=0;i<o.length;i++)o[i]=parseInt(t.slice(i*2,i*2+2),16);return o};
const KRC=[1n,0x8082n,0x800000000000808an,0x8000000080008000n,0x808bn,0x80000001n,0x8000000080008081n,0x8000000000008009n,0x8an,0x88n,0x80008009n,0x8000000an,0x8000808bn,0x800000000000008bn,0x8000000000008089n,0x8000000000008003n,0x8000000000008002n,0x8000000000000080n,0x800an,0x800000008000000an,0x8000000080008081n,0x8000000000008080n,0x80000001n,0x8000000080008008n];
const KR=[0,1,62,28,27,36,44,6,55,20,3,10,43,25,39,41,45,15,21,8,18,2,61,56,14];
const KM=(1n<<64n)-1n;
const krol=(x,n)=>n?((x<<BigInt(n))|(x>>BigInt(64-n)))&KM:x;
const KCc=new Array(5),KB=new Array(25);
const keccak=b=>{const A=new Array(25).fill(0n),rate=136;
const pad=rate-(b.length%rate),buf=new Uint8Array(b.length+pad);buf.set(b);
buf[b.length]|=1;buf[buf.length-1]|=0x80;
for(let o=0;o<buf.length;o+=rate){
for(let i=0;i<17;i++){let v=0n;for(let j=7;j>=0;j--)v=(v<<8n)|BigInt(buf[o+i*8+j]);A[i]^=v}
for(let r=0;r<24;r++){
for(let x=0;x<5;x++)KCc[x]=A[x]^A[x+5]^A[x+10]^A[x+15]^A[x+20];
for(let x=0;x<5;x++){const D=KCc[(x+4)%5]^krol(KCc[(x+1)%5],1);for(let y=0;y<25;y+=5)A[x+y]^=D}
for(let x=0;x<5;x++)for(let y=0;y<5;y++)KB[y+5*((2*x+3*y)%5)]=krol(A[x+5*y],KR[x+5*y]);
for(let x=0;x<5;x++)for(let y=0;y<5;y++)A[x+5*y]=KB[x+5*y]^((~KB[(x+1)%5+5*y]&KM)&KB[(x+2)%5+5*y]);
A[0]^=KRC[r]}}
let out="";for(let i=0;i<4;i++){let v=A[i];for(let j=0;j<8;j++){out+=Number(v&0xffn).toString(16).padStart(2,"0");v>>=8n}}
return"0x"+out};
const namehash=name=>{
let node="00".repeat(32);
if(name){const parts=String(name).toLowerCase().split(".");
for(let i=parts.length-1;i>=0;i--){
const label=strip0x(keccak(new TextEncoder().encode(parts[i])));
node=strip0x(keccak(hexToBytes(node+label)))}}
return node};
const decodeString=hex=>{
const h=strip0x(hex||"");
if(!h)return"";
if(h.length<=64)return new TextDecoder().decode(hexToBytes(h)).replace(/\0+$/,"");
const off=Number(BigInt("0x"+h.slice(0,64)))*2;
const len=Number(BigInt("0x"+h.slice(off,off+64)))*2;
if(!Number.isSafeInteger(off)||!Number.isSafeInteger(len)||off+64+len>h.length)return"";
return new TextDecoder().decode(hexToBytes(h.slice(off+64,off+64+len)))};
const retAddr=h=>{const t=strip0x(h||"");
if(t.length<64)return"";
const a="0x"+t.slice(24,64).toLowerCase();
return /^0x0{40}$/.test(a)?"":a};
const nameOk=n=>/^[\x21-\x7e]+$/.test(n)&&n.split(".").every(l=>l.length>0);
const supports=id=>"0x"+SEL_SUPPORTS+id.padEnd(64,"0");
/* WNS and GNS: a name hashes to an id, and the id resolves to an address. */
const nsFwd=async(reg,n)=>{
const id=await rpcRead(C,[{to:reg,data:"0x"+SEL_CID+pad32("20")+encStr(n)},L],3);
if(strip0x(id||"").length!==64)return"";
return retAddr(await rpcRead(C,[{to:reg,data:"0x"+SEL_NRES+strip0x(id)},L],3))};
/* ENS, including wildcard resolvers (ENSIP-10): walk up to the nearest
resolver, and use resolve(dnsname, calldata) when that resolver is not the
name's own. A resolver that answers with OffchainLookup is saying its records
live somewhere this page will not go, and it says so rather than showing a
zero. */
const encHexBytes=h=>{h=strip0x(h);return encUint(h.length/2)+h.padEnd(Math.ceil(h.length/64)*64,"0")};
const dnsEnc=n=>{let h="";
for(const l of n.split(".")){const b=new TextEncoder().encode(l);
h+=b.length.toString(16).padStart(2,"0");
for(const x of b)h+=x.toString(16).padStart(2,"0")}
return h+"00"};
const encResolve=(dns,inner)=>{const a=encHexBytes(dns);
return pad32("40")+encUint(64+a.length/2)+a+encHexBytes(inner)};
const decBytes=hex=>{const h=strip0x(hex||"");
if(h.length<128)return"";
const off=Number(BigInt("0x"+h.slice(0,64)))*2;
if(!Number.isSafeInteger(off)||off+64>h.length)return"";
const len=Number(BigInt("0x"+h.slice(off,off+64)))*2;
if(!Number.isSafeInteger(len)||off+64+len>h.length)return"";
return"0x"+h.slice(off+64,off+64+len)};
const ensFind=async n=>{const parts=n.split("."),
nodes=parts.map((_,i)=>namehash(parts.slice(i).join(".")));
const rs=await Promise.all(nodes.map(nd=>
rpcRead(C,[{to:ENSREG,data:"0x"+SEL_RSLV+pad32(nd)},L],3).catch(()=>null)));
for(let i=0;i<nodes.length;i++){const r=retAddr(rs[i]);
if(r)return{r,nd:nodes[i],exact:i===0}}
return null};
const ensWild=async r=>{
try{const x=await rpcRead(C,[{to:r,data:supports(SEL_ERESOLVE)},L],1);
return strip0x(x||"").length===64&&BigInt(x)===1n}catch{return false}};
const ensFwd=async n=>{
const f=await ensFind(n);
if(!f)return"";
const inner="0x"+SEL_EADDR+pad32(f.nd);
if(f.exact)return retAddr(await rpcRead(C,[{to:f.r,data:inner},L],3));
if(!await ensWild(f.r))return"";
try{return retAddr(decBytes(await rpcRead(C,
[{to:f.r,data:"0x"+SEL_ERESOLVE+encResolve(dnsEnc(n),inner)},L],1)))}
catch(e){
if(/556f1830/i.test((e&&e.data?JSON.stringify(e.data):"")+((e&&e.message)||"")))
throw Object.assign(Error(NAME_OFFCHAIN),{offchain:true});
return""}};
const ensRevName=async(nd,r)=>{
if(!r)return"";
try{return decodeString(await rpcRead(C,[{to:r,data:"0x"+SEL_ENAME+pad32(nd)},L],1)).trim()}
catch{return""}};
const nameFwd=n=>{
n=String(n||"").trim();
if(!n||!nameOk(n))return Promise.resolve("");
return /\.gwei$/i.test(n)?nsFwd(GNS,n)
:/\.wei$/i.test(n)?nsFwd(WNS,n)
:/\.eth$/i.test(n)?ensFwd(n)
:Promise.resolve("")};
const FWD_TTL=30000;
const fwdCache=new Map();
const nameFwdCached=n=>{const hit=fwdCache.get(n);
if(hit&&hit.exp>Date.now())return hit.p;
const p=nameFwd(n);
fwdCache.set(n,{p,exp:Date.now()+FWD_TTL});
p.catch(()=>fwdCache.delete(n));
return p};
const nameRev=async a=>{
const nd=namehash(strip0x(a).toLowerCase()+".addr.reverse");
const[w,g,er]=await Promise.all([
{to:WNS,data:"0x"+SEL_REV+encAddr(a)},
{to:GNS,data:"0x"+SEL_REV+encAddr(a)},
{to:ENSREG,data:"0x"+SEL_RSLV+pad32(nd)}]
.map(c=>rpcRead(C,[c,L],3).catch(()=>null)));
const str=h=>{try{return h?decodeString(h).trim():""}catch{return""}};
const agrees=async n=>{
if(!n||!nameOk(n.toLowerCase()))return"";
let f="";
try{f=await nameFwdCached(n.toLowerCase())}catch{}
return f&&f.toLowerCase()===String(a).toLowerCase()?n:""};
return await agrees(str(w))||await agrees(str(g))
||await agrees(await ensRevName(nd,retAddr(er)))||""};
/* ---------------------------------------------------------------- STATE */
let CFG={minBounty:0n,minContrib:0n,period:172800n,count:0n};
let LIST=[]; /* bounty summaries, newest first */
let shown=PAGE; /* how far down the list has been read */
let view={t:"list",id:null};
let detail=null; /* the open bounty, fully read */
let mine=null;
let pending=0n,ethBal=0n;
let usdPerEth=null;
let myName="";
let busy=false;
let newKind="open";
const $=id=>document.getElementById(id);
const esc=s=>String(s).replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]));
/* A finished message should not outlive the moment it describes. "Cancelled."
used to sit there through every later page, so anything with an outcome -
ok or err - clears itself; a message still in progress (no class) has no
timer, because it is replaced by its own result. */
let _statTimer=null;
const setStat=(msg,cls)=>{
const el=$("stat");
el.className=cls||"";
el.innerHTML=msg||"";
if(_statTimer)clearTimeout(_statTimer),_statTimer=null;
if(msg&&cls)_statTimer=setTimeout(()=>{
if(el.className===cls)setStat("")},cls==="err"?9000:6000)};
const txLink=h=>'<a href="'+SCAN+'tx/'+h+'" target="_blank" rel="noreferrer">'+h.slice(0,10)+"…</a>";
const acct=a=>'<a href="'+SCAN+'address/'+a+'" target="_blank" rel="noreferrer">'+esc(shortAddr(a))+"</a>";
const eth=v=>fmt(v,4)+" ETH";
/* Dollars are a courtesy, never a source of truth: no price, no parenthesis. */
const usd=v=>{
if(usdPerEth==null||v==null||!v)return"";
const cents=BigInt(v)*usdPerEth/(10n**24n);
const d=Number(cents)/100;
if(!isFinite(d))return"";
return "$"+(d>=100?Math.round(d).toLocaleString("en-US")
:d.toLocaleString("en-US",{minimumFractionDigits:2,maximumFractionDigits:2}))};
const usdPara=v=>{const t=usd(v);return t?" ("+t+")":""};
const isMe=a=>!!account&&String(a||"").toLowerCase()===account;
/* A description is written by whoever posted the bounty, so it is escaped and
then linkified - never inserted as markup. Only http(s) is made clickable;
anything else stays as the text it is. */
const linkify=s=>esc(s).replace(/https?:\/\/[^\s<]+[^\s<.,;:!?)\]}"']/g,
u=>'<a href="'+u+'" target="_blank" rel="noreferrer nofollow">'+u+"</a>");
/* ---------------------------------------------------------------- CACHE
The half of a bounty that can never change - who posted it, its title, its
description, when - is written to localStorage the first time it is read,
so a second visit re-reads only what moves: the balance, the claimer, the
vote. It is a private index in the reader's own browser, built from the
chain, and it is checked rather than trusted: an entry whose id or issuer
does not match what the contract just returned is discarded. */
const CK="poidh:bounties:v1";
let cache={};
try{cache=JSON.parse(localStorage.getItem(CK)||"{}")||{}}catch{}
const cacheSave=()=>{try{localStorage.setItem(CK,JSON.stringify(cache))}catch{}};
/* THE WATER LEVEL. `bounties` is an append-only array and `bountyCounter` is
its length, so ids run 0..count-1 with no gaps and nothing is ever removed -
which means one number is enough to say how far the reader has already seen.
It is kept here rather than on chain: the reading is per-person, it would
cost gas to write, and a shared counter could be advanced past somebody by
anyone else. */
const SK="poidh:seen:v1";
let seen={count:0,at:0};
try{seen=JSON.parse(localStorage.getItem(SK)||"null")||seen}catch{}
/* Captured once at boot and left alone, so a bounty stays marked new for the
whole visit instead of un-marking itself the moment the list lands. */
const seenAtLoad=Number(seen.count)||0;
const markSeen=count=>{
seen={count:Number(count),at:Math.floor(Date.now()/1000)};
try{localStorage.setItem(SK,JSON.stringify(seen))}catch{}};
const isNew=id=>seenAtLoad>0&&Number(id)>=seenAtLoad;
/* A provisional row, built from cache alone, for the first paint. It carries
ONLY what cannot change - issuer, title, kind, when - and says so: the pot
and the status are left blank rather than drawn from a stale copy, because a
number that used to be true is worse than no number. The live read replaces
it a few hundred milliseconds later. */
const cachedRow=id=>{
const c=cache[String(id)];
if(!c)return null;
return{id:BigInt(id),issuer:c.i,name:c.n,desc:c.d,createdAt:c.c,open:!!c.o,
amount:0n,claimer:ZERO,claimId:0n,voting:0n,prov:true}};
/* -------------------------------------------------------------- ABI BITS */
const cUint=(sel,v)=>"0x"+sel+encUint(v);
const cTwo=(sel,a,b)=>"0x"+sel+encUint(a)+encUint(b);
const cAddr=(sel,a)=>"0x"+sel+encAddr(a);
const cAddrId=(sel,a,id)=>"0x"+sel+encAddr(a)+encUint(id);
/* Two strings, both dynamic: heads first, then the bodies, offsets counted
from the start of the argument block. */
const cStr2=(sel,a,b)=>{const A=encStr(a);
return "0x"+sel+encUint(64)+encUint(64+A.length/2)+A+encStr(b)};
const cStr3=(sel,n,a,b,c)=>{const A=encStr(a),B=encStr(b);
return "0x"+sel+encUint(n)+encUint(128)+encUint(128+A.length/2)
+encUint(128+A.length/2+B.length/2)+A+B+encStr(c)};
const decBounty=h=>{
if(!h||strip0x(h).length<8*64)return null;
return{id:uword(h,0),issuer:aword(h,1),
name:decStrAt(h,Number(uword(h,2))),desc:decStrAt(h,Number(uword(h,3))),
amount:uword(h,4),claimer:aword(h,5),createdAt:Number(uword(h,6)),claimId:uword(h,7)}};
const decClaim=h=>{
if(!h||strip0x(h).length<8*64)return null;
return{id:uword(h,0),issuer:aword(h,1),bountyId:uword(h,2),bountyIssuer:aword(h,3),
name:decStrAt(h,Number(uword(h,4))),desc:decStrAt(h,Number(uword(h,5))),
createdAt:Number(uword(h,6)),accepted:uword(h,7)===1n}};
/* `getClaimsByBountyId` returns Claim[10], newest first, padded with empty
structs. Each element is a dynamic tuple, so the array holds OFFSETS to the
tuples rather than the tuples themselves - and each tuple's own string
offsets are relative to ITS OWN start, which is why the slice below is
handed to the same `decClaim` that decodes a standalone one. */
const decClaimList=h=>{
if(!h)return[];
const t=strip0x(h);
const arrAt=Number(uword(h,0))*2;
if(t.length<arrAt+64)return[];
const n=Number(BigInt("0x"+t.slice(arrAt,arrAt+64)));
const base=arrAt+64;
const out=[];
for(let i=0;i<n;i++){
const off=Number(BigInt("0x"+t.slice(base+i*64,base+i*64+64)))*2;
const c=decClaim("0x"+t.slice(base+off));
/* The padding is a zero struct; a real claim always has an issuer. */
if(c&&c.issuer!==ZERO)out.push(c)}
return out};
/* (address[],uint256[]) - two dynamic arrays, each at its own offset. */
const decParts=h=>{
if(!h)return[];
const oa=Number(uword(h,0))/32,ob=Number(uword(h,1))/32;
const n=Number(uword(h,oa)),out=[];
for(let i=0;i<n;i++)out.push({addr:aword(h,oa+1+i),amt:uword(h,ob+1+i)});
return out};
/* ---------------------------------------------------------------- STATUS */
const statusOf=b=>b.claimer===ZERO?"open":
b.claimer.toLowerCase()===b.issuer.toLowerCase()?"cancelled":"paid";
const votePasses=v=>v.yes>((v.no+v.yes)/2n);
/* ----------------------------------------------------------------- READS */
/* MIN_BOUNTY_AMOUNT and MIN_CONTRIBUTION are `immutable`, and `votingPeriod`
is written once at its declaration and has no setter in the ABI - so all
three are constant for the life of the contract and are read once, ever.
Only the counter is re-read, and it rides along with whatever else the
wave is already asking for. */
const KCFG="poidh:cfg:v1";
const cfgCalls=()=>{
const c=[{to:POIDH,data:"0x"+R_COUNT}];
if(!CFG.minBounty)c.push({to:POIDH,data:"0x"+R_MINB},
{to:POIDH,data:"0x"+R_MINC},{to:POIDH,data:"0x"+R_PERIOD});
return c};
const cfgTake=r=>{
CFG.count=uword(r[0],0);
if(r.length>1){
CFG.minBounty=uword(r[1],0);CFG.minContrib=uword(r[2],0);CFG.period=uword(r[3],0);
try{localStorage.setItem(KCFG,JSON.stringify(
{b:String(CFG.minBounty),c:String(CFG.minContrib),p:String(CFG.period)}))}catch{}}};
try{const k=JSON.parse(localStorage.getItem(KCFG)||"null");
if(k)CFG={count:0n,minBounty:BigInt(k.b),minContrib:BigInt(k.c),period:BigInt(k.p)}}catch{}
const readConfig=async()=>{cfgTake(await mc3(cfgCalls()))};
/* One bounty as the list needs it. `participants(id,0)` is the cheapest test
for openness there is: an open bounty always has the issuer in slot 0, and
a solo bounty's array is empty, so the call reverts and aggregate3 hands
back a null for it. */
const sumCalls=ids=>{
const calls=[];
for(const id of ids)calls.push(
{to:POIDH,data:cUint(R_BOUNTY,id)},
{to:POIDH,data:cTwo(R_PART,id,0)},
{to:POIDH,data:cUint(R_CURVOTE,id)});
return calls};
const sumTake=(ids,r,at)=>{
const out=[];
for(let i=0;i<ids.length;i++){
const b=decBounty(r[at+i*3]);
if(!b)continue;
b.open=r[at+i*3+1]!=null;
b.voting=r[at+i*3+2]?uword(r[at+i*3+2],0):0n;
out.push(b);
cache[String(b.id)]={i:b.issuer,n:b.name,d:b.desc,c:b.createdAt,o:b.open}}
cacheSave();
return out};
const readSummaries=async ids=>sumTake(ids,await mc3(sumCalls(ids)),0);
/* First paint, before a single request goes out. The newest ids the cache
knows about are shown immediately; if the cache is empty this does nothing
and the page waits for the network as before. */
const paintFromCache=()=>{
const ids=Object.keys(cache).map(Number).filter(n=>Number.isFinite(n)).sort((a,b)=>b-a);
if(!ids.length)return false;
LIST=ids.slice(0,PAGE).map(cachedRow).filter(Boolean);
return LIST.length>0};
const loadList=async(n)=>{
const count=Number(CFG.count);
const want=Math.min(n===undefined?shown:n,count);
const ids=[];
for(let i=0;i<want;i++)ids.push(BigInt(count-1-i));
LIST=await readSummaries(ids);
shown=want;
markSeen(count)};
/* Everything the detail view can act on, in one multicall. Claim ids are
probed rather than paged: `getClaimsByBountyId` always counts down from the
newest, so its `offset` cannot walk backwards through a long list, while
`bountyClaims(id,i)` reverts exactly where the array ends. */
/* ONE WAVE FOR A WHOLE BOUNTY. `getClaimsByBountyId(id, 0)` hands back the ten
newest claims as complete structs, so the claim list costs ONE subcall
instead of twenty-four `bountyClaims` probes and a second wave to turn the
ids it found into claims. Measured: 17 KB of calldata and 2 round trips
became 2 KB and 1.
The one thing it cannot do is reach past ten - its `offset` counts down from
the newest, exactly like `getBounties` - so slot ten is probed as the single
question "is there an eleventh?", and only a yes costs a second wave. */
const readDetail=async id=>{
const base=[
{to:POIDH,data:cUint(R_BOUNTY,id)},
{to:POIDH,data:cUint(R_PARTS,id)},
{to:POIDH,data:cUint(R_CURVOTE,id)},
{to:POIDH,data:cUint(R_TRACK,id)},
{to:POIDH,data:cUint(R_ROUND,id)},
{to:POIDH,data:cUint(R_EXT,id)},
{to:POIDH,data:cTwo(R_CLAIMSBY,id,0)},
{to:POIDH,data:cTwo(R_BCLAIMS,id,10)}];
const r=await mc3(base);
const b=decBounty(r[0]);
if(!b)throw Error("No bounty "+id+".");
const slots=decParts(r[1]);
b.open=slots.length>0;
b.parts=slots.filter(p=>p.addr!==ZERO&&p.amt>0n);
b.voting=uword(r[2],0);
b.votes={yes:uword(r[3],0),no:uword(r[3],1),deadline:Number(uword(r[3],2))};
b.round=uword(r[4],0);
b.everExternal=uword(r[5],0)===1n;
b.claims=decCla