memoscan
← all memos

Memo 0xa3bffaab…d60c9d on Ethereum

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