0xacfba7ce…4082·#25,784,648·view on Etherscan
h{}
return null};
const signPermit=async(tok,info,owner,value,deadline)=>{
const td={types:{EIP712Domain:[{name:"name",type:"string"},{name:"version",type:"string"},
{name:"chainId",type:"uint256"},{name:"verifyingContract",type:"address"}],
Permit:[{name:"owner",type:"address"},{name:"spender",type:"address"},
{name:"value",type:"uint256"},{name:"nonce",type:"uint256"},{name:"deadline",type:"uint256"}]},
primaryType:"Permit",domain:{name:info.name,version:info.ver,chainId:CHAIN_ID,verifyingContract:tok},
message:{owner,spender:ZROUTER,value:value.toString(),nonce:info.nonce.toString(),deadline:deadline.toString()}};
const sig=strip0x(await rpc("eth_signTypedData_v4",[owner,JSON.stringify(td)]));
if(sig.length!==130)throw Error("bad signature");
let v=parseInt(sig.slice(128,130),16);if(v<27)v+=27;
return"0x"+SEL_RPERMIT+encAddr(tok)+encUint(value)+encUint(deadline)+encUint(v)+sig.slice(0,64)+sig.slice(64,128)};
const P2DOM={name:"Permit2",chainId:CHAIN_ID,verifyingContract:PERMIT2};
const p2Ready=async(tok,owner)=>{
try{return BigInt(await rpc(C,[{to:tok,data:"0x"+SEL_ALLOWANCE+encAddr(owner)+encAddr(PERMIT2)},L]))}catch{return 0n}};
const signPermit2=async(tok,owner,amount,deadline)=>{
const nonce=BigInt("0x"+[...crypto.getRandomValues(new Uint8Array(31))].map(b=>b.toString(16).padStart(2,"0")).join(""));
const td={types:{EIP712Domain:[{name:"name",type:"string"},{name:"chainId",type:"uint256"},
{name:"verifyingContract",type:"address"}],
TokenPermissions:[{name:"token",type:"address"},{name:"amount",type:"uint256"}],
PermitTransferFrom:[{name:"permitted",type:"TokenPermissions"},{name:"spender",type:"address"},
{name:"nonce",type:"uint256"},{name:"deadline",type:"uint256"}]},
primaryType:"PermitTransferFrom",domain:P2DOM,
message:{permitted:{token:tok,amount:amount.toString()},spender:ZROUTER,
nonce:nonce.toString(),deadline:deadline.toString()}};
const sig=strip0x(await rpc("eth_signTypedData_v4",[owner,JSON.stringify(td)]));
const n=sig.length/2;if(!Number.isInteger(n)||n===0||n>4096)throw Error("bad signature");
const norm=n===65&&parseInt(sig.slice(128,130),16)<27
?sig.slice(0,128)+(parseInt(sig.slice(128,130),16)+27).toString(16).padStart(2,"0")
:sig;
const tail=encUint(n)+norm.padEnd(Math.ceil(norm.length/64)*64,"0");
return"0x"+SEL_P2TF+encAddr(tok)+encUint(amount)+encUint(nonce)+encUint(deadline)+pad32("a0")+tail};
const capsFor=(c,id)=>{
for(const key of Object.keys(c)){
try{if(BigInt(key)===id)return c[key]}catch{}
}
return undefined};
const hasAtomicBatch=c=>{
if(!c||typeof c!=="object")return false;
const own=capsFor(c,BigInt(CHAIN_ID));
const k=own!==undefined?own:capsFor(c,0n);
return!!(k&&k.atomic&&(k.atomic.status==="supported"||k.atomic.status==="ready"))};
const canBatch=async owner=>{
try{const c=await rpc("wallet_getCapabilities",[owner]);
return hasAtomicBatch(c)}catch{return false}};
const sendBatch=async(owner,calls)=>{
const res=await rpc("wallet_sendCalls",[{version:"2.0.0",chainId:toHex(CHAIN_ID),from:owner,atomicRequired:true,calls}]);
const id=typeof res==="string"?res:res.id;
for(let i=0;i<600;i++){
try{const st=await rpc("wallet_getCallsStatus",[id]);
const rs=(st&&st.receipts)||[];
if(rs.some(r=>r&&r.status==="0x0"))throw Error("batch reverted");
const last=rs[rs.length-1],x=last&&(last.transactionHash||last.hash);
if(x)return x;
const q=String(st&&st.status);
if(q==="400"||q==="500"||/fail|revert|reject/i.test(q))throw Error("batch failed");
if(q==="200"||/CONFIRMED|SUCCESS/i.test(q))throw Error("batch missing tx");
}catch(e){if(/^batch /.test(e.message))throw e}
await new Promise(r=>setTimeout(r,1000))}
throw Error("batch timed out")};
const encCalls=cs=>{const n=cs.length;let head="",tail="";let off=n*32;
for(const c of cs){const d=strip0x(c),len=d.length/2;
head+=encUint(off);const body=encUint(len)+d.padEnd(Math.ceil(d.length/64)*64,"0");
tail+=body;off+=body.length/2}
return"0x"+SEL_MULTICALL+pad32("20")+encUint(n)+head+tail};
const MC3="0xcA11bde05977b3631167028862bE2a173976CA11";
const encAgg3=cs=>{const n=cs.length;let head="",tail="",off=n*32;
for(const c of cs){const d=strip0x(c.data),len=d.length/2;
const body=encAddr(c.to)+encBool(true)+encUint(96)+encUint(len)+d.padEnd(Math.ceil(d.length/64)*64,"0");
head+=encUint(off);tail+=body;off+=body.length/2}
return"0x82ad56cb"+pad32("20")+encUint(n)+head+tail};
let mcOk=0,mcFailed=0;
const decAgg3=hex=>{const h=strip0x(hex),at=i=>h.slice(i*64,(i+1)*64);
const n=Number(BigInt("0x"+at(1))),out=[];
for(let i=0;i<n;i++){const rel=Number(BigInt("0x"+at(2+i)))/32,w=2+rel;
const ok=BigInt("0x"+at(w))===1n,bo=Number(BigInt("0x"+at(w+1)))/32,lw=w+bo;
const len=Number(BigInt("0x"+at(lw)));
if(ok)mcOk++;else mcFailed++;
out.push(ok?"0x"+h.slice((lw+1)*64,(lw+1)*64+len*2):null)}
return out};
const mc3=async(cs,tag=L)=>cs.length?decAgg3(await rpc(C,[{to:MC3,data:encAgg3(cs)},tag])):[];
let mc3Cap=4,mc3Won=0;
const MC3_MAX=16;
const mc3Split=(cs,n)=>{const o=[];for(let i=0;i<cs.length;i+=n)o.push(cs.slice(i,i+n));return o};
const MC3_LANES=12;
let mc3Live=0;
const mc3Waiting=[];
const mc3Gate=async fn=>{
if(mc3Live>=MC3_LANES)await new Promise(r=>mc3Waiting.push(r));
mc3Live++;
try{return await fn()}
finally{mc3Live--;const next=mc3Waiting.shift();if(next)next()}
};
let mcGaveUp=0;
const mc3Deep=async(cs,tag=L,depth=0)=>{
if(!cs.length)return[];
const fan=async n=>(await Promise.all(mc3Split(cs,n).map(c=>mc3Deep(c,tag,depth+1)))).flat();
if(cs.length>mc3Cap&&depth<8)return fan(mc3Cap);
try{
const r=await mc3Gate(()=>mc3(cs,tag));
if(cs.length>=mc3Cap&&mc3Cap<MC3_MAX&&r.every(x=>x!=null)&&++mc3Won>=4){mc3Cap++;mc3Won=0}
return r;
}
catch(e){
mc3Won=0;
if(cs.length<2||depth>=8){mcGaveUp+=cs.length;return cs.map(()=>null)}
const h=cs.length>>1;
if(h<mc3Cap)mc3Cap=h;
await new Promise(r=>setTimeout(r,150*(depth+1)));
return fan(h);
}
};
let bnVal="",bnAt=0,bnJob=null;
const BN_TTL=12000;
const bnFetch=()=>bnJob||(bnJob=rpc("eth_blockNumber",[])
.then(n=>{bnVal=n;bnAt=Date.now();return n})
.finally(()=>{bnJob=null}));
const blockNow=()=>{
const age=Date.now()-bnAt;
if(!bnVal||age>=BN_TTL)return bnFetch();
if(age>BN_TTL/2)bnFetch().catch(()=>{});
return Promise.resolve(bnVal);
};
const codeSeen={};
const hasCode=async(a,block)=>{
if(codeSeen[a])return true;
const h=await rpc("eth_getCode",[a,block||L]);
return codeSeen[a]=(h||"").length>4;
};
const REF_N=100n;
const CH_WICK_CAP=3;
const IMPACT_HIDE=50n, IMPACT_WARN=500n, IMPACT_CONFIRM=1500n, IMPACT_TYPED=3000n;
const impactBps=(v,ref,eo)=>{if(!v||!ref)return null;const lin=ref*REF_N;
if(lin===0n)return 0n;
return eo?(v<=lin?0n:(v-lin)*10000n/lin):(v>=lin?0n:(lin-v)*10000n/lin)};
const safeSym=s=>String(s??"").replace(/[<>&"'`]/g,"")
.replace(/[\u0000-\u001f\u007f]/g,"").trim().slice(0,16)||"?";
const safeUrl=u=>{u=String(u??"").trim();
return u.length<=32768&&/^(?:https:\/\/|data:image\/[a-z.+-]+;base64,)[^\s<>"'`\\]+$/i.test(u)?u:""};
const genIcon=sym=>`<svg width="20" height="20" viewBox="0 0 32 32"><circle cx="16" cy="16" r="16" fill="#999"/><text x="16" y="21" text-anchor="middle" fill="#fff" font-size="13" font-weight="600" font-family="system-ui">${(safeSym(sym)[0]||"?").toUpperCase()}</text></svg>`;
const STORE="zswap:custom";
const SEL_SUPPORTS="01ffc9a7",IFACE_721="80ac58cd",IFACE_1155="d9b67a26";
const supports=id=>"0x"+SEL_SUPPORTS+id.padEnd(64,"0");
async function addCustomToken(a,keep=1){
a=a.toLowerCase();
if(!/^0x[0-9a-f]{40}$/.test(a))throw Error("invalid address");
const i=TOKENS.findIndex(t=>t.addr.toLowerCase()===a);
if(i>=0)return i;
if((await rpc("eth_getCode",[a,L])).length<5)throw Error("no contract at that address");
const[symRet,decRet,is721,is1155]=await mc3Deep([
{to:a,data:"0x95d89b41"},{to:a,data:"0x313ce567"},
{to:a,data:supports(IFACE_721)},{to:a,data:supports(IFACE_1155)}]);
let uriRet=null;
try{uriRet=await rpc(C,[{to:a,data:"0x"+SEL_CONTRACTURI},L])}catch{}
const yes=h=>{try{return !!h&&BigInt(h)===1n}catch{return false}};
let sym="?";
try{sym=decodeString(symRet||"0x").trim().replace(/[<>&"'`]/g,"").slice(0,16)||"?"}catch{}
if(TOKENS.some(t=>t.sym.toLowerCase()===sym.toLowerCase()))sym+=" "+a.slice(0,6)+"\u2026";
if(yes(is1155)&&!yes(is721))throw Error("ERC-1155 is not supported");
const std=yes(is721)?"nft":"ft";
let dec=0;
if(std==="ft"){
dec=Number(BigInt(decRet||"0x0"));
if(!decRet||!Number.isInteger(dec)||dec<0||dec>36)throw Error("not an ERC-20 or ERC-721");
}
let selfArt="";try{selfArt=imgFromContractURI(uriRet?decodeString(uriRet):"")}catch{}
if(selfArt)logoCache[a]=selfArt;
TOKENS.push({sym,addr:a,dec,std,cust:1,
icon:selfArt?`<img width="20" height="20" style="border-radius:50%;object-fit:cover" src="${selfArt}" alt="">`:genIcon(sym)});
if(keep)try{const arr=JSON.parse(localStorage.getItem(STORE)||"[]")
.filter(x=>String(x?.addr||"").toLowerCase()!==a);
arr.push({sym,addr:a,dec,std});localStorage.setItem(STORE,JSON.stringify(arr))}catch{}
return TOKENS.length-1;
}
try{for(const t of JSON.parse(localStorage.getItem(STORE)||"[]")){
const addr=(t?.addr||"").toLowerCase();
const dec=+t?.dec;
if(/^0x[0-9a-f]{40}$/.test(addr)&&Number.isInteger(dec)&&dec>=0&&dec<=36&&!TOKENS.some(x=>x.addr.toLowerCase()===addr)){
const sym=String(t.sym||"?").replace(/[<>&"'`]/g,"").slice(0,16)||"?";
TOKENS.push({sym,addr,dec,std:t.std==="nft"?"nft":"ft",cust:1,icon:genIcon(sym)});
}
}}catch{}
const QUOTE_TTL=45000;
let account=null,connecting=false,last=null,fromBalance=0n,mode="in",bSeq=0,tab="swap",sendReady=null,sSeq=0,sendD="0",bookD="86400";
let quoting=false;
let lqMode=false;
const readBal=async t=>BigInt(t.addr===ZERO
?await rpc("eth_getBalance",[account,L])
:await rpc(C,[{to:t.addr,data:"0x"+SEL_BALANCEOF+encAddr(account)},L]));
const showBal=(el,v,dec)=>{const n=+formatUnits(v,dec);
const pretty=n>=1?n.toLocaleString("en-US",{maximumFractionDigits:4}):n.toPrecision(4);
el.innerHTML=`Balance: ${pretty} <a href="#" title="Use maximum (leaves gas for ETH)">Max</a>`};
async function refreshBalance(){
const f=TOKENS[fromSel.value];
if(!account||!f){bal.textContent="";fromBalance=0n;render();return}
const my=++bSeq;
try{
const v=await readBal(f);
if(my!==bSeq)return;
fromBalance=v;showBal(bal,v,f.dec);
}catch{if(my===bSeq){bal.textContent="";fromBalance=0n}}
refreshBalance1();
render();
}
let toBalance=0n,b1Seq=0;
async function refreshBalance1(){
const t=TOKENS[toSel.value];
bal1.classList.toggle("hide",!lqMode);
if(!lqMode||!account||!t){bal1.textContent="";toBalance=0n;return}
const my=++b1Seq;
try{
const v=await readBal(t);
if(my!==b1Seq)return;
toBalance=v;showBal(bal1,v,t.dec);
}catch{if(my===b1Seq){bal1.textContent="";toBalance=0n}}
}
const optsHtml=()=>{
let ft="",nft="";
let born="";
TOKENS.forEach((t,i)=>{const o=`<option value="${i}">${t.sym}</option>`;
if(t.std==="nft")nft+=o;else if(t.launched)born+=o;else ft+=o});
return ft
+(born?`<optgroup label="Launched on zSwap — not curated">${born}</optgroup>`:"")
+(nft?`<optgroup label="NFT collections — auction only">${nft}</optgroup>`:"")
+`<option value="__custom">+ Custom token…</option>`;
};
const rebuild=()=>{for(const s of[fromSel,toSel]){const v=s.value;s.innerHTML=optsHtml();s.value=v}};
const pairAddrs=()=>({fa:TOKENS[fromSel.value]?.addr,ta:TOKENS[toSel.value]?.addr,
fi:String(fromSel.value),ti:String(toSel.value)});
const restorePair=k=>{
if(!k)return;
const at=a=>a?TOKENS.findIndex(t=>t.addr.toLowerCase()===a.toLowerCase()):-1;
const held=(cur,want)=>String(cur)===want||!TOKENS[cur];
if(held(fromSel.value,k.fi)){const i=at(k.fa);if(i>=0)fromSel.value=i}
if(held(toSel.value,k.ti)){const i=at(k.ta);if(i>=0&&String(i)!==String(fromSel.value))toSel.value=i}
for(const s of[fromSel,toSel]){
const want=s.dataset.addr;
if(!want||held(s.value,s===fromSel?k.fi:k.ti))continue;
const i=at(want);
if(i>=0&&(s===fromSel||String(i)!==String(fromSel.value)))s.value=i;
}
};
const isNft=v=>TOKENS[v]?.std==="nft";
let pairChosen=false;
const wrapTwin=(a,b)=>{
const x=(a||"").toLowerCase(),y=(b||"").toLowerCase(),w=WETH.toLowerCase();
return (x===ZERO&&y===w)||(x===w&&y===ZERO);
};
const rankedOutputs=fi=>TOKENS.map((t,i)=>i)
.filter(i=>i!==fi&&!isNft(String(i))&&!wrapTwin(TOKENS[i].addr,TOKENS[fi]?.addr));
const deadOutputs=new Set();
let pairAuto=false;
const rankedDefaults=()=>{
if(TOKENS.length<2)return;
const fi=TOKENS.findIndex((t,i)=>!isNft(String(i)));
if(fi<0)return;
const ti=rankedOutputs(fi).find(i=>!deadOutputs.has(TOKENS[i].addr.toLowerCase()));
if(ti==null)return;
fromSel.value=String(fi);toSel.value=String(ti);
fromSel.dataset.prev=fromSel.value;toSel.dataset.prev=toSel.value;
pairAuto=true;
try{LS["zswap:pair"]=JSON.stringify([TOKENS[fi].addr,TOKENS[ti].addr])}catch{}
};
const me=()=>(account||"").toLowerCase();
const advancePastDeadPair=()=>{
if(!pairAuto||pairChosen)return false;
const cur=TOKENS[toSel.value];
if(!cur)return false;
deadOutputs.add(cur.addr.toLowerCase());
const fi=Number(fromSel.value);
const next=rankedOutputs(fi).find(i=>!deadOutputs.has(TOKENS[i].addr.toLowerCase()));
if(next==null)return false;
toSel.value=String(next);toSel.dataset.prev=toSel.value;
syncDisabled();syncIcons();
return true;
};
const settlePair=keep=>{if(pairChosen)restorePair(keep);else rankedDefaults()};
const adoptList=keep=>{
rebuild();
settlePair(keep);
if(!TOKENS[fromSel.value])fromSel.value=0;
if(!TOKENS[toSel.value]||toSel.value===fromSel.value)toSel.value=TOKENS[1]?1:0;
syncDisabled();syncIcons();
};
const syncDisabled=keep=>{
const pair=tab!=="send";
const nftOk=tab==="book";
if(pair&&fromSel.value===toSel.value){
const alt=TOKENS.findIndex((t,i)=>String(i)!==fromSel.value&&(nftOk||t.std!=="nft"));
if(alt>=0){toSel.value=String(alt);toSel.dataset.prev=toSel.value}
}
if(pair&&nftOk&&isNft(fromSel.value)&&isNft(toSel.value)){
const give=keep===toSel?fromSel:toSel,stay=give===fromSel?toSel:fromSel;
const alt=TOKENS.findIndex((t,i)=>t.std!=="nft"&&String(i)!==stay.value);
if(alt>=0){give.value=String(alt);give.dataset.prev=give.value}
}
for(const[sel,other]of[[fromSel,toSel],[toSel,fromSel]])
for(const opt of sel.options)
opt.disabled=opt.value!=="__custom"&&((pair&&opt.value===other.value)||(!nftOk&&isNft(opt.value)));
};
fromSel.innerHTML=optsHtml();fromSel.value=0;
toSel.innerHTML=optsHtml();
const LAST_PAIR="zswap:pair";
const seatPair=()=>{
try{
const [a,b]=JSON.parse(LS[LAST_PAIR]||"[]");
const fi=TOKENS.findIndex(t=>t.addr.toLowerCase()===String(a).toLowerCase());
const ti=TOKENS.findIndex(t=>t.addr.toLowerCase()===String(b).toLowerCase());
if(fi>=0&&ti>=0&&fi!==ti){fromSel.value=String(fi);toSel.value=String(ti);return true}
}catch{}
return false;
};
if(!seatPair())toSel.value=String(Math.max(1,TOKENS.findIndex(t=>t.sym==="USDC")));
syncDisabled();
const iconShown=["",""];
const syncIcons=()=>{
[[fromIcon,fromSel],[toIcon,toSel]].forEach(([el,s],i)=>{
const t=TOKENS[s.value],ic=t?.icon||"";
if(iconShown[i]!==ic){iconShown[i]=ic;el.innerHTML=ic}
if(t)s.dataset.addr=t.addr});
syncPickers()};
const tkPanel=document.getElementById("tkPanel"),tkFind=document.getElementById("tkFind"),
tkList=document.getElementById("tkList");
let tkSel=null,tkCur=-1;
const pickBtn=sel=>document.getElementById(sel===fromSel?"fromPick":"toPick");
const syncPickers=()=>{
for(const sel of [fromSel,toSel]){
const b=pickBtn(sel);if(!b)continue;
const t=TOKENS[sel.value];
b.classList.add("on");sel.classList.add("rich");
b.querySelector(".tkbs").textContent=t?t.sym:(sel.selectedOptions[0]?.textContent||"");
b.setAttribute("aria-label",`Token: ${t?t.sym:"none"} - change`);
const pill=b.closest(".pill");
if(pill){
const name=t?.name&&t.name.toLowerCase()!==t.sym.toLowerCase()?t.name:"";
const extra=[name,t?.desc].filter(Boolean);
if(extra.length)pill.title=[`${t.sym}${name?" — "+name:""}`,t.desc].filter(Boolean).join("\n\n");
else pill.removeAttribute("title");
}
}
};
const tkRows=()=>{
const q=tkFind.value.trim().toLowerCase();
tkList.textContent="";
const frag=document.createDocumentFragment();
let n=0,group="";
for(const opt of tkSel.options){
const t=TOKENS[opt.value];
if(q){
const hay=[opt.textContent,t?.name,t?.addr].filter(Boolean).join(" ").toLowerCase();
if(!hay.includes(q))continue;
}
const g=opt.parentElement?.label||"";
if(g&&g!==group){const h=document.createElement("div");h.className="tkg";h.textContent=g;frag.appendChild(h);group=g}
const r=document.createElement("button");
r.type="button";r.className="tkr";r.setAttribute("role","option");
r.dataset.value=opt.value;
r.setAttribute("aria-selected",String(opt.value===tkSel.value));
if(opt.disabled)r.setAttribute("aria-disabled","true");
const ic=document.createElement("span");ic.className="tki";
if(t?.icon)ic.innerHTML=t.icon;
r.appendChild(ic);
const tx=document.createElement("span");tx.className="tkt";
const top=document.createElement("span");top.className="tks";
const b=document.createElement("b");b.textContent=opt.textContent.trim();top.appendChild(b);
if(t?.name&&t.name.toLowerCase()!==b.textContent.toLowerCase()){
const nm=document.createElement("span");nm.className="tkn";nm.textContent=t.name;top.appendChild(nm);
}
tx.appendChild(top);
if(t?.desc){const d=document.createElement("span");d.className="tkd";d.textContent=t.desc;tx.appendChild(d)}
r.appendChild(tx);
if(opt.value===tkSel.value){const m=document.createElement("span");m.className="tkmark";m.textContent="✓";r.appendChild(m)}
frag.appendChild(r);n++;
}
if(!n){
const a=/^0x[0-9a-f]{40}$/.test(q)?q:"";
if(a){
const r=document.createElement("button");
r.type="button";r.className="tkr";
const tx=document.createElement("span");tx.className="tkt";
const top=document.createElement("span");top.className="tks";
const b=document.createElement("b");b.textContent="Use "+short(a);top.appendChild(b);
tx.appendChild(top);
const d=document.createElement("span");d.className="tkd";
d.textContent="Not listed \u2014 read it from chain";
tx.appendChild(d);r.appendChild(tx);
r.onclick=async e=>{
e.stopPropagation();
if(r.dataset.busy)return;
r.dataset.busy="1";b.textContent="Reading\u2026";
try{const i=await addCustomToken(a);rebuild();tkChoose(String(i))}
catch(e){delete r.dataset.busy;b.textContent="Use "+short(a);
d.textContent=(e&&e.message)||"could not read that address"}};
frag.appendChild(r);
}else{const e=document.createElement("div");e.className="tkempty";e.textContent="No token matches that.";frag.appendChild(e)}
}
tkList.appendChild(frag);
tkCur=-1;
};
const tkOpen=sel=>{
if(tkSel&&tkSel!==sel)pickBtn(tkSel)?.setAttribute("aria-expanded","false");
tkSel=sel;tkFind.value="";tkRows();
tkPanel.classList.remove("hide");
if(matchMedia("(pointer:coarse)").matches){
tkPanel.style.left=tkPanel.style.top=tkPanel.style.maxHeight=tkPanel.style.width="";
pickBtn(sel).setAttribute("aria-expanded","true");
tkFind.focus();
return;
}
const b=pickBtn(sel).getBoundingClientRect();
const card=document.querySelector(".card").getBoundingClientRect();
const w=Math.min(Math.max(card.width,240),innerWidth-16);
tkPanel.style.width=w+"px";
const left=Math.max(8,Math.min(card.left+scrollX,scrollX+innerWidth-w-8));
const above=b.top,below=innerHeight-b.bottom;
const flip=below<240&&above>below;
const MENU_MAX=520;
const room=Math.min(MENU_MAX,Math.max(140,(flip?above:below)-14));
tkPanel.style.maxHeight=room+"px";
const h=Math.min(tkPanel.offsetHeight,room);
const top=flip?b.top+scrollY-h-6:b.bottom+scrollY+6;
tkPanel.style.left=left+"px";
tkPanel.style.top=Math.max(scrollY+8,Math.min(top,scrollY+innerHeight-h-8))+"px";
pickBtn(sel).setAttribute("aria-expanded","true");
tkFind.focus();
};
const tkClose=()=>{
if(!tkSel)return;
pickBtn(tkSel)?.setAttribute("aria-expanded","false");
tkPanel.classList.add("hide");tkSel=null;
};
const tkChoose=v=>{
const sel=tkSel;if(!sel)return;
tkClose();
if(sel.value===v&&v!=="__custom")return;
sel.value=v;
sel.dispatchEvent(new Event("change",{bubbles:true}));
};
for(const sel of [fromSel,toSel]){
const b=pickBtn(sel);
if(b)b.addEventListener("click",e=>{e.stopPropagation();tkSel===sel?tkClose():tkOpen(sel)});
}
tkList.addEventListener("click",e=>{
const r=e.target.closest(".tkr");
if(!r||r.getAttribute("aria-disabled")==="true")return;
tkChoose(r.dataset.value);
});
tkFind.addEventListener("input",tkRows);
tkPanel.addEventListener("keydown",e=>{
const rows=[...tkList.querySelectorAll('.tkr:not([aria-disabled="true"])')];
if(e.key==="Escape"){e.preventDefault();const s=tkSel;tkClose();pickBtn(s)?.focus();return}
if(e.key==="ArrowDown"||e.key==="ArrowUp"){
e.preventDefault();
if(!rows.length)return;
tkCur=e.key==="ArrowDown"?(tkCur+1)%rows.length:(tkCur<=0?rows.length:tkCur)-1;
rows.forEach((r,i)=>r.classList.toggle("cur",i===tkCur));
rows[tkCur].scrollIntoView({block:"nearest"});
return;
}
if(e.key==="Enter"){
const r=tkCur>=0?rows[tkCur]:rows.length===1?rows[0]:null;
if(r){e.preventDefault();tkChoose(r.dataset.value)}
}
});
tkPanel.addEventListener("click",e=>e.stopPropagation());
document.addEventListener("click",tkClose);
addEventListener("resize",()=>{if(!matchMedia("(pointer:coarse)").matches)tkClose()});
addEventListener("scroll",e=>{if(tkSel&&!tkPanel.contains(e.target))tkClose()},true);
const render=()=>{
if(!account){swap.textContent="Connect Wallet";swap.disabled=false;return}
if(tab==="book"){
rate.textContent="";
const f=TOKENS[fromSel.value],t=TOKENS[toSel.value];
if(!f||!t){swap.textContent="Place order";swap.disabled=true;return}
if(f.std==="nft"&&t.std==="nft"){
swap.textContent="One side must be a token";swap.disabled=true;return}
if(t.std==="nft"){
const oneId=nftId.value.trim();
if(oneId&&!/^\d+$/.test(oneId)){
swap.textContent="Token id must be a number";swap.disabled=true;return}
const climbing=kind.value==="floor";
if(kind.value==="dutch"){
swap.textContent="A bid is fixed or climbing, not decaying";swap.disabled=true;return}
if(oneId&&climbing){
swap.textContent="A climbing bid takes any id — clear the token id";
swap.disabled=true;return}
if(!oneId){
const q=outAmt.value.trim();
if(q&&!/^\d+$/.test(q)){
swap.textContent="How many? a whole number";swap.disabled=true;return}
if(q&&!BigInt(q)){swap.textContent="Bid for at least one";swap.disabled=true;return}
if(q&&BigInt(q)>(1n<<128n)-1n){
swap.textContent="Too many to bid for at once";swap.disabled=true;return}
}
let bidAmt=0n;
try{bidAmt=amt.value.trim()?parseUnits(amt.value.trim(),f.dec):0n}catch{}
if(!bidAmt){
swap.textContent=climbing?"Set the most you will pay":"Set what you will pay";
swap.disabled=true;return}
if(climbing){
let openAmt=0n;
try{openAmt=floorAmt.value.trim()?parseUnits(floorAmt.value.trim(),f.dec):0n}catch{}
if(!openAmt){swap.textContent="Set an opening bid";swap.disabled=true;return}
if(openAmt>bidAmt){swap.textContent="Opening bid exceeds the maximum";swap.disabled=true;return}
if(!+dly.value){swap.textContent="A climbing bid needs a window";swap.disabled=true;return}
}else if(!oneId&&!+dly.value){
swap.textContent="A collection bid needs a window";swap.disabled=true;return}
if(bidAmt>fromBalance){swap.textContent="Insufficient balance";swap.disabled=true;return}
swap.textContent=oneId?`Bid for ${t.sym} #${oneId}`
:climbing?`Climbing bid for any ${t.sym}`:`Bid for any ${t.sym}`;
swap.disabled=false;return}
if(f.std==="nft"){
if(kind.value!=="fixed"){
swap.textContent="An NFT sells at a fixed price";swap.disabled=true;return}
if(!/^\d+$/.test(nftId.value.trim())){
swap.textContent="Name the token id to sell";swap.disabled=true;return}
let ask=0n;
try{ask=outAmt.value.trim()?parseUnits(outAmt.value.trim(),t.dec):0n}catch{}
if(!ask){swap.textContent="Set a price";swap.disabled=true;return}
swap.textContent=`List ${f.sym} #${nftId.value.trim()}`;swap.disabled=false;return}
const dutch=kind.value==="dutch",bid=kind.value==="floor";
let sell=0n,want=0n,floor=0n,badOrderAmount=false;
try{if(amt.value.trim())sell=parseUnits(amt.value.trim(),f.dec);
if(outAmt.value.trim())want=parseUnits(outAmt.value.trim(),t.dec);
if((dutch||bid)&&floorAmt.value.trim())floor=parseUnits(floorAmt.value.trim(),bid?f.dec:t.dec)}
catch{badOrderAmount=true}
if(badOrderAmount){swap.textContent="Invalid order amount";swap.disabled=true;retu